Skip to content

Commit 2920836

Browse files
committed
feat(logs): add OTEL_LOG_LEVEL support
1 parent 9c270da commit 2920836

File tree

2 files changed

+117
-1
lines changed

2 files changed

+117
-1
lines changed

opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
from opentelemetry.sdk.environment_variables import (
5757
OTEL_ATTRIBUTE_COUNT_LIMIT,
5858
OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT,
59+
OTEL_LOG_LEVEL,
5960
OTEL_SDK_DISABLED,
6061
)
6162
from opentelemetry.sdk.resources import Resource
@@ -75,8 +76,34 @@
7576
_DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT = 128
7677
_ENV_VALUE_UNSET = ""
7778

79+
# "warn" is included alongside "warning" because the OTel spec default is
80+
# "info" (lowercase OTel style) and OTel canonical short names use "WARN",
81+
# so users following OTel documentation will naturally try "warn".
82+
_OTEL_LOG_LEVEL_TO_PYTHON = {
83+
"debug": logging.DEBUG,
84+
"info": logging.INFO,
85+
"warn": logging.WARNING,
86+
"warning": logging.WARNING,
87+
"error": logging.ERROR,
88+
"critical": logging.CRITICAL,
89+
}
90+
7891
_logger = logging.getLogger(__name__)
7992

93+
# Target opentelemetry.sdk (not the module-level _logger) so the level
94+
# propagates to all SDK sub-modules: trace, metrics, logs, exporters.
95+
_otel_log_level_raw = environ.get(OTEL_LOG_LEVEL)
96+
_otel_log_level = (_otel_log_level_raw or "info").lower()
97+
if _otel_log_level_raw and _otel_log_level not in _OTEL_LOG_LEVEL_TO_PYTHON:
98+
_logger.warning(
99+
"Invalid value for OTEL_LOG_LEVEL: %r. "
100+
"Valid values: debug, info, warn, warning, error, critical. "
101+
"Defaulting to INFO.",
102+
_otel_log_level_raw,
103+
)
104+
_python_level = _OTEL_LOG_LEVEL_TO_PYTHON.get(_otel_log_level, logging.INFO)
105+
logging.getLogger("opentelemetry.sdk").setLevel(_python_level)
106+
80107

