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/5351.added
Original file line number Diff line number Diff line change
@@ -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` logger so SDK internal diagnostics respect the configured severity.
42 changes: 41 additions & 1 deletion opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,43 @@
from opentelemetry.sdk._configuration._tracer_provider import (
configure_tracer_provider,
)
from opentelemetry.sdk._configuration.models import OpenTelemetryConfiguration
from opentelemetry.sdk._configuration.models import (
OpenTelemetryConfiguration,
SeverityNumber,
)

_logger = logging.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.
Expand All @@ -39,6 +72,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
Expand All @@ -51,6 +87,10 @@ 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."
Expand Down
82 changes: 82 additions & 0 deletions opentelemetry-sdk/tests/_configuration/test_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.sdk._configuration._sdk import configure_sdk
from opentelemetry.sdk._configuration.models import (
OpenTelemetryConfiguration,
SeverityNumber,
)
from opentelemetry.sdk._configuration.models import (
Propagator as PropagatorConfig,
Expand Down Expand Up @@ -122,6 +124,86 @@ def test_absent_sections_pass_none(
self.assertEqual(mock_propagator.call_args.args[0], None)


class TestConfigureSdkLogLevel(unittest.TestCase):
def setUp(self):
# 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(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")
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.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:
with self.subTest(severity=severity):
configure_sdk(_config(log_level=severity))
self.assertEqual(
logging.getLogger("opentelemetry").level,
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."""

Expand Down
Loading