Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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: dict[str, Any]` field — stores the captured values

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: dict[str, Any] field

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: dict[str, Any] = field(default_factory=dict)
Comment thread
MikeGoldsmith marked this conversation as resolved.
Outdated
{%- 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 the ``additional_properties`` dict field
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,13 +1,17 @@
# generated by datamodel-codegen:
# filename: schema.json
# timestamp: 2026-04-13T15:12:07+00:00
# timestamp: 2026-04-20T15:24:40+00:00

from __future__ import annotations

from dataclasses import dataclass
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, 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: dict[str, Any] = field(default_factory=dict)


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: dict[str, Any] = field(default_factory=dict)


@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: dict[str, Any] = field(default_factory=dict)


@_additional_properties_support
@dataclass
class MetricProducer:
opencensus: OpenCensusMetricProducer | None = None
additional_properties: dict[str, Any] = field(default_factory=dict)


@_additional_properties_support
@dataclass
class PullMetricExporter:
prometheus_development: ExperimentalPrometheusMetricExporter | None = None
additional_properties: dict[str, Any] = field(default_factory=dict)


@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: dict[str, Any] = field(default_factory=dict)


@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: dict[str, Any] = field(default_factory=dict)


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


@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: dict[str, Any] = field(default_factory=dict)


@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: dict[str, Any] = field(default_factory=dict)


@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: dict[str, Any] = field(default_factory=dict)


@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: dict[str, Any] = field(default_factory=dict)


@dataclass
Expand Down
Loading
Loading