Skip to content

Commit 8188475

Browse files
committed
Support hybrid tracing between Datadog and AWS X-Ray
1 parent f209cc8 commit 8188475

21 files changed

+836
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
*.py[cod]
22

3+
# Layer zips
4+
.layers
5+
36
# C extensions
47
*.so
58

@@ -28,6 +31,7 @@ pip-log.txt
2831
nosetests.xml
2932

3033
#Misc
34+
.cache/
3135
.DS_Store
3236
.eggs/
3337
.env/

Dockerfile

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
ARG image
2+
FROM $image
3+
4+
ARG runtime
5+
6+
# Create the directory structure required for AWS Lambda Layer
7+
RUN mkdir -p /build/python/lib/$runtime/site-packages
8+
WORKDIR /build
9+
10+
# Install dependencies
11+
COPY requirements.txt requirements.txt
12+
RUN pip install -r requirements.txt -t ./python/lib/$runtime/site-packages
13+
14+
# Install datadog_lambda
15+
COPY datadog_lambda ./python/lib/$runtime/site-packages
16+
17+
# Remove *.pyc files
18+
RUN find ./python/lib/$runtime/site-packages -name \*.pyc -delete

datadog_lambda/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__version__ = "1"

datadog_lambda/constants.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Datadog trace sampling priority
2+
class SamplingPriority(object):
3+
USER_REJECT = -1
4+
AUTO_REJECT = 0
5+
AUTO_KEEP = 1
6+
USER_KEEP = 2
7+
8+
9+
# Datadog trace headers
10+
class TraceHeader(object):
11+
TRACE_ID = 'x-datadog-trace-id'
12+
PARENT_ID = 'x-datadog-parent-id'
13+
SAMPLING_PRIORITY = 'x-datadog-sampling-priority'
14+
15+
16+
# X-Ray subsegment to save Datadog trace metadata
17+
class XraySubsegment(object):
18+
NAME = 'datadog-metadata'
19+
KEY = 'trace'
20+
NAMESPACE = 'datadog'

datadog_lambda/datadog_lambda

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/Users/tian.chu/dd/datadog-lambda-layer-python/datadog_lambda

datadog_lambda/metric.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import os
2+
import sys
3+
4+
from datadog import api
5+
from datadog.threadstats import ThreadStats
6+
from datadog_lambda import __version__
7+
8+
lambda_stats = ThreadStats()
9+
lambda_stats.start()
10+
11+
12+
def _format_dd_lambda_layer_tag():
13+
"""
14+
Formats the dd_lambda_layer tag, e.g., 'dd_lambda_layer:datadog-python27_1'
15+
"""
16+
runtime = "python{}{}".format(sys.version_info[0], sys.version_info[1])
17+
return "dd_lambda_layer:datadog-{}_{}".format(runtime, __version__)
18+
19+
20+
def _tag_dd_lambda_layer(args, kwargs):
21+
"""
22+
Used by lambda_metric to insert the dd_lambda_layer tag
23+
"""
24+
dd_lambda_layer_tag = _format_dd_lambda_layer_tag()
25+
if 'tags' in kwargs:
26+
kwargs['tags'].append(dd_lambda_layer_tag)
27+
elif len(args) >= 4:
28+
args[3].append(dd_lambda_layer_tag)
29+
else:
30+
kwargs['tags'] = [dd_lambda_layer_tag]
31+
32+
33+
def lambda_metric(*args, **kwargs):
34+
"""
35+
Submit a data point to Datadog distribution metrics.
36+
https://docs.datadoghq.com/graphing/metrics/distributions/
37+
"""
38+
_tag_dd_lambda_layer(args, kwargs)
39+
lambda_stats.distribution(*args, **kwargs)
40+
41+
42+
def init_api_client():
43+
"""
44+
No-op GET to initialize the requests connection with DD's endpoints,
45+
to make the final flush faster.
46+
47+
We keep alive the Requests session, this means that we can re-use
48+
the connection. The consequence is that the HTTP Handshake, which
49+
can take hundreds of ms, is now made at the beginning of a lambda
50+
instead of at the end.
51+
52+
By making the initial request async, we spare a lot of execution
53+
time in the lambdas.
54+
"""
55+
api._api_key = os.environ.get('DATADOG_API_KEY')
56+
api._api_host = os.environ.get('DATADOG_HOST', 'https://api.datadoghq.com')
57+
try:
58+
api.api_client.APIClient.submit('GET', 'validate')
59+
except Exception:
60+
pass

