Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix incorrect headers parsing via environment variables #2103

Merged
merged 10 commits into from
Sep 20, 2021
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#2077](https://github.com/open-telemetry/opentelemetry-python/pull/2077))
- Fix propagation bug caused by counting skipped entries
([#2071](https://github.com/open-telemetry/opentelemetry-python/pull/2071))
- Fix incorrect headers parsing via environment variables
([#2103](https://github.com/open-telemetry/opentelemetry-python/pull/2103))

## [1.5.0-0.24b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.5.0-0.24b0) - 2021-08-26

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
OTEL_EXPORTER_OTLP_TIMEOUT,
)
from opentelemetry.sdk.resources import Resource as SDKResource
from opentelemetry.util._env import parse_headers

logger = logging.getLogger(__name__)
SDKDataT = TypeVar("SDKDataT")
Expand Down Expand Up @@ -228,19 +229,8 @@ def __init__(

self._headers = headers or environ.get(OTEL_EXPORTER_OTLP_HEADERS)
if isinstance(self._headers, str):
temp_headers = []
for header_pair in self._headers.split(","):
key, value = header_pair.split("=", maxsplit=1)
key = key.strip().lower()
value = value.strip()
temp_headers.append(
(
key,
value,
)
)

self._headers = tuple(temp_headers)
temp_headers = parse_headers(self._headers)
self._headers = tuple(temp_headers.items())

self._timeout = timeout or int(
environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, 10)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from opentelemetry.exporter.otlp.proto.http.trace_exporter.encoder import (
_ProtobufEncoder,
)
from opentelemetry.util._env import parse_headers


_logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -70,7 +71,11 @@ def __init__(
OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE,
environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE, True),
)
self._headers = headers or _headers_from_env()
headers_string = environ.get(
OTEL_EXPORTER_OTLP_TRACES_HEADERS,
environ.get(OTEL_EXPORTER_OTLP_HEADERS, ""),
)
self._headers = headers or parse_headers(headers_string)
self._timeout = timeout or int(
environ.get(
OTEL_EXPORTER_OTLP_TRACES_TIMEOUT,
Expand Down Expand Up @@ -155,24 +160,6 @@ def shutdown(self):
self._shutdown = True


def _headers_from_env() -> Optional[Dict[str, str]]:
headers_str = environ.get(
OTEL_EXPORTER_OTLP_TRACES_HEADERS,
environ.get(OTEL_EXPORTER_OTLP_HEADERS),
)
headers = {}
if headers_str:
for header in headers_str.split(","):
try:
header_name, header_value = header.split("=")
headers[header_name.strip()] = header_value.strip()
except ValueError:
_logger.warning(
"Skipped invalid OTLP exporter header: %r", header
)
return headers


def _compression_from_env() -> Compression:
compression = (
environ.get(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def test_constructor_default(self):
OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE: "traces/certificate.env",
OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: Compression.Deflate.value,
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "https://traces.endpoint.env",
OTEL_EXPORTER_OTLP_TRACES_HEADERS: "tracesEnv1=val1,tracesEnv2=val2",
OTEL_EXPORTER_OTLP_TRACES_HEADERS: "tracesEnv1=val1,tracesEnv2=val2,traceEnv3===val3==",
OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: "40",
},
)
Expand All @@ -77,7 +77,11 @@ def test_exporter_traces_env_take_priority(self):
self.assertIs(exporter._compression, Compression.Deflate)
self.assertEqual(
exporter._headers,
{"tracesEnv1": "val1", "tracesEnv2": "val2"},
{
"tracesenv1": "val1",
"tracesenv2": "val2",
"traceenv3": "==val3==",
},
)

@patch.dict(
Expand Down Expand Up @@ -127,7 +131,7 @@ def test_exporter_env(self):
self.assertEqual(exporter._timeout, int(OS_ENV_TIMEOUT))
self.assertIs(exporter._compression, Compression.Gzip)
self.assertEqual(
exporter._headers, {"envHeader1": "val1", "envHeader2": "val2"}
exporter._headers, {"envheader1": "val1", "envheader2": "val2"}
)

@patch.dict(
Expand All @@ -143,5 +147,5 @@ def test_headers_parse_from_env(self):

self.assertEqual(
cm.records[0].message,
"Skipped invalid OTLP exporter header: 'missingValue'",
"Header doesn't match the format: missingValue.",
)
50 changes: 50 additions & 0 deletions opentelemetry-api/src/opentelemetry/util/_env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
import re
from typing import Mapping

_logger = logging.getLogger(__name__)


# https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#specifying-headers-via-environment-variables
_OWS = r"[ \t]*"
# A key contains one or more US-ASCII character except CTLs or separators.
_KEY_FORMAT = (
r"[\x21\x23-\x27\x2a\x2b\x2d\x2e\x30-\x39\x41-\x5a\x5e-\x7a\x7c\x7e]+"
)
# A value contains a URL encoded UTF-8 string.
_VALUE_FORMAT = r"[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*"
_HEADER_FORMAT = _KEY_FORMAT + _OWS + r"=" + _OWS + _VALUE_FORMAT
_HEADER_PATTERN = re.compile(_HEADER_FORMAT)
_DELIMITER_PATTERN = re.compile(r"[ \t]*,[ \t]*")


def parse_headers(headers_string: str) -> Mapping[str, str]:
srikanthccv marked this conversation as resolved.
Show resolved Hide resolved
srikanthccv marked this conversation as resolved.
Show resolved Hide resolved
headers = {}
for header in re.split(_DELIMITER_PATTERN, headers_string):
if not header: # empty string
continue
match = _HEADER_PATTERN.fullmatch(header.strip())
if not match:
_logger.warning("Header doesn't match the format: %s.", header)
continue
# value may contain any number of `=`
name, value = match.string.split("=", 1)
name = name.strip().lower()
value = value.strip()
headers[name] = value

return headers