Skip to content

Commit b1f4e26

Browse files
feat[logging]: Replacement API for get_correlation_ids (#2654) (#2797)
* Replacement API for get_correlation_ids to match logging integration * Changed helper name, added release note. * Moved helper to be a tracer instance method, added to opentracer as well. * Spelling fix * Ensure get_log_correlation_context always returns a DDLogRecord * Replaced DDLogRecord with dictionary result * Updated docstrings * Update ddtrace/opentracer/tracer.py Co-authored-by: Brett Langdon <[email protected]> Co-authored-by: Brett Langdon <[email protected]> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> (cherry picked from commit 65f0634) Co-authored-by: Yun Kim <[email protected]>
1 parent f52002d commit b1f4e26

File tree

5 files changed

+184
-0
lines changed

5 files changed

+184
-0
lines changed

ddtrace/opentracer/tracer.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,3 +364,12 @@ def extract(self, format, carrier): # noqa: A002
364364
dd_span_ctx = ot_span_ctx._dd_context
365365
self._dd_tracer.context_provider.activate(dd_span_ctx)
366366
return ot_span_ctx
367+
368+
def get_log_correlation_context(self):
369+
# type: () -> Dict[str, str]
370+
"""Retrieves the data used to correlate a log with the current active trace.
371+
Generates a dictionary for custom logging instrumentation including the trace id and
372+
span id of the current active span, as well as the configured service, version, and environment names.
373+
If there is no active span, a dictionary with an empty string for each value will be returned.
374+
"""
375+
return self._dd_tracer.get_log_correlation_context()

ddtrace/tracer.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,25 @@ def current_trace_context(self, *args, **kwargs):
246246
return active.context
247247
return None
248248

249+
def get_log_correlation_context(self):
250+
# type: () -> Dict[str, str]
251+
"""Retrieves the data used to correlate a log with the current active trace.
252+
Generates a dictionary for custom logging instrumentation including the trace id and
253+
span id of the current active span, as well as the configured service, version, and environment names.
254+
If there is no active span, a dictionary with an empty string for each value will be returned.
255+
"""
256+
span = None
257+
if self.enabled:
258+
span = self.current_span()
259+
260+
return {
261+
"trace_id": str(span.trace_id) if span else "0",
262+
"span_id": str(span.span_id) if span else "0",
263+
"service": config.service or "",
264+
"version": config.version or "",
265+
"env": config.env or "",
266+
}
267+
249268
# TODO: deprecate this method and make sure users create a new tracer if they need different parameters
250269
def configure(
251270
self,
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
features:
3+
- |
4+
The ``ddtrace.Tracer.get_log_correlation_context`` method has been added to replace
5+
``ddtrace.helpers.get_correlation_ids``. It now returns a dictionary which includes the current span's
6+
trace and span ids, as well as the configured service, version, and environment names.

riotfile.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ def select_pys(min_version=MIN_PYTHON_VERSION, max_version=MAX_PYTHON_VERSION):
195195
"msgpack": latest,
196196
"attrs": ["==19.2.0", latest],
197197
"packaging": ["==17.1", latest],
198+
"structlog": latest,
198199
},
199200
)
200201
],
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import pytest
2+
import structlog
3+
4+
from ddtrace import Tracer
5+
from ddtrace import config
6+
from ddtrace import tracer
7+
from ddtrace.opentracer.tracer import Tracer as OT_Tracer
8+
from tests.utils import override_global_config
9+
10+
11+
@pytest.fixture
12+
def global_config():
13+
config.service = "test-service"
14+
config.env = "test-env"
15+
config.version = "test-version"
16+
yield
17+
config.service = config.env = config.version = None
18+
19+
20+
def tracer_injection(logger, log_method, event_dict):
21+
"""Inject tracer information into custom structlog log"""
22+
correlation_log_context = tracer.get_log_correlation_context()
23+
# add ids and configs to structlog event dictionary
24+
event_dict["dd"] = correlation_log_context
25+
return event_dict
26+
27+
28+
class TestCorrelationLogsContext(object):
29+
def test_get_log_correlation_context(self, global_config):
30+
"""Ensure expected DDLogRecord is generated via get_correlation_log_record."""
31+
with tracer.trace("test-span-1") as span1:
32+
dd_log_record = tracer.get_log_correlation_context()
33+
assert dd_log_record == {
34+
"span_id": str(span1.span_id),
35+
"trace_id": str(span1.trace_id),
36+
"service": "test-service",
37+
"env": "test-env",
38+
"version": "test-version",
39+
}
40+
test_tracer = Tracer()
41+
with test_tracer.trace("test-span-2") as span2:
42+
dd_log_record = test_tracer.get_log_correlation_context()
43+
assert dd_log_record == {
44+
"span_id": str(span2.span_id),
45+
"trace_id": str(span2.trace_id),
46+
"service": "test-service",
47+
"env": "test-env",
48+
"version": "test-version",
49+
}
50+
51+
def test_get_log_correlation_context_opentracer(self, global_config):
52+
"""Ensure expected DDLogRecord generated via get_correlation_log_record with an opentracing Tracer."""
53+
ot_tracer = OT_Tracer()
54+
with ot_tracer.start_active_span("operation") as scope:
55+
dd_span = scope._span._dd_span
56+
dd_log_record = ot_tracer.get_log_correlation_context()
57+
assert dd_log_record == {
58+
"span_id": str(dd_span.span_id),
59+
"trace_id": str(dd_span.trace_id),
60+
"service": "test-service",
61+
"env": "test-env",
62+
"version": "test-version",
63+
}
64+
65+
def test_get_log_correlation_context_no_active_span(self):
66+
"""Ensure empty DDLogRecord generated if no active span."""
67+
dd_log_record = tracer.get_log_correlation_context()
68+
assert dd_log_record == {
69+
"span_id": "0",
70+
"trace_id": "0",
71+
"service": "",
72+
"env": "",
73+
"version": "",
74+
}
75+
76+
def test_get_log_correlation_context_disabled_tracer(self):
77+
"""Ensure get_correlation_log_record returns None if tracer is disabled."""
78+
tracer = Tracer()
79+
tracer.enabled = False
80+
with tracer.trace("test-span"):
81+
dd_log_record = tracer.get_log_correlation_context()
82+
assert dd_log_record == {
83+
"span_id": "0",
84+
"trace_id": "0",
85+
"service": "",
86+
"env": "",
87+
"version": "",
88+
}
89+
90+
def test_custom_logging_injection(self):
91+
"""Ensure custom log injection via get_correlation_log_record returns proper active span information."""
92+
capture_log = structlog.testing.LogCapture()
93+
structlog.configure(processors=[tracer_injection, capture_log, structlog.processors.JSONRenderer()])
94+
logger = structlog.get_logger()
95+
96+
with tracer.trace("test span") as span:
97+
logger.msg("Hello!")
98+
99+
assert len(capture_log.entries) == 1
100+
assert capture_log.entries[0]["event"] == "Hello!"
101+
dd_log_record = capture_log.entries[0]["dd"]
102+
assert dd_log_record == {
103+
"span_id": str(span.span_id),
104+
"trace_id": str(span.trace_id),
105+
"service": "",
106+
"env": "",
107+
"version": "",
108+
}
109+
110+
def test_custom_logging_injection_global_config(self):
111+
"""Ensure custom log injection via get_correlation_log_record returns proper tracer information."""
112+
capture_log = structlog.testing.LogCapture()
113+
structlog.configure(processors=[tracer_injection, capture_log, structlog.processors.JSONRenderer()])
114+
logger = structlog.get_logger()
115+
116+
with override_global_config(dict(version="global-version", env="global-env", service="global-service")):
117+
with tracer.trace("test span") as span:
118+
logger.msg("Hello!")
119+
120+
assert len(capture_log.entries) == 1
121+
assert capture_log.entries[0]["event"] == "Hello!"
122+
dd_log_record = capture_log.entries[0]["dd"]
123+
assert dd_log_record == {
124+
"span_id": str(span.span_id),
125+
"trace_id": str(span.trace_id),
126+
"service": "global-service",
127+
"env": "global-env",
128+
"version": "global-version",
129+
}
130+
131+
def test_custom_logging_injection_no_span(self):
132+
"""Ensure custom log injection via get_correlation_log_record with no active span returns empty record."""
133+
capture_log = structlog.testing.LogCapture()
134+
structlog.configure(processors=[tracer_injection, capture_log, structlog.processors.JSONRenderer()])
135+
logger = structlog.get_logger()
136+
137+
with override_global_config(dict(version="global-version", env="global-env", service="global-service")):
138+
logger.msg("No Span!")
139+
140+
assert len(capture_log.entries) == 1
141+
assert capture_log.entries[0]["event"] == "No Span!"
142+
dd_log_record = capture_log.entries[0]["dd"]
143+
assert dd_log_record == {
144+
"span_id": "0",
145+
"trace_id": "0",
146+
"service": "global-service",
147+
"env": "global-env",
148+
"version": "global-version",
149+
}

0 commit comments

Comments
 (0)