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
- 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