Skip to content

Commit 80ca78e

Browse files
fix(config): allow deflate for OTLP HTTP exporters
Map declarative OTLP compression values through a shared helper that recognizes Deflate when the exporter enum supports it, while leaving gRPC validation unchanged. Add regression coverage for the shared mapping helper plus tracer and meter provider HTTP exporter construction.
1 parent e905594 commit 80ca78e

File tree

7 files changed

+144
-32
lines changed

7 files changed

+144
-32
lines changed

CHANGELOG.md

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

1313
## Unreleased
1414

15+
- `opentelemetry-sdk`: Allow declarative OTLP HTTP exporters to map `compression: deflate` instead of rejecting it as unsupported
16+
1517
## Version 1.41.0/0.62b0 (2026-04-09)
1618

1719
- `opentelemetry-sdk`: Add `host` resource detector support to declarative file configuration via `detection_development.detectors[].host`

opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_common.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import logging
1818
from typing import Optional
1919

20+
from opentelemetry.sdk._configuration._exceptions import ConfigurationError
21+
2022
_logger = logging.getLogger(__name__)
2123

2224

@@ -47,3 +49,24 @@ def _parse_headers(
4749
for pair in headers:
4850
result[pair.name] = pair.value or ""
4951
return result
52+
53+
54+
def _map_compression(
55+
value: Optional[str], compression_enum: type
56+
) -> Optional[object]:
57+
"""Map a compression string to the given Compression enum value."""
58+
if value is None or value.lower() == "none":
59+
return None
60+
if value.lower() == "gzip":
61+
return compression_enum.Gzip # type: ignore[attr-defined]
62+
if value.lower() == "deflate" and hasattr(compression_enum, "Deflate"):
63+
return compression_enum.Deflate # type: ignore[attr-defined]
64+
65+
supported_values = ["'gzip'", "'none'"]
66+
if hasattr(compression_enum, "Deflate"):
67+
supported_values.insert(1, "'deflate'")
68+
69+
raise ConfigurationError(
70+
f"Unsupported compression value '{value}'. Supported values: "
71+
f"{', '.join(supported_values)}."
72+
)

opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_meter_provider.py

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
from typing import Optional, Set, Type
1919

2020
from opentelemetry import metrics
21-
from opentelemetry.sdk._configuration._common import _parse_headers
21+
from opentelemetry.sdk._configuration._common import (
22+
_map_compression,
23+
_parse_headers,
24+
)
2225
from opentelemetry.sdk._configuration._exceptions import ConfigurationError
2326
from opentelemetry.sdk._configuration.models import (
2427
Aggregation as AggregationConfig,
@@ -265,19 +268,6 @@ def _create_console_metric_exporter(
265268
)
266269

267270

268-
def _map_compression_metric(
269-
value: Optional[str], compression_enum: type
270-
) -> Optional[object]:
271-
"""Map a compression string to the given Compression enum value."""
272-
if value is None or value.lower() == "none":
273-
return None
274-
if value.lower() == "gzip":
275-
return compression_enum.Gzip # type: ignore[attr-defined]
276-
raise ConfigurationError(
277-
f"Unsupported compression value '{value}'. Supported values: 'gzip', 'none'."
278-
)
279-
280-
281271
def _create_otlp_http_metric_exporter(
282272
config: OtlpHttpMetricExporterConfig,
283273
) -> MetricExporter:
@@ -296,7 +286,7 @@ def _create_otlp_http_metric_exporter(
296286
"Install it with: pip install opentelemetry-exporter-otlp-proto-http"
297287
) from exc
298288

299-
compression = _map_compression_metric(config.compression, Compression)
289+
compression = _map_compression(config.compression, Compression)
300290
headers = _parse_headers(config.headers, config.headers_list)
301291
timeout = (config.timeout / 1000.0) if config.timeout is not None else None
302292
preferred_temporality = _map_temporality(config.temporality_preference)
@@ -331,7 +321,7 @@ def _create_otlp_grpc_metric_exporter(
331321
"Install it with: pip install opentelemetry-exporter-otlp-proto-grpc"
332322
) from exc
333323

334-
compression = _map_compression_metric(config.compression, grpc.Compression)
324+
compression = _map_compression(config.compression, grpc.Compression)
335325
headers = _parse_headers(config.headers, config.headers_list)
336326
timeout = (config.timeout / 1000.0) if config.timeout is not None else None
337327
preferred_temporality = _map_temporality(config.temporality_preference)

opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_tracer_provider.py

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
from typing import Optional
1919

2020
from opentelemetry import trace
21-
from opentelemetry.sdk._configuration._common import _parse_headers
21+
from opentelemetry.sdk._configuration._common import (
22+
_map_compression,
23+
_parse_headers,
24+
)
2225
from opentelemetry.sdk._configuration._exceptions import ConfigurationError
2326
from opentelemetry.sdk._configuration.models import (
2427
OtlpGrpcExporter as OtlpGrpcExporterConfig,
@@ -103,20 +106,6 @@ def _create_otlp_http_span_exporter(
103106
compression=compression, # type: ignore[arg-type]
104107
)
105108

106-
107-
def _map_compression(
108-
value: Optional[str], compression_enum: type
109-
) -> Optional[object]:
110-
"""Map a compression string to the given Compression enum value."""
111-
if value is None or value.lower() == "none":
112-
return None
113-
if value.lower() == "gzip":
114-
return compression_enum.Gzip # type: ignore[attr-defined]
115-
raise ConfigurationError(
116-
f"Unsupported compression value '{value}'. Supported values: 'gzip', 'none'."
117-
)
118-
119-
120109
def _create_otlp_grpc_span_exporter(
121110
config: OtlpGrpcExporterConfig,
122111
) -> SpanExporter:

opentelemetry-sdk/tests/_configuration/test_common.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
import unittest
1616
from types import SimpleNamespace
1717

18-
from opentelemetry.sdk._configuration._common import _parse_headers
18+
from opentelemetry.sdk._configuration._common import (
19+
_map_compression,
20+
_parse_headers,
21+
)
22+
from opentelemetry.sdk._configuration._exceptions import ConfigurationError
1923

2024

2125
class TestParseHeaders(unittest.TestCase):
@@ -79,3 +83,53 @@ def test_struct_headers_override_headers_list(self):
7983

8084
def test_both_empty_struct_and_none_list_returns_empty_dict(self):
8185
self.assertEqual(_parse_headers([], None), {})
86+
87+
88+
class _CompressionWithDeflate:
89+
Gzip = "gzip"
90+
Deflate = "deflate"
91+
92+
93+
class _CompressionWithoutDeflate:
94+
Gzip = "gzip"
95+
96+
97+
class TestMapCompression(unittest.TestCase):
98+
def test_none_returns_none(self):
99+
self.assertIsNone(_map_compression(None, _CompressionWithDeflate))
100+
101+
def test_none_string_returns_none(self):
102+
self.assertIsNone(
103+
_map_compression("none", _CompressionWithDeflate)
104+
)
105+
106+
def test_gzip_maps_to_gzip(self):
107+
self.assertEqual(
108+
_map_compression("gzip", _CompressionWithDeflate), "gzip"
109+
)
110+
111+
def test_deflate_maps_when_supported(self):
112+
self.assertEqual(
113+
_map_compression("deflate", _CompressionWithDeflate),
114+
"deflate",
115+
)
116+
117+
def test_deflate_raises_when_unsupported(self):
118+
with self.assertRaises(ConfigurationError) as ctx:
119+
_map_compression("deflate", _CompressionWithoutDeflate)
120+
121+
self.assertEqual(
122+
str(ctx.exception),
123+
"Unsupported compression value 'deflate'. Supported values: "
124+
"'gzip', 'none'.",
125+
)
126+
127+
def test_http_error_message_includes_deflate(self):
128+
with self.assertRaises(ConfigurationError) as ctx:
129+
_map_compression("brotli", _CompressionWithDeflate)
130+
131+
self.assertEqual(
132+
str(ctx.exception),
133+
"Unsupported compression value 'brotli'. Supported values: "
134+
"'gzip', 'deflate', 'none'.",
135+
)

opentelemetry-sdk/tests/_configuration/test_meter_provider.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,34 @@ def test_otlp_http_created_with_endpoint(self):
274274
self.assertIsNone(kwargs["timeout"])
275275
self.assertIsNone(kwargs["compression"])
276276

277+
def test_otlp_http_created_with_deflate_compression(self):
278+
mock_exporter_cls = MagicMock()
279+
mock_compression_cls = MagicMock()
280+
mock_compression_cls.Deflate = "deflate_val"
281+
mock_http_module = MagicMock()
282+
mock_http_module.Compression = mock_compression_cls
283+
mock_module = MagicMock()
284+
mock_module.OTLPMetricExporter = mock_exporter_cls
285+
286+
with patch.dict(
287+
sys.modules,
288+
{
289+
"opentelemetry.exporter.otlp.proto.http.metric_exporter": mock_module,
290+
"opentelemetry.exporter.otlp.proto.http": mock_http_module,
291+
},
292+
):
293+
config = self._make_periodic_config(
294+
PushMetricExporterConfig(
295+
otlp_http=OtlpHttpMetricExporterConfig(
296+
compression="deflate"
297+
)
298+
)
299+
)
300+
create_meter_provider(config)
301+
302+
_, kwargs = mock_exporter_cls.call_args
303+
self.assertEqual(kwargs["compression"], "deflate_val")
304+
277305
def test_otlp_grpc_missing_package_raises(self):
278306
config = self._make_periodic_config(
279307
PushMetricExporterConfig(otlp_grpc=OtlpGrpcMetricExporterConfig())

opentelemetry-sdk/tests/_configuration/test_tracer_provider.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,32 @@ def test_otlp_http_created_with_endpoint(self):
309309
compression=None,
310310
)
311311

312+
def test_otlp_http_created_with_deflate_compression(self):
313+
mock_exporter_cls = MagicMock()
314+
mock_compression_cls = MagicMock()
315+
mock_compression_cls.Deflate = "deflate_val"
316+
mock_module = MagicMock()
317+
mock_module.OTLPSpanExporter = mock_exporter_cls
318+
mock_http_module = MagicMock()
319+
mock_http_module.Compression = mock_compression_cls
320+
321+
with patch.dict(
322+
sys.modules,
323+
{
324+
"opentelemetry.exporter.otlp.proto.http.trace_exporter": mock_module,
325+
"opentelemetry.exporter.otlp.proto.http": mock_http_module,
326+
},
327+
):
328+
config = self._make_batch_config(
329+
SpanExporterConfig(
330+
otlp_http=OtlpHttpExporterConfig(compression="deflate")
331+
)
332+
)
333+
create_tracer_provider(config)
334+
335+
_, kwargs = mock_exporter_cls.call_args
336+
self.assertEqual(kwargs["compression"], "deflate_val")
337+
312338
def test_otlp_http_headers_list(self):
313339
mock_exporter_cls = MagicMock()
314340
mock_http_module = MagicMock()

0 commit comments

Comments
 (0)