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`: add `additional_properties` support to generated config models via custom `datamodel-codegen` template, enabling plugin/custom component names to flow through typed dataclasses
([#5131](https://github.com/open-telemetry/opentelemetry-python/pull/5131))
- Fix incorrect code example in `create_tracer()` docstring
([#5072](https://github.com/open-telemetry/opentelemetry-python/issues/5072))
- `opentelemetry-sdk`: add `load_entry_point` shared utility to declarative file configuration for loading plugins via entry points; refactor propagator loading to use it
Expand Down
24 changes: 24 additions & 0 deletions opentelemetry-sdk/codegen/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Code Generation Templates

Custom [datamodel-code-generator](https://github.com/koxudaxi/datamodel-code-generator) templates used when generating `models.py` from the OpenTelemetry configuration JSON schema.

## `dataclass.jinja2`

Extends the default dataclass template to support `additionalProperties` from the JSON Schema. Schema types that allow additional properties (e.g. `Sampler`, `SpanExporter`, `TextMapPropagator`) get:

- `@_additional_properties_support` decorator — captures unknown constructor kwargs
- `additional_properties: ClassVar[dict[str, Any]]` annotation — satisfies type checkers without creating a dataclass field

This enables plugin/custom component names to flow through typed dataclasses without a post-processing step.

## Usage

Templates are applied automatically when regenerating models:

```sh
tox -e generate-config-from-jsonschema
```

The template directory is configured in `pyproject.toml` under `[tool.datamodel-codegen]`.

See `opentelemetry-sdk/src/opentelemetry/sdk/_configuration/README.md` for the full schema update workflow.
73 changes: 73 additions & 0 deletions opentelemetry-sdk/codegen/dataclass.jinja2
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{# Custom dataclass template for OpenTelemetry configuration models.
Extends the default datamodel-codegen dataclass template to support
JSON Schema additionalProperties. When a schema type allows additional
properties (e.g. Sampler, SpanExporter), this template adds:
- @_additional_properties_support decorator (captures unknown kwargs)
- additional_properties: ClassVar[dict[str, Any]] annotation (for type checkers)

The template checks two context variables set by datamodel-codegen:
- additionalProperties: set when the schema value is a boolean (true/false)
- additionalPropertiesType: set when the schema value is a type object
(e.g. {"type": ["object", "null"]}), which is how the OTel config
schema defines it for plugin-capable types.

See TestGeneratedModelsHaveAdditionalProperties in test_common.py for
regression tests that guard against changes in these context variables.
-#}
{% for decorator in decorators -%}
{{ decorator }}
{% endfor -%}
{%- set args = [] %}
{%- for k, v in (dataclass_arguments or {}).items() %}
{%- if v is not none and v is not false %}
{%- set _ = args.append(k ~ '=' ~ (v|pprint)) %}
{%- endif %}
{%- endfor %}
{%- set has_additional = (additionalProperties is defined and additionalProperties != false) or (additionalPropertiesType is defined) %}
{%- if has_additional %}
@_additional_properties_support
{%- endif %}
{%- if args %}
@dataclass({{ args | join(', ') }})
{%- else %}
@dataclass
{%- endif %}
{%- if base_class %}
class {{ class_name }}({{ base_class }}):
{%- else %}
class {{ class_name }}:
{%- endif %}
{%- if description %}
"""
{{ description | escape_docstring | indent(4) }}
"""
{%- endif %}
{%- if not fields and not description and not has_additional %}
pass
{%- endif %}
{%- for field in fields -%}
{%- if field.field %}
{{ field.name }}: {{ field.type_hint }} = {{ field.field }}
{%- else %}
{{ field.name }}: {{ field.type_hint }}
{%- if not (field.required or (field.represented_default == 'None' and field.strip_default_none))
%} = {{ field.represented_default }}
{%- endif -%}
{%- endif %}
{%- if field.docstring %}
"""
{{ field.docstring | escape_docstring | indent(4) }}
"""
{%- if field.use_inline_field_description and not loop.last %}

{% endif %}
{%- elif field.inline_field_docstring %}
{{ field.inline_field_docstring }}
{%- if not loop.last %}

{% endif %}
{%- endif %}
{%- endfor -%}
{%- if has_additional %}
additional_properties: ClassVar[dict[str, Any]]
{%- endif -%}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This package implements [OpenTelemetry file-based configuration](https://opentel
## Files

- `schema.json` — vendored copy of the [OpenTelemetry configuration JSON schema](https://github.com/open-telemetry/opentelemetry-configuration)
- `models.py` — Python dataclasses generated from `schema.json` by [datamodel-code-generator](https://github.com/koxudaxi/datamodel-code-generator)
- `models.py` — Python dataclasses generated from `schema.json` by [datamodel-code-generator](https://github.com/koxudaxi/datamodel-code-generator), using a custom template from `opentelemetry-sdk/codegen/` (see that directory's README for details)

## Updating the schema

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

from __future__ import annotations

import dataclasses
import inspect
import logging
from typing import Optional, Type

Expand All @@ -23,6 +25,40 @@
_logger = logging.getLogger(__name__)


def _additional_properties_support(cls):
"""Decorator for dataclasses whose JSON Schema sets additionalProperties.

Wraps the dataclass-generated ``__init__`` so that unknown keyword
arguments are captured into an ``additional_properties`` instance
attribute instead of raising ``TypeError``. This lets plugin/custom
component names flow through the config pipeline without modifying
the codegen output for known fields.

Applied automatically by the custom template in ``opentelemetry-sdk/codegen/``
when ``additionalPropertiesType`` is present in the template context
(set by ``datamodel-codegen`` for schema types with ``additionalProperties``).
"""
original_init = cls.__init__
original_sig = inspect.signature(original_init)
known_fields = frozenset(f.name for f in dataclasses.fields(cls))

def _init(self, **kwargs):
known = {k: v for k, v in kwargs.items() if k in known_fields}
extra = {k: v for k, v in kwargs.items() if k not in known_fields}
original_init(self, **known)
self.additional_properties = extra

# Preserve the original parameter list for IDE autocompletion and
# inspect.signature(), adding **kwargs to signal extras are accepted.
# setattr used because pyright rejects direct __signature__ assignment.
params = list(original_sig.parameters.values())
params.append(inspect.Parameter("kwargs", inspect.Parameter.VAR_KEYWORD))
setattr(_init, "__signature__", original_sig.replace(parameters=params)) # noqa: B010

cls.__init__ = _init
return cls


def load_entry_point(group: str, name: str) -> Type:
"""Load a plugin class from an entry point group by name.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
# generated by datamodel-codegen:
# filename: schema.json
# timestamp: 2026-04-13T15:12:07+00:00
# timestamp: 2026-04-21T09:43:57+00:00

from __future__ import annotations

from dataclasses import dataclass
from enum import Enum
from typing import Any, TypeAlias
from typing import Any, ClassVar, TypeAlias

from opentelemetry.sdk._configuration._common import (
_additional_properties_support,
)

AlwaysOffSampler: TypeAlias = dict[str, Any] | None

Expand Down Expand Up @@ -350,12 +354,14 @@ class SeverityNumber(Enum):
fatal4 = "fatal4"


@_additional_properties_support
@dataclass
class SpanExporter:
otlp_http: OtlpHttpExporter | None = None
otlp_grpc: OtlpGrpcExporter | None = None
otlp_file_development: ExperimentalOtlpFileExporter | None = None
console: ConsoleExporter | None = None
additional_properties: ClassVar[dict[str, Any]]


class SpanKind(Enum):
Expand Down Expand Up @@ -500,12 +506,14 @@ class ExperimentalPrometheusMetricExporter:
)


@_additional_properties_support
@dataclass
class ExperimentalResourceDetector:
container: ExperimentalContainerResourceDetector | None = None
host: ExperimentalHostResourceDetector | None = None
process: ExperimentalProcessResourceDetector | None = None
service: ExperimentalServiceResourceDetector | None = None
additional_properties: ClassVar[dict[str, Any]]


@dataclass
Expand All @@ -524,22 +532,28 @@ class ExperimentalTracerConfigurator:
tracers: list[ExperimentalTracerMatcherAndConfig] | None = None


@_additional_properties_support
@dataclass
class LogRecordExporter:
otlp_http: OtlpHttpExporter | None = None
otlp_grpc: OtlpGrpcExporter | None = None
otlp_file_development: ExperimentalOtlpFileExporter | None = None
console: ConsoleExporter | None = None
additional_properties: ClassVar[dict[str, Any]]


@_additional_properties_support
@dataclass
class MetricProducer:
opencensus: OpenCensusMetricProducer | None = None
additional_properties: ClassVar[dict[str, Any]]


@_additional_properties_support
@dataclass
class PullMetricExporter:
prometheus_development: ExperimentalPrometheusMetricExporter | None = None
additional_properties: ClassVar[dict[str, Any]]


@dataclass
Expand All @@ -549,12 +563,14 @@ class PullMetricReader:
cardinality_limits: CardinalityLimits | None = None


@_additional_properties_support
@dataclass
class PushMetricExporter:
otlp_http: OtlpHttpMetricExporter | None = None
otlp_grpc: OtlpGrpcMetricExporter | None = None
otlp_file_development: ExperimentalOtlpFileMetricExporter | None = None
console: ConsoleMetricExporter | None = None
additional_properties: ClassVar[dict[str, Any]]


@dataclass
Expand All @@ -567,18 +583,22 @@ class SimpleSpanProcessor:
exporter: SpanExporter


@_additional_properties_support
@dataclass
class SpanProcessor:
batch: BatchSpanProcessor | None = None
simple: SimpleSpanProcessor | None = None
additional_properties: ClassVar[dict[str, Any]]


@_additional_properties_support
@dataclass
class TextMapPropagator:
tracecontext: TraceContextPropagator | None = None
baggage: BaggagePropagator | None = None
b3: B3Propagator | None = None
b3multi: B3MultiPropagator | None = None
additional_properties: ClassVar[dict[str, Any]]


@dataclass
Expand Down Expand Up @@ -639,10 +659,12 @@ class ExperimentalResourceDetection:
detectors: list[ExperimentalResourceDetector] | None = None


@_additional_properties_support
@dataclass
class LogRecordProcessor:
batch: BatchLogRecordProcessor | None = None
simple: SimpleLogRecordProcessor | None = None
additional_properties: ClassVar[dict[str, Any]]


@dataclass
Expand Down Expand Up @@ -697,6 +719,7 @@ class MeterProvider:
meter_configurator_development: ExperimentalMeterConfigurator | None = None


@_additional_properties_support
@dataclass
class OpenTelemetryConfiguration:
file_format: str
Expand All @@ -710,6 +733,7 @@ class OpenTelemetryConfiguration:
resource: Resource | None = None
instrumentation_development: ExperimentalInstrumentation | None = None
distribution: Distribution | None = None
additional_properties: ClassVar[dict[str, Any]]


@dataclass
Expand Down Expand Up @@ -741,6 +765,7 @@ class ExperimentalComposableRuleBasedSamplerRule:
parent: list[ExperimentalSpanParent] | None = None


@_additional_properties_support
@dataclass
class ExperimentalComposableSampler:
always_off: ExperimentalComposableAlwaysOffSampler | None = None
Expand All @@ -750,6 +775,7 @@ class ExperimentalComposableSampler:
)
probability: ExperimentalComposableProbabilitySampler | None = None
rule_based: ExperimentalComposableRuleBasedSampler | None = None
additional_properties: ClassVar[dict[str, Any]]


@dataclass
Expand All @@ -768,6 +794,7 @@ class ParentBasedSampler:
local_parent_not_sampled: Sampler | None = None


@_additional_properties_support
@dataclass
class Sampler:
always_off: AlwaysOffSampler | None = None
Expand All @@ -777,6 +804,7 @@ class Sampler:
parent_based: ParentBasedSampler | None = None
probability_development: ExperimentalProbabilitySampler | None = None
trace_id_ratio_based: TraceIdRatioBasedSampler | None = None
additional_properties: ClassVar[dict[str, Any]]


@dataclass
Expand Down
Loading
Loading