Repository: open-telemetry/opentelemetry-python

Issue:
grpc OTLPSpanExporter does not support HTTP authorization headers anymore
Hi, first time reporting an issue I stumbled upon :) 

**Describe your environment** 
python 3.7+
opentelemetry-sdk==1.6.2
opentelemetry-api==1.6.2
opentelemetry-exporter-otlp==1.6.2
Traces exported to elastic APM via the standard **opentelemetry.exporter.otlp.proto.grpc.OTLPSpanExporter**
directly (providing it the endpoint and authorization headers)

**Steps to reproduce**
try to export traces using this instantiation of the OTLPSpanExporter:
```
apm_export = OTLPSpanExporter(endpoint={APM_URL}, \
                            headers=f"authorization=Bearer {APM SECRET TOKEN}"
                           )

trace.get_tracer_provider().add_span_processor(SimpleSpanProcessor(apm_export))
```

**What is the expected behavior?**
In previous versions (pre 1.6.0), the header was parsed correctly and passed to the exporter.
Then, each grpc request was authorizing with the bearer token and trace was being exported 
successfully

**What is the actual behavior?**
since v1.6.0, when this change was introduced: 
https://github.com/open-telemetry/opentelemetry-python/pull/2103
the headers given to OTLPSpanExporter __init__() function  are parsed using a regex library which expects key=value pairs in the header,
when the whitespace character inside the value portion is not matched, causing this types of headers
to not be passed anymore, and leads to a "Permission Denied" error code returned from the grpc client.

