Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e5d6932
recursively convert parsed dicts to typed dataclasses in loader
MikeGoldsmith Jun 3, 2026
582c37f
rename changelog fragment to PR #5269
MikeGoldsmith Jun 3, 2026
b302f93
tighten typing on conversion module
MikeGoldsmith Jun 3, 2026
3a6fd21
isolate typing.get_type_hints call to placate astroid 3.x on py3.14
MikeGoldsmith Jun 3, 2026
131378c
inline the typing.get_type_hints wrap
MikeGoldsmith Jun 3, 2026
3720621
add configure_sdk orchestrator for declarative config
MikeGoldsmith Jun 3, 2026
fd6c20a
rename changelog fragment to PR #5270
MikeGoldsmith Jun 3, 2026
b6e4702
use ExemplarFilter for enum coercion test fixture; allow 'astroid' in…
MikeGoldsmith Jun 3, 2026
83e17bd
Merge branch 'mike/config-recursive-dict-conversion' into mike/config…
MikeGoldsmith Jun 3, 2026
3fc2669
fix lint on test_sdk.py: hoist import, disable no-self-use
MikeGoldsmith Jun 3, 2026
41667ca
remove extra blank line after imports (ruff I001)
MikeGoldsmith Jun 3, 2026
70c93d9
add end-to-end loader tests covering YAML -> typed config -> factory
MikeGoldsmith Jun 5, 2026
828c54b
Merge branch 'mike/config-recursive-dict-conversion' into mike/config…
MikeGoldsmith Jun 5, 2026
e78822d
address review feedback on configure_sdk
MikeGoldsmith Jun 9, 2026
d9c330c
accept PathLike in load_config_file
MikeGoldsmith Jun 9, 2026
1b8aae0
Merge branch 'main' into mike/config-orchestrator
MikeGoldsmith Jun 9, 2026
4432c5e
Merge branch 'main' into mike/config-orchestrator
MikeGoldsmith Jun 12, 2026
d531f50
drop Union import from config conversion module
MikeGoldsmith Jun 15, 2026
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/5270.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`opentelemetry-sdk`: add `configure_sdk(config)` to the declarative configuration API. Single entry point that takes a parsed `OpenTelemetryConfiguration`, builds the resource, and applies the tracer/meter/logger providers and propagator globally. Honors the top-level `disabled` flag.
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,18 @@
import types
import typing
from collections.abc import Mapping
from typing import Any, TypeVar, Union, get_args, get_origin
from typing import Any, TypeVar, get_args, get_origin

_T = TypeVar("_T")


def _unwrap_optional(type_hint: Any) -> Any:
"""Strip ``None`` from a ``X | None`` / ``Optional[X]`` annotation.

Returns the unwrapped type, or the original hint if not a Union with None.
Returns the unwrapped type, or the original hint if not a union with None.
"""
origin = get_origin(type_hint)
if origin is Union or origin is types.UnionType:
if origin is types.UnionType or origin is typing.Union:
non_none = [t for t in get_args(type_hint) if t is not type(None)]
if len(non_none) == 1:
return non_none[0]
Expand Down
64 changes: 64 additions & 0 deletions opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_sdk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0

"""Top-level orchestrator for declarative SDK configuration.

Takes a parsed ``OpenTelemetryConfiguration`` and applies it by calling
each per-signal ``configure_*`` factory in order. This is the single
entry point for "apply this config" on the declarative path.
"""

from __future__ import annotations

import logging

from opentelemetry.sdk._configuration._logger_provider import (
configure_logger_provider,
)
from opentelemetry.sdk._configuration._meter_provider import (
configure_meter_provider,
)
from opentelemetry.sdk._configuration._propagator import configure_propagator
from opentelemetry.sdk._configuration._resource import create_resource
from opentelemetry.sdk._configuration._tracer_provider import (
configure_tracer_provider,
)
from opentelemetry.sdk._configuration.models import OpenTelemetryConfiguration

_logger = logging.getLogger(__name__)


def configure_sdk(config: OpenTelemetryConfiguration) -> None:
"""Configure the global SDK from a parsed declarative configuration.

Builds a :class:`Resource` from ``config.resource`` and applies it to
each signal provider. Sets the global tracer provider, meter provider,
logger provider, and text map propagator from their respective config
sections. Sections absent from the config (``None``) leave the
corresponding global untouched — matching the spec's "noop default"
behavior.

Honors the top-level ``disabled`` flag: when true, no globals are set.

Args:
config: Parsed ``OpenTelemetryConfiguration`` (typically from
``load_config_file``).

Example:
>>> from opentelemetry.sdk._configuration.file import (
... load_config_file, configure_sdk,
... )
>>> config = load_config_file("otel-config.yaml")
>>> configure_sdk(config)
"""
if config.disabled:
_logger.warning(
"Declarative configuration has disabled=true; skipping SDK setup."
)
return

