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
1 change: 1 addition & 0 deletions .changelog/5829.changed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[BREAKING] `opentelemetry-configuration`: split declarative configuration into `load_config` (Parse) and `create_sdk` (Create) per the configuration spec. `load_config_file` is renamed to `load_config` and the `configure_*_provider`/`configure_propagator` functions are replaced by `create_*` equivalents
27 changes: 21 additions & 6 deletions opentelemetry-configuration/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,36 @@ OpenTelemetry Python SDK Declarative Configuration
This package implements the `OpenTelemetry declarative configuration
specification <https://opentelemetry.io/docs/specs/otel/configuration/>`_ for
the Python SDK. It parses a YAML or JSON configuration file (or
programmatically-constructed configuration model) into typed dataclasses and
applies the result to the global SDK providers.
programmatically-constructed configuration model) into typed dataclasses,
creates the SDK components it describes, and can install them as the global
SDK providers and propagator.

The standard activation path is the ``OTEL_CONFIG_FILE`` environment variable,
which the SDK's configurator picks up automatically. Set the variable and run
your app under ``opentelemetry-instrument``; no code change is required.

For programmatic use:
For programmatic use, configure the global SDK from a file:

.. code-block:: python

from opentelemetry.configuration import configure_sdk, load_config_file
from opentelemetry.configuration import configure_sdk, load_config

config = load_config_file("otel-config.yaml")
configure_sdk(config)
components = configure_sdk(load_config("otel-config.yaml"))

``configure_sdk`` creates every component before setting any global, so a
configuration error leaves the global SDK untouched. To create components
without touching global state, use ``load_config`` and ``create_sdk``:

.. code-block:: python

from opentelemetry.configuration import create_sdk, load_config

components = create_sdk(load_config("otel-config.yaml"))
tracer = components.tracer_provider.get_tracer(__name__)

Sections omitted from the configuration yield no-op providers (and a no-op
propagator). With ``disabled: true`` all providers are no-op, while the
configured propagator is still created.

Installation
------------
Expand Down
6 changes: 6 additions & 0 deletions opentelemetry-configuration/codegen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ Extends the default dataclass template to support `additionalProperties` from th

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

Schema types that allow exactly one property (`minProperties: 1` and `maxProperties: 1`, e.g. `Sampler`, `SpanExporter`, `Aggregation`) get the `@_exactly_one_property` decorator, which raises `ValueError` at construction unless exactly one component choice (typed field or additional property) is set.

## `one_of_template_data.py`

datamodel-codegen does not expose `minProperties`/`maxProperties` to templates. This script derives an `exactly_one_property` flag for each such schema type and writes it as `--extra-template-data` for `dataclass.jinja2`.

## Usage

Templates are applied automatically when regenerating models:
Expand Down
10 changes: 10 additions & 0 deletions opentelemetry-configuration/codegen/dataclass.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@

See TestGeneratedModelsHaveAdditionalProperties in test_common.py for
regression tests that guard against changes in these context variables.

Schema types constrained to exactly one property (minProperties: 1,
maxProperties: 1, e.g. Sampler) get @_exactly_one_property, placed above
@_additional_properties so it validates after extras are captured. The
exactly_one_property flag comes from --extra-template-data generated by
one_of_template_data.py, since datamodel-codegen does not expose
minProperties/maxProperties to templates.
-#}
{% for decorator in decorators -%}
{{ decorator }}
Expand All @@ -24,6 +31,9 @@
{%- endif %}
{%- endfor %}
{%- set has_additional = (additionalProperties is defined and additionalProperties != false) or (additionalPropertiesType is defined) %}
{%- if exactly_one_property is defined and exactly_one_property %}
@_exactly_one_property
{%- endif %}
{%- if has_additional %}
@_additional_properties
{%- endif %}
Expand Down
38 changes: 38 additions & 0 deletions opentelemetry-configuration/codegen/one_of_template_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0

"""Write datamodel-codegen ``--extra-template-data`` for one-of schema types.

The configuration schema expresses "exactly one component choice" (e.g. a
``Sampler`` is one of ``always_on``, ``always_off``, ...) as
``minProperties: 1, maxProperties: 1``. datamodel-codegen does not pass those
keywords to templates, so this script derives a per-model
``exactly_one_property`` flag from the schema for ``dataclass.jinja2`` to
consume.

Usage:
python one_of_template_data.py SCHEMA_JSON OUTPUT_JSON
"""

import json
import sys
from pathlib import Path


