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/5115.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`opentelemetry-sdk`: honour `OTEL_LOG_LEVEL` to set SDK internal log level
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@
LoggingHandler,
LogRecordProcessor,
)
from opentelemetry.sdk._logs._internal import _LoggerConfiguratorT
from opentelemetry.sdk._logs._internal import (
_configure_otel_log_level,
_LoggerConfiguratorT,
)
from opentelemetry.sdk._logs.export import (
BatchLogRecordProcessor,
LogRecordExporter,
Expand Down Expand Up @@ -558,6 +561,8 @@ def _initialize_components(
logger_configurator: _LoggerConfiguratorT | None = None,
):
# pylint: disable=too-many-locals
_configure_otel_log_level()

if trace_exporter_names is None:
trace_exporter_names = []
if metric_exporter_names is None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
from opentelemetry.sdk.environment_variables import (
OTEL_ATTRIBUTE_COUNT_LIMIT,
OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT,
OTEL_LOG_LEVEL,
OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED,
OTEL_SDK_DISABLED,
)
Expand All @@ -78,9 +79,42 @@
_DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT = 128
_ENV_VALUE_UNSET = ""

# "warn" is accepted alongside "warning" because OTel canonical short names
# use "WARN", so users following OTel documentation will naturally try "warn".
# "trace" maps to DEBUG because Python has no TRACE level and the OTel
# declarative config schema includes "trace" as a valid log level value.
_OTEL_LOG_LEVEL_TO_PYTHON = {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should also map trace to debug. The OTEL_LOG_LEVEL spec doesn't define accepted values, but the declarative config schema includes trace. Since Python doesn't have trace, mapping trace to debug would align with the config model and with the debug startup messages PR that Tammy mentioned.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call - done. "trace" now maps to DEBUG. Updated the dict, the warning message listing valid values, and the tests.

"trace": logging.DEBUG,
"debug": logging.DEBUG,
"info": logging.INFO,
"warn": logging.WARNING,
"warning": logging.WARNING,
"error": logging.ERROR,
"critical": logging.CRITICAL,
}

_logger = logging.getLogger(__name__)


def _configure_otel_log_level() -> None:
"""Apply OTEL_LOG_LEVEL to the ``opentelemetry.sdk`` logger hierarchy."""
otel_log_level_raw = environ.get(OTEL_LOG_LEVEL)
if not otel_log_level_raw:
return
otel_log_level = otel_log_level_raw.lower()
if otel_log_level in _OTEL_LOG_LEVEL_TO_PYTHON:
logging.getLogger("opentelemetry.sdk").setLevel(
_OTEL_LOG_LEVEL_TO_PYTHON[otel_log_level]
)
else:
_logger.warning(
"Invalid value for OTEL_LOG_LEVEL: %r. "
"Valid values: trace, debug, info, warn, warning, error, critical. "
"Logger level unchanged.",
otel_log_level_raw,
)


class BytesEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, bytes):
Expand Down
92 changes: 91 additions & 1 deletion opentelemetry-sdk/tests/logs/test_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

# pylint: disable=protected-access

import logging
import unittest
from unittest.mock import Mock, patch

Expand All @@ -16,14 +17,19 @@
ReadWriteLogRecord,
)
from opentelemetry.sdk._logs._internal import (
_OTEL_LOG_LEVEL_TO_PYTHON,
NoOpLogger,
SynchronousMultiLogRecordProcessor,
_configure_otel_log_level,
_disable_logger_configurator,
_LoggerConfig,
_RuleBasedLoggerConfigurator,
create_logger_metrics,
)
from opentelemetry.sdk.environment_variables import OTEL_SDK_DISABLED
from opentelemetry.sdk.environment_variables import (
OTEL_LOG_LEVEL,
OTEL_SDK_DISABLED,
)
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.util.instrumentation import (
InstrumentationScope,
Expand Down Expand Up @@ -451,3 +457,87 @@ def test_emit_readwrite_logrecord_uses_exception(self):
self.assertEqual(
attributes[exception_attributes.EXCEPTION_TYPE], "RuntimeError"
)


class TestOtelLogLevelEnvVar(unittest.TestCase):
"""Tests for OTEL_LOG_LEVEL → SDK internal logger level."""

def setUp(self):
self._sdk_logger = logging.getLogger("opentelemetry.sdk")

def tearDown(self):
self._sdk_logger.setLevel(logging.NOTSET)

def test_otel_log_level_to_python_mapping_accepted_keys(self):
expected_keys = {
"trace",
"debug",
"info",
"warn",
"warning",
"error",
"critical",
}
self.assertEqual(set(_OTEL_LOG_LEVEL_TO_PYTHON.keys()), expected_keys)

@patch.dict("os.environ", {OTEL_LOG_LEVEL: ""})
def test_unset_env_var_does_not_modify_logger_level(self):
_configure_otel_log_level()
self.assertEqual(self._sdk_logger.level, logging.NOTSET)

def test_invalid_value_warns_and_leaves_level_unchanged(self):
for invalid in ("INVALID", "verbose", "none", "0"):
with self.subTest(invalid=invalid):
with patch.dict("os.environ", {OTEL_LOG_LEVEL: invalid}):
with self.assertLogs(
"opentelemetry.sdk._logs._internal",
level=logging.WARNING,
):
_configure_otel_log_level()
self.assertEqual(self._sdk_logger.level, logging.NOTSET)

def test_case_insensitive(self):
for env_value, expected_level in (
("DEBUG", logging.DEBUG),
("WARN", logging.WARNING),
("Warning", logging.WARNING),
("cRiTiCaL", logging.CRITICAL),
):
with self.subTest(env_value=env_value):
with patch.dict("os.environ", {OTEL_LOG_LEVEL: env_value}):
_configure_otel_log_level()
self.assertEqual(self._sdk_logger.level, expected_level)
self._sdk_logger.setLevel(logging.NOTSET)

@patch.dict("os.environ", {OTEL_LOG_LEVEL: "critical"})
def test_level_propagates_to_child_loggers(self):
_configure_otel_log_level()
self.assertEqual(
self._sdk_logger.getChild("trace").getEffectiveLevel(),
logging.CRITICAL,
)
self.assertEqual(
self._sdk_logger.getChild("metrics").getEffectiveLevel(),
logging.CRITICAL,
)
self.assertEqual(
self._sdk_logger.getChild("logs").getEffectiveLevel(),
logging.CRITICAL,
)

def test_all_valid_values_map_to_correct_level(self):
cases = [
("trace", logging.DEBUG),
("debug", logging.DEBUG),
("info", logging.INFO),
("warn", logging.WARNING),
("warning", logging.WARNING),
("error", logging.ERROR),
("critical", logging.CRITICAL),
]
for env_value, expected_level in cases:
with self.subTest(env_value=env_value):
with patch.dict("os.environ", {OTEL_LOG_LEVEL: env_value}):
_configure_otel_log_level()
self.assertEqual(self._sdk_logger.level, expected_level)
self._sdk_logger.setLevel(logging.NOTSET)
Loading