Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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 @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#4494](https://github.com/open-telemetry/opentelemetry-python/pull/4494))
- Improve CI by cancelling stale runs and setting timeouts
([#4498](https://github.com/open-telemetry/opentelemetry-python/pull/4498))
- Patch logging.basicConfig so OTel logs don't cause console logs to disappear
([#4436](https://github.com/open-telemetry/opentelemetry-python/pull/4436))

## Version 1.31.0/0.52b0 (2025-03-12)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,12 +253,33 @@ def _init_logging(
set_event_logger_provider(event_logger_provider)

if setup_logging_handler:
_patch_basic_config()

# Add OTel handler
handler = LoggingHandler(
level=logging.NOTSET, logger_provider=provider
)
logging.getLogger().addHandler(handler)


def _patch_basic_config():
original_basic_config = logging.basicConfig

def patched_basic_config(*args, **kwargs):
root = logging.getLogger()
has_only_otel = len(root.handlers) == 1 and isinstance(
root.handlers[0], LoggingHandler
)
if has_only_otel:
otel_handler = root.handlers.pop()
original_basic_config(*args, **kwargs)
root.addHandler(otel_handler)
else:
original_basic_config(*args, **kwargs)

logging.basicConfig = patched_basic_config


def _import_exporters(
trace_exporter_names: Sequence[str],
metric_exporter_names: Sequence[str],
Expand Down
95 changes: 94 additions & 1 deletion opentelemetry-sdk/tests/test_configurator.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# pylint: skip-file
from __future__ import annotations

import logging
from logging import WARNING, getLogger
from os import environ
from typing import Iterable, Optional, Sequence
Expand Down Expand Up @@ -44,6 +45,7 @@
_OTelSDKConfigurator,
)
from opentelemetry.sdk._logs import LoggingHandler
from opentelemetry.sdk._logs._internal.export import LogExporter
from opentelemetry.sdk._logs.export import ConsoleLogExporter
from opentelemetry.sdk.environment_variables import (
OTEL_TRACES_SAMPLER,
Expand Down Expand Up @@ -203,7 +205,7 @@ class OTLPSpanExporter:
pass


class DummyOTLPLogExporter:
class DummyOTLPLogExporter(LogExporter):
def __init__(self, *args, **kwargs):
self.export_called = False

Expand Down Expand Up @@ -841,6 +843,60 @@ def test_initialize_components_kwargs(
True,
)

def test_basicConfig_works_with_otel_handler(self):
with ClearLoggingHandlers():
_init_logging(
{"otlp": DummyOTLPLogExporter},
Resource.create({}),
setup_logging_handler=True,
)

logging.basicConfig(level=logging.INFO)

root_logger = logging.getLogger()
stream_handlers = [
h
for h in root_logger.handlers
if isinstance(h, logging.StreamHandler)
]
self.assertEqual(
len(stream_handlers),
1,
"basicConfig should add a StreamHandler even when OTel handler exists",
)

def test_basicConfig_preserves_otel_handler(self):
with ClearLoggingHandlers():
_init_logging(
{"otlp": DummyOTLPLogExporter},
Resource.create({}),
setup_logging_handler=True,
)

root_logger = logging.getLogger()
self.assertEqual(
len(root_logger.handlers),
1,
"Should be exactly one OpenTelemetry LoggingHandler",
)
handler = root_logger.handlers[0]
self.assertIsInstance(handler, LoggingHandler)

logging.basicConfig()

self.assertGreater(len(root_logger.handlers), 1)

logging_handlers = [
h
for h in root_logger.handlers
if isinstance(h, LoggingHandler)
]
self.assertEqual(
len(logging_handlers),
1,
"Should still have exactly one OpenTelemetry LoggingHandler",
)


class TestMetricsInit(TestCase):
def setUp(self):
Expand Down Expand Up @@ -1076,3 +1132,40 @@ def test_custom_configurator(self, mock_init_comp):
"sampler": "TEST_SAMPLER",
}
mock_init_comp.assert_called_once_with(**kwargs)


class ClearLoggingHandlers:
def __init__(self):
self.root_logger = getLogger()
self.original_handlers = None

def __enter__(self):
self.original_handlers = self.root_logger.handlers[:]
self.root_logger.handlers = []
return self

def __exit__(self, exc_type, exc_val, exc_tb):
self.root_logger.handlers = []
for handler in self.original_handlers:
self.root_logger.addHandler(handler)


class TestClearLoggingHandlers(TestCase):
def test_preserves_handlers(self):
root_logger = getLogger()
initial_handlers = root_logger.handlers[:]

test_handler = logging.StreamHandler()
root_logger.addHandler(test_handler)
expected_handlers = initial_handlers + [test_handler]

with ClearLoggingHandlers():
self.assertEqual(len(root_logger.handlers), 0)
temp_handler = logging.StreamHandler()
root_logger.addHandler(temp_handler)

self.assertEqual(len(root_logger.handlers), len(expected_handlers))
for h1, h2 in zip(root_logger.handlers, expected_handlers):
self.assertIs(h1, h2)

root_logger.removeHandler(test_handler)