Skip to content

Commit a2d9f1b

Browse files
zhammermajorgreys
andcommitted
fix: avoid context deadlock with logs injection enabled (#1338)
* don't try to get current span on patched internal logger * run black * add span directly to log record * add test * run black * contextually group test a bit better * don't add issue number Co-Authored-By: Tahir H. Butt <[email protected]> * rename logger_arg to logger_override 1/2 Co-Authored-By: Tahir H. Butt <[email protected]> * rename logger_arg to logger_override 2/2 Co-Authored-By: Tahir H. Butt <[email protected]> * fix arg name Co-authored-by: Tahir H. Butt <[email protected]>
1 parent 8ece999 commit a2d9f1b

File tree

4 files changed

+45
-7
lines changed

4 files changed

+45
-7
lines changed

ddtrace/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@
1717

1818
MANUAL_DROP_KEY = 'manual.drop'
1919
MANUAL_KEEP_KEY = 'manual.keep'
20+
21+
LOG_SPAN_KEY = '__datadog_log_span'

ddtrace/context.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import logging
22
import threading
33

4-
from .constants import HOSTNAME_KEY, SAMPLING_PRIORITY_KEY, ORIGIN_KEY
4+
from .constants import HOSTNAME_KEY, SAMPLING_PRIORITY_KEY, ORIGIN_KEY, LOG_SPAN_KEY
55
from .internal.logger import get_logger
66
from .internal import hostname
77
from .settings import config
@@ -139,12 +139,13 @@ def close_span(self, span):
139139
# some children. On the other hand, asynchronous web frameworks still expect
140140
# to close the root span after all the children.
141141
if span.tracer and span.tracer.log.isEnabledFor(logging.DEBUG) and span._parent is None:
142+
extra = {LOG_SPAN_KEY: span}
142143
unfinished_spans = [x for x in self._trace if not x.finished]
143144
if unfinished_spans:
144145
log.debug('Root span "%s" closed, but the trace has %d unfinished spans:',
145-
span.name, len(unfinished_spans))
146+
span.name, len(unfinished_spans), extra=extra)
146147
for wrong_span in unfinished_spans:
147-
log.debug('\n%s', wrong_span.pprint())
148+
log.debug('\n%s', wrong_span.pprint(), extra=extra)
148149

149150
def _is_sampled(self):
150151
return any(span.sampled for span in self._trace)

ddtrace/contrib/logging/patch.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,14 @@ def _w_makeRecord(func, instance, args, kwargs):
3535
setattr(record, RECORD_ATTR_ENV, ddtrace.config.env or "")
3636
setattr(record, RECORD_ATTR_SERVICE, ddtrace.config.service or "")
3737

38-
span = _get_current_span(tracer=ddtrace.config.logging.tracer)
38+
# logs from internal logger may explicitly pass the current span to
39+
# avoid deadlocks in getting the current span while already in locked code.
40+
span_from_log = getattr(record, ddtrace.constants.LOG_SPAN_KEY, None)
41+
if isinstance(span_from_log, ddtrace.Span):
42+
span = span_from_log
43+
else:
44+
span = _get_current_span(tracer=ddtrace.config.logging.tracer)
45+
3946
if span:
4047
setattr(record, RECORD_ATTR_TRACE_ID, span.trace_id)
4148
setattr(record, RECORD_ATTR_SPAN_ID, span.span_id)

tests/contrib/logging/test_logging.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,23 @@ def current_span(tracer=None):
2424
return tracer.current_span()
2525

2626

27-
def capture_function_log(func, fmt=DEFAULT_FORMAT):
27+
def capture_function_log(func, fmt=DEFAULT_FORMAT, logger_override=None):
28+
if logger_override is not None:
29+
logger_to_capture = logger_override
30+
else:
31+
logger_to_capture = logger
32+
2833
# add stream handler to capture output
2934
out = StringIO()
3035
sh = logging.StreamHandler(out)
3136

3237
try:
3338
formatter = logging.Formatter(fmt)
3439
sh.setFormatter(formatter)
35-
logger.addHandler(sh)
40+
logger_to_capture.addHandler(sh)
3641
result = func()
3742
finally:
38-
logger.removeHandler(sh)
43+
logger_to_capture.removeHandler(sh)
3944

4045
return out.getvalue().strip(), result
4146

@@ -158,3 +163,26 @@ def create_span():
158163

159164
with self.override_global_config(dict(version="global.version", env="global.env")):
160165
self._test_logging(create_span=create_span, version="global.version", env="global.env")
166+
167+
def test_unfinished_child(self):
168+
"""
169+
Check that closing a span with unfinished children correctly logs out
170+
unfinished spans.
171+
"""
172+
# unfinished child spans only logged if tracer log level is debug
173+
self.tracer.log.setLevel(logging.DEBUG)
174+
175+
# the actual logger used for these message is ddtrace.context logger,
176+
# so debug logging has to be enabled here as well.
177+
context_logger = logging.getLogger("ddtrace.context")
178+
context_logger.setLevel(logging.DEBUG)
179+
180+
# finish parent span without finishing child span
181+
parent = self.tracer.trace("parent")
182+
child = self.tracer.trace("child")
183+
out, span = capture_function_log(parent.finish, logger_override=context_logger)
184+
185+
assert 'Root span "parent" closed, but the trace has 1 unfinished spans' in out
186+
assert "parent_id {}".format(parent.span_id) in out
187+
assert "trace_id {}".format(child.trace_id) in out
188+
assert "id {}".format(child.span_id) in out

0 commit comments

Comments
 (0)