Skip to content

Commit 36264e9

Browse files
authored
Capture common HTTP attributes from API Gateway proxy events in Lambda instrumentor (#1233)
1 parent a8cf644 commit 36264e9

File tree

6 files changed

+297
-2
lines changed

6 files changed

+297
-2
lines changed

Diff for: CHANGELOG.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515

1616
### Added
1717

18+
- Capture common HTTP attributes from API Gateway proxy events in `opentelemetry-instrumentation-aws-lambda`
19+
([#1233](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1233))
1820
- Add metric instrumentation for tornado
1921
([#1252](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1252))
2022
- `opentelemetry-instrumentation-django` Fixed bug where auto-instrumentation fails when django is installed and settings are not configured.
@@ -60,7 +62,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6062

6163
### Added
6264

63-
- `opentelemetry-instrumentation-grpc` add supports to filter requests to instrument. ([#1241](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1241))
65+
- `opentelemetry-instrumentation-grpc` add supports to filter requests to instrument.
66+
([#1241](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1241))
6467
- Flask sqlalchemy psycopg2 integration
6568
([#1224](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1224))
6669
- Add metric instrumentation in Falcon

Diff for: instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py

+99-1
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,11 @@ def custom_event_context_extractor(lambda_event):
6464
event_context_extractor=custom_event_context_extractor
6565
)
6666
"""
67-
6867
import logging
6968
import os
7069
from importlib import import_module
7170
from typing import Any, Callable, Collection
71+
from urllib.parse import urlencode
7272

7373
from wrapt import wrap_function_wrapper
7474

@@ -85,6 +85,7 @@ def custom_event_context_extractor(lambda_event):
8585
from opentelemetry.semconv.resource import ResourceAttributes
8686
from opentelemetry.semconv.trace import SpanAttributes
8787
from opentelemetry.trace import (
88+
Span,
8889
SpanKind,
8990
TracerProvider,
9091
get_tracer,
@@ -171,6 +172,86 @@ def _determine_parent_context(
171172
return parent_context
172173

173174

175+
def _set_api_gateway_v1_proxy_attributes(
176+
lambda_event: Any, span: Span
177+
) -> Span:
178+
"""Sets HTTP attributes for REST APIs and v1 HTTP APIs
179+
180+
More info:
181+
https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
182+
"""
183+
span.set_attribute(
184+
SpanAttributes.HTTP_METHOD, lambda_event.get("httpMethod")
185+
)
186+
span.set_attribute(SpanAttributes.HTTP_ROUTE, lambda_event.get("resource"))
187+
188+
if lambda_event.get("headers"):
189+
span.set_attribute(
190+
SpanAttributes.HTTP_USER_AGENT,
191+
lambda_event["headers"].get("User-Agent"),
192+
)
193+
span.set_attribute(
194+
SpanAttributes.HTTP_SCHEME,
195+
lambda_event["headers"].get("X-Forwarded-Proto"),
196+
)
197+
span.set_attribute(
198+
SpanAttributes.NET_HOST_NAME, lambda_event["headers"].get("Host")
199+
)
200+
201+
if lambda_event.get("queryStringParameters"):
202+
span.set_attribute(
203+
SpanAttributes.HTTP_TARGET,
204+
f"{lambda_event.get('resource')}?{urlencode(lambda_event.get('queryStringParameters'))}",
205+
)
206+
else:
207+
span.set_attribute(
208+
SpanAttributes.HTTP_TARGET, lambda_event.get("resource")
209+
)
210+
211+
return span
212+
213+
214+
def _set_api_gateway_v2_proxy_attributes(
215+
lambda_event: Any, span: Span
216+
) -> Span:
217+
"""Sets HTTP attributes for v2 HTTP APIs
218+
219+
More info:
220+
https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html
221+
"""
222+
span.set_attribute(
223+
SpanAttributes.NET_HOST_NAME,
224+
lambda_event["requestContext"].get("domainName"),
225+
)
226+
227+
if lambda_event["requestContext"].get("http"):
228+
span.set_attribute(
229+
SpanAttributes.HTTP_METHOD,
230+
lambda_event["requestContext"]["http"].get("method"),
231+
)
232+
span.set_attribute(
233+
SpanAttributes.HTTP_USER_AGENT,
234+
lambda_event["requestContext"]["http"].get("userAgent"),
235+
)
236+
span.set_attribute(
237+
SpanAttributes.HTTP_ROUTE,
238+
lambda_event["requestContext"]["http"].get("path"),
239+
)
240+
241+
if lambda_event.get("rawQueryString"):
242+
span.set_attribute(
243+
SpanAttributes.HTTP_TARGET,
244+
f"{lambda_event['requestContext']['http'].get('path')}?{lambda_event.get('rawQueryString')}",
245+
)
246+
else:
247+
span.set_attribute(
248+
SpanAttributes.HTTP_TARGET,
249+
lambda_event["requestContext"]["http"].get("path"),
250+
)
251+
252+
return span
253+
254+
174255
def _instrument(
175256
wrapped_module_name,
176257
wrapped_function_name,
@@ -233,6 +314,23 @@ def _instrumented_lambda_handler_call(
233314

234315
result = call_wrapped(*args, **kwargs)
235316

317+
# If the request came from an API Gateway, extract http attributes from the event
318+
# https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/instrumentation/aws-lambda.md#api-gateway
319+
# https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-server-semantic-conventions
320+
if lambda_event and lambda_event.get("requestContext"):
321+
span.set_attribute(SpanAttributes.FAAS_TRIGGER, "http")
322+
323+
if lambda_event.get("version") == "2.0":
324+
_set_api_gateway_v2_proxy_attributes(lambda_event, span)
325+
else:
326+
_set_api_gateway_v1_proxy_attributes(lambda_event, span)
327+
328+
if isinstance(result, dict) and result.get("statusCode"):
329+
span.set_attribute(
330+
SpanAttributes.HTTP_STATUS_CODE,
331+
result.get("statusCode"),
332+
)
333+
236334
_tracer_provider = tracer_provider or get_tracer_provider()
237335
try:
238336
# NOTE: `force_flush` before function quit in case of Lambda freeze.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Generated via `sam local generate-event apigateway http-api-proxy`
2+
3+
MOCK_LAMBDA_API_GATEWAY_HTTP_API_EVENT = {
4+
"version": "2.0",
5+
"routeKey": "$default",
6+
"rawPath": "/path/to/resource",
7+
"rawQueryString": "parameter1=value1&parameter1=value2&parameter2=value",
8+
"cookies": ["cookie1", "cookie2"],
9+
"headers": {"header1": "value1", "Header2": "value1,value2"},
10+
"queryStringParameters": {
11+
"parameter1": "value1,value2",
12+
"parameter2": "value",
13+
},
14+
"requestContext": {
15+
"accountId": "123456789012",
16+
"apiId": "api-id",
17+
"authentication": {
18+
"clientCert": {
19+
"clientCertPem": "CERT_CONTENT",
20+
"subjectDN": "www.example.com",
21+
"issuerDN": "Example issuer",
22+
"serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1",
23+
"validity": {
24+
"notBefore": "May 28 12:30:02 2019 GMT",
25+
"notAfter": "Aug 5 09:36:04 2021 GMT",
26+
},
27+
}
28+
},
29+
"authorizer": {
30+
"jwt": {
31+
"claims": {"claim1": "value1", "claim2": "value2"},
32+
"scopes": ["scope1", "scope2"],
33+
}
34+
},
35+
"domainName": "id.execute-api.us-east-1.amazonaws.com",
36+
"domainPrefix": "id",
37+
"http": {
38+
"method": "POST",
39+
"path": "/path/to/resource",
40+
"protocol": "HTTP/1.1",
41+
"sourceIp": "192.168.0.1/32",
42+
"userAgent": "agent",
43+
},
44+
"requestId": "id",
45+
"routeKey": "$default",
46+
"stage": "$default",
47+
"time": "12/Mar/2020:19:03:58 +0000",
48+
"timeEpoch": 1583348638390,
49+
},
50+
"body": "eyJ0ZXN0IjoiYm9keSJ9",
51+
"pathParameters": {"parameter1": "value1"},
52+
"isBase64Encoded": True,
53+
"stageVariables": {"stageVariable1": "value1", "stageVariable2": "value2"},
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Generated via `sam local generate-event apigateway aws-proxy`
2+
3+
MOCK_LAMBDA_API_GATEWAY_PROXY_EVENT = {
4+
"body": "eyJ0ZXN0IjoiYm9keSJ9",
5+
"resource": "/{proxy+}",
6+
"path": "/path/to/resource",
7+
"httpMethod": "POST",
8+
"isBase64Encoded": True,
9+
"queryStringParameters": {"foo": "bar"},
10+
"multiValueQueryStringParameters": {"foo": ["bar"]},
11+
"pathParameters": {"proxy": "/path/to/resource"},
12+
"stageVariables": {"baz": "qux"},
13+
"headers": {
14+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
15+
"Accept-Encoding": "gzip, deflate, sdch",
16+
"Accept-Language": "en-US,en;q=0.8",
17+
"Cache-Control": "max-age=0",
18+
"CloudFront-Forwarded-Proto": "https",
19+
"CloudFront-Is-Desktop-Viewer": "true",
20+
"CloudFront-Is-Mobile-Viewer": "false",
21+
"CloudFront-Is-SmartTV-Viewer": "false",
22+
"CloudFront-Is-Tablet-Viewer": "false",
23+
"CloudFront-Viewer-Country": "US",
24+
"Host": "1234567890.execute-api.us-east-1.amazonaws.com",
25+
"Upgrade-Insecure-Requests": "1",
26+
"User-Agent": "Custom User Agent String",
27+
"Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
28+
"X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
29+
"X-Forwarded-For": "127.0.0.1, 127.0.0.2",
30+
"X-Forwarded-Port": "443",
31+
"X-Forwarded-Proto": "https",
32+
},
33+
"multiValueHeaders": {
34+
"Accept": [
35+
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
36+
],
37+
"Accept-Encoding": ["gzip, deflate, sdch"],
38+
"Accept-Language": ["en-US,en;q=0.8"],
39+
"Cache-Control": ["max-age=0"],
40+
"CloudFront-Forwarded-Proto": ["https"],
41+
"CloudFront-Is-Desktop-Viewer": ["true"],
42+
"CloudFront-Is-Mobile-Viewer": ["false"],
43+
"CloudFront-Is-SmartTV-Viewer": ["false"],
44+
"CloudFront-Is-Tablet-Viewer": ["false"],
45+
"CloudFront-Viewer-Country": ["US"],
46+
"Host": ["0123456789.execute-api.us-east-1.amazonaws.com"],
47+
"Upgrade-Insecure-Requests": ["1"],
48+
"User-Agent": ["Custom User Agent String"],
49+
"Via": [
50+
"1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)"
51+
],
52+
"X-Amz-Cf-Id": [
53+
"cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA=="
54+
],
55+
"X-Forwarded-For": ["127.0.0.1, 127.0.0.2"],
56+
"X-Forwarded-Port": ["443"],
57+
"X-Forwarded-Proto": ["https"],
58+
},
59+
"requestContext": {
60+
"accountId": "123456789012",
61+
"resourceId": "123456",
62+
"stage": "prod",
63+
"requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
64+
"requestTime": "09/Apr/2015:12:34:56 +0000",
65+
"requestTimeEpoch": 1428582896000,
66+
"identity": {
67+
"cognitoIdentityPoolId": None,
68+
"accountId": None,
69+
"cognitoIdentityId": None,
70+
"caller": None,
71+
"accessKey": None,
72+
"sourceIp": "127.0.0.1",
73+
"cognitoAuthenticationType": None,
74+
"cognitoAuthenticationProvider": None,
75+
"userArn": None,
76+
"userAgent": "Custom User Agent String",
77+
"user": None,
78+
},
79+
"path": "/prod/path/to/resource",
80+
"resourcePath": "/{proxy+}",
81+
"httpMethod": "POST",
82+
"apiId": "1234567890",
83+
"protocol": "HTTP/1.1",
84+
},
85+
}

Diff for: instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/lambda_function.py

+4
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@
1515

1616
def handler(event, context):
1717
return "200 ok"
18+
19+
20+
def rest_api_handler(event, context):
21+
return {"statusCode": 200, "body": "200 ok"}

Diff for: instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation_manual.py

+51
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
from importlib import import_module
1616
from unittest import mock
1717

18+
from mocks.api_gateway_http_api_event import (
19+
MOCK_LAMBDA_API_GATEWAY_HTTP_API_EVENT,
20+
)
21+
from mocks.api_gateway_proxy_event import MOCK_LAMBDA_API_GATEWAY_PROXY_EVENT
22+
1823
from opentelemetry.environment_variables import OTEL_PROPAGATORS
1924
from opentelemetry.instrumentation.aws_lambda import (
2025
_HANDLER,
@@ -300,3 +305,49 @@ def test_lambda_handles_multiple_consumers(self):
300305
assert spans
301306

302307
test_env_patch.stop()
308+
309+
def test_api_gateway_proxy_event_sets_attributes(self):
310+
handler_patch = mock.patch.dict(
311+
"os.environ",
312+
{_HANDLER: "mocks.lambda_function.rest_api_handler"},
313+
)
314+
handler_patch.start()
315+
316+
AwsLambdaInstrumentor().instrument()
317+
318+
mock_execute_lambda(MOCK_LAMBDA_API_GATEWAY_PROXY_EVENT)
319+
320+
span = self.memory_exporter.get_finished_spans()[0]
321+
322+
self.assertSpanHasAttributes(
323+
span,
324+
{
325+
SpanAttributes.FAAS_TRIGGER: "http",
326+
SpanAttributes.HTTP_METHOD: "POST",
327+
SpanAttributes.HTTP_ROUTE: "/{proxy+}",
328+
SpanAttributes.HTTP_TARGET: "/{proxy+}?foo=bar",
329+
SpanAttributes.NET_HOST_NAME: "1234567890.execute-api.us-east-1.amazonaws.com",
330+
SpanAttributes.HTTP_USER_AGENT: "Custom User Agent String",
331+
SpanAttributes.HTTP_SCHEME: "https",
332+
SpanAttributes.HTTP_STATUS_CODE: 200,
333+
},
334+
)
335+
336+
def test_api_gateway_http_api_proxy_event_sets_attributes(self):
337+
AwsLambdaInstrumentor().instrument()
338+
339+
mock_execute_lambda(MOCK_LAMBDA_API_GATEWAY_HTTP_API_EVENT)
340+
341+
span = self.memory_exporter.get_finished_spans()[0]
342+
343+
self.assertSpanHasAttributes(
344+
span,
345+
{
346+
SpanAttributes.FAAS_TRIGGER: "http",
347+
SpanAttributes.HTTP_METHOD: "POST",
348+
SpanAttributes.HTTP_ROUTE: "/path/to/resource",
349+
SpanAttributes.HTTP_TARGET: "/path/to/resource?parameter1=value1&parameter1=value2&parameter2=value",
350+
SpanAttributes.NET_HOST_NAME: "id.execute-api.us-east-1.amazonaws.com",
351+
SpanAttributes.HTTP_USER_AGENT: "agent",
352+
},
353+
)

0 commit comments

Comments
 (0)