From 92fc2f278849b66eaa78032d00251078dfb13b13 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Fri, 21 Nov 2025 00:53:59 -0500 Subject: [PATCH 1/4] update invocation span to have kind SERVER and have name equal to the function name --- .../instrumentation/aws_lambda/__init__.py | 108 +++++++++--------- 1 file changed, 51 insertions(+), 57 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py index 9b450cdf21..2aa3456f9e 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py @@ -72,6 +72,7 @@ def custom_event_context_extractor(lambda_event): import logging import os import time +import typing from importlib import import_module from typing import Any, Callable, Collection from urllib.parse import urlencode @@ -124,6 +125,27 @@ def custom_event_context_extractor(lambda_event): ) +class _LambdaContext(typing.Protocol): + """Type definition for AWS Lambda context object. + + This Protocol defines the interface for the context object passed to Lambda + function handlers, providing information about the invocation, function, and + execution environment. + + See Also: + AWS Lambda Context Object documentation: + https://docs.aws.amazon.com/lambda/latest/dg/python-context.html + """ + + function_name: str + function_version: str + invoked_function_arn: str + memory_limit_in_mb: int + aws_request_id: str + log_group_name: str + log_stream_name: str + + def _default_event_context_extractor(lambda_event: Any) -> Context: """Default way of extracting the context from the Lambda Event. @@ -264,6 +286,30 @@ def _set_api_gateway_v2_proxy_attributes( return span +def _get_lambda_context_attributes( + lambda_context: _LambdaContext, +) -> dict[str, str]: + function_arn_parts: list[str] = lambda_context.invoked_function_arn.split( + ":" + ) + aws_account_id: str = function_arn_parts[4] + # Remove potential function alias or version from ARN by keeping only the + # first 7 parts (arn:aws:lambda:region:account:function:name) + formatted_function_arn: str = ":".join(function_arn_parts[:7]) + + # NOTE: The specs mention an exception here, allowing the + # `SpanAttributes.CLOUD_RESOURCE_ID` attribute to be set as a span + # attribute instead of a resource attribute. + # + # See more: + # https://github.com/open-telemetry/semantic-conventions/blob/main/docs/faas/aws-lambda.md#resource-detector + return { + CLOUD_ACCOUNT_ID: aws_account_id, + CLOUD_RESOURCE_ID: formatted_function_arn, + FAAS_INVOCATION_ID: lambda_context.aws_request_id, + } + + # pylint: disable=too-many-statements def _instrument( wrapped_module_name, @@ -278,38 +324,14 @@ def _instrument( def _instrumented_lambda_handler_call( # noqa pylint: disable=too-many-branches call_wrapped, instance, args, kwargs ): - orig_handler_name = ".".join( - [wrapped_module_name, wrapped_function_name] - ) - - lambda_event = args[0] + lambda_event: Any = args[0] + lambda_context: _LambdaContext = args[1] parent_context = _determine_parent_context( lambda_event, event_context_extractor, ) - try: - event_source = lambda_event["Records"][0].get( - "eventSource" - ) or lambda_event["Records"][0].get("EventSource") - if event_source in { - "aws:sqs", - "aws:s3", - "aws:sns", - "aws:dynamodb", - }: - # See more: - # https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html - # https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html - # https://docs.aws.amazon.com/AmazonS3/latest/userguide/notification-content-structure.html - # https://docs.aws.amazon.com/lambda/latest/dg/with-ddb.html - span_kind = SpanKind.CONSUMER - else: - span_kind = SpanKind.SERVER - except (IndexError, KeyError, TypeError): - span_kind = SpanKind.SERVER - tracer = get_tracer( __name__, __version__, @@ -320,38 +342,10 @@ def _instrumented_lambda_handler_call( # noqa pylint: disable=too-many-branches token = context_api.attach(parent_context) try: with tracer.start_as_current_span( - name=orig_handler_name, - kind=span_kind, + name=lambda_context.function_name, + kind=SpanKind.SERVER, + attributes=_get_lambda_context_attributes(lambda_context), ) as span: - if span.is_recording(): - lambda_context = args[1] - # NOTE: The specs mention an exception here, allowing the - # `SpanAttributes.CLOUD_RESOURCE_ID` attribute to be set as a span - # attribute instead of a resource attribute. - # - # See more: - # https://github.com/open-telemetry/semantic-conventions/blob/main/docs/faas/aws-lambda.md#resource-detector - span.set_attribute( - CLOUD_RESOURCE_ID, - lambda_context.invoked_function_arn, - ) - span.set_attribute( - FAAS_INVOCATION_ID, - lambda_context.aws_request_id, - ) - - # NOTE: `cloud.account.id` can be parsed from the ARN as the fifth item when splitting on `:` - # - # See more: - # https://github.com/open-telemetry/semantic-conventions/blob/main/docs/faas/aws-lambda.md#all-triggers - account_id = lambda_context.invoked_function_arn.split( - ":" - )[4] - span.set_attribute( - CLOUD_ACCOUNT_ID, - account_id, - ) - exception = None result = None try: From 27907f1a724a887319934fe77fad8e5f028a9eb3 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Fri, 21 Nov 2025 14:57:15 -0500 Subject: [PATCH 2/4] update Lambda invocation span name and kind --- .../instrumentation/aws_lambda/__init__.py | 24 +++++++++++++++-- .../test_aws_lambda_instrumentation_manual.py | 27 ++++++++++++------- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py index 2aa3456f9e..12bfa0bc86 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py @@ -289,12 +289,32 @@ def _set_api_gateway_v2_proxy_attributes( def _get_lambda_context_attributes( lambda_context: _LambdaContext, ) -> dict[str, str]: + """Extracts OpenTelemetry span attributes from AWS Lambda context. + + Extract FaaS specific attributes from the AWS Lambda context + according to OpenTelemetry semantic conventions for FaaS & AWS Lambda. + + Args: + lambda_context: The AWS Lambda context object. + + Returns: + A dictionary mapping of OpenTelemetry attribute names to their values. + """ function_arn_parts: list[str] = lambda_context.invoked_function_arn.split( ":" ) + # NOTE: `cloud.account.id` can be parsed from the ARN as the fifth item when splitting on `:` + # + # See more: + # https://github.com/open-telemetry/semantic-conventions/blob/main/docs/faas/aws-lambda.md#all-triggers aws_account_id: str = function_arn_parts[4] - # Remove potential function alias or version from ARN by keeping only the - # first 7 parts (arn:aws:lambda:region:account:function:name) + # NOTE: The unmodified function ARN may contain an alias extension e.g. + # `arn:aws:lambda:region:account:function:name:alias`. We can ensure + # the alias extension is not included in the `cloud.resource_id` by keeping + # only the first 7 parts of the original ARN. + # + # See more: + # https://docs.aws.amazon.com/lambda/latest/dg/python-context.html formatted_function_arn: str = ":".join(function_arn_parts[:7]) # NOTE: The specs mention an exception here, allowing the diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation_manual.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation_manual.py index a468cb986a..9f10a9f0fa 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation_manual.py +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation_manual.py @@ -74,18 +74,22 @@ class MockLambdaContext: - def __init__(self, aws_request_id, invoked_function_arn): + def __init__(self, function_name, aws_request_id, invoked_function_arn): + self.function_name = function_name self.invoked_function_arn = invoked_function_arn self.aws_request_id = aws_request_id MOCK_LAMBDA_CONTEXT = MockLambdaContext( + function_name="myfunction", aws_request_id="mock_aws_request_id", invoked_function_arn="arn:aws:lambda:us-east-1:123456:function:myfunction:myalias", ) MOCK_LAMBDA_CONTEXT_ATTRIBUTES = { - CLOUD_RESOURCE_ID: MOCK_LAMBDA_CONTEXT.invoked_function_arn, + CLOUD_RESOURCE_ID: ":".join( + MOCK_LAMBDA_CONTEXT.invoked_function_arn.split(":")[:7] + ), FAAS_INVOCATION_ID: MOCK_LAMBDA_CONTEXT.aws_request_id, CLOUD_ACCOUNT_ID: MOCK_LAMBDA_CONTEXT.invoked_function_arn.split(":")[4], } @@ -115,7 +119,7 @@ def __init__(self, aws_request_id, invoked_function_arn): MOCK_W3C_BAGGAGE_VALUE = "baggage_value" -def mock_execute_lambda(event=None): +def mock_execute_lambda(event=None, context=None): """Mocks the AWS Lambda execution. NOTE: We don't use `moto`'s `mock_lambda` because we are not instrumenting @@ -127,11 +131,14 @@ def mock_execute_lambda(event=None): Args: event: The Lambda event which may or may not be used by instrumentation. + context: The AWS Lambda context to call the handler with """ module_name, handler_name = os.environ[_HANDLER].rsplit(".", 1) handler_module = import_module(module_name.replace("/", ".")) - return getattr(handler_module, handler_name)(event, MOCK_LAMBDA_CONTEXT) + return getattr(handler_module, handler_name)( + event, context or MOCK_LAMBDA_CONTEXT + ) class TestAwsLambdaInstrumentorBase(TestBase): @@ -183,7 +190,7 @@ def test_active_tracing(self): self.assertEqual(len(spans), 1) span = spans[0] - self.assertEqual(span.name, os.environ[_HANDLER]) + self.assertEqual(span.name, MOCK_LAMBDA_CONTEXT.function_name) self.assertEqual(span.get_span_context().trace_id, MOCK_XRAY_TRACE_ID) self.assertEqual(span.kind, SpanKind.SERVER) self.assertSpanHasAttributes( @@ -419,7 +426,7 @@ def test_lambda_handles_multiple_consumers(self): assert len(spans) == 4 for span in spans: - assert span.kind == SpanKind.CONSUMER + assert span.kind == SpanKind.SERVER test_env_patch.stop() @@ -676,7 +683,7 @@ def test_dynamo_db_event_sets_attributes(self): self.assertEqual(len(spans), 1) span, *_ = spans - self.assertEqual(span.kind, SpanKind.CONSUMER) + self.assertEqual(span.kind, SpanKind.SERVER) self.assertSpanHasAttributes( span, MOCK_LAMBDA_CONTEXT_ATTRIBUTES, @@ -691,7 +698,7 @@ def test_s3_event_sets_attributes(self): self.assertEqual(len(spans), 1) span, *_ = spans - self.assertEqual(span.kind, SpanKind.CONSUMER) + self.assertEqual(span.kind, SpanKind.SERVER) self.assertSpanHasAttributes( span, MOCK_LAMBDA_CONTEXT_ATTRIBUTES, @@ -706,7 +713,7 @@ def test_sns_event_sets_attributes(self): self.assertEqual(len(spans), 1) span, *_ = spans - self.assertEqual(span.kind, SpanKind.CONSUMER) + self.assertEqual(span.kind, SpanKind.SERVER) self.assertSpanHasAttributes( span, MOCK_LAMBDA_CONTEXT_ATTRIBUTES, @@ -721,7 +728,7 @@ def test_sqs_event_sets_attributes(self): self.assertEqual(len(spans), 1) span, *_ = spans - self.assertEqual(span.kind, SpanKind.CONSUMER) + self.assertEqual(span.kind, SpanKind.SERVER) self.assertSpanHasAttributes( span, MOCK_LAMBDA_CONTEXT_ATTRIBUTES, From f20d687e8008da967b5aa331e4f292a0ebfc375f Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Fri, 21 Nov 2025 15:03:18 -0500 Subject: [PATCH 3/4] update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c124687a33..f6afe03a50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3941](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3941)) - `opentelemetry-instrumentation-pymongo`: Fix invalid mongodb collection attribute type ([#3942](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3942)) +- `opentelemetry-instrumentation-aws-lambda`: Fix improper invocation `Span` name and kind. + ([#3966](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3966)) ## Version 1.38.0/0.59b0 (2025-10-16) From 44e62910eec0027fec52e60fba26de203ff301b2 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Fri, 21 Nov 2025 19:51:38 -0500 Subject: [PATCH 4/4] add type checking check --- .../instrumentation/aws_lambda/__init__.py | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py index 12bfa0bc86..e666250c63 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py @@ -69,12 +69,13 @@ def custom_event_context_extractor(lambda_event): --- """ +from __future__ import annotations + import logging import os import time -import typing from importlib import import_module -from typing import Any, Callable, Collection +from typing import TYPE_CHECKING, Any, Callable, Collection from urllib.parse import urlencode from wrapt import wrap_function_wrapper @@ -124,26 +125,28 @@ def custom_event_context_extractor(lambda_event): "OTEL_INSTRUMENTATION_AWS_LAMBDA_FLUSH_TIMEOUT" ) +if TYPE_CHECKING: + import typing -class _LambdaContext(typing.Protocol): - """Type definition for AWS Lambda context object. + class LambdaContext(typing.Protocol): + """Type definition for AWS Lambda context object. - This Protocol defines the interface for the context object passed to Lambda - function handlers, providing information about the invocation, function, and - execution environment. + This Protocol defines the interface for the context object passed to Lambda + function handlers, providing information about the invocation, function, and + execution environment. - See Also: - AWS Lambda Context Object documentation: - https://docs.aws.amazon.com/lambda/latest/dg/python-context.html - """ + See Also: + AWS Lambda Context Object documentation: + https://docs.aws.amazon.com/lambda/latest/dg/python-context.html + """ - function_name: str - function_version: str - invoked_function_arn: str - memory_limit_in_mb: int - aws_request_id: str - log_group_name: str - log_stream_name: str + function_name: str + function_version: str + invoked_function_arn: str + memory_limit_in_mb: int + aws_request_id: str + log_group_name: str + log_stream_name: str def _default_event_context_extractor(lambda_event: Any) -> Context: @@ -287,7 +290,7 @@ def _set_api_gateway_v2_proxy_attributes( def _get_lambda_context_attributes( - lambda_context: _LambdaContext, + lambda_context: LambdaContext, ) -> dict[str, str]: """Extracts OpenTelemetry span attributes from AWS Lambda context. @@ -345,7 +348,7 @@ def _instrumented_lambda_handler_call( # noqa pylint: disable=too-many-branches call_wrapped, instance, args, kwargs ): lambda_event: Any = args[0] - lambda_context: _LambdaContext = args[1] + lambda_context: LambdaContext = args[1] parent_context = _determine_parent_context( lambda_event,