Skip to content

Commit 1eca3e6

Browse files
herin049xrmx
andauthored
opentelemetry-instrumentation-aws-lambda: Fix aws lambda span creation (#3966)
* update invocation span to have kind SERVER and have name equal to the function name * update Lambda invocation span name and kind * add type checking check * move changelog entry to 'Breaking changes' --------- Co-authored-by: Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com>
1 parent d5aebef commit 1eca3e6

3 files changed

Lines changed: 93 additions & 68 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
117117
([#4112](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4112))
118118
- `opentelemetry-instrumentation-django`: Drop support for Django < 2.0
119119
([#4083](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4083))
120+
- `opentelemetry-instrumentation-aws-lambda`: Fix improper invocation `Span` name and kind.
120121

121122
## Version 1.39.0/0.60b0 (2025-12-03)
122123

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

Lines changed: 75 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,13 @@ def custom_event_context_extractor(lambda_event):
6969
---
7070
"""
7171

72+
from __future__ import annotations
73+
7274
import logging
7375
import os
7476
import time
7577
from importlib import import_module
76-
from typing import Any, Callable, Collection
78+
from typing import TYPE_CHECKING, Any, Callable, Collection
7779
from urllib.parse import urlencode
7880

7981
from wrapt import wrap_function_wrapper
@@ -123,6 +125,29 @@ def custom_event_context_extractor(lambda_event):
123125
"OTEL_INSTRUMENTATION_AWS_LAMBDA_FLUSH_TIMEOUT"
124126
)
125127

128+
if TYPE_CHECKING:
129+
import typing
130+
131+
class LambdaContext(typing.Protocol):
132+
"""Type definition for AWS Lambda context object.
133+
134+
This Protocol defines the interface for the context object passed to Lambda
135+
function handlers, providing information about the invocation, function, and
136+
execution environment.
137+
138+
See Also:
139+
AWS Lambda Context Object documentation:
140+
https://docs.aws.amazon.com/lambda/latest/dg/python-context.html
141+
"""
142+
143+
function_name: str
144+
function_version: str
145+
invoked_function_arn: str
146+
memory_limit_in_mb: int
147+
aws_request_id: str
148+
log_group_name: str
149+
log_stream_name: str
150+
126151

127152
def _default_event_context_extractor(lambda_event: Any) -> Context:
128153
"""Default way of extracting the context from the Lambda Event.
@@ -264,6 +289,50 @@ def _set_api_gateway_v2_proxy_attributes(
264289
return span
265290

266291

292+
def _get_lambda_context_attributes(
293+
lambda_context: LambdaContext,
294+
) -> dict[str, str]:
295+
"""Extracts OpenTelemetry span attributes from AWS Lambda context.
296+
297+
Extract FaaS specific attributes from the AWS Lambda context
298+
according to OpenTelemetry semantic conventions for FaaS & AWS Lambda.
299+
300+
Args:
301+
lambda_context: The AWS Lambda context object.
302+
303+
Returns:
304+
A dictionary mapping of OpenTelemetry attribute names to their values.
305+
"""
306+
function_arn_parts: list[str] = lambda_context.invoked_function_arn.split(
307+
":"
308+
)
309+
# NOTE: `cloud.account.id` can be parsed from the ARN as the fifth item when splitting on `:`
310+
#
311+
# See more:
312+
# https://github.com/open-telemetry/semantic-conventions/blob/main/docs/faas/aws-lambda.md#all-triggers
313+
aws_account_id: str = function_arn_parts[4]
314+
# NOTE: The unmodified function ARN may contain an alias extension e.g.
315+
# `arn:aws:lambda:region:account:function:name:alias`. We can ensure
316+
# the alias extension is not included in the `cloud.resource_id` by keeping
317+
# only the first 7 parts of the original ARN.
318+
#
319+
# See more:
320+
# https://docs.aws.amazon.com/lambda/latest/dg/python-context.html
321+
formatted_function_arn: str = ":".join(function_arn_parts[:7])
322+
323+
# NOTE: The specs mention an exception here, allowing the
324+
# `SpanAttributes.CLOUD_RESOURCE_ID` attribute to be set as a span
325+
# attribute instead of a resource attribute.
326+
#
327+
# See more:
328+
# https://github.com/open-telemetry/semantic-conventions/blob/main/docs/faas/aws-lambda.md#resource-detector
329+
return {
330+
CLOUD_ACCOUNT_ID: aws_account_id,
331+
CLOUD_RESOURCE_ID: formatted_function_arn,
332+
FAAS_INVOCATION_ID: lambda_context.aws_request_id,
333+
}
334+
335+
267336
# pylint: disable=too-many-statements
268337
def _instrument(
269338
wrapped_module_name,
@@ -278,38 +347,14 @@ def _instrument(
278347
def _instrumented_lambda_handler_call( # noqa pylint: disable=too-many-branches
279348
call_wrapped, instance, args, kwargs
280349
):
281-
orig_handler_name = ".".join(
282-
[wrapped_module_name, wrapped_function_name]
283-
)
284-
285-
lambda_event = args[0]
350+
lambda_event: Any = args[0]
351+
lambda_context: LambdaContext = args[1]
286352

287353
parent_context = _determine_parent_context(
288354
lambda_event,
289355
event_context_extractor,
290356
)
291357

292-
try:
293-
event_source = lambda_event["Records"][0].get(
294-
"eventSource"
295-
) or lambda_event["Records"][0].get("EventSource")
296-
if event_source in {
297-
"aws:sqs",
298-
"aws:s3",
299-
"aws:sns",
300-
"aws:dynamodb",
301-
}:
302-
# See more:
303-
# https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html
304-
# https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html
305-
# https://docs.aws.amazon.com/AmazonS3/latest/userguide/notification-content-structure.html
306-
# https://docs.aws.amazon.com/lambda/latest/dg/with-ddb.html
307-
span_kind = SpanKind.CONSUMER
308-
else:
309-
span_kind = SpanKind.SERVER
310-
except (IndexError, KeyError, TypeError):
311-
span_kind = SpanKind.SERVER
312-
313358
tracer = get_tracer(
314359
__name__,
315360
__version__,
@@ -320,38 +365,10 @@ def _instrumented_lambda_handler_call( # noqa pylint: disable=too-many-branches
320365
token = context_api.attach(parent_context)
321366
try:
322367
with tracer.start_as_current_span(
323-
name=orig_handler_name,
324-
kind=span_kind,
368+
name=lambda_context.function_name,
369+
kind=SpanKind.SERVER,
370+
attributes=_get_lambda_context_attributes(lambda_context),
325371
) as span:
326-
if span.is_recording():
327-
lambda_context = args[1]
328-
# NOTE: The specs mention an exception here, allowing the
329-
# `CLOUD_RESOURCE_ID` attribute to be set as a span
330-
# attribute instead of a resource attribute.
331-
#
332-
# See more:
333-
# https://github.com/open-telemetry/semantic-conventions/blob/main/docs/faas/aws-lambda.md#resource-detector
334-
span.set_attribute(
335-
CLOUD_RESOURCE_ID,
336-
lambda_context.invoked_function_arn,
337-
)
338-
span.set_attribute(
339-
FAAS_INVOCATION_ID,
340-
lambda_context.aws_request_id,
341-
)
342-
343-
# NOTE: `cloud.account.id` can be parsed from the ARN as the fifth item when splitting on `:`
344-
#
345-
# See more:
346-
# https://github.com/open-telemetry/semantic-conventions/blob/main/docs/faas/aws-lambda.md#all-triggers
347-
account_id = lambda_context.invoked_function_arn.split(
348-
":"
349-
)[4]
350-
span.set_attribute(
351-
CLOUD_ACCOUNT_ID,
352-
account_id,
353-
)
354-
355372
exception = None
356373
result = None
357374
try:

instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation_manual.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,18 +74,22 @@
7474

7575

7676
class MockLambdaContext:
77-
def __init__(self, aws_request_id, invoked_function_arn):
77+
def __init__(self, function_name, aws_request_id, invoked_function_arn):
78+
self.function_name = function_name
7879
self.invoked_function_arn = invoked_function_arn
7980
self.aws_request_id = aws_request_id
8081

8182

8283
MOCK_LAMBDA_CONTEXT = MockLambdaContext(
84+
function_name="myfunction",
8385
aws_request_id="mock_aws_request_id",
8486
invoked_function_arn="arn:aws:lambda:us-east-1:123456:function:myfunction:myalias",
8587
)
8688

8789
MOCK_LAMBDA_CONTEXT_ATTRIBUTES = {
88-
CLOUD_RESOURCE_ID: MOCK_LAMBDA_CONTEXT.invoked_function_arn,
90+
CLOUD_RESOURCE_ID: ":".join(
91+
MOCK_LAMBDA_CONTEXT.invoked_function_arn.split(":")[:7]
92+
),
8993
FAAS_INVOCATION_ID: MOCK_LAMBDA_CONTEXT.aws_request_id,
9094
CLOUD_ACCOUNT_ID: MOCK_LAMBDA_CONTEXT.invoked_function_arn.split(":")[4],
9195
}
@@ -115,7 +119,7 @@ def __init__(self, aws_request_id, invoked_function_arn):
115119
MOCK_W3C_BAGGAGE_VALUE = "baggage_value"
116120

117121

118-
def mock_execute_lambda(event=None):
122+
def mock_execute_lambda(event=None, context=None):
119123
"""Mocks the AWS Lambda execution.
120124
121125
NOTE: We don't use `moto`'s `mock_lambda` because we are not instrumenting
@@ -127,11 +131,14 @@ def mock_execute_lambda(event=None):
127131
128132
Args:
129133
event: The Lambda event which may or may not be used by instrumentation.
134+
context: The AWS Lambda context to call the handler with
130135
"""
131136

132137
module_name, handler_name = os.environ[_HANDLER].rsplit(".", 1)
133138
handler_module = import_module(module_name.replace("/", "."))
134-
return getattr(handler_module, handler_name)(event, MOCK_LAMBDA_CONTEXT)
139+
return getattr(handler_module, handler_name)(
140+
event, context or MOCK_LAMBDA_CONTEXT
141+
)
135142

136143

137144
class TestAwsLambdaInstrumentorBase(TestBase):
@@ -183,7 +190,7 @@ def test_active_tracing(self):
183190

184191
self.assertEqual(len(spans), 1)
185192
span = spans[0]
186-
self.assertEqual(span.name, os.environ[_HANDLER])
193+
self.assertEqual(span.name, MOCK_LAMBDA_CONTEXT.function_name)
187194
self.assertEqual(span.get_span_context().trace_id, MOCK_XRAY_TRACE_ID)
188195
self.assertEqual(span.kind, SpanKind.SERVER)
189196
self.assertSpanHasAttributes(
@@ -419,7 +426,7 @@ def test_lambda_handles_multiple_consumers(self):
419426
assert len(spans) == 4
420427

421428
for span in spans:
422-
assert span.kind == SpanKind.CONSUMER
429+
assert span.kind == SpanKind.SERVER
423430

424431
test_env_patch.stop()
425432

@@ -676,7 +683,7 @@ def test_dynamo_db_event_sets_attributes(self):
676683
self.assertEqual(len(spans), 1)
677684

678685
span, *_ = spans
679-
self.assertEqual(span.kind, SpanKind.CONSUMER)
686+
self.assertEqual(span.kind, SpanKind.SERVER)
680687
self.assertSpanHasAttributes(
681688
span,
682689
MOCK_LAMBDA_CONTEXT_ATTRIBUTES,
@@ -691,7 +698,7 @@ def test_s3_event_sets_attributes(self):
691698
self.assertEqual(len(spans), 1)
692699

693700
span, *_ = spans
694-
self.assertEqual(span.kind, SpanKind.CONSUMER)
701+
self.assertEqual(span.kind, SpanKind.SERVER)
695702
self.assertSpanHasAttributes(
696703
span,
697704
MOCK_LAMBDA_CONTEXT_ATTRIBUTES,
@@ -706,7 +713,7 @@ def test_sns_event_sets_attributes(self):
706713
self.assertEqual(len(spans), 1)
707714

708715
span, *_ = spans
709-
self.assertEqual(span.kind, SpanKind.CONSUMER)
716+
self.assertEqual(span.kind, SpanKind.SERVER)
710717
self.assertSpanHasAttributes(
711718
span,
712719
MOCK_LAMBDA_CONTEXT_ATTRIBUTES,
@@ -721,7 +728,7 @@ def test_sqs_event_sets_attributes(self):
721728
self.assertEqual(len(spans), 1)
722729

723730
span, *_ = spans
724-
self.assertEqual(span.kind, SpanKind.CONSUMER)
731+
self.assertEqual(span.kind, SpanKind.SERVER)
725732
self.assertSpanHasAttributes(
726733
span,
727734
MOCK_LAMBDA_CONTEXT_ATTRIBUTES,

0 commit comments

Comments
 (0)