Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

- `opentelemetry-sdk`: Allow declarative OTLP HTTP exporters to map `compression: deflate` instead of rejecting it as unsupported

## Version 1.41.0/0.62b0 (2026-04-09)

- `opentelemetry-sdk`: Add `host` resource detector support to declarative file configuration via `detection_development.detectors[].host`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import logging
from typing import Optional

from opentelemetry.sdk._configuration._exceptions import ConfigurationError

_logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -47,3 +49,24 @@ def _parse_headers(
for pair in headers:
result[pair.name] = pair.value or ""
return result


def _map_compression(
value: Optional[str], compression_enum: type
) -> Optional[object]:
"""Map a compression string to the given Compression enum value."""
if value is None or value.lower() == "none":
return None
if value.lower() == "gzip":
return compression_enum.Gzip # type: ignore[attr-defined]
if value.lower() == "deflate" and hasattr(compression_enum, "Deflate"):
return compression_enum.Deflate # type: ignore[attr-defined]
Comment on lines +62 to +63
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict deflate mapping to OTLP HTTP exporters

The shared mapper now returns Deflate for any enum that exposes that member, and both gRPC exporter builders (_create_otlp_grpc_span_exporter and _create_otlp_grpc_metric_exporter) call this helper, so declarative otlp_grpc.compression: deflate is now accepted. That changes prior validation behavior and creates a mismatch with the gRPC exporter’s own env parsing (environ_to_compression only accepts gzip/none), so users can get different accepted values depending on configuration path.

Useful? React with 👍 / 👎.


supported_values = ["'gzip'", "'none'"]
if hasattr(compression_enum, "Deflate"):
supported_values.insert(1, "'deflate'")

raise ConfigurationError(
f"Unsupported compression value '{value}'. Supported values: "
f"{', '.join(supported_values)}."
)
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
from typing import Optional, Set, Type

from opentelemetry import metrics
from opentelemetry.sdk._configuration._common import _parse_headers
from opentelemetry.sdk._configuration._common import (
_map_compression,
_parse_headers,
)
from opentelemetry.sdk._configuration._exceptions import ConfigurationError
from opentelemetry.sdk._configuration.models import (
Aggregation as AggregationConfig,
Expand Down Expand Up @@ -265,19 +268,6 @@ def _create_console_metric_exporter(
)


def _map_compression_metric(
value: Optional[str], compression_enum: type
) -> Optional[object]:
"""Map a compression string to the given Compression enum value."""
if value is None or value.lower() == "none":
return None
if value.lower() == "gzip":
return compression_enum.Gzip # type: ignore[attr-defined]
raise ConfigurationError(
f"Unsupported compression value '{value}'. Supported values: 'gzip', 'none'."
)


def _create_otlp_http_metric_exporter(
config: OtlpHttpMetricExporterConfig,
) -> MetricExporter:
Expand All @@ -296,7 +286,7 @@ def _create_otlp_http_metric_exporter(
"Install it with: pip install opentelemetry-exporter-otlp-proto-http"
) from exc

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

compression = _map_compression_metric(config.compression, grpc.Compression)
compression = _map_compression(config.compression, grpc.Compression)
headers = _parse_headers(config.headers, config.headers_list)
timeout = (config.timeout / 1000.0) if config.timeout is not None else None
preferred_temporality = _map_temporality(config.temporality_preference)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
from typing import Optional

from opentelemetry import trace
from opentelemetry.sdk._configuration._common import _parse_headers
from opentelemetry.sdk._configuration._common import (
_map_compression,
_parse_headers,
)
from opentelemetry.sdk._configuration._exceptions import ConfigurationError
from opentelemetry.sdk._configuration.models import (
OtlpGrpcExporter as OtlpGrpcExporterConfig,
Expand Down Expand Up @@ -103,20 +106,6 @@ def _create_otlp_http_span_exporter(
compression=compression, # type: ignore[arg-type]
)


def _map_compression(
value: Optional[str], compression_enum: type
) -> Optional[object]:
"""Map a compression string to the given Compression enum value."""
if value is None or value.lower() == "none":
return None
if value.lower() == "gzip":
return compression_enum.Gzip # type: ignore[attr-defined]
raise ConfigurationError(
f"Unsupported compression value '{value}'. Supported values: 'gzip', 'none'."
)


def _create_otlp_grpc_span_exporter(
config: OtlpGrpcExporterConfig,
) -> SpanExporter:
Expand Down
56 changes: 55 additions & 1 deletion opentelemetry-sdk/tests/_configuration/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
import unittest
from types import SimpleNamespace

from opentelemetry.sdk._configuration._common import _parse_headers
from opentelemetry.sdk._configuration._common import (
_map_compression,
_parse_headers,
)
from opentelemetry.sdk._configuration._exceptions import ConfigurationError


class TestParseHeaders(unittest.TestCase):
Expand Down Expand Up @@ -79,3 +83,53 @@ def test_struct_headers_override_headers_list(self):

def test_both_empty_struct_and_none_list_returns_empty_dict(self):
self.assertEqual(_parse_headers([], None), {})


class _CompressionWithDeflate:
Gzip = "gzip"
Deflate = "deflate"


class _CompressionWithoutDeflate:
Gzip = "gzip"


class TestMapCompression(unittest.TestCase):
def test_none_returns_none(self):
self.assertIsNone(_map_compression(None, _CompressionWithDeflate))

def test_none_string_returns_none(self):
self.assertIsNone(
_map_compression("none", _CompressionWithDeflate)
)

def test_gzip_maps_to_gzip(self):
self.assertEqual(
_map_compression("gzip", _CompressionWithDeflate), "gzip"
)

def test_deflate_maps_when_supported(self):
self.assertEqual(
_map_compression("deflate", _CompressionWithDeflate),
"deflate",
)

def test_deflate_raises_when_unsupported(self):
with self.assertRaises(ConfigurationError) as ctx:
_map_compression("deflate", _CompressionWithoutDeflate)

self.assertEqual(
str(ctx.exception),
"Unsupported compression value 'deflate'. Supported values: "
"'gzip', 'none'.",
)

def test_http_error_message_includes_deflate(self):
with self.assertRaises(ConfigurationError) as ctx:
_map_compression("brotli", _CompressionWithDeflate)

self.assertEqual(
str(ctx.exception),
"Unsupported compression value 'brotli'. Supported values: "
"'gzip', 'deflate', 'none'.",
)
28 changes: 28 additions & 0 deletions opentelemetry-sdk/tests/_configuration/test_meter_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,34 @@ def test_otlp_http_created_with_endpoint(self):
self.assertIsNone(kwargs["timeout"])
self.assertIsNone(kwargs["compression"])

def test_otlp_http_created_with_deflate_compression(self):
mock_exporter_cls = MagicMock()
mock_compression_cls = MagicMock()
mock_compression_cls.Deflate = "deflate_val"
mock_http_module = MagicMock()
mock_http_module.Compression = mock_compression_cls
mock_module = MagicMock()
mock_module.OTLPMetricExporter = mock_exporter_cls

with patch.dict(
sys.modules,
{
"opentelemetry.exporter.otlp.proto.http.metric_exporter": mock_module,
"opentelemetry.exporter.otlp.proto.http": mock_http_module,
},
):
config = self._make_periodic_config(
PushMetricExporterConfig(
otlp_http=OtlpHttpMetricExporterConfig(
compression="deflate"
)
)
)
create_meter_provider(config)

_, kwargs = mock_exporter_cls.call_args
self.assertEqual(kwargs["compression"], "deflate_val")

def test_otlp_grpc_missing_package_raises(self):
config = self._make_periodic_config(
PushMetricExporterConfig(otlp_grpc=OtlpGrpcMetricExporterConfig())
Expand Down
26 changes: 26 additions & 0 deletions opentelemetry-sdk/tests/_configuration/test_tracer_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,32 @@ def test_otlp_http_created_with_endpoint(self):
compression=None,
)

def test_otlp_http_created_with_deflate_compression(self):
mock_exporter_cls = MagicMock()
mock_compression_cls = MagicMock()
mock_compression_cls.Deflate = "deflate_val"
mock_module = MagicMock()
mock_module.OTLPSpanExporter = mock_exporter_cls
mock_http_module = MagicMock()
mock_http_module.Compression = mock_compression_cls

with patch.dict(
sys.modules,
{
"opentelemetry.exporter.otlp.proto.http.trace_exporter": mock_module,
"opentelemetry.exporter.otlp.proto.http": mock_http_module,
},
):
config = self._make_batch_config(
SpanExporterConfig(
otlp_http=OtlpHttpExporterConfig(compression="deflate")
)
)
create_tracer_provider(config)

_, kwargs = mock_exporter_cls.call_args
self.assertEqual(kwargs["compression"], "deflate_val")

def test_otlp_http_headers_list(self):
mock_exporter_cls = MagicMock()
mock_http_module = MagicMock()
Expand Down