Skip to content
Open
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
- Refactor `BatchLogRecordProcessor` to simplify code and make the control flow more
clear ([#4562](https://github.com/open-telemetry/opentelemetry-python/pull/4562/)
and [#4535](https://github.com/open-telemetry/opentelemetry-python/pull/4535)).
- Enable configuration of logging format and level in auto-instrumentation
([#4203](https://github.com/open-telemetry/opentelemetry-python/pull/4203))

## Version 1.33.0/0.54b0 (2025-05-09)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
OTEL_EXPORTER_OTLP_METRICS_PROTOCOL,
OTEL_EXPORTER_OTLP_PROTOCOL,
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,
OTEL_PYTHON_LOG_FORMAT,
OTEL_PYTHON_LOG_LEVEL,
OTEL_TRACES_SAMPLER,
OTEL_TRACES_SAMPLER_ARG,
)
Expand Down Expand Up @@ -89,6 +91,15 @@

_OTEL_SAMPLER_ENTRY_POINT_GROUP = "opentelemetry_traces_sampler"

_OTEL_PYTHON_LOG_LEVEL_BY_NAME = {
"notset": logging.NOTSET,
"debug": logging.DEBUG,
"info": logging.INFO,
"warn": logging.WARNING,
"warning": logging.WARNING,
"error": logging.ERROR,
}

_logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -133,6 +144,13 @@ def _get_id_generator() -> str:
return environ.get(OTEL_PYTHON_ID_GENERATOR, _DEFAULT_ID_GENERATOR)


def _get_log_level() -> int:
return _OTEL_PYTHON_LOG_LEVEL_BY_NAME.get(
environ.get(OTEL_PYTHON_LOG_LEVEL, "notset").lower().strip(),
logging.NOTSET,
)


def _get_exporter_entry_point(
exporter_name: str, signal_type: Literal["traces", "metrics", "logs"]
):
Expand Down Expand Up @@ -255,11 +273,19 @@ def _init_logging(
if setup_logging_handler:
_patch_basic_config()

# Add OTel handler
handler = LoggingHandler(
level=logging.NOTSET, logger_provider=provider
)
logging.getLogger().addHandler(handler)
# Log Handler
root_logger = logging.getLogger()
handler = LoggingHandler(logger_provider=provider)
# Log level
if OTEL_PYTHON_LOG_LEVEL in environ:
handler.setLevel(_get_log_level())
# Log format
if OTEL_PYTHON_LOG_FORMAT in environ:
log_format = environ.get(
OTEL_PYTHON_LOG_FORMAT, logging.BASIC_FORMAT
)
handler.setFormatter(logging.Formatter(log_format))
root_logger.addHandler(handler)


def _patch_basic_config():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,22 @@
Default: "info"
"""

OTEL_PYTHON_LOG_FORMAT = "OTEL_PYTHON_LOG_FORMAT"
"""
.. envvar:: OTEL_PYTHON_LOG_FORMAT

The :envvar:`OTEL_PYTHON_LOG_FORMAT` environment variable sets the log format for the OpenTelemetry LoggingHandler's Formatter
Default: "logging.BASIC_FORMAT"
"""

OTEL_PYTHON_LOG_LEVEL = "OTEL_PYTHON_LOG_LEVEL"
"""
.. envvar:: OTEL_PYTHON_LOG_LEVEL

The :envvar:`OTEL_PYTHON_LOG_LEVEL` environment variable sets the log level for the OpenTelemetry LoggingHandler
Default: "logging.NOTSET"
"""

OTEL_TRACES_SAMPLER = "OTEL_TRACES_SAMPLER"
"""
.. envvar:: OTEL_TRACES_SAMPLER
Expand Down
254 changes: 253 additions & 1 deletion opentelemetry-sdk/tests/test_configurator.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@
from __future__ import annotations

import logging
from logging import WARNING, getLogger
from logging import (
DEBUG,
ERROR,
INFO,
NOTSET,
WARNING,
getLogger,
)
from os import environ
from typing import Iterable, Optional, Sequence
from unittest import TestCase, mock
Expand All @@ -33,6 +40,7 @@
_EXPORTER_OTLP_PROTO_HTTP,
_get_exporter_names,
_get_id_generator,
_get_log_level,
_get_sampler,
_import_config_components,
_import_exporters,
Expand Down Expand Up @@ -75,6 +83,8 @@
from opentelemetry.trace.span import TraceState
from opentelemetry.util.types import Attributes

CUSTOM_LOG_FORMAT = "CUSTOM FORMAT %(levelname)s:%(name)s:%(message)s"


class Provider:
def __init__(self, resource=None, sampler=None, id_generator=None):
Expand Down Expand Up @@ -610,6 +620,8 @@ def setUp(self):
self.set_event_logger_provider_patch.start()
)

getLogger().handlers.clear()

def tearDown(self):
self.processor_patch.stop()
self.set_provider_patch.stop()
Expand Down Expand Up @@ -692,6 +704,151 @@ def test_logging_init_exporter_without_handler_setup(self):
)
getLogger(__name__).error("hello")
self.assertFalse(provider.processor.exporter.export_called)
root_logger = getLogger()
for handler in root_logger.handlers:
if isinstance(handler, LoggingHandler):
self.fail()

@patch.dict(
environ,
{
"OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service",
"OTEL_PYTHON_LOG_LEVEL": "CUSTOM_LOG_LEVEL",
},
clear=True,
)
@patch("opentelemetry.sdk._configuration._get_log_level", return_value=39)
def test_logging_init_exporter_level_under(self, log_level_mock):
resource = Resource.create({})
_init_logging(
{"otlp": DummyOTLPLogExporter},
resource=resource,
)
self.assertEqual(self.set_provider_mock.call_count, 1)
provider = self.set_provider_mock.call_args[0][0]
self.assertIsInstance(provider, DummyLoggerProvider)
self.assertIsInstance(provider.resource, Resource)
self.assertEqual(
provider.resource.attributes.get("service.name"),
"otlp-service",
)
self.assertIsInstance(provider.processor, DummyLogRecordProcessor)
self.assertIsInstance(
provider.processor.exporter, DummyOTLPLogExporter
)
getLogger(__name__).error("hello")
self.assertTrue(provider.processor.exporter.export_called)
root_logger = getLogger()
handler_present = False
for handler in root_logger.handlers:
if isinstance(handler, LoggingHandler):
handler_present = True
self.assertEqual(handler.level, 39)
self.assertTrue(handler_present)

@patch.dict(
environ,
{
"OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service",
"OTEL_PYTHON_LOG_LEVEL": "CUSTOM_LOG_LEVEL",
},
clear=True,
)
@patch("opentelemetry.sdk._configuration._get_log_level", return_value=41)
def test_logging_init_exporter_level_over(self, log_level_mock):
resource = Resource.create({})
_init_logging(
{"otlp": DummyOTLPLogExporter},
resource=resource,
)
self.assertEqual(self.set_provider_mock.call_count, 1)
provider = self.set_provider_mock.call_args[0][0]
self.assertIsInstance(provider, DummyLoggerProvider)
self.assertIsInstance(provider.resource, Resource)
self.assertEqual(
provider.resource.attributes.get("service.name"),
"otlp-service",
)
self.assertIsInstance(provider.processor, DummyLogRecordProcessor)
self.assertIsInstance(
provider.processor.exporter, DummyOTLPLogExporter
)
getLogger(__name__).error("hello")
self.assertFalse(provider.processor.exporter.export_called)
root_logger = getLogger()
handler_present = False
for handler in root_logger.handlers:
if isinstance(handler, LoggingHandler):
handler_present = True
self.assertEqual(handler.level, 41)
self.assertTrue(handler_present)

@patch.dict(
environ,
{
"OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service",
"OTEL_PYTHON_LOG_FORMAT": CUSTOM_LOG_FORMAT,
},
)
def test_logging_init_exporter_format(self):
resource = Resource.create({})
_init_logging(
{"otlp": DummyOTLPLogExporter},
resource=resource,
)
self.assertEqual(self.set_provider_mock.call_count, 1)
provider = self.set_provider_mock.call_args[0][0]
self.assertIsInstance(provider, DummyLoggerProvider)
self.assertIsInstance(provider.resource, Resource)
self.assertEqual(
provider.resource.attributes.get("service.name"),
"otlp-service",
)
self.assertIsInstance(provider.processor, DummyLogRecordProcessor)
self.assertIsInstance(
provider.processor.exporter, DummyOTLPLogExporter
)
getLogger(__name__).error("hello")
self.assertTrue(provider.processor.exporter.export_called)
root_logger = getLogger()
handler_present = False
for handler in root_logger.handlers:
if isinstance(handler, LoggingHandler):
self.assertEqual(handler.formatter._fmt, CUSTOM_LOG_FORMAT)
handler_present = True
self.assertTrue(handler_present)

@patch.dict(environ, {}, clear=True)
def test_otel_log_level_by_name_default(self):
self.assertEqual(_get_log_level(), NOTSET)

@patch.dict(environ, {"OTEL_PYTHON_LOG_LEVEL": "NOTSET "}, clear=True)
def test_otel_log_level_by_name_notset(self):
self.assertEqual(_get_log_level(), NOTSET)

@patch.dict(environ, {"OTEL_PYTHON_LOG_LEVEL": " DeBug "}, clear=True)
def test_otel_log_level_by_name_debug(self):
self.assertEqual(_get_log_level(), DEBUG)

@patch.dict(environ, {"OTEL_PYTHON_LOG_LEVEL": " info "}, clear=True)
def test_otel_log_level_by_name_info(self):
self.assertEqual(_get_log_level(), INFO)

@patch.dict(environ, {"OTEL_PYTHON_LOG_LEVEL": " warn"}, clear=True)
def test_otel_log_level_by_name_warn(self):
self.assertEqual(_get_log_level(), WARNING)

@patch.dict(environ, {"OTEL_PYTHON_LOG_LEVEL": " warnING "}, clear=True)
def test_otel_log_level_by_name_warning(self):
self.assertEqual(_get_log_level(), WARNING)

@patch.dict(environ, {"OTEL_PYTHON_LOG_LEVEL": " eRroR"}, clear=True)
def test_otel_log_level_by_name_error(self):
self.assertEqual(_get_log_level(), ERROR)

@patch.dict(environ, {"OTEL_PYTHON_LOG_LEVEL": "foobar"}, clear=True)
def test_otel_log_level_by_name_invalid(self):
self.assertEqual(_get_log_level(), NOTSET)

@patch.dict(
environ,
Expand Down Expand Up @@ -843,6 +1000,101 @@ def test_initialize_components_kwargs(
True,
)

@patch.dict(
environ,
{
"OTEL_TRACES_EXPORTER": _EXPORTER_OTLP,
"OTEL_METRICS_EXPORTER": _EXPORTER_OTLP_PROTO_GRPC,
"OTEL_LOGS_EXPORTER": _EXPORTER_OTLP_PROTO_HTTP,
},
)
@patch.dict(
environ,
{
"OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service, custom.key.1=env-value",
"OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED": "False",
},
)
@patch("opentelemetry.sdk._configuration.Resource")
@patch("opentelemetry.sdk._configuration._import_exporters")
@patch("opentelemetry.sdk._configuration._get_exporter_names")
@patch("opentelemetry.sdk._configuration._init_tracing")
@patch("opentelemetry.sdk._configuration._init_logging")
@patch("opentelemetry.sdk._configuration._init_metrics")
def test_initialize_components_kwargs_disable_logging_handler(
self,
metrics_mock,
logging_mock,
tracing_mock,
exporter_names_mock,
import_exporters_mock,
resource_mock,
):
exporter_names_mock.return_value = [
"env_var_exporter_1",
"env_var_exporter_2",
]
import_exporters_mock.return_value = (
"TEST_SPAN_EXPORTERS_DICT",
"TEST_METRICS_EXPORTERS_DICT",
"TEST_LOG_EXPORTERS_DICT",
)
resource_mock.create.return_value = "TEST_RESOURCE"
kwargs = {
"auto_instrumentation_version": "auto-version",
"trace_exporter_names": ["custom_span_exporter"],
"metric_exporter_names": ["custom_metric_exporter"],
"log_exporter_names": ["custom_log_exporter"],
"sampler": "TEST_SAMPLER",
"resource_attributes": {
"custom.key.1": "pass-in-value-1",
"custom.key.2": "pass-in-value-2",
},
"id_generator": "TEST_GENERATOR",
}
_initialize_components(**kwargs)

import_exporters_mock.assert_called_once_with(
[
"custom_span_exporter",
"env_var_exporter_1",
"env_var_exporter_2",
],
[
"custom_metric_exporter",
"env_var_exporter_1",
"env_var_exporter_2",
],
[
"custom_log_exporter",
"env_var_exporter_1",
"env_var_exporter_2",
],
)
resource_mock.create.assert_called_once_with(
{
"telemetry.auto.version": "auto-version",
"custom.key.1": "pass-in-value-1",
"custom.key.2": "pass-in-value-2",
}
)
# Resource is checked separates
tracing_mock.assert_called_once_with(
exporters="TEST_SPAN_EXPORTERS_DICT",
id_generator="TEST_GENERATOR",
sampler="TEST_SAMPLER",
resource="TEST_RESOURCE",
)
metrics_mock.assert_called_once_with(
"TEST_METRICS_EXPORTERS_DICT",
"TEST_RESOURCE",
)
logging_mock.assert_called_once_with(
"TEST_LOG_EXPORTERS_DICT",
"TEST_RESOURCE",
False,
)

def test_basicConfig_works_with_otel_handler(self):
with ClearLoggingHandlers():
_init_logging(
Expand Down
Loading