def one_of_template_data(schema: dict) -> dict[str, dict[str, bool]]:
"""Map each one-of ``$defs`` model name to ``{"exactly_one_property": True}``."""
return {
definition.get("title", name): {"exactly_one_property": True}
for name, definition in schema.get("$defs", {}).items()
if definition.get("minProperties") == 1 and definition.get("maxProperties") == 1
}


def main(argv: list[str]) -> None:
schema_path, output_path = argv
schema = json.loads(Path(schema_path).read_text(encoding="utf-8"))
Path(output_path).write_text(json.dumps(one_of_template_data(schema), indent=2, sort_keys=True), encoding="utf-8")


if __name__ == "__main__":
main(sys.argv[1:])
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,49 @@
"""OpenTelemetry SDK Declarative Configuration.

This package implements the OpenTelemetry declarative configuration
specification for the Python SDK. Load a YAML or JSON configuration file
(or build a configuration model programmatically) and apply it to the
process-global SDK providers.
specification for the Python SDK: parse a YAML or JSON configuration file
into a configuration model, create SDK components from a model or do both
and install the result as the process-global SDK.

The standard activation path is the ``OTEL_CONFIG_FILE`` environment
variable, which the SDK's configurator picks up automatically. For
programmatic use:
programmatic use, configure the global SDK from a file:

>>> from opentelemetry.configuration import (
... load_config_file,
... configure_sdk,
... )
>>> config = load_config_file("otel-config.yaml")
>>> configure_sdk(config)
>>> from opentelemetry.configuration import configure_sdk, load_config
>>> components = configure_sdk(load_config("otel-config.yaml"))

Or parse and create components without touching any global state:

>>> from opentelemetry.configuration import create_sdk, load_config
>>> components = create_sdk(load_config("otel-config.yaml"))
>>> tracer = components.tracer_provider.get_tracer(__name__)

Construct a configuration programmatically and apply it:
A configuration model can also be built in code:

>>> from opentelemetry.configuration import (
... OpenTelemetryConfiguration,
... configure_sdk,
... create_sdk,
... )
>>> configure_sdk(OpenTelemetryConfiguration(file_format="1.0-rc.1"))
>>> components = create_sdk(OpenTelemetryConfiguration(file_format="1.0"))

This package is **experimental**. The API surface, type names, and
behaviour may change between minor versions.
behavior may change between minor versions.
"""

from opentelemetry.configuration._exceptions import ConfigurationError
from opentelemetry.configuration._sdk import configure_sdk
from opentelemetry.configuration.file._loader import load_config_file
from opentelemetry.configuration._sdk import (
SdkComponents,
configure_sdk,
create_sdk,
)
from opentelemetry.configuration.file._loader import load_config
from opentelemetry.configuration.models import OpenTelemetryConfiguration

__all__ = [
"ConfigurationError",
"OpenTelemetryConfiguration",
"SdkComponents",
"configure_sdk",
"load_config_file",
"create_sdk",
"load_config",
]
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from __future__ import annotations

import dataclasses
import functools
import inspect
import logging
from collections.abc import Callable
Expand Down Expand Up @@ -75,6 +76,33 @@ def _init(self, **kwargs):
return cls


def _exactly_one_property(cls):
"""Decorator for dataclasses whose JSON Schema allows exactly one property.

The configuration schema models mutually exclusive component choices
(e.g. a ``Sampler`` is one of ``always_on``, ``always_off``, ...) with
``minProperties: 1, maxProperties: 1``. Wraps ``__init__`` so that
constructing such a model with zero or several choices raises
``ValueError``.
"""
original_init = cls.__init__
field_names = tuple(f.name for f in dataclasses.fields(cls))

@functools.wraps(original_init)
def _init(self, *args, **kwargs):
original_init(self, *args, **kwargs)
chosen = [name for name in field_names if getattr(self, name) is not None]
chosen.extend(getattr(self, "additional_properties", {}))
if len(chosen) != 1:
raise ValueError(
f"{cls.__name__} requires exactly one of {sorted(field_names)} "
f"or a plugin component; got {len(chosen)}: {chosen}"
)

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 Expand Up @@ -127,9 +155,8 @@ def _resolve_component(
for plugin components found in ``config.additional_properties``.

The JSON schema enforces exactly one component per config block
(``minProperties: 1, maxProperties: 1``). If multiple typed fields or
``additional_properties`` entries are set (e.g. when schema validation
is bypassed), the first registry match wins.
(``minProperties: 1, maxProperties: 1``), and the generated models
enforce it at construction via ``@_exactly_one_property``.

Args:
config: A dataclass with ``additional_properties`` (from the
Expand Down
Loading
Loading