Skip to content

Commit b36d053

Browse files
committed
feat(logs): add OTEL_LOG_LEVEL support
1 parent 82128af commit b36d053

2 files changed

Lines changed: 117 additions & 1 deletion

File tree

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
from opentelemetry.sdk.environment_variables import (
6262
OTEL_ATTRIBUTE_COUNT_LIMIT,
6363
OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT,
64+
OTEL_LOG_LEVEL,
6465
OTEL_SDK_DISABLED,
6566
)
6667
from opentelemetry.sdk.resources import Resource
@@ -80,8 +81,34 @@
8081
_DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT = 128
8182
_ENV_VALUE_UNSET = ""
8283

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

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

86113
class BytesEncoder(json.JSONEncoder):
87114
def default(self, o):
@@ -724,6 +751,7 @@ def emit(
724751
"""
725752
if not self._is_enabled():
726753
return
754+
727755
# If a record is provided, use it directly
728756
if record is not None:
729757
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.attributes import BoundedAttributes
2225
from opentelemetry.context import get_current
@@ -28,14 +31,18 @@
2831
ReadWriteLogRecord,
2932
)
3033
from opentelemetry.sdk._logs._internal import (
34+
_OTEL_LOG_LEVEL_TO_PYTHON,
3135
LoggerMetrics,
3236
NoOpLogger,
3337
SynchronousMultiLogRecordProcessor,
3438
_disable_logger_configurator,
3539
_LoggerConfig,
3640
_RuleBasedLoggerConfigurator,
3741
)
38-
from opentelemetry.sdk.environment_variables import OTEL_SDK_DISABLED
42+
from opentelemetry.sdk.environment_variables import (
43+
OTEL_LOG_LEVEL,
44+
OTEL_SDK_DISABLED,
45+
)
3946
from opentelemetry.sdk.resources import Resource
4047
from opentelemetry.sdk.util.instrumentation import (
4148
InstrumentationScope,
@@ -463,3 +470,84 @@ def test_emit_readwrite_logrecord_uses_exception(self):
463470
self.assertEqual(
464471
attributes[exception_attributes.EXCEPTION_TYPE], "RuntimeError"
465472
)
473+
474+
475+
class TestOtelLogLevelEnvVar(unittest.TestCase):
476+
"""Tests for OTEL_LOG_LEVEL → SDK internal logger level."""
477+
478+
def setUp(self):
479+
self._sdk_logger = logging.getLogger("opentelemetry.sdk")
480+
481+
def tearDown(self):
482+
importlib.reload(_logs_internal)
483+
484+
def test_otel_log_level_to_python_mapping_accepted_values(self):
485+
expected_keys = {
486+
"debug",
487+
"info",
488+
"warn",
489+
"warning",
490+
"error",
491+
"critical",
492+
}
493+
self.assertEqual(set(_OTEL_LOG_LEVEL_TO_PYTHON.keys()), expected_keys)
494+
495+
@patch.dict("os.environ", {OTEL_LOG_LEVEL: ""})
496+
def test_default_level_is_info(self):
497+
importlib.reload(_logs_internal)
498+
self.assertEqual(self._sdk_logger.level, logging.INFO)
499+
500+
def test_invalid_value_warns_and_defaults_to_info(self):
501+
# "trace", "verbose", "none" are valid in other SDKs but not accepted here
502+
for invalid in ("INVALID", "trace", "verbose", "none", "0"):
503+
with self.subTest(invalid=invalid):
504+
with patch.dict("os.environ", {OTEL_LOG_LEVEL: invalid}):
505+
with self.assertLogs(
506+
"opentelemetry.sdk._logs._internal",
507+
level=logging.WARNING,
508+
):
509+
importlib.reload(_logs_internal)
510+
self.assertEqual(self._sdk_logger.level, logging.INFO)
511+
512+
def test_case_insensitive(self):
513+
for env_value, expected_level in (
514+
("DEBUG", logging.DEBUG),
515+
("WARN", logging.WARNING),
516+
("Warning", logging.WARNING),
517+
("cRiTiCaL", logging.CRITICAL),
518+
):
519+
with self.subTest(env_value=env_value):
520+
with patch.dict("os.environ", {OTEL_LOG_LEVEL: env_value}):
521+
importlib.reload(_logs_internal)
522+
self.assertEqual(self._sdk_logger.level, expected_level)
523+
524+
@patch.dict("os.environ", {OTEL_LOG_LEVEL: "critical"})
525+
def test_level_propagates_to_child_loggers(self):
526+
importlib.reload(_logs_internal)
527+
self.assertEqual(
528+
self._sdk_logger.getChild("trace").getEffectiveLevel(),
529+
logging.CRITICAL,
530+
)
531+
self.assertEqual(
532+
self._sdk_logger.getChild("metrics").getEffectiveLevel(),
533+
logging.CRITICAL,
534+
)
535+
self.assertEqual(
536+
self._sdk_logger.getChild("logs").getEffectiveLevel(),
537+
logging.CRITICAL,
538+
)
539+
540+
def test_all_valid_values_map_to_correct_level(self):
541+
cases = [
542+
("debug", logging.DEBUG),
543+
("info", logging.INFO),
544+
("warn", logging.WARNING),
545+
("warning", logging.WARNING),
546+
("error", logging.ERROR),
547+
("critical", logging.CRITICAL),
548+
]
549+
for env_value, expected_level in cases:
550+
with self.subTest(env_value=env_value):
551+
with patch.dict("os.environ", {OTEL_LOG_LEVEL: env_value}):
552+
importlib.reload(_logs_internal)
553+
self.assertEqual(self._sdk_logger.level, expected_level)

0 commit comments

Comments
 (0)