Skip to content

Commit 3644a1e

Browse files
pmcollinsxrmx
andauthored
Ensure a console logging handler is set when using auto-instrumentation (#4436)
* Add ClearLoggingHandlers test helper * Monkeypatch basicConfig * Make DummyOTLPLogExporter subclass LogExporter * Minor refactor of basic config patch * Add unit test * Add changelog entry * Address PR feedback --------- Co-authored-by: Riccardo Magliocchetti <[email protected]>
1 parent 80593c2 commit 3644a1e

File tree

3 files changed

+117
-1
lines changed

3 files changed

+117
-1
lines changed

Diff for: CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
([#4494](https://github.com/open-telemetry/opentelemetry-python/pull/4494))
1818
- Improve CI by cancelling stale runs and setting timeouts
1919
([#4498](https://github.com/open-telemetry/opentelemetry-python/pull/4498))
20+
- Patch logging.basicConfig so OTel logs don't cause console logs to disappear
21+
([#4436](https://github.com/open-telemetry/opentelemetry-python/pull/4436))
2022
- Fix ExplicitBucketHistogramAggregation to handle multiple explicit bucket boundaries advisories
2123
([#4521](https://github.com/open-telemetry/opentelemetry-python/pull/4521))
2224

Diff for: opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py

+21
Original file line numberDiff line numberDiff line change
@@ -253,12 +253,33 @@ def _init_logging(
253253
set_event_logger_provider(event_logger_provider)
254254

255255
if setup_logging_handler:
256+
_patch_basic_config()
257+
258+
# Add OTel handler
256259
handler = LoggingHandler(
257260
level=logging.NOTSET, logger_provider=provider
258261
)
259262
logging.getLogger().addHandler(handler)
260263

261264

265+
def _patch_basic_config():
266+
original_basic_config = logging.basicConfig
267+
268+
def patched_basic_config(*args, **kwargs):
269+
root = logging.getLogger()
270+
has_only_otel = len(root.handlers) == 1 and isinstance(
271+
root.handlers[0], LoggingHandler
272+
)
273+
if has_only_otel:
274+
otel_handler = root.handlers.pop()
275+
original_basic_config(*args, **kwargs)
276+
root.addHandler(otel_handler)
277+
else:
278+
original_basic_config(*args, **kwargs)
279+
280+
logging.basicConfig = patched_basic_config
281+
282+
262283
def _import_exporters(
263284
trace_exporter_names: Sequence[str],
264285
metric_exporter_names: Sequence[str],

Diff for: opentelemetry-sdk/tests/test_configurator.py

+94-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# pylint: skip-file
1616
from __future__ import annotations
1717

18+
import logging
1819
from logging import WARNING, getLogger
1920
from os import environ
2021
from typing import Iterable, Optional, Sequence
@@ -44,6 +45,7 @@
4445
_OTelSDKConfigurator,
4546
)
4647
from opentelemetry.sdk._logs import LoggingHandler
48+
from opentelemetry.sdk._logs._internal.export import LogExporter
4749
from opentelemetry.sdk._logs.export import ConsoleLogExporter
4850
from opentelemetry.sdk.environment_variables import (
4951
OTEL_TRACES_SAMPLER,
@@ -203,7 +205,7 @@ class OTLPSpanExporter:
203205
pass
204206

205207

206-
class DummyOTLPLogExporter:
208+
class DummyOTLPLogExporter(LogExporter):
207209
def __init__(self, *args, **kwargs):
208210
self.export_called = False
209211

@@ -841,6 +843,60 @@ def test_initialize_components_kwargs(
841843
True,
842844
)
843845

846+
def test_basicConfig_works_with_otel_handler(self):
847+
with ClearLoggingHandlers():
848+
_init_logging(
849+
{"otlp": DummyOTLPLogExporter},
850+
Resource.create({}),
851+
setup_logging_handler=True,
852+
)
853+
854+
logging.basicConfig(level=logging.INFO)
855+
856+
root_logger = logging.getLogger()
857+
stream_handlers = [
858+
h
859+
for h in root_logger.handlers
860+
if isinstance(h, logging.StreamHandler)
861+
]
862+
self.assertEqual(
863+
len(stream_handlers),
864+
1,
865+
"basicConfig should add a StreamHandler even when OTel handler exists",
866+
)
867+
868+
def test_basicConfig_preserves_otel_handler(self):
869+
with ClearLoggingHandlers():
870+
_init_logging(
871+
{"otlp": DummyOTLPLogExporter},
872+
Resource.create({}),
873+
setup_logging_handler=True,
874+
)
875+
876+
root_logger = logging.getLogger()
877+
self.assertEqual(
878+
len(root_logger.handlers),
879+
1,
880+
"Should be exactly one OpenTelemetry LoggingHandler",
881+
)
882+
handler = root_logger.handlers[0]
883+
self.assertIsInstance(handler, LoggingHandler)
884+
885+
logging.basicConfig()
886+
887+
self.assertGreater(len(root_logger.handlers), 1)
888+
889+
logging_handlers = [
890+
h
891+
for h in root_logger.handlers
892+
if isinstance(h, LoggingHandler)
893+
]
894+
self.assertEqual(
895+
len(logging_handlers),
896+
1,
897+
"Should still have exactly one OpenTelemetry LoggingHandler",
898+
)
899+
844900

845901
class TestMetricsInit(TestCase):
846902
def setUp(self):
@@ -1076,3 +1132,40 @@ def test_custom_configurator(self, mock_init_comp):
10761132
"sampler": "TEST_SAMPLER",
10771133
}
10781134
mock_init_comp.assert_called_once_with(**kwargs)
1135+
1136+
1137+
class ClearLoggingHandlers:
1138+
def __init__(self):
1139+
self.root_logger = getLogger()
1140+
self.original_handlers = None
1141+
1142+
def __enter__(self):
1143+
self.original_handlers = self.root_logger.handlers[:]
1144+
self.root_logger.handlers = []
1145+
return self
1146+
1147+
def __exit__(self, exc_type, exc_val, exc_tb):
1148+
self.root_logger.handlers = []
1149+
for handler in self.original_handlers:
1150+
self.root_logger.addHandler(handler)
1151+
1152+
1153+
class TestClearLoggingHandlers(TestCase):
1154+
def test_preserves_handlers(self):
1155+
root_logger = getLogger()
1156+
initial_handlers = root_logger.handlers[:]
1157+
1158+
test_handler = logging.StreamHandler()
1159+
root_logger.addHandler(test_handler)
1160+
expected_handlers = initial_handlers + [test_handler]
1161+
1162+
with ClearLoggingHandlers():
1163+
self.assertEqual(len(root_logger.handlers), 0)
1164+
temp_handler = logging.StreamHandler()
1165+
root_logger.addHandler(temp_handler)
1166+
1167+
self.assertEqual(len(root_logger.handlers), len(expected_handlers))
1168+
for h1, h2 in zip(root_logger.handlers, expected_handlers):
1169+
self.assertIs(h1, h2)
1170+
1171+
root_logger.removeHandler(test_handler)

0 commit comments

Comments
 (0)