Skip to content

Commit edbd7dd

Browse files
authored
Merge pull request #15 from DataDog/tian.chu/trace-log-correlation
Support trace and log correlation
2 parents 47aae81 + bb27697 commit edbd7dd

File tree

6 files changed

+148
-18
lines changed

6 files changed

+148
-18
lines changed

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ If your Lambda function powers a performance-critical task (e.g., a consumer-fac
4949

5050
- DD_FLUSH_TO_LOG
5151

52+
To connect logs and traces, set the environment variable below to `True`. The default format of the AWS provided `LambdaLoggerHandler` will be overridden to inject `dd.trace_id` and `dd.span_id`. The default Datadog lambda log integration pipeline will automatically parse them and map the `dd.trace_id` into the reserved [trace_id attribute](https://docs.datadoghq.com/logs/processing/#trace-id-attribute).
53+
54+
- DD_LOGS_INJECTION
55+
56+
To debug the Datadog Lambda Layer, set the environment variable below to `DEBUG`.
57+
58+
- DD_LOG_LEVEL
59+
5260
### The Serverless Framework
5361

5462
If your Lambda function is deployed using the Serverless Framework, refer to this sample `serverless.yml`.
@@ -211,6 +219,28 @@ If your Lambda function is triggered by API Gateway via [the non-proxy integrati
211219

212220
If your Lambda function is deployed by the Serverless Framework, such a mapping template gets created by default.
213221

222+
## Log and Trace Correlations
223+
224+
To connect logs and traces, set the environment variable `DD_LOGS_INJECTION` to `True`. The log format of the AWS provided `LambdaLoggerHandler` will be overridden to inject `dd.trace_id` and `dd.span_id`. The default Datadog lambda log integration pipeline will automatically parse them and map the `dd.trace_id` into the reserved attribute [trace_id](https://docs.datadoghq.com/logs/processing/#trace-id-attribute).
225+
226+
If you use a custom logger handler to log in json, you can manually inject the ids using the helper function `get_correlation_ids`.
227+
228+
```python
229+
from datadog_lambda.wrapper import datadog_lambda_wrapper
230+
from ddtrace.helpers import get_correlation_ids
231+
232+
@datadog_lambda_wrapper
233+
def lambda_handler(event, context):
234+
trace_id, span_id = get_correlation_ids()
235+
logger.info({
236+
"message": "hello world",
237+
"dd": {
238+
"trace_id": trace_id,
239+
"span_id": span_id
240+
}
241+
})
242+
```
243+
214244
## Opening Issues
215245

216246
If you encounter a bug with this package, we want to hear about it. Before opening a new issue, search the existing issues to avoid duplicates.

datadog_lambda/tracing.py

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from aws_xray_sdk.core import xray_recorder
99

10+
from ddtrace import patch, tracer
1011
from datadog_lambda.constants import (
1112
SamplingPriority,
1213
TraceHeader,
@@ -18,6 +19,28 @@
1819
dd_trace_context = {}
1920

2021

22+
def _convert_xray_trace_id(xray_trace_id):
23+
"""
24+
Convert X-Ray trace id (hex)'s last 63 bits to a Datadog trace id (int).
25+
"""
26+
return str(0x7FFFFFFFFFFFFFFF & int(xray_trace_id[-16:], 16))
27+
28+
29+
def _convert_xray_entity_id(xray_entity_id):
30+
"""
31+
Convert X-Ray (sub)segement id (hex) to a Datadog span id (int).
32+
"""
33+
return str(int(xray_entity_id, 16))
34+
35+
36+
def _convert_xray_sampling(xray_sampled):
37+
"""
38+
Convert X-Ray sampled (True/False) to its Datadog counterpart.
39+
"""
40+
return str(SamplingPriority.USER_KEEP) if xray_sampled \
41+
else str(SamplingPriority.USER_REJECT)
42+
43+
2144
def extract_dd_trace_context(event):
2245
"""
2346
Extract Datadog trace context from the Lambda `event` object.
@@ -56,6 +79,8 @@ def extract_dd_trace_context(event):
5679
# reset to avoid using the context from the last invocation.
5780
dd_trace_context = {}
5881

82+
logger.debug('extracted dd trace context %s', dd_trace_context)
83+
5984

6085
def get_dd_trace_context():
6186
"""
@@ -91,23 +116,41 @@ def get_dd_trace_context():
91116
}
92117

93118

94-
def _convert_xray_trace_id(xray_trace_id):
119+
def set_correlation_ids():
95120
"""
96-
Convert X-Ray trace id (hex)'s last 63 bits to a Datadog trace id (int).
97-
"""
98-
return str(0x7FFFFFFFFFFFFFFF & int(xray_trace_id[-16:], 16))
121+
Create a dummy span, and overrides its trace_id and span_id, to make
122+
ddtrace.helpers.get_correlation_ids() return the correct ids for both
123+
auto and manual log correlations.
99124
100-
101-
def _convert_xray_entity_id(xray_entity_id):
125+
TODO: Remove me when Datadog tracer is natively supported in Lambda.
102126
"""
103-
Convert X-Ray (sub)segement id (hex) to a Datadog span id (int).
104-
"""
105-
return str(int(xray_entity_id, 16))
127+
context = get_dd_trace_context()
128+
span = tracer.trace('dummy.span')
129+
span.trace_id = context[TraceHeader.TRACE_ID]
130+
span.span_id = context[TraceHeader.PARENT_ID]
106131

132+
logger.debug('correlation ids set')
107133

108-
def _convert_xray_sampling(xray_sampled):
134+
135+
def inject_correlation_ids():
109136
"""
110-
Convert X-Ray sampled (True/False) to its Datadog counterpart.
137+
Override the formatter of LambdaLoggerHandler to inject datadog trace and
138+
span id for log correlation.
139+
140+
For manual injections to custom log handlers, use `ddtrace.helpers.get_correlation_ids`
141+
to retrieve correlation ids (trace_id, span_id).
111142
"""
112-
return str(SamplingPriority.USER_KEEP) if xray_sampled \
113-
else str(SamplingPriority.USER_REJECT)
143+
# Override the log format of the AWS provided LambdaLoggerHandler
144+
root_logger = logging.getLogger()
145+
for handler in root_logger.handlers:
146+
if handler.__class__.__name__ == 'LambdaLoggerHandler':
147+
handler.setFormatter(logging.Formatter(
148+
'[%(levelname)s]\t%(asctime)s.%(msecs)dZ\t%(aws_request_id)s\t'
149+
'[dd.trace_id=%(dd.trace_id)s dd.span_id=%(dd.span_id)s]\t%(message)s\n',
150+
'%Y-%m-%dT%H:%M:%S'
151+
))
152+
153+
# Patch `logging.Logger.makeRecord` to actually inject correlation ids
154+
patch(logging=True)
155+
156+
logger.debug('logs injection configured')

datadog_lambda/wrapper.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@
88
import traceback
99

1010
from datadog_lambda.metric import lambda_stats
11-
from datadog_lambda.tracing import extract_dd_trace_context
1211
from datadog_lambda.patch import patch_all
12+
from datadog_lambda.tracing import (
13+
extract_dd_trace_context,
14+
set_correlation_ids,
15+
inject_correlation_ids,
16+
)
1317

1418
logger = logging.getLogger(__name__)
1519

@@ -37,15 +41,23 @@ class _LambdaDecorator(object):
3741
def __init__(self, func):
3842
self.func = func
3943
self.flush_to_log = os.environ.get('DD_FLUSH_TO_LOG', '').lower() == 'true'
44+
self.logs_injection = os.environ.get('DD_LOGS_INJECTION', '').lower() == 'true'
45+
46+
# Inject trace correlation ids to logs
47+
if self.logs_injection:
48+
inject_correlation_ids()
49+
50+
# Patch HTTP clients to propagate Datadog trace context
51+
patch_all()
4052
logger.debug('datadog_lambda_wrapper initialized')
4153

4254
def _before(self, event, context):
4355
try:
4456
# Extract Datadog trace context from incoming requests
4557
extract_dd_trace_context(event)
4658

47-
# Patch HTTP clients to propogate Datadog trace context
48-
patch_all()
59+
# Set log correlation ids using extracted trace context
60+
set_correlation_ids()
4961
except Exception:
5062
traceback.print_exc()
5163

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
install_requires=[
3030
'aws-xray-sdk==2.4.2',
3131
'datadog==0.28.0',
32+
'ddtrace==0.28.0',
3233
'wrapt==1.11.1',
3334
'setuptools==40.8.0',
3435
'boto3==1.9.160'

tests/test_tracing.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
except ImportError:
55
from mock import MagicMock, patch
66

7+
from ddtrace.helpers import get_correlation_ids
78
from datadog_lambda.constants import (
89
SamplingPriority,
910
TraceHeader,
@@ -12,6 +13,7 @@
1213
from datadog_lambda.tracing import (
1314
extract_dd_trace_context,
1415
get_dd_trace_context,
16+
set_correlation_ids,
1517
_convert_xray_trace_id,
1618
_convert_xray_entity_id,
1719
_convert_xray_sampling,
@@ -150,3 +152,21 @@ def test_convert_xray_sampling(self):
150152
_convert_xray_sampling(False),
151153
str(SamplingPriority.USER_REJECT)
152154
)
155+
156+
157+
class TestLogsInjection(unittest.TestCase):
158+
159+
def setUp(self):
160+
patcher = patch('datadog_lambda.tracing.get_dd_trace_context')
161+
self.mock_get_dd_trace_context = patcher.start()
162+
self.mock_get_dd_trace_context.return_value = {
163+
TraceHeader.TRACE_ID: '123',
164+
TraceHeader.PARENT_ID: '456',
165+
}
166+
self.addCleanup(patcher.stop)
167+
168+
def test_set_correlation_ids(self):
169+
set_correlation_ids()
170+
trace_id, span_id = get_correlation_ids()
171+
self.assertEqual(trace_id, '123')
172+
self.assertEqual(span_id, '456')

tests/test_wrapper.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ def setUp(self):
2424
self.mock_extract_dd_trace_context = patcher.start()
2525
self.addCleanup(patcher.stop)
2626

27+
patcher = patch('datadog_lambda.wrapper.set_correlation_ids')
28+
self.mock_set_correlation_ids = patcher.start()
29+
self.addCleanup(patcher.stop)
30+
31+
patcher = patch('datadog_lambda.wrapper.inject_correlation_ids')
32+
self.mock_inject_correlation_ids = patcher.start()
33+
self.addCleanup(patcher.stop)
34+
2735
patcher = patch('datadog_lambda.wrapper.patch_all')
2836
self.mock_patch_all = patcher.start()
2937
self.addCleanup(patcher.stop)
@@ -42,6 +50,8 @@ def lambda_handler(event, context):
4250
])
4351
self.mock_wrapper_lambda_stats.flush.assert_called()
4452
self.mock_extract_dd_trace_context.assert_called_with(lambda_event)
53+
self.mock_set_correlation_ids.assert_called()
54+
self.mock_inject_correlation_ids.assert_not_called()
4555
self.mock_patch_all.assert_called()
4656

4757
def test_datadog_lambda_wrapper_flush_to_log(self):
@@ -57,7 +67,21 @@ def lambda_handler(event, context):
5767

5868
self.mock_metric_lambda_stats.distribution.assert_not_called()
5969
self.mock_wrapper_lambda_stats.flush.assert_not_called()
60-
self.mock_extract_dd_trace_context.assert_called_with(lambda_event)
61-
self.mock_patch_all.assert_called()
6270

6371
del os.environ["DD_FLUSH_TO_LOG"]
72+
73+
def test_datadog_lambda_wrapper_inject_correlation_ids(self):
74+
os.environ["DD_LOGS_INJECTION"] = 'True'
75+
76+
@datadog_lambda_wrapper
77+
def lambda_handler(event, context):
78+
lambda_metric("test.metric", 100)
79+
80+
lambda_event = {}
81+
lambda_context = {}
82+
lambda_handler(lambda_event, lambda_context)
83+
84+
self.mock_set_correlation_ids.assert_called()
85+
self.mock_inject_correlation_ids.assert_called()
86+
87+
del os.environ["DD_LOGS_INJECTION"]

0 commit comments

Comments
 (0)