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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

- `opentelemetry-sdk`: Honour `OTEL_LOG_LEVEL` environment variable to configure the SDK's internal diagnostic logger; accepted values: `debug`, `info`, `warn`, `warning`, `error`, `critical`; invalid values emit a warning and default to `info`
([#5115](https://github.com/open-telemetry/opentelemetry-python/issues/5115))
- `opentelemetry-sdk`: add `additional_properties` support to generated config models via custom `datamodel-codegen` template, enabling plugin/custom component names to flow through typed dataclasses
([#5131](https://github.com/open-telemetry/opentelemetry-python/pull/5131))
- Fix incorrect code example in `create_tracer()` docstring
Expand Down
47 changes: 47 additions & 0 deletions opentelemetry-sdk/src/opentelemetry/sdk/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
The OpenTelemetry SDK package is an implementation of the OpenTelemetry API.
"""

import logging
from os import environ

from opentelemetry.sdk.environment_variables import OTEL_LOG_LEVEL

# "warn" is accepted alongside "warning" because OTel canonical short names
# use "WARN", so users following OTel documentation will naturally try "warn".
_OTEL_LOG_LEVEL_TO_PYTHON = {
"debug": logging.DEBUG,
"info": logging.INFO,
"warn": logging.WARNING,
"warning": logging.WARNING,
"error": logging.ERROR,
"critical": logging.CRITICAL,
}

_otel_log_level_raw = environ.get(OTEL_LOG_LEVEL)
if _otel_log_level_raw:
_logger = logging.getLogger(__name__)
_otel_log_level = _otel_log_level_raw.lower()
if _otel_log_level in _OTEL_LOG_LEVEL_TO_PYTHON:
_logger.setLevel(_OTEL_LOG_LEVEL_TO_PYTHON[_otel_log_level])
else:
_logger.warning(
"Invalid value for OTEL_LOG_LEVEL: %r. "
"Valid values: debug, info, warn, warning, error, critical. "
"Logger level unchanged.",
_otel_log_level_raw,
)
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,7 @@ def emit(
"""
if not self._is_enabled():
return

# If a record is provided, use it directly
if record is not None:
if not isinstance(record, ReadWriteLogRecord):
Expand Down
91 changes: 90 additions & 1 deletion opentelemetry-sdk/tests/logs/test_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@

# pylint: disable=protected-access

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

import opentelemetry.sdk as _sdk
from opentelemetry._logs import LogRecord, SeverityNumber
from opentelemetry.attributes import BoundedAttributes
from opentelemetry.context import get_current
from opentelemetry.metrics import NoOpMeterProvider
from opentelemetry.sdk import _OTEL_LOG_LEVEL_TO_PYTHON
from opentelemetry.sdk._logs import (
Logger,
LoggerProvider,
Expand All @@ -35,7 +39,10 @@
_LoggerConfig,
_RuleBasedLoggerConfigurator,
)
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 @@ -463,3 +470,85 @@ 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)
importlib.reload(_sdk)

def test_otel_log_level_to_python_mapping_accepted_keys(self):
expected_keys = {
"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):
importlib.reload(_sdk)
self.assertEqual(self._sdk_logger.level, logging.NOTSET)

def test_invalid_value_warns_and_leaves_level_unchanged(self):
# "trace", "verbose", "none" are valid in other SDKs but not accepted here
for invalid in ("INVALID", "trace", "verbose", "none", "0"):
with self.subTest(invalid=invalid):
with patch.dict("os.environ", {OTEL_LOG_LEVEL: invalid}):
with self.assertLogs(
"opentelemetry.sdk",
level=logging.WARNING,
):
importlib.reload(_sdk)
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}):
importlib.reload(_sdk)
self.assertEqual(self._sdk_logger.level, expected_level)

@patch.dict("os.environ", {OTEL_LOG_LEVEL: "critical"})
def test_level_propagates_to_child_loggers(self):
importlib.reload(_sdk)
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 = [
("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}):
importlib.reload(_sdk)
self.assertEqual(self._sdk_logger.level, expected_level)