From 119bd0e5db92b61ff7982f10dc37311afbaf471b Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 24 Jun 2026 19:07:52 -0600 Subject: [PATCH 1/5] feat(config): wire log_level field into configure_sdk Map the top-level `log_level` field from declarative configuration to the `opentelemetry` root logger level so SDK internal diagnostics respect the configured severity. --- .../src/opentelemetry/configuration/_sdk.py | 39 +++++++++++- opentelemetry-configuration/tests/test_sdk.py | 62 +++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/opentelemetry-configuration/src/opentelemetry/configuration/_sdk.py b/opentelemetry-configuration/src/opentelemetry/configuration/_sdk.py index f7d8c9f16ad..38237d6c105 100644 --- a/opentelemetry-configuration/src/opentelemetry/configuration/_sdk.py +++ b/opentelemetry-configuration/src/opentelemetry/configuration/_sdk.py @@ -26,10 +26,43 @@ from opentelemetry.configuration.instrumentation import ( configure_instrumentation, ) -from opentelemetry.configuration.models import OpenTelemetryConfiguration +from opentelemetry.configuration.models import ( + OpenTelemetryConfiguration, + SeverityNumber, +) _logger = getLogger(__name__) +# Maps OTel SeverityNumber groups to Python logging levels. +# The numbered variants (debug2, info3, …) are sub-levels within the same +# Python tier, so they collapse to the same level constant. +_SEVERITY_TO_LOGGING_LEVEL: dict[SeverityNumber, int] = { + SeverityNumber.trace: logging.DEBUG, + SeverityNumber.trace2: logging.DEBUG, + SeverityNumber.trace3: logging.DEBUG, + SeverityNumber.trace4: logging.DEBUG, + SeverityNumber.debug: logging.DEBUG, + SeverityNumber.debug2: logging.DEBUG, + SeverityNumber.debug3: logging.DEBUG, + SeverityNumber.debug4: logging.DEBUG, + SeverityNumber.info: logging.INFO, + SeverityNumber.info2: logging.INFO, + SeverityNumber.info3: logging.INFO, + SeverityNumber.info4: logging.INFO, + SeverityNumber.warn: logging.WARNING, + SeverityNumber.warn2: logging.WARNING, + SeverityNumber.warn3: logging.WARNING, + SeverityNumber.warn4: logging.WARNING, + SeverityNumber.error: logging.ERROR, + SeverityNumber.error2: logging.ERROR, + SeverityNumber.error3: logging.ERROR, + SeverityNumber.error4: logging.ERROR, + SeverityNumber.fatal: logging.CRITICAL, + SeverityNumber.fatal2: logging.CRITICAL, + SeverityNumber.fatal3: logging.CRITICAL, + SeverityNumber.fatal4: logging.CRITICAL, +} + def configure_sdk(config: OpenTelemetryConfiguration) -> None: """Configure the global SDK from a parsed declarative configuration. @@ -60,6 +93,10 @@ def configure_sdk(config: OpenTelemetryConfiguration) -> None: ) return + if config.log_level is not None: + level = _SEVERITY_TO_LOGGING_LEVEL[config.log_level] + logging.getLogger("opentelemetry").setLevel(level) + resource = create_resource(config.resource) configure_tracer_provider(config.tracer_provider, resource) configure_meter_provider(config.meter_provider, resource) diff --git a/opentelemetry-configuration/tests/test_sdk.py b/opentelemetry-configuration/tests/test_sdk.py index 1637eba8d7f..1bb464379e0 100644 --- a/opentelemetry-configuration/tests/test_sdk.py +++ b/opentelemetry-configuration/tests/test_sdk.py @@ -4,12 +4,14 @@ # Tests access private members of SDK classes to assert correct configuration. # pylint: disable=protected-access +import logging import unittest from unittest.mock import patch from opentelemetry.configuration._sdk import configure_sdk from opentelemetry.configuration.models import ( OpenTelemetryConfiguration, + SeverityNumber, ) from opentelemetry.configuration.models import ( Propagator as PropagatorConfig, @@ -122,6 +124,66 @@ def test_absent_sections_pass_none( self.assertEqual(mock_propagator.call_args.args[0], None) +class TestConfigureSdkLogLevel(unittest.TestCase): + def setUp(self): + # Reset the opentelemetry logger level before each test. + logging.getLogger("opentelemetry").setLevel(logging.NOTSET) + + def tearDown(self): + logging.getLogger("opentelemetry").setLevel(logging.NOTSET) + + @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_sets_opentelemetry_logger_level(self, *_mocks): + configure_sdk(_config(log_level=SeverityNumber.warn)) + self.assertEqual( + logging.getLogger("opentelemetry").level, logging.WARNING + ) + + @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_log_level_leaves_logger_unchanged(self, *_mocks): + logging.getLogger("opentelemetry").setLevel(logging.ERROR) + configure_sdk(_config()) + self.assertEqual( + logging.getLogger("opentelemetry").level, logging.ERROR + ) + + @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_severity_number_variants_map_correctly(self, *_mocks): + cases = [ + (SeverityNumber.trace, logging.DEBUG), + (SeverityNumber.trace4, logging.DEBUG), + (SeverityNumber.debug, logging.DEBUG), + (SeverityNumber.debug4, logging.DEBUG), + (SeverityNumber.info, logging.INFO), + (SeverityNumber.info4, logging.INFO), + (SeverityNumber.warn, logging.WARNING), + (SeverityNumber.warn4, logging.WARNING), + (SeverityNumber.error, logging.ERROR), + (SeverityNumber.error4, logging.ERROR), + (SeverityNumber.fatal, logging.CRITICAL), + (SeverityNumber.fatal4, logging.CRITICAL), + ] + for severity, expected_level in cases: + with self.subTest(severity=severity): + configure_sdk(_config(log_level=severity)) + self.assertEqual( + logging.getLogger("opentelemetry").level, + expected_level, + ) + + class TestConfigureSdkIntegration(unittest.TestCase): """End-to-end: build a real OpenTelemetryConfiguration and apply it.""" From 615f68968f04f7bf3d0ac38fdf6241b4ac8be665 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 24 Jun 2026 19:08:34 -0600 Subject: [PATCH 2/5] chore: add changelog entry for #5351 --- .changelog/5351.added | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changelog/5351.added diff --git a/.changelog/5351.added b/.changelog/5351.added new file mode 100644 index 00000000000..22a6079640f --- /dev/null +++ b/.changelog/5351.added @@ -0,0 +1 @@ +`opentelemetry-sdk`: wire the top-level `log_level` field in declarative configuration — when set, maps the OTel `SeverityNumber` value to a Python logging level and applies it to the `opentelemetry` root logger so SDK internal diagnostics respect the configured severity. From 565f7fb7fdb42cab4221727708b4d9c55be46cda Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 26 Jun 2026 15:30:16 -0600 Subject: [PATCH 3/5] Update opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_sdk.py Co-authored-by: Mike Goldsmith --- .../src/opentelemetry/configuration/_sdk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-configuration/src/opentelemetry/configuration/_sdk.py b/opentelemetry-configuration/src/opentelemetry/configuration/_sdk.py index 38237d6c105..d1d77b3bed7 100644 --- a/opentelemetry-configuration/src/opentelemetry/configuration/_sdk.py +++ b/opentelemetry-configuration/src/opentelemetry/configuration/_sdk.py @@ -94,7 +94,7 @@ def configure_sdk(config: OpenTelemetryConfiguration) -> None: return if config.log_level is not None: - level = _SEVERITY_TO_LOGGING_LEVEL[config.log_level] + level = _SEVERITY_TO_LOGGING_LEVEL.get(config.log_level, logging.INFO) logging.getLogger("opentelemetry").setLevel(level) resource = create_resource(config.resource) From d67ecd186fe88f51fcab37e0245ce9a9924f6133 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 3 Jul 2026 18:10:25 -0600 Subject: [PATCH 4/5] fix(config): address review feedback on log_level wiring Apply log_level before the disabled early-return so SDK self-diagnostics respect the configured severity even when disabled=true, per spec (they are independent sibling fields). Cover all SeverityNumber variants (*2/*3, not just base and *4) in the mapping test, restore the original opentelemetry logger level in test teardown instead of hardcoding NOTSET, and correct the changelog wording from "root logger" to "opentelemetry logger". --- .changelog/5351.added | 2 +- .../src/opentelemetry/configuration/_sdk.py | 11 +++++--- opentelemetry-configuration/tests/test_sdk.py | 26 ++++++++++++++++--- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/.changelog/5351.added b/.changelog/5351.added index 22a6079640f..a1dc9d12dc4 100644 --- a/.changelog/5351.added +++ b/.changelog/5351.added @@ -1 +1 @@ -`opentelemetry-sdk`: wire the top-level `log_level` field in declarative configuration — when set, maps the OTel `SeverityNumber` value to a Python logging level and applies it to the `opentelemetry` root logger so SDK internal diagnostics respect the configured severity. +`opentelemetry-sdk`: wire the top-level `log_level` field in declarative configuration — when set, maps the OTel `SeverityNumber` value to a Python logging level and applies it to the `opentelemetry` logger so SDK internal diagnostics respect the configured severity. diff --git a/opentelemetry-configuration/src/opentelemetry/configuration/_sdk.py b/opentelemetry-configuration/src/opentelemetry/configuration/_sdk.py index d1d77b3bed7..fc28e656cfd 100644 --- a/opentelemetry-configuration/src/opentelemetry/configuration/_sdk.py +++ b/opentelemetry-configuration/src/opentelemetry/configuration/_sdk.py @@ -75,6 +75,9 @@ def configure_sdk(config: OpenTelemetryConfiguration) -> None: behavior. Honors the top-level ``disabled`` flag: when true, no globals are set. + The ``log_level`` field, when present, is applied to the internal + ``opentelemetry`` logger regardless of ``disabled`` — it configures + SDK self-diagnostics, not telemetry emission. Args: config: Parsed ``OpenTelemetryConfiguration`` (typically from @@ -87,16 +90,16 @@ def configure_sdk(config: OpenTelemetryConfiguration) -> None: >>> config = load_config_file("otel-config.yaml") >>> configure_sdk(config) """ + if config.log_level is not None: + level = _SEVERITY_TO_LOGGING_LEVEL.get(config.log_level, logging.INFO) + logging.getLogger("opentelemetry").setLevel(level) + if config.disabled: _logger.warning( "Declarative configuration has disabled=true; skipping SDK setup." ) return - if config.log_level is not None: - level = _SEVERITY_TO_LOGGING_LEVEL.get(config.log_level, logging.INFO) - logging.getLogger("opentelemetry").setLevel(level) - resource = create_resource(config.resource) configure_tracer_provider(config.tracer_provider, resource) configure_meter_provider(config.meter_provider, resource) diff --git a/opentelemetry-configuration/tests/test_sdk.py b/opentelemetry-configuration/tests/test_sdk.py index 1bb464379e0..b8bd52c587c 100644 --- a/opentelemetry-configuration/tests/test_sdk.py +++ b/opentelemetry-configuration/tests/test_sdk.py @@ -126,11 +126,13 @@ def test_absent_sections_pass_none( class TestConfigureSdkLogLevel(unittest.TestCase): def setUp(self): - # Reset the opentelemetry logger level before each test. - logging.getLogger("opentelemetry").setLevel(logging.NOTSET) + # Preserve whatever level was set before this test so we can + # restore it in tearDown, keeping tests isolated from each other + # and from the ambient logging configuration. + self._original_level = logging.getLogger("opentelemetry").level def tearDown(self): - logging.getLogger("opentelemetry").setLevel(logging.NOTSET) + logging.getLogger("opentelemetry").setLevel(self._original_level) @patch("opentelemetry.sdk._configuration._sdk.configure_propagator") @patch("opentelemetry.sdk._configuration._sdk.configure_logger_provider") @@ -163,16 +165,28 @@ def test_absent_log_level_leaves_logger_unchanged(self, *_mocks): def test_severity_number_variants_map_correctly(self, *_mocks): cases = [ (SeverityNumber.trace, logging.DEBUG), + (SeverityNumber.trace2, logging.DEBUG), + (SeverityNumber.trace3, logging.DEBUG), (SeverityNumber.trace4, logging.DEBUG), (SeverityNumber.debug, logging.DEBUG), + (SeverityNumber.debug2, logging.DEBUG), + (SeverityNumber.debug3, logging.DEBUG), (SeverityNumber.debug4, logging.DEBUG), (SeverityNumber.info, logging.INFO), + (SeverityNumber.info2, logging.INFO), + (SeverityNumber.info3, logging.INFO), (SeverityNumber.info4, logging.INFO), (SeverityNumber.warn, logging.WARNING), + (SeverityNumber.warn2, logging.WARNING), + (SeverityNumber.warn3, logging.WARNING), (SeverityNumber.warn4, logging.WARNING), (SeverityNumber.error, logging.ERROR), + (SeverityNumber.error2, logging.ERROR), + (SeverityNumber.error3, logging.ERROR), (SeverityNumber.error4, logging.ERROR), (SeverityNumber.fatal, logging.CRITICAL), + (SeverityNumber.fatal2, logging.CRITICAL), + (SeverityNumber.fatal3, logging.CRITICAL), (SeverityNumber.fatal4, logging.CRITICAL), ] for severity, expected_level in cases: @@ -183,6 +197,12 @@ def test_severity_number_variants_map_correctly(self, *_mocks): expected_level, ) + def test_log_level_applies_even_when_disabled(self): + configure_sdk(_config(disabled=True, log_level=SeverityNumber.error)) + self.assertEqual( + logging.getLogger("opentelemetry").level, logging.ERROR + ) + class TestConfigureSdkIntegration(unittest.TestCase): """End-to-end: build a real OpenTelemetryConfiguration and apply it.""" From f0f6660ea19f9091bea877121e84dfa4950c1f6c Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 15 Jul 2026 18:20:38 -0500 Subject: [PATCH 5/5] fix(config): import logging levels and correct test patch paths The log_level wiring referenced logging.DEBUG/getLogger while only getLogger was imported, so the module raised NameError at import time and every configuration test, lint, and typecheck job failed. Import the level constants directly and repoint the new log_level tests from the stale opentelemetry.sdk._configuration._sdk path to the current opentelemetry.configuration._sdk module. --- .../src/opentelemetry/configuration/_sdk.py | 54 +++++++++---------- opentelemetry-configuration/tests/test_sdk.py | 30 +++++------ 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/opentelemetry-configuration/src/opentelemetry/configuration/_sdk.py b/opentelemetry-configuration/src/opentelemetry/configuration/_sdk.py index fc28e656cfd..80934a72bc6 100644 --- a/opentelemetry-configuration/src/opentelemetry/configuration/_sdk.py +++ b/opentelemetry-configuration/src/opentelemetry/configuration/_sdk.py @@ -10,7 +10,7 @@ from __future__ import annotations -from logging import getLogger +from logging import CRITICAL, DEBUG, ERROR, INFO, WARNING, getLogger from opentelemetry.configuration._logger_provider import ( configure_logger_provider, @@ -37,30 +37,30 @@ # The numbered variants (debug2, info3, …) are sub-levels within the same # Python tier, so they collapse to the same level constant. _SEVERITY_TO_LOGGING_LEVEL: dict[SeverityNumber, int] = { - SeverityNumber.trace: logging.DEBUG, - SeverityNumber.trace2: logging.DEBUG, - SeverityNumber.trace3: logging.DEBUG, - SeverityNumber.trace4: logging.DEBUG, - SeverityNumber.debug: logging.DEBUG, - SeverityNumber.debug2: logging.DEBUG, - SeverityNumber.debug3: logging.DEBUG, - SeverityNumber.debug4: logging.DEBUG, - SeverityNumber.info: logging.INFO, - SeverityNumber.info2: logging.INFO, - SeverityNumber.info3: logging.INFO, - SeverityNumber.info4: logging.INFO, - SeverityNumber.warn: logging.WARNING, - SeverityNumber.warn2: logging.WARNING, - SeverityNumber.warn3: logging.WARNING, - SeverityNumber.warn4: logging.WARNING, - SeverityNumber.error: logging.ERROR, - SeverityNumber.error2: logging.ERROR, - SeverityNumber.error3: logging.ERROR, - SeverityNumber.error4: logging.ERROR, - SeverityNumber.fatal: logging.CRITICAL, - SeverityNumber.fatal2: logging.CRITICAL, - SeverityNumber.fatal3: logging.CRITICAL, - SeverityNumber.fatal4: logging.CRITICAL, + SeverityNumber.trace: DEBUG, + SeverityNumber.trace2: DEBUG, + SeverityNumber.trace3: DEBUG, + SeverityNumber.trace4: DEBUG, + SeverityNumber.debug: DEBUG, + SeverityNumber.debug2: DEBUG, + SeverityNumber.debug3: DEBUG, + SeverityNumber.debug4: DEBUG, + SeverityNumber.info: INFO, + SeverityNumber.info2: INFO, + SeverityNumber.info3: INFO, + SeverityNumber.info4: INFO, + SeverityNumber.warn: WARNING, + SeverityNumber.warn2: WARNING, + SeverityNumber.warn3: WARNING, + SeverityNumber.warn4: WARNING, + SeverityNumber.error: ERROR, + SeverityNumber.error2: ERROR, + SeverityNumber.error3: ERROR, + SeverityNumber.error4: ERROR, + SeverityNumber.fatal: CRITICAL, + SeverityNumber.fatal2: CRITICAL, + SeverityNumber.fatal3: CRITICAL, + SeverityNumber.fatal4: CRITICAL, } @@ -91,8 +91,8 @@ def configure_sdk(config: OpenTelemetryConfiguration) -> None: >>> configure_sdk(config) """ if config.log_level is not None: - level = _SEVERITY_TO_LOGGING_LEVEL.get(config.log_level, logging.INFO) - logging.getLogger("opentelemetry").setLevel(level) + level = _SEVERITY_TO_LOGGING_LEVEL.get(config.log_level, INFO) + getLogger("opentelemetry").setLevel(level) if config.disabled: _logger.warning( diff --git a/opentelemetry-configuration/tests/test_sdk.py b/opentelemetry-configuration/tests/test_sdk.py index b8bd52c587c..58ac1b1f2b4 100644 --- a/opentelemetry-configuration/tests/test_sdk.py +++ b/opentelemetry-configuration/tests/test_sdk.py @@ -134,22 +134,22 @@ def setUp(self): def tearDown(self): logging.getLogger("opentelemetry").setLevel(self._original_level) - @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") + @patch("opentelemetry.configuration._sdk.configure_propagator") + @patch("opentelemetry.configuration._sdk.configure_logger_provider") + @patch("opentelemetry.configuration._sdk.configure_meter_provider") + @patch("opentelemetry.configuration._sdk.configure_tracer_provider") + @patch("opentelemetry.configuration._sdk.create_resource") def test_sets_opentelemetry_logger_level(self, *_mocks): configure_sdk(_config(log_level=SeverityNumber.warn)) self.assertEqual( logging.getLogger("opentelemetry").level, logging.WARNING ) - @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") + @patch("opentelemetry.configuration._sdk.configure_propagator") + @patch("opentelemetry.configuration._sdk.configure_logger_provider") + @patch("opentelemetry.configuration._sdk.configure_meter_provider") + @patch("opentelemetry.configuration._sdk.configure_tracer_provider") + @patch("opentelemetry.configuration._sdk.create_resource") def test_absent_log_level_leaves_logger_unchanged(self, *_mocks): logging.getLogger("opentelemetry").setLevel(logging.ERROR) configure_sdk(_config()) @@ -157,11 +157,11 @@ def test_absent_log_level_leaves_logger_unchanged(self, *_mocks): logging.getLogger("opentelemetry").level, logging.ERROR ) - @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") + @patch("opentelemetry.configuration._sdk.configure_propagator") + @patch("opentelemetry.configuration._sdk.configure_logger_provider") + @patch("opentelemetry.configuration._sdk.configure_meter_provider") + @patch("opentelemetry.configuration._sdk.configure_tracer_provider") + @patch("opentelemetry.configuration._sdk.create_resource") def test_severity_number_variants_map_correctly(self, *_mocks): cases = [ (SeverityNumber.trace, logging.DEBUG),