Skip to content

Commit 9b62ced

Browse files
Merge pull request #53 from DataDog/darcy.rayner/dd-trace-support
Darcy.rayner/dd trace support
2 parents 1503b3c + 3b47b39 commit 9b62ced

15 files changed

+268
-44
lines changed

README.md

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Datadog Lambda Layer for Python (2.7, 3.6, 3.7 and 3.8) enables custom metric su
1010

1111
## IMPORTANT NOTE
1212

13-
AWS Lambda is expected to recieve a [breaking change](https://aws.amazon.com/blogs/compute/upcoming-changes-to-the-python-sdk-in-aws-lambda/) on **January 30, 2021**. If you are using Datadog Python Lambda layer version 7 or below, please upgrade to version 11.
13+
AWS Lambda is expected to recieve a [breaking change](https://aws.amazon.com/blogs/compute/upcoming-changes-to-the-python-sdk-in-aws-lambda/) on **January 30, 2021**. If you are using Datadog Python Lambda layer version 7 or below, please upgrade to version 11.
1414

1515
## Installation
1616

@@ -21,6 +21,7 @@ arn:aws:lambda:<AWS_REGION>:464622532012:layer:Datadog-<PYTHON_RUNTIME>:<VERSION
2121
```
2222

2323
Replace `<AWS_REGION>` with the AWS region where your Lambda function is published to. Replace `<PYTHON_RUNTIME>` with one of the following that matches your Lambda's Python runtime:
24+
2425
- `Datadog-Python27`
2526
- `Datadog-Python36`
2627
- `Datadog-Python37`
@@ -81,7 +82,7 @@ If `DD_FLUSH_TO_LOG` is set to false (not recommended), the Datadog API Key must
8182
- DD_KMS_API_KEY - the KMS-encrypted API Key, requires the `kms:Decrypt` permission
8283
- DD_API_KEY_SECRET_ARN - the Secret ARN to fetch API Key from the Secrets Manager, requires the `secretsmanager:GetSecretValue` permission (and `kms:Decrypt` if using a customer managed CMK)
8384

84-
You can also supply or override the API key at runtime (not recommended):
85+
You can also supply or override the API key at runtime (not recommended):
8586

8687
```python
8788
# Override DD API Key after importing datadog_lambda packages
@@ -243,6 +244,7 @@ If your Lambda function is triggered by API Gateway via [the non-proxy integrati
243244
If your Lambda function is deployed by the Serverless Framework, such a mapping template gets created by default.
244245

245246
## Log and Trace Correlations
247+
246248
By default, the Datadog trace id gets automatically injected into the logs for correlation, if using the standard python `logging` library.
247249

248250
If you use a custom logger handler to log in json, you can inject the ids using the helper function `get_correlation_ids` [manually](https://docs.datadoghq.com/tracing/connect_logs_and_traces/?tab=python#manual-trace-id-injection).
@@ -265,6 +267,36 @@ def lambda_handler(event, context):
265267
})
266268
```
267269

270+
## Datadog Tracer (**Experimental**)
271+
272+
You can now trace Lambda functions using Datadog APM's tracing libraries ([dd-trace-py](https://github.com/DataDog/dd-trace-py)).
273+
274+
1. If you are using the Lambda layer, upgrade it to at least version 15.
275+
1. If you are using the pip package `datadog-lambda-python`, upgrade it to at least version `v2.15.0`.
276+
1. Install (or update to) the latest version of [Datadog forwarder Lambda function](https://docs.datadoghq.com/integrations/amazon_web_services/?tab=allpermissions#set-up-the-datadog-lambda-function). Ensure the trace forwarding layer is attached to the forwarder, e.g., ARN for Python 2.7 `arn:aws:lambda:<AWS_REGION>:464622532012:layer:Datadog-Trace-Forwarder-Python27:4`.
277+
1. Set the environment variable `DD_TRACE_ENABLED` to true on your function.
278+
1. Instrument your function using `dd-trace`.
279+
280+
```py
281+
from datadog_lambda.metric import lambda_metric
282+
from datadog_lambda.wrapper import datadog_lambda_wrapper
283+
284+
from ddtrace import tracer
285+
286+
@datadog_lambda_wrapper
287+
def hello(event, context):
288+
return {
289+
"statusCode": 200,
290+
"body": get_message()
291+
}
292+
293+
@tracer.wrap()
294+
def get_message():
295+
return "hello world"
296+
```
297+
298+
You can also use `dd-trace` and the X-Ray tracer together and merge the traces into one, using the environment variable `DD_MERGE_XRAY_TRACES` to true on your function.
299+
268300
## Opening Issues
269301

270302
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/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# The minor version corresponds to the Lambda layer version.
22
# E.g.,, version 0.5.0 gets packaged into layer version 5.
3-
__version__ = "2.14.0"
3+
__version__ = "2.15.0"
44

55

66
import os

datadog_lambda/constants.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,12 @@ class XraySubsegment(object):
2424
NAME = "datadog-metadata"
2525
KEY = "trace"
2626
NAMESPACE = "datadog"
27+
28+
29+
# TraceContextSource of datadog context. The DD_MERGE_XRAY_TRACES
30+
# feature uses this to determine when to use X-Ray as the parent
31+
# trace.
32+
class TraceContextSource(object):
33+
XRAY = "xray"
34+
EVENT = "event"
35+
DDTRACE = "ddtrace"

datadog_lambda/tracing.py

Lines changed: 119 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,26 @@
44
# Copyright 2019 Datadog, Inc.
55

66
import logging
7+
import os
78

89
from aws_xray_sdk.core import xray_recorder
910
from aws_xray_sdk.core.lambda_launcher import LambdaContext
1011

11-
from ddtrace import patch, tracer
12-
from datadog_lambda.constants import SamplingPriority, TraceHeader, XraySubsegment
12+
from datadog_lambda.constants import (
13+
SamplingPriority,
14+
TraceHeader,
15+
XraySubsegment,
16+
TraceContextSource,
17+
)
18+
from ddtrace import tracer, patch
19+
from ddtrace.propagation.http import HTTPPropagator
1320

1421
logger = logging.getLogger(__name__)
1522

1623
dd_trace_context = {}
24+
dd_tracing_enabled = os.environ.get("DD_TRACE_ENABLED", "false").lower() == "true"
25+
26+
propagator = HTTPPropagator()
1727

1828

1929
def _convert_xray_trace_id(xray_trace_id):
@@ -41,6 +51,43 @@ def _convert_xray_sampling(xray_sampled):
4151
)
4252

4353

54+
def _get_xray_trace_context():
55+
if not is_lambda_context():
56+
return None
57+
58+
xray_trace_entity = xray_recorder.get_trace_entity() # xray (sub)segment
59+
return {
60+
"trace-id": _convert_xray_trace_id(xray_trace_entity.trace_id),
61+
"parent-id": _convert_xray_entity_id(xray_trace_entity.id),
62+
"sampling-priority": _convert_xray_sampling(xray_trace_entity.sampled),
63+
"source": TraceContextSource.XRAY,
64+
}
65+
66+
67+
def _get_dd_trace_py_context():
68+
span = tracer.current_span()
69+
if not span:
70+
return None
71+
72+
parent_id = span.context.span_id
73+
trace_id = span.context.trace_id
74+
sampling_priority = span.context.sampling_priority
75+
return {
76+
"parent-id": str(parent_id),
77+
"trace-id": str(trace_id),
78+
"sampling-priority": str(sampling_priority),
79+
"source": TraceContextSource.DDTRACE,
80+
}
81+
82+
83+
def _context_obj_to_headers(obj):
84+
return {
85+
TraceHeader.TRACE_ID: str(obj.get("trace-id")),
86+
TraceHeader.PARENT_ID: str(obj.get("parent-id")),
87+
TraceHeader.SAMPLING_PRIORITY: str(obj.get("sampling-priority")),
88+
}
89+
90+
4491
def extract_dd_trace_context(event):
4592
"""
4693
Extract Datadog trace context from the Lambda `event` object.
@@ -61,23 +108,24 @@ def extract_dd_trace_context(event):
61108
sampling_priority = lowercase_headers.get(TraceHeader.SAMPLING_PRIORITY)
62109
if trace_id and parent_id and sampling_priority:
63110
logger.debug("Extracted Datadog trace context from headers")
64-
dd_trace_context = {
111+
metadata = {
65112
"trace-id": trace_id,
66113
"parent-id": parent_id,
67114
"sampling-priority": sampling_priority,
68115
}
69116
xray_recorder.begin_subsegment(XraySubsegment.NAME)
70117
subsegment = xray_recorder.current_subsegment()
71-
subsegment.put_metadata(
72-
XraySubsegment.KEY, dd_trace_context, XraySubsegment.NAMESPACE
73-
)
118+
119+
subsegment.put_metadata(XraySubsegment.KEY, metadata, XraySubsegment.NAMESPACE)
120+
dd_trace_context = metadata.copy()
121+
dd_trace_context["source"] = TraceContextSource.EVENT
74122
xray_recorder.end_subsegment()
75123
else:
76124
# AWS Lambda runtime caches global variables between invocations,
77125
# reset to avoid using the context from the last invocation.
78-
dd_trace_context = {}
79-
126+
dd_trace_context = _get_xray_trace_context()
80127
logger.debug("extracted dd trace context %s", dd_trace_context)
128+
return dd_trace_context
81129

82130

83131
def get_dd_trace_context():
@@ -86,32 +134,38 @@ def get_dd_trace_context():
86134
87135
If the Lambda function is invoked by a Datadog-traced service, a Datadog
88136
trace context may already exist, and it should be used. Otherwise, use the
89-
current X-Ray trace entity.
137+
current X-Ray trace entity, or the dd-trace-py context if DD_TRACE_ENABLED is true.
90138
91139
Most of widely-used HTTP clients are patched to inject the context
92140
automatically, but this function can be used to manually inject the trace
93141
context to an outgoing request.
94142
"""
95-
if not is_lambda_context():
96-
logger.debug("get_dd_trace_context is only supported in LambdaContext")
97-
return {}
98-
99143
global dd_trace_context
100-
xray_trace_entity = xray_recorder.get_trace_entity() # xray (sub)segment
101-
if dd_trace_context:
102-
return {
103-
TraceHeader.TRACE_ID: dd_trace_context["trace-id"],
104-
TraceHeader.PARENT_ID: _convert_xray_entity_id(xray_trace_entity.id),
105-
TraceHeader.SAMPLING_PRIORITY: dd_trace_context["sampling-priority"],
106-
}
107-
else:
108-
return {
109-
TraceHeader.TRACE_ID: _convert_xray_trace_id(xray_trace_entity.trace_id),
110-
TraceHeader.PARENT_ID: _convert_xray_entity_id(xray_trace_entity.id),
111-
TraceHeader.SAMPLING_PRIORITY: _convert_xray_sampling(
112-
xray_trace_entity.sampled
113-
),
114-
}
144+
145+
context = None
146+
xray_context = None
147+
148+
try:
149+
xray_context = _get_xray_trace_context() # xray (sub)segment
150+
except Exception as e:
151+
logger.debug(
152+
"get_dd_trace_context couldn't read from segment from x-ray, with error %s"
153+
% e
154+
)
155+
156+
if xray_context and not dd_trace_context:
157+
context = xray_context
158+
elif xray_context and dd_trace_context:
159+
context = dd_trace_context.copy()
160+
context["parent-id"] = xray_context["parent-id"]
161+
162+
if dd_tracing_enabled:
163+
dd_trace_py_context = _get_dd_trace_py_context()
164+
if dd_trace_py_context is not None:
165+
logger.debug("get_dd_trace_context using dd-trace context")
166+
context = dd_trace_py_context
167+
168+
return _context_obj_to_headers(context) if context is not None else {}
115169

116170

117171
def set_correlation_ids():
@@ -125,6 +179,9 @@ def set_correlation_ids():
125179
if not is_lambda_context():
126180
logger.debug("set_correlation_ids is only supported in LambdaContext")
127181
return
182+
if dd_tracing_enabled:
183+
logger.debug("using ddtrace implementation for spans")
184+
return
128185

129186
context = get_dd_trace_context()
130187

@@ -167,3 +224,37 @@ def is_lambda_context():
167224
regular `Context` (e.g., when testing lambda functions locally).
168225
"""
169226
return type(xray_recorder.context) == LambdaContext
227+
228+
229+
def set_dd_trace_py_root(trace_context, merge_xray_traces):
230+
if trace_context["source"] == TraceContextSource.EVENT or merge_xray_traces:
231+
headers = get_dd_trace_context()
232+
span_context = propagator.extract(headers)
233+
tracer.context_provider.activate(span_context)
234+
235+
236+
def create_function_execution_span(
237+
context, function_name, is_cold_start, trace_context
238+
):
239+
tags = {}
240+
if context:
241+
tags = {
242+
"cold_start": str(is_cold_start).lower(),
243+
"function_arn": context.invoked_function_arn,
244+
"request_id": context.aws_request_id,
245+
"resource_names": context.function_name,
246+
}
247+
source = trace_context["source"]
248+
if source != TraceContextSource.DDTRACE:
249+
tags["_dd.parent_source"] = source
250+
251+
args = {
252+
"service": "aws.lambda",
253+
"resource": function_name,
254+
"span_type": "serverless",
255+
}
256+
tracer.set_tags({"_dd.origin": "lambda"})
257+
span = tracer.trace("aws.lambda", **args)
258+
if span:
259+
span.set_tags(tags)
260+
return span

datadog_lambda/wrapper.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import logging
88
import traceback
99

10-
from datadog_lambda.cold_start import set_cold_start
10+
from datadog_lambda.cold_start import set_cold_start, is_cold_start
1111
from datadog_lambda.metric import (
1212
lambda_stats,
1313
submit_invocations_metric,
@@ -16,10 +16,13 @@
1616
from datadog_lambda.patch import patch_all
1717
from datadog_lambda.tracing import (
1818
extract_dd_trace_context,
19-
set_correlation_ids,
2019
inject_correlation_ids,
20+
dd_tracing_enabled,
21+
set_correlation_ids,
22+
set_dd_trace_py_root,
23+
create_function_execution_span,
2124
)
22-
25+
from ddtrace import patch_all as patch_all_dd
2326

2427
logger = logging.getLogger(__name__)
2528

@@ -81,13 +84,21 @@ def __init__(self, func):
8184
self.logs_injection = (
8285
os.environ.get("DD_LOGS_INJECTION", "true").lower() == "true"
8386
)
87+
self.merge_xray_traces = (
88+
os.environ.get("DD_MERGE_XRAY_TRACES", "false").lower() == "true"
89+
)
90+
self.function_name = os.environ.get("AWS_LAMBDA_FUNCTION_NAME", "function")
8491

8592
# Inject trace correlation ids to logs
8693
if self.logs_injection:
8794
inject_correlation_ids()
8895

89-
# Patch HTTP clients to propagate Datadog trace context
90-
patch_all()
96+
if not dd_tracing_enabled:
97+
# When using dd_trace_py it will patch all the http clients for us,
98+
# Patch HTTP clients to propagate Datadog trace context
99+
patch_all()
100+
else:
101+
patch_all_dd()
91102
logger.debug("datadog_lambda_wrapper initialized")
92103
except Exception:
93104
traceback.print_exc()
@@ -105,19 +116,29 @@ def __call__(self, event, context, **kwargs):
105116

106117
def _before(self, event, context):
107118
try:
119+
108120
set_cold_start()
109121
submit_invocations_metric(context)
110122
# Extract Datadog trace context from incoming requests
111-
extract_dd_trace_context(event)
123+
dd_context = extract_dd_trace_context(event)
124+
125+
self.span = None
126+
if dd_tracing_enabled:
127+
set_dd_trace_py_root(dd_context, self.merge_xray_traces)
128+
self.span = create_function_execution_span(
129+
context, self.function_name, is_cold_start(), dd_context
130+
)
131+
else:
132+
set_correlation_ids()
112133

113-
# Set log correlation ids using extracted trace context
114-
set_correlation_ids()
115134
logger.debug("datadog_lambda_wrapper _before() done")
116135
except Exception:
117136
traceback.print_exc()
118137

119138
def _after(self, event, context):
120139
try:
140+
if self.span:
141+
self.span.finish()
121142
if not self.flush_to_log:
122143
lambda_stats.flush(float("inf"))
123144
logger.debug("datadog_lambda_wrapper _after() done")

scripts/publish_staging.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/bin/bash
2+
set -e
3+
4+
./scripts/build_layers.sh
5+
./scripts/publish_layers.sh us-east-1

0 commit comments

Comments
 (0)