81108
class BytesEncoder(json.JSONEncoder):
82109
def default(self, o):
@@ -718,6 +745,7 @@ def emit(
718745
"""
719746
if not self._is_enabled():
720747
return
748+
721749
# If a record is provided, use it directly
722750
if record is not None:
723751
if not isinstance(record, ReadWriteLogRecord):

opentelemetry-sdk/tests/logs/test_logs.py

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@
1414

1515
# pylint: disable=protected-access
1616

17+
import importlib
18+
import logging
1719
import unittest
1820
from unittest.mock import Mock, patch
1921

22+
import opentelemetry.sdk._logs._internal as _logs_internal
2023
from opentelemetry._logs import LogRecord, SeverityNumber
2124
from opentelemetry.context import get_current
2225
from opentelemetry.metrics import NoOpMeterProvider
@@ -26,14 +29,18 @@
2629
ReadableLogRecord,
2730
)
2831
from opentelemetry.sdk._logs._internal import (
32+
_OTEL_LOG_LEVEL_TO_PYTHON,
2933
LoggerMetrics,
3034
NoOpLogger,
3135
SynchronousMultiLogRecordProcessor,
3236
_disable_logger_configurator,
3337
_LoggerConfig,
3438
_RuleBasedLoggerConfigurator,
3539
)
36-
from opentelemetry.sdk.environment_variables import OTEL_SDK_DISABLED
40+
from opentelemetry.sdk.environment_variables import (
41+
OTEL_LOG_LEVEL,
42+
OTEL_SDK_DISABLED,
43+
)
3744
from opentelemetry.sdk.resources import Resource
3845
from opentelemetry.sdk.util.instrumentation import (
3946
InstrumentationScope,
@@ -354,3 +361,84 @@ def test_can_emit_with_keywords_arguments(self):
354361
self.assertEqual(result_log_record.attributes, {"some": "attributes"})
355362
self.assertEqual(result_log_record.event_name, "event_name")
356363
self.assertEqual(log_data.resource, logger.resource)
364+
365+
366+
class TestOtelLogLevelEnvVar(unittest.TestCase):
367+
"""Tests for OTEL_LOG_LEVEL → SDK internal logger level."""
368+
369+
def setUp(self):
370+
self._sdk_logger = logging.getLogger("opentelemetry.sdk")
371+
372+
def tearDown(self):
373+
importlib.reload(_logs_internal)
374+
375+
def test_otel_log_level_to_python_mapping_accepted_values(self):
376+
expected_keys = {
377+
"debug",
378+
"info",
379+
"warn",
380+
"warning",
381+
"error",
382+
"critical",
383+
}
384+
self.assertEqual(set(_OTEL_LOG_LEVEL_TO_PYTHON.keys()), expected_keys)
385+
386+
@patch.dict("os.environ", {OTEL_LOG_LEVEL: ""})
387+
def test_default_level_is_info(self):
388+
importlib.reload(_logs_internal)
389+
self.assertEqual(self._sdk_logger.level, logging.INFO)
390+
391+
def test_invalid_value_warns_and_defaults_to_info(self):
392+
# "trace", "verbose", "none" are valid in other SDKs but not accepted here
393+
for invalid in ("INVALID", "trace", "verbose", "none", "0"):
394+
with self.subTest(invalid=invalid):
395+
with patch.dict("os.environ", {OTEL_LOG_LEVEL: invalid}):
396+
with self.assertLogs(
397+
"opentelemetry.sdk._logs._internal",
398+
level=logging.WARNING,
399+
):
400+
importlib.reload(_logs_internal)
401+
self.assertEqual(self._sdk_logger.level, logging.INFO)
402+
403+
def test_case_insensitive(self):
404+
for env_value, expected_level in (
405+
("DEBUG", logging.DEBUG),
406+
("WARN", logging.WARNING),
407+
("Warning", logging.WARNING),
408+
("cRiTiCaL", logging.CRITICAL),
409+
):
410+
with self.subTest(env_value=env_value):
411+
with patch.dict("os.environ", {OTEL_LOG_LEVEL: env_value}):
412+
importlib.reload(_logs_internal)
413+
self.assertEqual(self._sdk_logger.level, expected_level)
414+
415+
@patch.dict("os.environ", {OTEL_LOG_LEVEL: "critical"})
416+
def test_level_propagates_to_child_loggers(self):
417+
importlib.reload(_logs_internal)
418+
self.assertEqual(
419+
self._sdk_logger.getChild("trace").getEffectiveLevel(),
420+
logging.CRITICAL,
421+
)
422+
self.assertEqual(
423+
self._sdk_logger.getChild("metrics").getEffectiveLevel(),
424+
logging.CRITICAL,
425+
)
426+
self.assertEqual(
427+
self._sdk_logger.getChild("logs").getEffectiveLevel(),
428+
logging.CRITICAL,
429+
)
430+
431+
def test_all_valid_values_map_to_correct_level(self):
432+
cases = [
433+
("debug", logging.DEBUG),
434+
("info", logging.INFO),
435+
("warn", logging.WARNING),
436+
("warning", logging.WARNING),
437+
("error", logging.ERROR),
438+
("critical", logging.CRITICAL),
439+
]
440+
for env_value, expected_level in cases:
441+
with self.subTest(env_value=env_value):
442+
with patch.dict("os.environ", {OTEL_LOG_LEVEL: env_value}):
443+
importlib.reload(_logs_internal)
444+
self.assertEqual(self._sdk_logger.level, expected_level)

0 commit comments

Comments
 (0)