**Additional context**
![image (2)](https://user-images.githubusercontent.com/80201864/139253611-dfc3c2f7-5dc4-4778-b125-5d360eeaaf9b.png)
This is the exact place where the header is newly parsed (diff between 1.5.0-1.6.0)



Discussion:
That change was based on the spec recommendation https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#specifying-headers-via-environment-variables. If you are not using the env you could just pass the mapping for header names and values.
ok, fair enough.  but its not this clear what format the headers should be in. I mean, if I could pass a dict representing the mapping directly it would be nice. I had to dive into the implementation and see that I have to make a tuple out of the mapping's items in order for the exporter to treat it properly. Its not very intuitive
I didn't notice this was about gRPC. You could pass the header name-value mapping in HTTP exporter but gRPC has it different. I believe the types provided weren't really helpful either. I think we can change gRPC exporter to accept them in backward compatible manner.
The http OTLPSpanExporter also rejects authorization headers (or more generally headers values containing a blank character). The [specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#specifying-headers-via-environment-variable) for the environment variables in fact says that the value needs to be an URL encoded UTF-8 string, but i think this just refers only to the environment variables itself. I think exporters, like the http OTLP exporter, would need to decode the value before putting it in the HTTP headers to have them in the expected format. See for example the [authorization header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization), which will most likely be rejected by most backends if encountered in URL encoded format.
I've also encountered this issue with respect to `Authorization` headers.
I took a look at a few other `opentelemetry-{lang}` libraries:

- [opentelemetry-go](https://github.com/open-telemetry/opentelemetry-go/blob/4d9d882c382641e5f11e72aa9ac9603281fb5a4b/exporters/otlp/otlpmetric/internal/otlpconfig/envconfig.go#L186): decodes header keys and values :green_circle: 
- [opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java/blob/65e7145e9c6151416042757941d45f8970819248/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/OtlpConfigUtil.java#L72-L76): does not appear to decode keys or values :red_circle: 
- [opentelemetry-js](https://github.com/open-telemetry/opentelemetry-js/blob/df12218cc4c97ad951c5f8c59f1a9f14987c00fe/packages/opentelemetry-core/src/baggage/utils.ts#L43-L59): decodes header keys and values :green_circle: 
- [opentelemetry-ruby](https://github.com/open-telemetry/opentelemetry-ruby/blob/bf12dcc6df42c76c98e1591340e8c46b7ed904df/exporter/otlp/lib/opentelemetry/exporter/otlp/exporter.rb#L394): decodes header keys and values :green_circle: 
- [opentelemetry-php](https://github.com/open-telemetry/opentelemetry-php/blob/44575d74e6e253c89d658e949adc1d92c662c702/src/Contrib/OtlpGrpc/Exporter.php#L139-L161): does not decode keys or values :red_circle: 

Decoding the header values appears to be the correct behavior, but I'd want to validate that assumption before opening a PR.

Resolving patch:
diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py
--- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py
+++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py
@@ -19,7 +19,7 @@
 from collections.abc import Sequence
 from os import environ
 from time import sleep
-from typing import Any, Callable, Dict, Generic, List, Optional
+from typing import Any, Callable, Dict, Generic, List, Optional, Tuple, Union
 from typing import Sequence as TypingSequence
 from typing import TypeVar
 from urllib.parse import urlparse
@@ -204,7 +204,9 @@ def __init__(
         endpoint: Optional[str] = None,
         insecure: Optional[bool] = None,
         credentials: Optional[ChannelCredentials] = None,
-        headers: Optional[Sequence] = None,
+        headers: Optional[
+            Union[TypingSequence[Tuple[str, str]], Dict[str, str], str]
+        ] = None,
         timeout: Optional[int] = None,
         compression: Optional[Compression] = None,
     ):
@@ -229,6 +231,8 @@ def __init__(
         if isinstance(self._headers, str):
             temp_headers = parse_headers(self._headers)
             self._headers = tuple(temp_headers.items())
+        elif isinstance(self._headers, dict):
+            self._headers = tuple(self._headers.items())
 
         self._timeout = timeout or int(
             environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, 10)
diff --git a/opentelemetry-api/src/opentelemetry/util/re.py b/opentelemetry-api/src/opentelemetry/util/re.py
--- a/opentelemetry-api/src/opentelemetry/util/re.py
+++ b/opentelemetry-api/src/opentelemetry/util/re.py
@@ -15,6 +15,7 @@
 import logging
 from re import compile, split
 from typing import Mapping
+from urllib.parse import unquote
 
 _logger = logging.getLogger(__name__)
 
@@ -51,8 +52,8 @@ def parse_headers(s: str) -> Mapping[str, str]:
             continue
         # value may contain any number of `=`
         name, value = match.string.split("=", 1)
-        name = name.strip().lower()
-        value = value.strip()
+        name = unquote(name).strip().lower()
+        value = unquote(value).strip()
         headers[name] = value
 
     return headers


Test patch:
diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py
--- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py
+++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py
@@ -281,6 +281,13 @@ def test_otlp_headers_from_env(self, mock_ssl_channel, mock_secure):
         self.assertEqual(
             exporter._headers, (("key3", "value3"), ("key4", "value4"))
         )
+        exporter = OTLPSpanExporter(
+            headers={"key5": "value5", "key6": "value6"}
+        )
+        # pylint: disable=protected-access
+        self.assertEqual(
+            exporter._headers, (("key5", "value5"), ("key6", "value6"))
+        )
 
     # pylint: disable=no-self-use
     @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel")
diff --git a/opentelemetry-api/tests/util/test_re.py b/opentelemetry-api/tests/util/test_re.py
--- a/opentelemetry-api/tests/util/test_re.py
+++ b/opentelemetry-api/tests/util/test_re.py
@@ -34,6 +34,12 @@ def test_parse_headers(self):
             # different header values
             ("name=", [("name", "")], False),
             ("name===value=", [("name", "==value=")], False),
+            # url-encoded headers
+            ("key=value%20with%20space", [("key", "value with space")], False),
+            ("key%21=value", [("key!", "value")], False),
+            ("%20key%20=%20value%20", [("key", "value")], False),
+            # header name case normalization
+            ("Key=Value", [("key", "Value")], False),
             # mix of valid and invalid headers
             (
                 "name1=value1,invalidName, name2 =   value2   , name3=value3==",