Skip to content

Commit c079d96

Browse files
committed
Add instrumentation for AWS Lambda Service - Implementation
1 parent dbc6a86 commit c079d96

File tree

5 files changed

+528
-0
lines changed

5 files changed

+528
-0
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.6.2-0.25b2...HEAD)
99
- `opentelemetry-instrumentation-aws-lambda` Add instrumentation for AWS Lambda Service - pkg metadata files (Part 1/2)
1010
([#739](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/739))
11+
- `opentelemetry-instrumentation-aws-lambda` Add instrumentation for AWS Lambda Service - Implementation (Part 2/2)
12+
([#777](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/777))
1113

1214
### Fixed
1315

instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py

+255
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,258 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# 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, Callable, 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+
def _default_event_context_extractor(lambda_event: Any) -> Context:
103+
"""Default way of extracting the context from the Lambda Event.
104+
105+
Assumes the Lambda Event is a map with the headers under the 'headers' key.
106+
This is the mapping to use when the Lambda is invoked by an API Gateway
107+
REST API where API Gateway is acting as a pure proxy for the request.
108+
109+
See more:
110+
https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
111+
112+
Args:
113+
lambda_event: user-defined, so it could be anything, but this
114+
method counts on it being a map with a 'headers' key
115+
Returns:
116+
A Context with configuration found in the event.
117+
"""
118+
try:
119+
headers = lambda_event["headers"]
120+
except (TypeError, KeyError):
121+
logger.debug(
122+
"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."
123+
)
124+
headers = {}
125+
return get_global_textmap().extract(headers)
126+
127+
128+
def _determine_parent_context(
129+
lambda_event: Any, event_context_extractor: Callable[[Any], Context]
130+
) -> Context:
131+
"""Determine the parent context for the current Lambda invocation.
132+
133+
See more:
134+
https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/instrumentation/aws-lambda.md#determining-the-parent-of-a-span
135+
136+
Args:
137+
lambda_event: user-defined, so it could be anything, but this
138+
method counts it being a map with a 'headers' key
139+
Returns:
140+
A Context with configuration found in the carrier.
141+
"""
142+
parent_context = None
143+
144+
xray_env_var = os.environ.get(_X_AMZN_TRACE_ID)
145+
146+
if xray_env_var:
147+
parent_context = AwsXRayPropagator().extract(
148+
{TRACE_HEADER_KEY: xray_env_var}
149+
)
150+
151+
if (
152+
parent_context
153+
and get_current_span(parent_context)
154+
.get_span_context()
155+
.trace_flags.sampled
156+
):
157+
return parent_context
158+
159+
if event_context_extractor:
160+
parent_context = event_context_extractor(lambda_event)
161+
else:
162+
parent_context = _default_event_context_extractor(lambda_event)
163+
164+
return parent_context
165+
166+
167+
def _instrument(
168+
wrapped_module_name,
169+
wrapped_function_name,
170+
event_context_extractor: Callable[[Any], Context],
171+
tracer_provider: TracerProvider = None,
172+
):
173+
def _instrumented_lambda_handler_call(call_wrapped, instance, args, kwargs):
174+
orig_handler_name = ".".join(
175+
[wrapped_module_name, wrapped_function_name]
176+
)
177+
178+
lambda_event = args[0]
179+
180+
parent_context = _determine_parent_context(
181+
lambda_event, event_context_extractor
182+
)
183+
184+
tracer = get_tracer(__name__, __version__, tracer_provider)
185+
186+
with tracer.start_as_current_span(
187+
name=orig_handler_name,
188+
context=parent_context,
189+
kind=SpanKind.SERVER,
190+
) as span:
191+
if span.is_recording():
192+
lambda_context = args[1]
193+
# NOTE: The specs mention an exception here, allowing the
194+
# `ResourceAttributes.FAAS_ID` attribute to be set as a span
195+
# attribute instead of a resource attribute.
196+
#
197+
# See more:
198+
# https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/faas.md#example
199+
span.set_attribute(
200+
ResourceAttributes.FAAS_ID,
201+
lambda_context.invoked_function_arn,
202+
)
203+
span.set_attribute(
204+
SpanAttributes.FAAS_EXECUTION,
205+
lambda_context.aws_request_id,
206+
)
207+
208+
result = call_wrapped(*args, **kwargs)
209+
210+
_tracer_provider = tracer_provider or get_tracer_provider()
211+
try:
212+
# NOTE: `force_flush` before function quit in case of Lambda freeze.
213+
# Assumes we are using the OpenTelemetry SDK implementation of the
214+
# `TracerProvider`.
215+
_tracer_provider.force_flush()
216+
except Exception: # pylint: disable=broad-except
217+
logger.error(
218+
"TracerProvider was missing `force_flush` method. This is necessary in case of a Lambda freeze and would exist in the OTel SDK implementation."
219+
)
220+
221+
return result
222+
223+
wrap_function_wrapper(
224+
wrapped_module_name,
225+
wrapped_function_name,
226+
_instrumented_lambda_handler_call,
227+
)
228+
229+
230+
class AwsLambdaInstrumentor(BaseInstrumentor):
231+
def instrumentation_dependencies(self) -> Collection[str]:
232+
return _instruments
233+
234+
def _instrument(self, **kwargs):
235+
"""Instruments Lambda Handlers on AWS Lambda.
236+
237+
See more:
238+
https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/instrumentation/aws-lambda.md#instrumenting-aws-lambda
239+
240+
Args:
241+
**kwargs: Optional arguments
242+
``tracer_provider``: a TracerProvider, defaults to global
243+
``event_context_extractor``: a method which takes the Lambda
244+
Event as input and extracts an OTel Context from it. By default,
245+
the context is extracted from the HTTP headers of an API Gateway
246+
request.
247+
"""
248+
lambda_handler = os.environ.get(ORIG_HANDLER, os.environ.get(_HANDLER))
249+
# pylint: disable=attribute-defined-outside-init
250+
(
251+
self._wrapped_module_name,
252+
self._wrapped_function_name,
253+
) = lambda_handler.rsplit(".", 1)
254+
255+
_instrument(
256+
self._wrapped_module_name,
257+
self._wrapped_function_name,
258+
event_context_extractor=kwargs.get(
259+
"event_context_extractor", _default_event_context_extractor
260+
),
261+
tracer_provider=kwargs.get("tracer_provider"),
262+
)
263+
264+
def _uninstrument(self, **kwargs):
265+
unwrap(
266+
import_module(self._wrapped_module_name),
267+
self._wrapped_function_name,
268+
)
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)