Skip to content

Commit 5ef26f2

Browse files
committed
Merge branch 'main' of github.com:DylanRussell/opentelemetry-python into add_cred_envvar
2 parents a6dcd1c + 57cb935 commit 5ef26f2

File tree

4 files changed

+151
-70
lines changed

4 files changed

+151
-70
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
- Overwrite logging.config.fileConfig and logging.config.dictConfig to ensure
11+
the OTLP `LogHandler` remains attached to the root logger. Fix a bug that
12+
can cause a deadlock to occur over `logging._lock` in some cases ([#4636](https://github.com/open-telemetry/opentelemetry-python/pull/4636)).
13+
1014
## Version 1.35.0/0.56b0 (2025-07-11)
1115

1216
- Update OTLP proto to v1.7 [#4645](https://github.com/open-telemetry/opentelemetry-python/pull/4645).

opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import inspect
2323
import logging
24+
import logging.config
2425
import os
2526
from abc import ABC, abstractmethod
2627
from os import environ
@@ -378,31 +379,37 @@ def _init_logging(
378379
set_event_logger_provider(event_logger_provider)
379380

380381
if setup_logging_handler:
381-
_patch_basic_config()
382-
383382
# Add OTel handler
384383
handler = LoggingHandler(
385384
level=logging.NOTSET, logger_provider=provider
386385
)
387386
logging.getLogger().addHandler(handler)
387+
_overwrite_logging_config_fns(handler)
388388

389389

390-
def _patch_basic_config():
391-
original_basic_config = logging.basicConfig
390+
def _overwrite_logging_config_fns(handler: LoggingHandler) -> None:
391+
root = logging.getLogger()
392392

393-
def patched_basic_config(*args, **kwargs):
394-
root = logging.getLogger()
395-
has_only_otel = len(root.handlers) == 1 and isinstance(
396-
root.handlers[0], LoggingHandler
397-
)
398-
if has_only_otel:
399-
otel_handler = root.handlers.pop()
400-
original_basic_config(*args, **kwargs)
401-
root.addHandler(otel_handler)
402-
else:
403-
original_basic_config(*args, **kwargs)
393+
def wrapper(config_fn: Callable) -> Callable:
394+
def overwritten_config_fn(*args, **kwargs):
395+
removed_handler = False
396+
# We don't want the OTLP handler to be modified or deleted by the logging config functions.
397+
# So we remove it and then add it back after the function call.
398+
if handler in root.handlers:
399+
removed_handler = True
400+
root.handlers.remove(handler)
401+
try:
402+
config_fn(*args, **kwargs)
403+
finally:
404+
# Ensure handler is added back if logging function throws exception.
405+
if removed_handler:
406+
root.addHandler(handler)
407+
408+
return overwritten_config_fn
404409

405-
logging.basicConfig = patched_basic_config
410+
logging.config.fileConfig = wrapper(logging.config.fileConfig)
411+
logging.config.dictConfig = wrapper(logging.config.dictConfig)
412+
logging.basicConfig = wrapper(logging.basicConfig)
406413

407414

408415
def _import_exporters(

opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -648,7 +648,10 @@ def flush(self) -> None:
648648
if hasattr(self._logger_provider, "force_flush") and callable(
649649
self._logger_provider.force_flush
650650
):
651-
self._logger_provider.force_flush()
651+
# This is done in a separate thread to avoid a potential deadlock, for
652+
# details see https://github.com/open-telemetry/opentelemetry-python/pull/4636.
653+
thread = threading.Thread(target=self._logger_provider.force_flush)
654+
thread.start()
652655

653656

654657
class Logger(APILogger):

opentelemetry-sdk/tests/test_configurator.py

Lines changed: 120 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from __future__ import annotations
1717

1818
import logging
19+
import logging.config
1920
from logging import WARNING, getLogger
2021
from os import environ
2122
from typing import Iterable, Optional, Sequence
@@ -680,62 +681,65 @@ def tearDown(self):
680681
]
681682

682683
def test_logging_init_empty(self):
683-
auto_resource = Resource.create(
684-
{
685-
"telemetry.auto.version": "auto-version",
686-
}
687-
)
688-
_init_logging({}, resource=auto_resource)
689-
self.assertEqual(self.set_provider_mock.call_count, 1)
690-
provider = self.set_provider_mock.call_args[0][0]
691-
self.assertIsInstance(provider, DummyLoggerProvider)
692-
self.assertIsInstance(provider.resource, Resource)
693-
self.assertEqual(
694-
provider.resource.attributes.get("telemetry.auto.version"),
695-
"auto-version",
696-
)
697-
self.event_logger_provider_mock.assert_called_once_with(
698-
logger_provider=provider
699-
)
700-
self.set_event_logger_provider_mock.assert_called_once_with(
701-
self.event_logger_provider_instance_mock
702-
)
684+
with ResetGlobalLoggingState():
685+
auto_resource = Resource.create(
686+
{
687+
"telemetry.auto.version": "auto-version",
688+
}
689+
)
690+
_init_logging({}, resource=auto_resource)
691+
self.assertEqual(self.set_provider_mock.call_count, 1)
692+
provider = self.set_provider_mock.call_args[0][0]
693+
self.assertIsInstance(provider, DummyLoggerProvider)
694+
self.assertIsInstance(provider.resource, Resource)
695+
self.assertEqual(
696+
provider.resource.attributes.get("telemetry.auto.version"),
697+
"auto-version",
698+
)
699+
self.event_logger_provider_mock.assert_called_once_with(
700+
logger_provider=provider
701+
)
702+
self.set_event_logger_provider_mock.assert_called_once_with(
703+
self.event_logger_provider_instance_mock
704+
)
703705

704706
@patch.dict(
705707
environ,
706708
{"OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service"},
707709
)
708710
def test_logging_init_exporter(self):
709-
resource = Resource.create({})
710-
_init_logging({"otlp": DummyOTLPLogExporter}, resource=resource)
711-
self.assertEqual(self.set_provider_mock.call_count, 1)
712-
provider = self.set_provider_mock.call_args[0][0]
713-
self.assertIsInstance(provider, DummyLoggerProvider)
714-
self.assertIsInstance(provider.resource, Resource)
715-
self.assertEqual(
716-
provider.resource.attributes.get("service.name"),
717-
"otlp-service",
718-
)
719-
self.assertIsInstance(provider.processor, DummyLogRecordProcessor)
720-
self.assertIsInstance(
721-
provider.processor.exporter, DummyOTLPLogExporter
722-
)
723-
getLogger(__name__).error("hello")
724-
self.assertTrue(provider.processor.exporter.export_called)
711+
with ResetGlobalLoggingState():
712+
resource = Resource.create({})
713+
_init_logging({"otlp": DummyOTLPLogExporter}, resource=resource)
714+
self.assertEqual(self.set_provider_mock.call_count, 1)
715+
provider = self.set_provider_mock.call_args[0][0]
716+
self.assertIsInstance(provider, DummyLoggerProvider)
717+
self.assertIsInstance(provider.resource, Resource)
718+
self.assertEqual(
719+
provider.resource.attributes.get("service.name"),
720+
"otlp-service",
721+
)
722+
self.assertIsInstance(provider.processor, DummyLogRecordProcessor)
723+
self.assertIsInstance(
724+
provider.processor.exporter, DummyOTLPLogExporter
725+
)
726+
getLogger(__name__).error("hello")
727+
self.assertTrue(provider.processor.exporter.export_called)
725728

726729
def test_logging_init_exporter_uses_exporter_args_map(self):
727-
resource = Resource.create({})
728-
_init_logging(
729-
{"otlp": DummyOTLPLogExporter},
730-
resource=resource,
731-
exporter_args_map={
732-
DummyOTLPLogExporter: {"compression": "gzip"},
733-
DummyOTLPMetricExporter: {"compression": "no"},
734-
},
735-
)
736-
self.assertEqual(self.set_provider_mock.call_count, 1)
737-
provider = self.set_provider_mock.call_args[0][0]
738-
self.assertEqual(provider.processor.exporter.compression, "gzip")
730+
with ResetGlobalLoggingState():
731+
resource = Resource.create({})
732+
_init_logging(
733+
{"otlp": DummyOTLPLogExporter},
734+
resource=resource,
735+
exporter_args_map={
736+
DummyOTLPLogExporter: {"compression": "gzip"},
737+
DummyOTLPMetricExporter: {"compression": "no"},
738+
},
739+
)
740+
self.assertEqual(self.set_provider_mock.call_count, 1)
741+
provider = self.set_provider_mock.call_args[0][0]
742+
self.assertEqual(provider.processor.exporter.compression, "gzip")
739743

740744
@patch.dict(
741745
environ,
@@ -925,7 +929,7 @@ def test_initialize_components_kwargs(
925929
)
926930

927931
def test_basicConfig_works_with_otel_handler(self):
928-
with ClearLoggingHandlers():
932+
with ResetGlobalLoggingState():
929933
_init_logging(
930934
{"otlp": DummyOTLPLogExporter},
931935
Resource.create({}),
@@ -947,7 +951,7 @@ def test_basicConfig_works_with_otel_handler(self):
947951
)
948952

949953
def test_basicConfig_preserves_otel_handler(self):
950-
with ClearLoggingHandlers():
954+
with ResetGlobalLoggingState():
951955
_init_logging(
952956
{"otlp": DummyOTLPLogExporter},
953957
Resource.create({}),
@@ -962,7 +966,6 @@ def test_basicConfig_preserves_otel_handler(self):
962966
)
963967
handler = root_logger.handlers[0]
964968
self.assertIsInstance(handler, LoggingHandler)
965-
966969
logging.basicConfig()
967970

968971
self.assertGreater(len(root_logger.handlers), 1)
@@ -978,6 +981,49 @@ def test_basicConfig_preserves_otel_handler(self):
978981
"Should still have exactly one OpenTelemetry LoggingHandler",
979982
)
980983

984+
def test_dictConfig_preserves_otel_handler(self):
985+
with ResetGlobalLoggingState():
986+
_init_logging(
987+
{"otlp": DummyOTLPLogExporter},
988+
Resource.create({}),
989+
setup_logging_handler=True,
990+
)
991+
992+
root = logging.getLogger()
993+
self.assertEqual(
994+
len(root.handlers),
995+
1,
996+
"Should be exactly one OpenTelemetry LoggingHandler",
997+
)
998+
logging.config.dictConfig(
999+
{
1000+
"version": 1,
1001+
"disable_existing_loggers": False, # If this is True all loggers are disabled. Many unit tests assert loggers emit logs.
1002+
"handlers": {
1003+
"console": {
1004+
"class": "logging.StreamHandler",
1005+
"level": "DEBUG",
1006+
"stream": "ext://sys.stdout",
1007+
},
1008+
},
1009+
"loggers": {
1010+
"": { # root logger
1011+
"handlers": ["console"],
1012+
},
1013+
},
1014+
}
1015+
)
1016+
self.assertEqual(len(root.handlers), 2)
1017+
1018+
logging_handlers = [
1019+
h for h in root.handlers if isinstance(h, LoggingHandler)
1020+
]
1021+
self.assertEqual(
1022+
len(logging_handlers),
1023+
1,
1024+
"Should still have exactly one OpenTelemetry LoggingHandler",
1025+
)
1026+
9811027

9821028
class TestMetricsInit(TestCase):
9831029
def setUp(self):
@@ -1229,8 +1275,14 @@ def test_custom_configurator(self, mock_init_comp):
12291275
mock_init_comp.assert_called_once_with(**kwargs)
12301276

12311277

1232-
class ClearLoggingHandlers:
1278+
# Any test that calls _init_logging with setup_logging_handler=True
1279+
# should call _init_logging within this context manager, to
1280+
# ensure the global logging state is reset after the test.
1281+
class ResetGlobalLoggingState:
12331282
def __init__(self):
1283+
self.original_basic_config = logging.basicConfig
1284+
self.original_dict_config = logging.config.dictConfig
1285+
self.original_file_config = logging.config.fileConfig
12341286
self.root_logger = getLogger()
12351287
self.original_handlers = None
12361288

@@ -1243,6 +1295,9 @@ def __exit__(self, exc_type, exc_val, exc_tb):
12431295
self.root_logger.handlers = []
12441296
for handler in self.original_handlers:
12451297
self.root_logger.addHandler(handler)
1298+
logging.basicConfig = self.original_basic_config
1299+
logging.config.dictConfig = self.original_dict_config
1300+
logging.config.fileConfig = self.original_file_config
12461301

12471302

12481303
class TestClearLoggingHandlers(TestCase):
@@ -1254,7 +1309,7 @@ def test_preserves_handlers(self):
12541309
root_logger.addHandler(test_handler)
12551310
expected_handlers = initial_handlers + [test_handler]
12561311

1257-
with ClearLoggingHandlers():
1312+
with ResetGlobalLoggingState():
12581313
self.assertEqual(len(root_logger.handlers), 0)
12591314
temp_handler = logging.StreamHandler()
12601315
root_logger.addHandler(temp_handler)
@@ -1264,3 +1319,15 @@ def test_preserves_handlers(self):
12641319
self.assertIs(h1, h2)
12651320

12661321
root_logger.removeHandler(test_handler)
1322+
1323+
def test_preserves_original_logging_fns(self):
1324+
def f(x):
1325+
print("f")
1326+
1327+
with ResetGlobalLoggingState():
1328+
logging.basicConfig = f
1329+
logging.config.dictConfig = f
1330+
logging.config.fileConfig = f
1331+
self.assertEqual(logging.config.dictConfig.__name__, "dictConfig")
1332+
self.assertEqual(logging.basicConfig.__name__, "basicConfig")
1333+
self.assertEqual(logging.config.fileConfig.__name__, "fileConfig")

0 commit comments

Comments
 (0)