Skip to content

Commit ce122d7

Browse files
committed
Add instrumentation for AWS Lambda Service - Implementation
1 parent 4b9b6dc commit ce122d7

File tree

7 files changed

+539
-0
lines changed

7 files changed

+539
-0
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.6.2-0.25b2...HEAD)
9+
- `opentelemetry-instrumentation-aws_lambda` Add instrumentation for AWS Lambda Service - Implementation (Part 2/2)
10+
([#777](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/777))
911

1012
### Fixed
1113

instrumentation/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
| [opentelemetry-instrumentation-aiopg](./opentelemetry-instrumentation-aiopg) | aiopg >= 0.13.0, < 1.3.0 |
66
| [opentelemetry-instrumentation-asgi](./opentelemetry-instrumentation-asgi) | asgiref ~= 3.0 |
77
| [opentelemetry-instrumentation-asyncpg](./opentelemetry-instrumentation-asyncpg) | asyncpg >= 0.12.0 |
8+
| [opentelemetry-instrumentation-aws_lambda](./opentelemetry-instrumentation-aws_lambda) | aws_lambda |
89
| [opentelemetry-instrumentation-boto](./opentelemetry-instrumentation-boto) | boto~=2.0 |
910
| [opentelemetry-instrumentation-botocore](./opentelemetry-instrumentation-botocore) | botocore ~= 1.0 |
1011
| [opentelemetry-instrumentation-celery](./opentelemetry-instrumentation-celery) | celery >= 4.0, < 6.0 |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
# Copyright 2020, OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
The opentelemetry-instrumentation-aws-lambda package provides an `Instrumentor`
17+
to traces calls whithin a Python AWS Lambda function.
18+
19+
Usage
20+
-----
21+
22+
.. code:: python
23+
24+
# Copy this snippet into an AWS Lambda function
25+
26+
import boto3
27+
from opentelemetry.instrumentation.botocore import AwsBotocoreInstrumentor
28+
from opentelemetry.instrumentation.aws_lambda import AwsLambdaInstrumentor
29+
30+
31+
# Enable instrumentation
32+
AwsBotocoreInstrumentor().instrument()
33+
AwsLambdaInstrumentor().instrument()
34+
35+
# Lambda function
36+
def lambda_handler(event, context):
37+
s3 = boto3.resource('s3')
38+
for bucket in s3.buckets.all():
39+
print(bucket.name)
40+
41+
return "200 OK"
42+
43+
API
44+
---
45+
46+
The `instrument` method accepts the following keyword args:
47+
48+
tracer_provider (TracerProvider) - an optional tracer provider
49+
event_context_extractor (Callable) - a function that returns an OTel Trace
50+
Context given the Lambda Event the AWS Lambda was invoked with
51+
this function signature is: def event_context_extractor(lambda_event: Any) -> Context
52+
for example:
53+
54+
.. code:: python
55+
56+
from opentelemetry.instrumentation.aws_lambda import AwsLambdaInstrumentor
57+
58+
def custom_event_context_extractor(lambda_event):
59+
# If the `TraceContextTextMapPropagator` is the global propagator, we
60+
# can use it to parse out the context from the HTTP Headers.
61+
return get_global_textmap().extract(lambda_event["foo"]["headers"])
62+
63+
AwsLambdaInstrumentor().instrument(
64+
event_context_extractor=custom_event_context_extractor
65+
)
66+
"""
67+
68+
import logging
69+
import os
70+
from importlib import import_module
71+
from typing import Any, Collection
72+
73+
from wrapt import wrap_function_wrapper
74+
75+
from opentelemetry.context.context import Context
76+
from opentelemetry.instrumentation.aws_lambda.package import _instruments
77+
from opentelemetry.instrumentation.aws_lambda.version import __version__
78+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
79+
from opentelemetry.instrumentation.utils import unwrap
80+
from opentelemetry.propagate import get_global_textmap
81+
from opentelemetry.propagators.aws.aws_xray_propagator import (
82+
TRACE_HEADER_KEY,
83+
AwsXRayPropagator,
84+
)
85+
from opentelemetry.semconv.resource import ResourceAttributes
86+
from opentelemetry.semconv.trace import SpanAttributes
87+
from opentelemetry.trace import (
88+
SpanKind,
89+
TracerProvider,
90+
get_tracer,
91+
get_tracer_provider,
92+
)
93+
from opentelemetry.trace.propagation import get_current_span
94+
95+
logger = logging.getLogger(__name__)
96+
97+
_HANDLER = "_HANDLER"
98+
_X_AMZN_TRACE_ID = "_X_AMZN_TRACE_ID"
99+
ORIG_HANDLER = "ORIG_HANDLER"
100+
101+
102+
class AwsLambdaInstrumentor(BaseInstrumentor):
103+
def instrumentation_dependencies(self) -> Collection[str]:
104+
return _instruments
105+
106+
def _instrument(self, **kwargs):
107+
"""Instruments Lambda Handlers on AWS Lambda.
108+
109+
See more:
110+
https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/instrumentation/aws-lambda.md#instrumenting-aws-lambda
111+
112+
Args:
113+
**kwargs: Optional arguments
114+
``tracer_provider``: a TracerProvider, defaults to global
115+
``event_context_extractor``: a method which takes the Lambda
116+
Event as input and extracts an OTel Context from it. By default,
117+
the context is extracted from the HTTP headers of an API Gateway
118+
request.
119+
"""
120+
lambda_handler = os.environ.get(ORIG_HANDLER, os.environ.get(_HANDLER))
121+
# pylint: disable=attribute-defined-outside-init
122+
(
123+
self._wrapped_module_name,
124+
self._wrapped_function_name,
125+
) = lambda_handler.rsplit(".", 1)
126+
127+
_instrument(
128+
self._wrapped_module_name,
129+
self._wrapped_function_name,
130+
tracer_provider=kwargs.get("tracer_provider"),
131+
event_context_extractor=kwargs.get("event_context_extractor"),
132+
)
133+
134+
def _uninstrument(self, **kwargs):
135+
unwrap(
136+
import_module(self._wrapped_module_name),
137+
self._wrapped_function_name,
138+
)
139+
140+
141+
def _default_event_context_extractor(lambda_event: Any) -> Context:
142+
"""Default way of extracting the context from the Lambda Event.
143+
144+
Assumes the Lambda Event is a map with the headers under the 'headers' key.
145+
This is the mapping to use when the Lambda is invoked by an API Gateway
146+
REST API where API Gateway is acting as a pure proxy for the request.
147+
148+
See more:
149+
https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
150+
151+
Args:
152+
lambda_event: user-defined, so it could be anything, but this
153+
method counts on it being a map with a 'headers' key
154+
Returns:
155+
A Context with configuration found in the event.
156+
"""
157+
try:
158+
headers = lambda_event["headers"]
159+
except (TypeError, KeyError):
160+
logger.debug(
161+
"Extracting context from Lambda Event failed: either enable X-Ray active tracing or configure API Gateway to trigger this Lambda function as a pure proxy. Otherwise, generated spans will have an invalid (empty) parent context."
162+
)
163+
headers = {}
164+
return get_global_textmap().extract(headers)
165+
166+
167+
def _instrument(
168+
wrapped_module_name,
169+
wrapped_function_name,
170+
tracer_provider: TracerProvider = None,
171+
event_context_extractor=None,
172+
):
173+
def _determine_parent_context(lambda_event: Any) -> Context:
174+
"""Determine the parent context for the current Lambda invocation.
175+
176+
See more:
177+
https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/instrumentation/aws-lambda.md#determining-the-parent-of-a-span
178+
179+
Args:
180+
lambda_event: user-defined, so it could be anything, but this
181+
method counts it being a map with a 'headers' key
182+
Returns:
183+
A Context with configuration found in the carrier.
184+
"""
185+
parent_context = None
186+
187+
xray_env_var = os.environ.get(_X_AMZN_TRACE_ID)
188+
189+
if xray_env_var:
190+
parent_context = AwsXRayPropagator().extract(
191+
{TRACE_HEADER_KEY: xray_env_var}
192+
)
193+
194+
if (
195+
parent_context
196+
and get_current_span(parent_context)
197+
.get_span_context()
198+
.trace_flags.sampled
199+
):
200+
return parent_context
201+
202+
if event_context_extractor:
203+
parent_context = event_context_extractor(lambda_event)
204+
else:
205+
parent_context = _default_event_context_extractor(lambda_event)
206+
207+
return parent_context
208+
209+
def _instrumented_lambda_handler_call(
210+
call_wrapped, instance, args, kwargs
211+
):
212+
orig_handler_name = ".".join(
213+
[wrapped_module_name, wrapped_function_name]
214+
)
215+
216+
lambda_event = args[0]
217+
218+
parent_context = _determine_parent_context(lambda_event)
219+
220+
tracer = get_tracer(__name__, __version__, tracer_provider)
221+
222+
with tracer.start_as_current_span(
223+
name=orig_handler_name,
224+
context=parent_context,
225+
kind=SpanKind.SERVER,
226+
) as span:
227+
if span.is_recording():
228+
lambda_context = args[1]
229+
# NOTE: The specs mention an exception here, allowing the
230+
# `ResourceAttributes.FAAS_ID` attribute to be set as a span
231+
# attribute instead of a resource attribute.
232+
#
233+
# See more:
234+
# https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/faas.md#example
235+
span.set_attribute(
236+
ResourceAttributes.FAAS_ID,
237+
lambda_context.invoked_function_arn,
238+
)
239+
span.set_attribute(
240+
SpanAttributes.FAAS_EXECUTION,
241+
lambda_context.aws_request_id,
242+
)
243+
244+
result = call_wrapped(*args, **kwargs)
245+
246+
_tracer_provider = tracer_provider or get_tracer_provider()
247+
if hasattr(_tracer_provider, "force_flush"):
248+
# NOTE: force_flush before function quit in case of Lambda freeze.
249+
# Assumes we are using the OpenTelemetry SDK implementation of the
250+
# `TracerProvider`.
251+
_tracer_provider.force_flush()
252+
else:
253+
logger.error(
254+
"TracerProvider was missing `force_flush` method. This is necessary in case of a Lambda freeze and would exist in the OTel SDK implementation."
255+
)
256+
257+
return result
258+
259+
wrap_function_wrapper(
260+
wrapped_module_name,
261+
wrapped_function_name,
262+
_instrumented_lambda_handler_call,
263+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
def handler(event, context):
17+
return "200 ok"

0 commit comments

Comments
 (0)