datadog_lambda/patch.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import sys
2+
3+
from wrapt import wrap_function_wrapper as wrap
4+
5+
from datadog_lambda.tracing import get_dd_trace_context
6+
7+
if sys.version_info >= (3, 0, 0):
8+
httplib_module = 'http.client'
9+
else:
10+
httplib_module = 'httplib'
11+
12+
_httplib_patched = False
13+
_requests_patched = False
14+
_botocore_requests_patched = False
15+
16+
17+
def patch_all():
18+
"""
19+
Patch the widely-used HTTP clients to automatically inject
20+
Datadog trace context.
21+
"""
22+
_patch_httplib()
23+
_patch_requests()
24+
_patch_botocore_requests()
25+
26+
27+
def _patch_httplib():
28+
"""
29+
Patch the Python built-in `httplib` (Python 2) or
30+
`http.client` (Python 3) module.
31+
"""
32+
global _httplib_patched
33+
if not _httplib_patched:
34+
_httplib_patched = True
35+
wrap(
36+
httplib_module,
37+
'HTTPConnection.request',
38+
_wrap_httplib_request
39+
)
40+
41+
42+
def _patch_requests():
43+
"""
44+
Patch the high-level HTTP client module `requests`
45+
if it's installed.
46+
"""
47+
global _requests_patched
48+
if not _requests_patched:
49+
_requests_patched = True
50+
try:
51+
wrap(
52+
'requests',
53+
'Session.request',
54+
_wrap_requests_request
55+
)
56+
except ImportError:
57+
pass
58+
59+
60+
def _patch_botocore_requests():
61+
"""
62+
Patch the `requests` module that is packaged into `botocore`.
63+
https://stackoverflow.com/questions/40741282/cannot-use-requests-module-on-aws-lambda
64+
"""
65+
global _botocore_requests_patched
66+
if not _botocore_requests_patched:
67+
_botocore_requests_patched = True
68+
try:
69+
wrap(
70+
'botocore.vendored.requests',
71+
'Session.request',
72+
_wrap_requests_request
73+
)
74+
except ImportError:
75+
pass
76+
77+
78+
def _wrap_requests_request(func, instance, args, kwargs):
79+
"""
80+
Wrap `requests.Session.request` to inject the Datadog trace headers
81+
into the outgoing requests.
82+
"""
83+
context = get_dd_trace_context()
84+
if 'headers' in kwargs:
85+
kwargs['headers'].update(context)
86+
elif len(args) >= 5:
87+
args[4].update(context)
88+
else:
89+
kwargs['headers'] = context
90+
return func(*args, **kwargs)
91+
92+
93+
def _wrap_httplib_request(func, instance, args, kwargs):
94+
"""
95+
Wrap `httplib` (python2) or `http.client` (python3) to inject
96+
the Datadog trace headers into the outgoing requests.
97+
"""
98+
context = get_dd_trace_context()
99+
if 'headers' in kwargs:
100+
kwargs['headers'].update(context)
101+
elif len(args) >= 4:
102+
args[3].update(context)
103+
else:
104+
kwargs['headers'] = context
105+
return func(*args, **kwargs)

