diff --git a/CHANGELOG.md b/CHANGELOG.md index f3f389805fa..c009a1836dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.8.0-0.27b0...HEAD) +- Decode URL-encoded headers in environment variables + ([#2312](https://github.com/open-telemetry/opentelemetry-python/pull/2312)) + ## [1.8.0-0.27b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.8.0-0.27b0) - 2021-12-17 - Adds Aggregation and instruments as part of Metrics SDK 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 index 691c7c2dc1b..248b970b6f3 100644 --- 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/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 index 198ff442718..fa3c24e0f9c 100644 --- 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/src/opentelemetry/util/re.py b/opentelemetry-api/src/opentelemetry/util/re.py index 32c3e3ffb4c..c107ee91508 100644 --- 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 diff --git a/opentelemetry-api/tests/util/test_re.py b/opentelemetry-api/tests/util/test_re.py index 9c726a7e574..e7834ac15ad 100644 --- 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==",