diff --git a/CHANGELOG.md b/CHANGELOG.md index 1935034aac0..d4eccf8ae69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) - Fix ExplicitBucketHistogramAggregation to handle multiple explicit bucket boundaries advisories ([#4521](https://github.com/open-telemetry/opentelemetry-python/pull/4521)) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index c1852edd957..745a83385f9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -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], diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index 12cfd5c1d2e..9fda75b66f0 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -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 @@ -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, @@ -203,7 +205,7 @@ class OTLPSpanExporter: pass -class DummyOTLPLogExporter: +class DummyOTLPLogExporter(LogExporter): def __init__(self, *args, **kwargs): self.export_called = False @@ -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): @@ -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)