Skip to content

Commit c924c39

Browse files
author
Andrey Slotin
authored
Add support for API Gateway v2 Lambda payload (#288)
* Detect API Gateway v2 trigger events on AWS Lambda * Extract trace context from API Gateway v2 events * Extract tags from API Gateway v2 trigger events for the entry span * Safely read HTTP headers from AWL Lambda proxy event payloads
1 parent 262b7d2 commit c924c39

File tree

3 files changed

+179
-2
lines changed

3 files changed

+179
-2
lines changed

instana/instrumentation/aws/triggers.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@
1313

1414
def get_context(tracer, event):
1515
# TODO: Search for more types of trigger context
16-
if is_api_gateway_proxy_trigger(event) or is_application_load_balancer_trigger(event):
17-
return tracer.extract('http_headers', event['headers'])
16+
is_proxy_event = is_api_gateway_proxy_trigger(event) or \
17+
is_api_gateway_v2_proxy_trigger(event) or \
18+
is_application_load_balancer_trigger(event)
19+
20+
if is_proxy_event:
21+
return tracer.extract('http_headers', event.get('headers', {}))
1822

1923
return tracer.extract('http_headers', event)
2024

@@ -26,6 +30,20 @@ def is_api_gateway_proxy_trigger(event):
2630
return True
2731

2832

33+
def is_api_gateway_v2_proxy_trigger(event):
34+
for key in ["version", "requestContext"]:
35+
if key not in event:
36+
return False
37+
38+
if event["version"] != "2.0":
39+
return False
40+
41+
for key in ["apiId", "stage", "http"]:
42+
if key not in event["requestContext"]:
43+
return False
44+
45+
return True
46+
2947
def is_application_load_balancer_trigger(event):
3048
if 'requestContext' in event and 'elb' in event['requestContext']:
3149
return True
@@ -143,6 +161,23 @@ def enrich_lambda_span(agent, span, event, context):
143161
if agent.options.extra_http_headers is not None:
144162
capture_extra_headers(event, span, agent.options.extra_http_headers)
145163

164+
elif is_api_gateway_v2_proxy_trigger(event):
165+
logger.debug("Detected as API Gateway v2.0 Proxy Trigger")
166+
167+
reqCtx = event["requestContext"]
168+
169+
# trim optional HTTP method prefix
170+
route_path = event["routeKey"].split(" ", 2)[-1]
171+
172+
span.set_tag(STR_LAMBDA_TRIGGER, 'aws:api.gateway')
173+
span.set_tag('http.method', reqCtx["http"]["method"])
174+
span.set_tag('http.url', reqCtx["http"]["path"])
175+
span.set_tag('http.path_tpl', route_path)
176+
span.set_tag('http.params', read_http_query_params(event))
177+
178+
if agent.options.extra_http_headers is not None:
179+
capture_extra_headers(event, span, agent.options.extra_http_headers)
180+
146181
elif is_application_load_balancer_trigger(event):
147182
logger.debug("Detected as Application Load Balancer Trigger")
148183
span.set_tag(STR_LAMBDA_TRIGGER, 'aws:application.load.balancer')
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
{
2+
"version": "2.0",
3+
"routeKey": "ANY /my/{resource}",
4+
"rawPath": "/my/path",
5+
"rawQueryString": "parameter1=value1&parameter1=value2&parameter2=value",
6+
"cookies": [
7+
"cookie1",
8+
"cookie2"
9+
],
10+
"headers": {
11+
"Header1": "value1",
12+
"Header2": "value1,value2",
13+
"X-Instana-T": "0000000000001234",
14+
"X-Instana-S": "0000000000004567",
15+
"X-Instana-L": "1",
16+
"X-Instana-Synthetic": "1",
17+
"X-Custom-Header-1": "value1",
18+
"x-custom-header-2": "value2"
19+
},
20+
"queryStringParameters": {
21+
"secret": "key",
22+
"q": "term"
23+
},
24+
"requestContext": {
25+
"accountId": "123456789012",
26+
"apiId": "api-id",
27+
"authentication": {
28+
"clientCert": {
29+
"clientCertPem": "CERT_CONTENT",
30+
"subjectDN": "www.example.com",
31+
"issuerDN": "Example issuer",
32+
"serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1",
33+
"validity": {
34+
"notBefore": "May 28 12:30:02 2019 GMT",
35+
"notAfter": "Aug 5 09:36:04 2021 GMT"
36+
}
37+
}
38+
},
39+
"authorizer": {
40+
"jwt": {
41+
"claims": {
42+
"claim1": "value1",
43+
"claim2": "value2"
44+
},
45+
"scopes": [
46+
"scope1",
47+
"scope2"
48+
]
49+
}
50+
},
51+
"domainName": "id.execute-api.us-east-1.amazonaws.com",
52+
"domainPrefix": "id",
53+
"http": {
54+
"method": "POST",
55+
"path": "/my/path",
56+
"protocol": "HTTP/1.1",
57+
"sourceIp": "IP",
58+
"userAgent": "agent"
59+
},
60+
"requestId": "id",
61+
"routeKey": "$default",
62+
"stage": "$default",
63+
"time": "12/Mar/2020:19:03:58 +0000",
64+
"timeEpoch": 1583348638390
65+
},
66+
"body": "Hello from Lambda",
67+
"pathParameters": {
68+
"parameter1": "value1"
69+
},
70+
"isBase64Encoded": false,
71+
"stageVariables": {
72+
"stageVariable1": "value1",
73+
"stageVariable2": "value2"
74+
}
75+
}

tests/platforms/test_lambda.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,73 @@ def test_api_gateway_trigger_tracing(self):
300300
else:
301301
self.assertEqual("foo=['bar']", span.data['http']['params'])
302302

303+
def test_api_gateway_v2_trigger_tracing(self):
304+
with open(self.pwd + '/../data/lambda/api_gateway_v2_event.json', 'r') as json_file:
305+
event = json.load(json_file)
306+
307+
self.create_agent_and_setup_tracer()
308+
309+
# Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then
310+
# figure out the original (the users') Lambda Handler and execute it.
311+
# The original Lambda handler is set in os.environ["LAMBDA_HANDLER"]
312+
result = lambda_handler(event, self.context)
313+
314+
assert isinstance(result, dict)
315+
assert 'headers' in result
316+
assert 'Server-Timing' in result['headers']
317+
318+
time.sleep(1)
319+
payload = self.agent.collector.prepare_payload()
320+
321+
self.assertTrue("metrics" in payload)
322+
self.assertTrue("spans" in payload)
323+
self.assertEqual(2, len(payload.keys()))
324+
325+
self.assertTrue(isinstance(payload['metrics']['plugins'], list))
326+
self.assertTrue(len(payload['metrics']['plugins']) == 1)
327+
plugin_data = payload['metrics']['plugins'][0]
328+
329+
self.assertEqual('com.instana.plugin.aws.lambda', plugin_data['name'])
330+
self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', plugin_data['entityId'])
331+
332+
self.assertEqual(1, len(payload['spans']))
333+
334+
span = payload['spans'][0]
335+
self.assertEqual('aws.lambda.entry', span.n)
336+
self.assertEqual('0000000000001234', span.t)
337+
self.assertIsNotNone(span.s)
338+
self.assertEqual('0000000000004567', span.p)
339+
self.assertIsNotNone(span.ts)
340+
self.assertIsNotNone(span.d)
341+
342+
server_timing_value = "intid;desc=%s" % span.t
343+
assert result['headers']['Server-Timing'] == server_timing_value
344+
345+
self.assertEqual({'hl': True, 'cp': 'aws', 'e': 'arn:aws:lambda:us-east-2:12345:function:TestPython:1'},
346+
span.f)
347+
348+
self.assertTrue(span.sy)
349+
350+
self.assertIsNone(span.ec)
351+
self.assertIsNone(span.data['lambda']['error'])
352+
353+
self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', span.data['lambda']['arn'])
354+
self.assertEqual(None, span.data['lambda']['alias'])
355+
self.assertEqual('python', span.data['lambda']['runtime'])
356+
self.assertEqual('TestPython', span.data['lambda']['functionName'])
357+
self.assertEqual('1', span.data['lambda']['functionVersion'])
358+
self.assertIsNone(span.data['service'])
359+
360+
self.assertEqual('aws:api.gateway', span.data['lambda']['trigger'])
361+
self.assertEqual('POST', span.data['http']['method'])
362+
self.assertEqual('/my/path', span.data['http']['url'])
363+
self.assertEqual('/my/{resource}', span.data['http']['path_tpl'])
364+
if sys.version[:3] == '2.7':
365+
self.assertEqual(u"q=term&secret=key", span.data['http']['params'])
366+
else:
367+
self.assertEqual("secret=key&q=term", span.data['http']['params'])
368+
369+
303370
def test_application_lb_trigger_tracing(self):
304371
with open(self.pwd + '/../data/lambda/api_gateway_event.json', 'r') as json_file:
305372
event = json.load(json_file)

0 commit comments

Comments
 (0)