datadog_lambda/tracing.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
from aws_xray_sdk.core import xray_recorder
2+
3+
from datadog_lambda.constants import (
4+
SamplingPriority,
5+
TraceHeader,
6+
XraySubsegment,
7+
)
8+
9+
dd_trace_context = {}
10+
11+
12+
def extract_dd_trace_context(event):
13+
"""
14+
Extract Datadog trace context from the Lambda `event` object.
15+
16+
Write the context to a global `dd_trace_context`, so the trace
17+
can be continued on the outgoing requests with the context injected.
18+
19+
Save the context to an X-Ray subsegment's metadata field, so the X-Ray
20+
trace can be converted to a Datadog trace in the Datadog backend with
21+
the correct context.
22+
"""
23+
global dd_trace_context
24+
headers = event.get('headers', {})
25+
trace_id = headers.get(TraceHeader.TRACE_ID)
26+
parent_id = headers.get(TraceHeader.PARENT_ID)
27+
sampling_priority = headers.get(TraceHeader.SAMPLING_PRIORITY)
28+
if trace_id and parent_id and sampling_priority:
29+
dd_trace_context = {
30+
'trace-id': trace_id,
31+
'parent-id': parent_id,
32+
'sampling-priority': sampling_priority,
33+
}
34+
xray_recorder.begin_subsegment(XraySubsegment.NAME)
35+
subsegment = xray_recorder.current_subsegment()
36+
subsegment.put_metadata(
37+
XraySubsegment.KEY,
38+
dd_trace_context,
39+
XraySubsegment.NAMESPACE
40+
)
41+
xray_recorder.end_subsegment()
42+
else:
43+
# AWS Lambda runtime caches global variables between invocations,
44+
# reset to avoid using the context from the last invocation.
45+
dd_trace_context = {}
46+
47+
48+
def get_dd_trace_context():
49+
"""
50+
Return the Datadog trace context to be propogated on the outgoing requests.
51+
52+
If the Lambda function is invoked by a Datadog-traced service, a Datadog
53+
trace
54+
context may already exist, and it should be used. Otherwise, use the
55+
current X-Ray trace entity.
56+
57+
Most of widely-used HTTP clients are patched to inject the context
58+
automatically, but this function can be used to manually inject the trace
59+
context to an outgoing request.
60+
"""
61+
global dd_trace_context
62+
xray_trace_entity = xray_recorder.get_trace_entity() # xray (sub)segment
63+
if dd_trace_context:
64+
return {
65+
TraceHeader.TRACE_ID:
66+
dd_trace_context['trace-id'],
67+
TraceHeader.PARENT_ID: _convert_xray_entity_id(
68+
xray_trace_entity.id),
69+
TraceHeader.SAMPLING_PRIORITY:
70+
dd_trace_context['sampling-priority'],
71+
}
72+
else:
73+
return {
74+
TraceHeader.TRACE_ID: _convert_xray_trace_id(
75+
xray_trace_entity.trace_id),
76+
TraceHeader.PARENT_ID: _convert_xray_entity_id(
77+
xray_trace_entity.id),
78+
TraceHeader.SAMPLING_PRIORITY: _convert_xray_sampling(
79+
xray_trace_entity.sampled),
80+
}
81+
82+
83+
def _convert_xray_trace_id(xray_trace_id):
84+
"""
85+
Convert X-Ray trace id (hex)'s last 63 bits to a Datadog trace id (int).
86+
"""
87+
return str(0x7FFFFFFFFFFFFFFF & int(xray_trace_id[-16:], 16))
88+
89+
90+
def _convert_xray_entity_id(xray_entity_id):
91+
"""
92+
Convert X-Ray (sub)segement id (hex) to a Datadog span id (int).
93+
"""
94+
return str(int(xray_entity_id, 16))
95+
96+
97+
def _convert_xray_sampling(xray_sampled):
98+
"""
99+
Convert X-Ray sampled (True/False) to its Datadog counterpart.
100+
"""
101+
return str(SamplingPriority.USER_KEEP) if xray_sampled \
102+
else str(SamplingPriority.USER_REJECT)

datadog_lambda/wrapper.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import traceback
2+
from threading import Thread
3+
4+
from datadog_lambda.metric import init_api_client, lambda_stats
5+
from datadog_lambda.tracing import extract_dd_trace_context
6+
from datadog_lambda.patch import patch_all
7+
8+
9+
"""
10+
Usage:
11+
12+
import requests
13+
from datadog_lambda.wrapper import datadog_lambda_wrapper
14+
from datadog_lambda.metric import lambda_metric
15+
16+
@datadog_lambda_wrapper
17+
def my_lambda_handle(event, context):
18+
lambda_metric("my_metric", 10)
19+
requests.get("https://www.datadoghq.com")
20+
"""
21+
22+
23+
class _LambdaDecorator(object):
24+
"""
25+
Decorator to automatically initialize Datadog API client, flush metrics,
26+
and extracts/injects trace context.
27+
"""
28+
29+
def __init__(self, func):
30+
self.func = func
31+
32+
def _before(self, event, context):
33+
try:
34+
# Async initialization of the TLS connection with Datadog API,
35+
# and reduces the overhead to the final metric flush at the end.
36+
Thread(target=init_api_client).start()
37+
38+
# Extract Datadog trace context from incoming requests
39+
extract_dd_trace_context(event)
40+
41+
# Patch HTTP clients to propogate Datadog trace context
42+
patch_all()
43+
except Exception:
44+
traceback.print_exc()
45+
46+
def _after(self, event, context):
47+
try:
48+
lambda_stats.flush(float("inf"))
49+
except Exception:
50+
traceback.print_exc()
51+
52+
def __call__(self, event, context):
53+
self._before(event, context)
54+
try:
55+
return self.func(event, context)
56+
finally:
57+
self._after(event, context)
58+
59+
60+
datadog_lambda_wrapper = _LambdaDecorator

requirements-dev.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
nose2==0.9.1
2+
flake8==3.7.7
3+
requests==2.21.0

0 commit comments

Comments
 (0)