diff --git a/CHANGELOG.md b/CHANGELOG.md index dfc9179da3..352f5c89c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Permit to override default HTTP OTLP exporters headers + ([#4634](https://github.com/open-telemetry/opentelemetry-python/pull/4634)) + ## Version 1.36.0/0.57b0 (2025-07-29) - Add missing Prometheus exporter documentation diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py index 765bc5c7f5..ee29151b72 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py @@ -118,8 +118,9 @@ def __init__( ) self._compression = compression or _compression_from_env() self._session = session or requests.Session() - self._session.headers.update(self._headers) self._session.headers.update(_OTLP_HTTP_HEADERS) + # let users override our defaults + self._session.headers.update(self._headers) if self._compression is not Compression.NoCompression: self._session.headers.update( {"Content-Encoding": self._compression.value} diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py index 3b7079f7fc..bcf4c29739 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py @@ -160,8 +160,9 @@ def __init__( ) self._compression = compression or _compression_from_env() self._session = session or requests.Session() - self._session.headers.update(self._headers) self._session.headers.update(_OTLP_HTTP_HEADERS) + # let users override our defaults + self._session.headers.update(self._headers) if self._compression is not Compression.NoCompression: self._session.headers.update( {"Content-Encoding": self._compression.value} diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py index 8ea73d4c0f..1ed0400048 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py @@ -116,8 +116,9 @@ def __init__( ) self._compression = compression or _compression_from_env() self._session = session or requests.Session() - self._session.headers.update(self._headers) self._session.headers.update(_OTLP_HTTP_HEADERS) + # let users override our defaults + self._session.headers.update(self._headers) if self._compression is not Compression.NoCompression: self._session.headers.update( {"Content-Encoding": self._compression.value} diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py index 815761397e..2453dc2dab 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py @@ -81,7 +81,7 @@ OS_ENV_CERTIFICATE = "os/env/base.crt" OS_ENV_CLIENT_CERTIFICATE = "os/env/client-cert.pem" OS_ENV_CLIENT_KEY = "os/env/client-key.pem" -OS_ENV_HEADERS = "envHeader1=val1,envHeader2=val2" +OS_ENV_HEADERS = "envHeader1=val1,envHeader2=val2,User-agent=Overridden" OS_ENV_TIMEOUT = "30" @@ -151,7 +151,7 @@ def test_constructor_default(self): OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY: "metrics/client-key.pem", OTEL_EXPORTER_OTLP_METRICS_COMPRESSION: Compression.Deflate.value, OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: "https://metrics.endpoint.env", - OTEL_EXPORTER_OTLP_METRICS_HEADERS: "metricsEnv1=val1,metricsEnv2=val2,metricEnv3===val3==", + OTEL_EXPORTER_OTLP_METRICS_HEADERS: "metricsEnv1=val1,metricsEnv2=val2,metricEnv3===val3==,User-agent=metrics-user-agent", OTEL_EXPORTER_OTLP_METRICS_TIMEOUT: "40", }, ) @@ -172,9 +172,18 @@ def test_exporter_metrics_env_take_priority(self): "metricsenv1": "val1", "metricsenv2": "val2", "metricenv3": "==val3==", + "user-agent": "metrics-user-agent", }, ) self.assertIsInstance(exporter._session, Session) + self.assertEqual( + exporter._session.headers.get("User-Agent"), + "metrics-user-agent", + ) + self.assertEqual( + exporter._session.headers.get("Content-Type"), + "application/x-protobuf", + ) @patch.dict( "os.environ", @@ -237,7 +246,12 @@ 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", + "user-agent": "Overridden", + }, ) @patch.dict( diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py index 19183029ed..269b1143a8 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py @@ -71,7 +71,7 @@ ENV_CERTIFICATE = "/etc/base.crt" ENV_CLIENT_CERTIFICATE = "/etc/client-cert.pem" ENV_CLIENT_KEY = "/etc/client-key.pem" -ENV_HEADERS = "envHeader1=val1,envHeader2=val2" +ENV_HEADERS = "envHeader1=val1,envHeader2=val2,User-agent=Overridden" ENV_TIMEOUT = "30" @@ -114,7 +114,7 @@ def test_constructor_default(self): OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY: "logs/client-key.pem", OTEL_EXPORTER_OTLP_LOGS_COMPRESSION: Compression.Deflate.value, OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "https://logs.endpoint.env", - OTEL_EXPORTER_OTLP_LOGS_HEADERS: "logsEnv1=val1,logsEnv2=val2,logsEnv3===val3==", + OTEL_EXPORTER_OTLP_LOGS_HEADERS: "logsEnv1=val1,logsEnv2=val2,logsEnv3===val3==,User-agent=LogsUserAgent", OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: "40", }, ) @@ -135,9 +135,18 @@ def test_exporter_metrics_env_take_priority(self): "logsenv1": "val1", "logsenv2": "val2", "logsenv3": "==val3==", + "user-agent": "LogsUserAgent", }, ) self.assertIsInstance(exporter._session, requests.Session) + self.assertEqual( + exporter._session.headers.get("User-Agent"), + "LogsUserAgent", + ) + self.assertEqual( + exporter._session.headers.get("Content-Type"), + "application/x-protobuf", + ) @patch.dict( "os.environ", @@ -202,7 +211,12 @@ def test_exporter_env(self): self.assertEqual(exporter._timeout, int(ENV_TIMEOUT)) self.assertIs(exporter._compression, Compression.Gzip) self.assertEqual( - exporter._headers, {"envheader1": "val1", "envheader2": "val2"} + exporter._headers, + { + "envheader1": "val1", + "envheader2": "val2", + "user-agent": "Overridden", + }, ) self.assertIsInstance(exporter._session, requests.Session) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py index 224227a7f5..f01a9efc91 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py @@ -54,7 +54,7 @@ OS_ENV_CERTIFICATE = "os/env/base.crt" OS_ENV_CLIENT_CERTIFICATE = "os/env/client-cert.pem" OS_ENV_CLIENT_KEY = "os/env/client-key.pem" -OS_ENV_HEADERS = "envHeader1=val1,envHeader2=val2" +OS_ENV_HEADERS = "envHeader1=val1,envHeader2=val2,User-agent=Overridden" OS_ENV_TIMEOUT = "30" BASIC_SPAN = _Span( "abc", @@ -108,7 +108,7 @@ def test_constructor_default(self): OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY: "traces/client-key.pem", 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,traceEnv3===val3==", + OTEL_EXPORTER_OTLP_TRACES_HEADERS: "tracesEnv1=val1,tracesEnv2=val2,traceEnv3===val3==,User-agent=TraceUserAgent", OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: "40", }, ) @@ -129,9 +129,18 @@ def test_exporter_traces_env_take_priority(self): "tracesenv1": "val1", "tracesenv2": "val2", "traceenv3": "==val3==", + "user-agent": "TraceUserAgent", }, ) self.assertIsInstance(exporter._session, requests.Session) + self.assertEqual( + exporter._session.headers.get("Content-Type"), + "application/x-protobuf", + ) + self.assertEqual( + exporter._session.headers.get("User-Agent"), + "TraceUserAgent", + ) @patch.dict( "os.environ", @@ -194,7 +203,12 @@ 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", + "user-agent": "Overridden", + }, ) @patch.dict(