Skip to content
Closed
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
4 changes: 4 additions & 0 deletions .changelog/34.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
`opentelemetry-configuration`: treat a present-but-null component value (e.g.
`console:` with no value) as "create the component with defaults" instead of
raising a `ConfigurationError`, per the declarative configuration spec's
requirement to distinguish a missing key from a present-null one
26 changes: 25 additions & 1 deletion opentelemetry-configuration/tests/test_conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from opentelemetry.configuration._common import _additional_properties
from opentelemetry.configuration._conversion import _dict_to_dataclass
from opentelemetry.configuration.models import ExemplarFilter
from opentelemetry.configuration.models import ExemplarFilter, SpanExporter
from opentelemetry.configuration.models import Sampler as SamplerConfig


Expand Down Expand Up @@ -83,6 +83,7 @@ def test_present_null_dataclass_coerced_to_empty_instance(self):
result = _dict_to_dataclass({"middle": None, "name": "test"}, _Outer)
self.assertIsInstance(result.middle, _Middle)
self.assertIsNone(result.middle.inner)
self.assertIsNone(result.middle.items)
self.assertEqual(result.name, "test")

def test_present_null_dataclass_with_required_field_stays_none(self):
Expand All @@ -94,10 +95,33 @@ def test_present_null_dataclass_with_required_field_stays_none(self):
self.assertIsNone(result.jaeger_remote_development)

def test_missing_optional_fields_default_to_none(self):
# Absent keys stay None; this is what "not configured" looks like and
# must remain distinguishable from present-null.
result = _dict_to_dataclass({}, _Outer)
self.assertIsNone(result.middle)
self.assertIsNone(result.name)

def test_present_null_mapping_alias_becomes_empty_dict(self):
# The console exporter field is typed as ``dict[str, Any] | None``.
# A present-null value must become an empty mapping so a component
# factory selecting on ``value is not None`` still fires and builds
# the console exporter with defaults.
result = _dict_to_dataclass({"console": None}, SpanExporter)
self.assertEqual(result.console, {})

def test_absent_component_stays_none(self):
# Absent component keys must remain None ("not configured").
result = _dict_to_dataclass({}, SpanExporter)
self.assertIsNone(result.console)
self.assertIsNone(result.otlp_http)

def test_populated_component_mapping_still_converts(self):
# A populated component mapping must still convert into a typed
# dataclass instance with its values carried through.
result = _dict_to_dataclass({"otlp_http": {"endpoint": "http://localhost:4318"}}, SpanExporter)
self.assertIsNone(result.console)
self.assertEqual(result.otlp_http.endpoint, "http://localhost:4318")

def test_unknown_keys_routed_to_additional_properties(self):
result = _dict_to_dataclass({"known": "yes", "my_plugin": {"opt": True}}, _WithExtras)
self.assertEqual(result.known, "yes")
Expand Down
19 changes: 19 additions & 0 deletions opentelemetry-configuration/tests/test_tracer_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,25 @@ def test_console_exporter_simple(self):
self.assertIsInstance(procs[0], SimpleSpanProcessor)
self.assertIsInstance(procs[0].span_exporter, ConsoleSpanExporter)

def test_present_null_console_exporter_builds_with_defaults(self):
# A present-but-null console exporter (``console:`` with no value in
# YAML) is carried through conversion as an empty mapping and must
# build a ConsoleSpanExporter with defaults instead of raising.
exporter_config = _dict_to_dataclass({"console": None}, SpanExporterConfig)
config = self._make_batch_config(exporter_config)
provider = create_tracer_provider(config)
procs = provider._active_span_processor._span_processors
self.assertEqual(len(procs), 1)
self.assertIsInstance(procs[0].span_exporter, ConsoleSpanExporter)

def test_absent_exporter_type_raises(self):
# An absent exporter (empty mapping, no key present) must still raise
# "no exporter type specified".
exporter_config = _dict_to_dataclass({}, SpanExporterConfig)
config = self._make_batch_config(exporter_config)
with self.assertRaises(ConfigurationError):
create_tracer_provider(config)

def test_otlp_http_missing_package_raises(self):
config = self._make_batch_config(SpanExporterConfig(otlp_http=OtlpHttpExporterConfig()))
with patch.dict(
Expand Down
Loading