Skip to content

Commit d2083ed

Browse files
fix(internal): call _fixupChildren when retrieving DDLogger [backport 1.15] (#6350)
Backport 11a8d09 from #6080 to 1.15. DDLogger is essentially a reimplementation of cPython's logger, but only called _fixupParents to cleanup PlaceHolders in the logger tree, not _fixupChildren. This led to bugs inheriting log level as demonstrated in the new unit tests. I filed #4856 5 months ago, but never received any comments on it. Personally, I disagree with the decision to re-implement cPython's logger by calling cPython internals. In the issue I filed, I describe a reasonable alternative using exclusively the public API. But, that's a bigger project, and I just need the ability to silence ddtrace logs in my application. So for now, I left the implementation as-is and brought both the code structure and functionality in line with the cPython implementation it's imitating. ## Checklist - [x] Change(s) are motivated and described in the PR description. - [x] Testing strategy is described if automated tests are not included in the PR. - [x] Risk is outlined (performance impact, potential for breakage, maintainability, etc). - [x] Change is maintainable (easy to change, telemetry, documentation). - [x] [Library release note guidelines](https://ddtrace.readthedocs.io/en/stable/contributing.html#Release-Note-Guidelines) are followed. - I don't have all of ddtrace's tooling installed. If someone could help me generate the release notes that would be awesome. - [x] Documentation is included (in-code, generated user docs, [public corp docs](https://github.com/DataDog/documentation/)). ## Reviewer Checklist - [x] Title is accurate. - [x] No unnecessary changes are introduced. - [x] Description motivates each change. - [x] Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes unless absolutely necessary. - [x] Testing strategy adequately addresses listed risk(s). - [x] Change is maintainable (easy to change, telemetry, documentation). - [x] Release note makes sense to a user of the library. - [x] Reviewer has explicitly acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment. Co-authored-by: mattalbr <[email protected]>
1 parent e5f763a commit d2083ed

File tree

3 files changed

+52
-18
lines changed

3 files changed

+52
-18
lines changed

ddtrace/internal/logger.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,26 @@ def get_logger(name):
4141
# DEV: This is a simplified version of `logging.Manager.getLogger`
4242
# https://github.com/python/cpython/blob/48769a28ad6ef4183508951fa6a378531ace26a4/Lib/logging/__init__.py#L1221-L1253 # noqa
4343
# DEV: _fixupParents could be adding a placeholder, we want to replace it if that's the case
44-
if name not in manager.loggerDict or isinstance(manager.loggerDict[name], logging.PlaceHolder):
45-
manager.loggerDict[name] = DDLogger(name=name)
46-
47-
# Get our logger
48-
logger = cast(DDLogger, manager.loggerDict[name])
49-
50-
# If this log manager has a `_fixupParents` method then call it on our logger
51-
# DEV: This helper is used to ensure our logger has an appropriate `Logger.parent` set,
52-
# without this then we cannot take advantage of the root loggers handlers
53-
# https://github.com/python/cpython/blob/7c7839329c2c66d051960ab1df096aed1cc9343e/Lib/logging/__init__.py#L1272-L1294 # noqa
54-
# DEV: `_fixupParents` has been around for awhile, but add the `hasattr` guard... just in case.
55-
if hasattr(manager, "_fixupParents"):
56-
manager._fixupParents(logger)
44+
if name in manager.loggerDict:
45+
logger = manager.loggerDict[name]
46+
if isinstance(manager.loggerDict[name], logging.PlaceHolder):
47+
placeholder = logger
48+
logger = DDLogger(name=name)
49+
manager.loggerDict[name] = logger
50+
# DEV: `_fixupChildren` and `_fixupParents` have been around for awhile,
51+
# DEV: but add the `hasattr` guard... just in case.
52+
if hasattr(manager, "_fixupChildren"):
53+
manager._fixupChildren(placeholder, logger)
54+
if hasattr(manager, "_fixupParents"):
55+
manager._fixupParents(logger)
56+
else:
57+
logger = DDLogger(name=name)
58+
manager.loggerDict[name] = logger
59+
if hasattr(manager, "_fixupParents"):
60+
manager._fixupParents(logger)
5761

5862
# Return our logger
59-
return logger
63+
return cast(DDLogger, logger)
6064

6165

6266
def hasHandlers(self):
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
fixes:
3+
- |
4+
internal: call ``_fixupChildren`` when retrieving ``DDLogger``

tests/tracer/test_logger.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,38 @@ def test_get_logger(self):
9595

9696
# If a PlaceHolder is in place of the logger
9797
# We should return the DDLogger
98-
placeholder = logging.PlaceHolder("test")
99-
self.manager.loggerDict["test.name.logger"] = placeholder
100-
log = get_logger("test.name.logger")
101-
self.assertEqual(log.name, "test.name.logger")
98+
self.assertIsInstance(self.manager.loggerDict["new.test"], logging.PlaceHolder)
99+
log = get_logger("new.test")
100+
self.assertEqual(log.name, "new.test")
102101
self.assertIsInstance(log, DDLogger)
103102

103+
def test_get_logger_children(self):
104+
"""
105+
When using `get_logger` to get a logger
106+
We appropriately assign children loggers
107+
108+
DEV: This test case is to ensure we are calling `manager._fixupChildren(logger)`
109+
"""
110+
root = get_logger("test")
111+
root.setLevel(logging.WARNING)
112+
113+
child_logger = get_logger("test.newplaceholder.long.component")
114+
self.assertEqual(child_logger.parent, root)
115+
116+
parent_logger = get_logger("test.newplaceholder")
117+
self.assertEqual(child_logger.parent, parent_logger)
118+
119+
parent_logger.setLevel(logging.INFO)
120+
# Because the child logger's level remains unset, it should inherit
121+
# the level of its closest parent, which is INFO.
122+
# If we did not properly maintain the logger tree, this would fail
123+
# because child_logger would be set to the default when it was created
124+
# which was logging.WARNING.
125+
self.assertEqual(child_logger.getEffectiveLevel(), logging.INFO)
126+
127+
# Clean up for future tests.
128+
root.setLevel(logging.NOTSET)
129+
104130
def test_get_logger_parents(self):
105131
"""
106132
When using `get_logger` to get a logger

0 commit comments

Comments
 (0)