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..0c9bc99d51 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/configuration/__init__.py @@ -0,0 +1,61 @@ +# 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 __future__ import annotations + +import os + +from opentelemetry.sdk._configuration._exceptions import ConfigurationError +from opentelemetry.sdk._configuration._sdk import configure_sdk +from opentelemetry.sdk._configuration.models import OpenTelemetryConfiguration + + +def load_config_file( + file_path: str | os.PathLike[str], +) -> OpenTelemetryConfiguration: + """Load and parse an OpenTelemetry configuration file. + + Thin wrapper that defers importing the file loader until first call so + the optional ``[file-configuration]`` extras (``pyyaml``, ``jsonschema``) + are not required just to import this module. See + :func:`opentelemetry.sdk._configuration.file._loader.load_config_file` + for the full behaviour and error contract. + """ + # pylint: disable=import-outside-toplevel + from opentelemetry.sdk._configuration.file._loader import ( # noqa: PLC0415 + load_config_file as _load_config_file, + ) + + return _load_config_file(file_path) + + +__all__ = [ + "ConfigurationError", + "OpenTelemetryConfiguration", + "configure_sdk", + "load_config_file", +] 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..6dbd26662f --- /dev/null +++ b/opentelemetry-sdk/tests/_configuration/test_public_namespace.py @@ -0,0 +1,66 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=protected-access + +import os +import tempfile +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.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 for the eager binds, + # not copies. + self.assertIs(configuration.ConfigurationError, ConfigurationError) + self.assertIs( + configuration.OpenTelemetryConfiguration, + OpenTelemetryConfiguration, + ) + self.assertIs(configuration.configure_sdk, configure_sdk) + + def test_load_config_file_delegates_to_loader(self): + # ``load_config_file`` is a thin wrapper that defers importing the + # file loader until first call (so the optional ``[file-configuration]`` + # extras stay optional). Behaviourally it must round-trip a minimal + # valid file to an ``OpenTelemetryConfiguration``. + with tempfile.NamedTemporaryFile( + mode="w", suffix=".yaml", delete=False + ) as fh: + fh.write('file_format: "1.0"\n') + path = fh.name + try: + result = configuration.load_config_file(path) + finally: + os.unlink(path) + self.assertIsInstance(result, OpenTelemetryConfiguration) + self.assertEqual(result.file_format, "1.0") + + def test_unknown_attribute_raises(self): + with self.assertRaises(AttributeError): + # pylint: disable=no-member + _ = configuration.no_such_thing + + def test_dunder_all_is_exhaustive(self): + self.assertEqual(sorted(configuration.__all__), list(_PUBLIC_NAMES))