diff --git a/.changelog/5276.added b/.changelog/5276.added new file mode 100644 index 0000000000..5be7ec4e63 --- /dev/null +++ b/.changelog/5276.added @@ -0,0 +1 @@ +`opentelemetry-sdk`: add public `opentelemetry.sdk.configuration` module that re-exports `configure_sdk`, `load_config_file`, `OpenTelemetryConfiguration`, and `ConfigurationError`. `load_config_file` is resolved lazily so the file-configuration extras (pyyaml, jsonschema) remain optional for callers that build an `OpenTelemetryConfiguration` programmatically. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/configuration/__init__.py new file mode 100644 index 0000000000..59fc63e659 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/configuration/__init__.py @@ -0,0 +1,56 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +"""Public API for the OpenTelemetry SDK's declarative configuration. + +Load a parsed configuration from a YAML/JSON file and apply it to the +process-global SDK providers: + +>>> from opentelemetry.sdk.configuration import ( +... load_config_file, configure_sdk, +... ) +>>> config = load_config_file("otel-config.yaml") +>>> configure_sdk(config) + +Construct a configuration programmatically and apply it: + +>>> from opentelemetry.sdk.configuration import ( +... OpenTelemetryConfiguration, configure_sdk, +... ) +>>> configure_sdk(OpenTelemetryConfiguration(file_format="1.0-rc.1")) + +Loading from a file requires the optional ``[file-configuration]`` extras +(``pyyaml`` and ``jsonschema``). ``configure_sdk`` itself has no extra +dependencies — callers that construct an ``OpenTelemetryConfiguration`` +directly can use it without installing the extras. +""" + +from opentelemetry.sdk._configuration._exceptions import ConfigurationError +from opentelemetry.sdk._configuration._sdk import configure_sdk +from opentelemetry.sdk._configuration.models import OpenTelemetryConfiguration + + +def __getattr__(name: str): + # ``load_config_file`` lives behind the optional file-configuration + # extras (pyyaml, jsonschema). Resolve it lazily so importing this + # module does not require those extras for callers that only use + # ``configure_sdk`` with a programmatically built configuration. + if name == "load_config_file": + # pylint: disable=import-outside-toplevel + from opentelemetry.sdk._configuration.file._loader import ( # noqa: PLC0415 + load_config_file, + ) + + return load_config_file + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +# ``load_config_file`` is exposed via ``__getattr__`` rather than a module-level +# binding so the file-configuration extras stay optional. Pylint's static +# analysis doesn't see ``__getattr__`` and flags it as undefined; suppress. +__all__ = [ + "ConfigurationError", + "OpenTelemetryConfiguration", + "configure_sdk", + "load_config_file", # pylint: disable=undefined-all-variable +] diff --git a/opentelemetry-sdk/tests/_configuration/test_public_namespace.py b/opentelemetry-sdk/tests/_configuration/test_public_namespace.py new file mode 100644 index 0000000000..dceb60105e --- /dev/null +++ b/opentelemetry-sdk/tests/_configuration/test_public_namespace.py @@ -0,0 +1,47 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=protected-access + +import unittest + +from opentelemetry.sdk import configuration +from opentelemetry.sdk._configuration._exceptions import ConfigurationError +from opentelemetry.sdk._configuration._sdk import configure_sdk +from opentelemetry.sdk._configuration.file._loader import load_config_file +from opentelemetry.sdk._configuration.models import ( + OpenTelemetryConfiguration, +) + +_PUBLIC_NAMES = ( + "ConfigurationError", + "OpenTelemetryConfiguration", + "configure_sdk", + "load_config_file", +) + + +class TestPublicNamespace(unittest.TestCase): + def test_public_symbols_resolve(self): + for name in _PUBLIC_NAMES: + self.assertTrue( + hasattr(configuration, name), + f"{name!r} missing from public namespace", + ) + + def test_public_symbols_match_private(self): + # Public namespace re-exports the same objects, not copies. + self.assertIs(configuration.ConfigurationError, ConfigurationError) + self.assertIs( + configuration.OpenTelemetryConfiguration, + OpenTelemetryConfiguration, + ) + self.assertIs(configuration.configure_sdk, configure_sdk) + self.assertIs(configuration.load_config_file, load_config_file) + + def test_unknown_attribute_raises(self): + with self.assertRaises(AttributeError): + _ = configuration.no_such_thing + + def test_dunder_all_is_exhaustive(self): + self.assertEqual(sorted(configuration.__all__), list(_PUBLIC_NAMES))