resource = create_resource(config.resource)
configure_tracer_provider(config.tracer_provider, resource)
configure_meter_provider(config.meter_provider, resource)
configure_logger_provider(config.logger_provider, resource)
configure_propagator(config.propagator)
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
create_propagator,
)
from opentelemetry.sdk._configuration._resource import create_resource
from opentelemetry.sdk._configuration._sdk import configure_sdk
from opentelemetry.sdk._configuration._tracer_provider import (
configure_tracer_provider,
create_tracer_provider,
Expand All @@ -39,6 +40,7 @@

__all__ = [
"load_config_file",
"configure_sdk",
"substitute_env_vars",
"ConfigurationError",
"EnvSubstitutionError",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import importlib.resources
import json
import logging
import os
from pathlib import Path
from typing import Any

Expand Down Expand Up @@ -50,7 +51,9 @@ def _get_schema() -> dict:
_logger = logging.getLogger(__name__)


def load_config_file(file_path: str) -> OpenTelemetryConfiguration:
def load_config_file(
file_path: str | os.PathLike[str],
) -> OpenTelemetryConfiguration:
"""Load and parse an OpenTelemetry configuration file.

Supports YAML and JSON formats. Performs environment variable substitution
Expand Down
149 changes: 149 additions & 0 deletions opentelemetry-sdk/tests/_configuration/test_sdk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0

# Tests access private members of SDK classes to assert correct configuration.
# pylint: disable=protected-access

import unittest
from unittest.mock import patch

from opentelemetry.sdk._configuration._sdk import configure_sdk
from opentelemetry.sdk._configuration.models import (
OpenTelemetryConfiguration,
)
from opentelemetry.sdk._configuration.models import (
Propagator as PropagatorConfig,
)
from opentelemetry.sdk._configuration.models import (
Resource as ResourceConfig,
)
from opentelemetry.sdk._configuration.models import (
SimpleSpanProcessor as SimpleSpanProcessorConfig,
)
from opentelemetry.sdk._configuration.models import (
SpanExporter as SpanExporterConfig,
)
from opentelemetry.sdk._configuration.models import (
SpanProcessor as SpanProcessorConfig,
)
from opentelemetry.sdk._configuration.models import (
TracerProvider as TracerProviderConfig,
)
from opentelemetry.sdk.trace import TracerProvider as SdkTracerProvider

_MIN_CONFIG_KWARGS = {"file_format": "1.0-rc.1"}


def _config(**kwargs) -> OpenTelemetryConfiguration:
return OpenTelemetryConfiguration(**{**_MIN_CONFIG_KWARGS, **kwargs})


class TestConfigureSdk(unittest.TestCase):
@patch("opentelemetry.sdk._configuration._sdk.configure_propagator")
@patch("opentelemetry.sdk._configuration._sdk.configure_logger_provider")
@patch("opentelemetry.sdk._configuration._sdk.configure_meter_provider")
@patch("opentelemetry.sdk._configuration._sdk.configure_tracer_provider")
@patch("opentelemetry.sdk._configuration._sdk.create_resource")
# pylint: disable=no-self-use
def test_calls_each_signal_with_resource(
self,
mock_create_resource,
mock_tracer,
mock_meter,
mock_logger,
mock_propagator,
):
sentinel_resource = object()
mock_create_resource.return_value = sentinel_resource

resource_cfg = ResourceConfig()
tracer_cfg = TracerProviderConfig(processors=[])
propagator_cfg = PropagatorConfig()
config = _config(
resource=resource_cfg,
tracer_provider=tracer_cfg,
propagator=propagator_cfg,
)

configure_sdk(config)

mock_create_resource.assert_called_once_with(resource_cfg)
mock_tracer.assert_called_once_with(tracer_cfg, sentinel_resource)
mock_meter.assert_called_once_with(None, sentinel_resource)
mock_logger.assert_called_once_with(None, sentinel_resource)
mock_propagator.assert_called_once_with(propagator_cfg)

@patch("opentelemetry.sdk._configuration._sdk.configure_propagator")
@patch("opentelemetry.sdk._configuration._sdk.configure_logger_provider")
@patch("opentelemetry.sdk._configuration._sdk.configure_meter_provider")
@patch("opentelemetry.sdk._configuration._sdk.configure_tracer_provider")
@patch("opentelemetry.sdk._configuration._sdk.create_resource")
# pylint: disable=no-self-use
def test_disabled_skips_everything(
self,
mock_create_resource,
mock_tracer,
mock_meter,
mock_logger,
mock_propagator,
):
config = _config(
disabled=True,
tracer_provider=TracerProviderConfig(processors=[]),
)

configure_sdk(config)

mock_create_resource.assert_not_called()
mock_tracer.assert_not_called()
mock_meter.assert_not_called()
mock_logger.assert_not_called()
mock_propagator.assert_not_called()

@patch("opentelemetry.sdk._configuration._sdk.configure_propagator")
@patch("opentelemetry.sdk._configuration._sdk.configure_logger_provider")
@patch("opentelemetry.sdk._configuration._sdk.configure_meter_provider")
@patch("opentelemetry.sdk._configuration._sdk.configure_tracer_provider")
@patch("opentelemetry.sdk._configuration._sdk.create_resource")
def test_absent_sections_pass_none(
self,
mock_create_resource, # noqa: ARG002
mock_tracer,
mock_meter,
mock_logger,
mock_propagator,
):
configure_sdk(_config())

# Each configure_* is called exactly once, with config=None.
self.assertEqual(mock_tracer.call_args.args[0], None)
self.assertEqual(mock_meter.call_args.args[0], None)
self.assertEqual(mock_logger.call_args.args[0], None)
self.assertEqual(mock_propagator.call_args.args[0], None)


class TestConfigureSdkIntegration(unittest.TestCase):
"""End-to-end: build a real OpenTelemetryConfiguration and apply it."""

@patch(
"opentelemetry.sdk._configuration._tracer_provider.trace.set_tracer_provider"
)
def test_applies_tracer_provider_globally(self, mock_set_tracer):
config = _config(
tracer_provider=TracerProviderConfig(
processors=[
SpanProcessorConfig(
simple=SimpleSpanProcessorConfig(
exporter=SpanExporterConfig(console={})
)
)
]
)
)

configure_sdk(config)

mock_set_tracer.assert_called_once()
self.assertIsInstance(
mock_set_tracer.call_args[0][0], SdkTracerProvider
)
Loading