opentelemetry-configuration: treat present-null config value as empty mapping#5454
opentelemetry-configuration: treat present-null config value as empty mapping#5454bourbonkk wants to merge 6 commits into
Conversation
|
|
A YAML mapping key present with an empty (null) value parses to `None`,
which the config conversion layer could not distinguish from an absent
key. Since sampler and resource-detector type dispatch selects a type via
`is not None`, writing a leaf node as `always_on:` (rather than
`always_on: {}`) produced an all-`None` config and failed with
"Unsupported sampler type in config"; detectors written as `- service:`
were silently skipped.
Per the declarative-config spec a key present with a null value on a
`dict[str, Any]`-typed node means "select this with an empty config", so
coerce such a `None` to an empty mapping in `_convert_value`. Scalar and
dataclass fields keep `None`, so an absent optional section stays unset.
|
/easycla |
2422177 to
bd1aa15
Compare
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
The issue is not only for sampler, detectors, but for every dataclass configuration that can be instantiated with an empty configuration. Let's say: ConsoleMetricExporter. I think we should also cover the dataclasses besides dict. e.g.,
meter_provider:
readers:
- periodic:
exporter:
console:
I believe this should work without requiring us to pass console: {}.
Great catch, I assumed that everything was becoming a dict. Something like the following may do but I really haven't grokked the code: |
…-telemetry#5451) Address review feedback: the empty-value issue affects not only dict-aliased nodes (sampler/detector leaves) but any dataclass config that can be instantiated with no arguments, e.g. a metric `console:` exporter written without an explicit `console: {}`. _convert_value now also coerces a present null to a defaulted dataclass instance when the annotated type is a dataclass whose fields are all optional. Dataclasses with required fields, and scalar fields, keep None so an absent optional section stays unset and no TypeError is raised. Adds regression coverage for the null-valued metric console exporter and for the required-field guard.
|
Good point, thanks — you're right that it's broader than dict-aliased nodes. I've extended the coercion so a present null is also mapped to a defaulted instance when the annotated type is a dataclass that can be built with no arguments, which covers the def _coerce_present_null(unwrapped, origin):
if origin is dict:
return {}
if (
origin is None
and isinstance(unwrapped, type)
and is_dataclass(unwrapped)
and all(
field.default is not MISSING or field.default_factory is not MISSING
for field in fields(unwrapped)
)
):
return _dict_to_dataclass({}, unwrapped)
return NoneI kept the "all fields optional" guard so a dataclass with required fields (e.g. one that would need |
…ry#5451) Adopt @emdneto's suggestion to substitute an empty mapping and let the existing dict/dataclass conversion path build it, instead of a separate coercion helper. Keep an "all fields optional" guard (_is_empty_constructible_dataclass): 21 config dataclasses have required fields (e.g. BatchSpanProcessor.exporter, PeriodicMetricReader.exporter), and coercing a present null into those via cls() would raise TypeError. Leaving them None routes to the existing "Unsupported type" ConfigurationError instead.
|
Adopted the structure — substitute I kept one guard on top of it: only coerce when the dataclass is constructible with no arguments. 21 config dataclasses have a required field — e.g. processors:
- batch:would crash instead of producing the existing clean if origin is dict or _is_empty_constructible_dataclass(unwrapped):
value = {}
else:
return NoneHappy to drop the guard if you'd prefer required-field nodes to error too, but a raw |
I don't get your point here. If I leave the batch configuration empty, it will trigger a ConfigurationError as expected. |
…emetry#5451) Replace the synthetic required-field dataclass fixture with the one real node that is schema-nullable yet has required fields: ExperimentalJaegerRemoteSampler (endpoint, initial_sampler). Unlike batch/periodic (schema type: object, so a null is rejected before conversion), jaeger_remote_development is schema type: [object, null], so jaeger_remote_development: passes validation and reaches the conversion layer. Coercing it would raise TypeError; the guard leaves it None. Adds an end-to-end loader test proving the schema accepts the null and conversion survives, alongside a sibling console: node that is still coerced to {}.
|
@emdneto I cross-checked all 21 required-field config dataclasses against the schema, and there's exactly one that is schema-nullable ( tracer_provider:
sampler:
jaeger_remote_development:passes schema validation (it IS nullable), reaches conversion, and without the guard The guard leaves it If you'd rather not carry the guard for a single experimental sampler, I'm happy to drop it and instead flag the schema inconsistency upstream (a nullable node whose object form has required properties). Let me know which you prefer. |
Address review: instead of re-implementing the tempfile write/load/unlink dance in the new test, give TestConfigLoaderEndToEnd._load an optional yaml parameter (defaulting to the class _YAML) and pass the jaeger config to it.
Description
Fixes #5451
A YAML mapping key present with an empty (null) value (e.g.
always_on:)parses to
None, which the config conversion layer could not distinguishfrom an absent key. Since sampler and resource-detector type dispatch selects
a type via
is not None, a config like:or the
parent_basedexample from the issue failed withConfigurationError: Unsupported sampler type in config: Sampler(always_on=None, ...),while
always_on: {}worked. The same root cause silently skipped detectorswritten as
- service:(vs- service: {}), as noted by @emdneto.Per the declarative-config spec, a key present with a null value on a
dict[str, Any]-typed node means "select this with an empty config", so_convert_valuenow coerces such aNoneto an empty mapping. Scalar anddataclass fields keep
None, so an absent optional section stays unset(existing
test_none_value_preservedbehavior is unchanged).Type of change
How Has This Been Tested?
uv run tox -e py312-test-opentelemetry-configuration(372 passed).Added regression tests, verified they fail without the fix (with the exact
error from the issue) and pass with it:
test_conversion.py: present-null mapping →{}, explicit{}preserved,absent key stays
None, present-null scalar staysNone.test_tracer_provider.py:{"always_on": None}and the full null-valuedparent_baseddelegate config from the issue, driven through_dict_to_dataclass→create_tracer_provider.test_resource.py: null-valued detector entry ({"service": None}) runsthe detector.
Does This PR Require a Contrib Repo Change?
Checklist:
🤖 Generated with Claude Code