Skip to content

Commit 212d50a

Browse files
authored
Add a more informative error message when parsing ENV headers (#3044)
* Add a more informative error message when parsing ENV headers Also, rename the function to make it clear that this parsing is specific to headers provided via ENV variables. Fixes #2376 * Use parse_env_headers in metrics and logs as well * Fix lint * Fix mypy
1 parent 638988c commit 212d50a

File tree

9 files changed

+53
-23
lines changed

9 files changed

+53
-23
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
- Rename parse_headers to parse_env_headers and improve error message
11+
([#2376](https://github.com/open-telemetry/opentelemetry-python/pull/2376))
1012
- Add url decode values from OTEL_RESOURCE_ATTRIBUTES
1113
([#3046](https://github.com/open-telemetry/opentelemetry-python/pull/3046))
1214
- Fixed circular dependency issue with custom samplers

exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@
5252
OTEL_EXPORTER_OTLP_TIMEOUT,
5353
)
5454
from opentelemetry.sdk.resources import Resource as SDKResource
55-
from opentelemetry.util.re import parse_headers
5655
from opentelemetry.sdk.metrics.export import MetricsData
56+
from opentelemetry.util.re import parse_env_headers
5757

5858
logger = getLogger(__name__)
5959
SDKDataT = TypeVar("SDKDataT")
@@ -247,7 +247,7 @@ def __init__(
247247

248248
self._headers = headers or environ.get(OTEL_EXPORTER_OTLP_HEADERS)
249249
if isinstance(self._headers, str):
250-
temp_headers = parse_headers(self._headers)
250+
temp_headers = parse_env_headers(self._headers)
251251
self._headers = tuple(temp_headers.items())
252252
elif isinstance(self._headers, dict):
253253
self._headers = tuple(self._headers.items())

exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
from opentelemetry.exporter.otlp.proto.http._log_exporter.encoder import (
4343
_ProtobufEncoder,
4444
)
45-
from opentelemetry.util.re import parse_headers
45+
from opentelemetry.util.re import parse_env_headers
4646

4747

4848
_logger = logging.getLogger(__name__)
@@ -86,7 +86,7 @@ def __init__(
8686
OTEL_EXPORTER_OTLP_CERTIFICATE, True
8787
)
8888
headers_string = environ.get(OTEL_EXPORTER_OTLP_HEADERS, "")
89-
self._headers = headers or parse_headers(headers_string)
89+
self._headers = headers or parse_env_headers(headers_string)
9090
self._timeout = timeout or int(
9191
environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, DEFAULT_TIMEOUT)
9292
)

exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
Sum,
6565
)
6666
from opentelemetry.sdk.resources import Resource as SDKResource
67-
from opentelemetry.util.re import parse_headers
67+
from opentelemetry.util.re import parse_env_headers
6868

6969
import backoff
7070
import requests
@@ -119,7 +119,7 @@ def __init__(
119119
OTEL_EXPORTER_OTLP_METRICS_HEADERS,
120120
environ.get(OTEL_EXPORTER_OTLP_HEADERS, ""),
121121
)
122-
self._headers = headers or parse_headers(headers_string)
122+
self._headers = headers or parse_env_headers(headers_string)
123123
self._timeout = timeout or int(
124124
environ.get(
125125
OTEL_EXPORTER_OTLP_METRICS_TIMEOUT,

exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
from opentelemetry.exporter.otlp.proto.http.trace_exporter.encoder import (
4444
_ProtobufEncoder,
4545
)
46-
from opentelemetry.util.re import parse_headers
46+
from opentelemetry.util.re import parse_env_headers
4747

4848

4949
_logger = logging.getLogger(__name__)
@@ -94,7 +94,7 @@ def __init__(
9494
OTEL_EXPORTER_OTLP_TRACES_HEADERS,
9595
environ.get(OTEL_EXPORTER_OTLP_HEADERS, ""),
9696
)
97-
self._headers = headers or parse_headers(headers_string)
97+
self._headers = headers or parse_env_headers(headers_string)
9898
self._timeout = timeout or int(
9999
environ.get(
100100
OTEL_EXPORTER_OTLP_TRACES_TIMEOUT,

exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,11 @@ def test_headers_parse_from_env(self):
220220

221221
self.assertEqual(
222222
cm.records[0].message,
223-
"Header doesn't match the format: missingValue.",
223+
(
224+
"Header format invalid! Header values in environment "
225+
"variables must be URL encoded per the OpenTelemetry "
226+
"Protocol Exporter specification: missingValue"
227+
),
224228
)
225229

226230
@patch.object(requests.Session, "post")

exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,11 @@ def test_headers_parse_from_env(self):
189189

190190
self.assertEqual(
191191
cm.records[0].message,
192-
"Header doesn't match the format: missingValue.",
192+
(
193+
"Header format invalid! Header values in environment "
194+
"variables must be URL encoded per the OpenTelemetry "
195+
"Protocol Exporter specification: missingValue"
196+
),
193197
)
194198

195199
# pylint: disable=no-self-use

opentelemetry-api/src/opentelemetry/util/re.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,34 +12,48 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
import logging
15+
from logging import getLogger
1616
from re import compile, split
1717
from typing import Dict, List, Mapping
1818
from urllib.parse import unquote
1919

20-
_logger = logging.getLogger(__name__)
20+
from deprecated import deprecated
2121

22+
_logger = getLogger(__name__)
2223

23-
# https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#specifying-headers-via-environment-variables
24+
# The following regexes reference this spec: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#specifying-headers-via-environment-variables
25+
26+
# Optional whitespace
2427
_OWS = r"[ \t]*"
25-
# A key contains one or more US-ASCII character except CTLs or separators.
28+
# A key contains printable US-ASCII characters except: SP and "(),/:;<=>?@[\]{}
2629
_KEY_FORMAT = (
2730
r"[\x21\x23-\x27\x2a\x2b\x2d\x2e\x30-\x39\x41-\x5a\x5e-\x7a\x7c\x7e]+"
2831
)
29-
# A value contains a URL encoded UTF-8 string.
32+
# A value contains a URL-encoded UTF-8 string. The encoded form can contain any
33+
# printable US-ASCII characters (0x20-0x7f) other than SP, DEL, and ",;/
3034
_VALUE_FORMAT = r"[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*"
35+
# A key-value is key=value, with optional whitespace surrounding key and value
3136
_KEY_VALUE_FORMAT = rf"{_OWS}{_KEY_FORMAT}{_OWS}={_OWS}{_VALUE_FORMAT}{_OWS}"
37+
3238
_HEADER_PATTERN = compile(_KEY_VALUE_FORMAT)
3339
_DELIMITER_PATTERN = compile(r"[ \t]*,[ \t]*")
3440

3541
_BAGGAGE_PROPERTY_FORMAT = rf"{_KEY_VALUE_FORMAT}|{_OWS}{_KEY_FORMAT}{_OWS}"
3642

3743

3844
# pylint: disable=invalid-name
45+
46+
47+
@deprecated(version="1.15.0", reason="You should use parse_env_headers") # type: ignore
3948
def parse_headers(s: str) -> Mapping[str, str]:
49+
return parse_env_headers(s)
50+
51+
52+
def parse_env_headers(s: str) -> Mapping[str, str]:
4053
"""
41-
Parse ``s`` (a ``str`` instance containing HTTP headers). Uses W3C Baggage
42-
HTTP header format https://www.w3.org/TR/baggage/#baggage-http-header-format, except that
54+
Parse ``s``, which is a ``str`` instance containing HTTP headers encoded
55+
for use in ENV variables per the W3C Baggage HTTP header format at
56+
https://www.w3.org/TR/baggage/#baggage-http-header-format, except that
4357
additional semi-colon delimited metadata is not supported.
4458
"""
4559
headers: Dict[str, str] = {}
@@ -49,7 +63,11 @@ def parse_headers(s: str) -> Mapping[str, str]:
4963
continue
5064
match = _HEADER_PATTERN.fullmatch(header.strip())
5165
if not match:
52-
_logger.warning("Header doesn't match the format: %s.", header)
66+
_logger.warning(
67+
"Header format invalid! Header values in environment variables must be "
68+
"URL encoded per the OpenTelemetry Protocol Exporter specification: %s",
69+
header,
70+
)
5371
continue
5472
# value may contain any number of `=`
5573
name, value = match.string.split("=", 1)

opentelemetry-api/tests/util/test_re.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616

1717
import unittest
1818

19-
from opentelemetry.util.re import parse_headers
19+
from opentelemetry.util.re import parse_env_headers
2020

2121

2222
class TestParseHeaders(unittest.TestCase):
23-
def test_parse_headers(self):
23+
def test_parse_env_headers(self):
2424
inp = [
2525
# invalid header name
2626
("=value", [], True),
@@ -63,10 +63,12 @@ def test_parse_headers(self):
6363
s, expected, warn = case
6464
if warn:
6565
with self.assertLogs(level="WARNING") as cm:
66-
self.assertEqual(parse_headers(s), dict(expected))
66+
self.assertEqual(parse_env_headers(s), dict(expected))
6767
self.assertTrue(
68-
"Header doesn't match the format:"
68+
"Header format invalid! Header values in environment "
69+
"variables must be URL encoded per the OpenTelemetry "
70+
"Protocol Exporter specification:"
6971
in cm.records[0].message,
7072
)
7173
else:
72-
self.assertEqual(parse_headers(s), dict(expected))
74+
self.assertEqual(parse_env_headers(s), dict(expected))

0 commit comments

Comments
 (0)