From 905fcfe8d6806be5502f385e5f80353394090481 Mon Sep 17 00:00:00 2001 From: Luke Zhang Date: Tue, 30 Sep 2025 15:37:26 -0700 Subject: [PATCH 1/3] Add AWS_LAMBDA_RESOURCE_MAPPING_ID Semantic Convention Support for AWS Lambda SDK This PR adds support for the AWS_LAMBDA_RESOURCE_MAPPING_ID semantic convention attribute in the AWS Lambda SDK instrumentation library. https://github.com/open-telemetry/semantic-conventions/blob/main/docs/registry/attributes/aws.md#amazon-lambda-attributes It also introduces the following two experimental attributes. Work is currently underway to add these keys to the AWS section of the Semantic Conventions registry: aws.lambda.function.arn aws.lambda.function.name Name is extracted from request object. ARN is extracted from response object. Resource Mapping ID is extracted from both request and response objects. This behavior is covered by unit tests. Tests Added new unit tests (passing). Verified with: tox -e py312-test-instrumentation-botocore tox -e spellcheck tox -e lint-instrumentation-botocore tox -e ruff Backward Compatibility This change is backward compatible. It only adds instrumentation for additional AWS resources and does not modify existing behavior in the auto-instrumentation library. --- .../botocore/extensions/lmbd.py | 49 +++++++- .../tests/test_botocore_lambda.py | 107 ++++++++++++++++++ 2 files changed, 150 insertions(+), 6 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/lmbd.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/lmbd.py index 0d4a1656a3..b49b8e29e4 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/lmbd.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/lmbd.py @@ -16,18 +16,27 @@ import inspect import json import re -from typing import Dict +from typing import Dict, Final from opentelemetry.instrumentation.botocore.extensions.types import ( _AttributeMapT, _AwsSdkCallContext, _AwsSdkExtension, _BotocoreInstrumentorContext, + _BotoResultT, ) from opentelemetry.propagate import inject +from opentelemetry.semconv._incubating.attributes.aws_attributes import ( + AWS_LAMBDA_RESOURCE_MAPPING_ID, +) from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace.span import Span +# Work is underway to add these two keys to the SemConv AWS registry, in line with other AWS resources. +# https://github.com/open-telemetry/semantic-conventions/blob/main/docs/registry/attributes/aws.md#amazon-lambda-attributes +AWS_LAMBDA_FUNCTION_ARN: Final = "aws.lambda.function.arn" +AWS_LAMBDA_FUNCTION_NAME: Final = "aws.lambda.function.name" + class _LambdaOperation(abc.ABC): @classmethod @@ -68,12 +77,16 @@ def extract_attributes( ) attributes[SpanAttributes.FAAS_INVOKED_REGION] = call_context.region - @classmethod - def _parse_function_name(cls, call_context: _AwsSdkCallContext): + @staticmethod + def _parse_function_name(call_context: _AwsSdkCallContext): function_name_or_arn = call_context.params.get("FunctionName") - matches = cls.ARN_LAMBDA_PATTERN.match(function_name_or_arn) - function_name = matches.group(1) - return function_name_or_arn if function_name is None else function_name + if function_name_or_arn is None: + return None + matches = _OpInvoke.ARN_LAMBDA_PATTERN.match(function_name_or_arn) + if matches: + function_name = matches.group(1) + return function_name if function_name else function_name_or_arn + return function_name_or_arn @classmethod def before_service_call(cls, call_context: _AwsSdkCallContext, span: Span): @@ -115,6 +128,13 @@ def __init__(self, call_context: _AwsSdkCallContext): self._op = _OPERATION_MAPPING.get(call_context.operation) def extract_attributes(self, attributes: _AttributeMapT): + function_name = _OpInvoke._parse_function_name(self._call_context) + if function_name: + attributes[AWS_LAMBDA_FUNCTION_NAME] = function_name + resource_mapping_id = self._call_context.params.get("UUID") + if resource_mapping_id: + attributes[AWS_LAMBDA_RESOURCE_MAPPING_ID] = resource_mapping_id + if self._op is None: return @@ -127,3 +147,20 @@ def before_service_call( return self._op.before_service_call(self._call_context, span) + + def on_success( + self, + span: Span, + result: _BotoResultT, + instrumentor_context: _BotocoreInstrumentorContext, + ): + resource_mapping_id = result.get("UUID") + if resource_mapping_id: + span.set_attribute( + AWS_LAMBDA_RESOURCE_MAPPING_ID, resource_mapping_id + ) + + lambda_configuration = result.get("Configuration", {}) + function_arn = lambda_configuration.get("FunctionArn") + if function_arn: + span.set_attribute(AWS_LAMBDA_FUNCTION_ARN, function_arn) diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_lambda.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_lambda.py index 4b2d359ac9..3e2c2dadae 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_lambda.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_lambda.py @@ -16,6 +16,7 @@ import json import sys import zipfile +from typing import Final from unittest import mock import botocore.session @@ -27,11 +28,17 @@ _LambdaExtension, ) from opentelemetry.propagate import get_global_textmap, set_global_textmap +from opentelemetry.semconv._incubating.attributes.aws_attributes import ( + AWS_LAMBDA_RESOURCE_MAPPING_ID, +) from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.mock_textmap import MockTextMapPropagator from opentelemetry.test.test_base import TestBase from opentelemetry.trace.span import Span +AWS_LAMBDA_FUNCTION_ARN: Final = "aws.lambda.function.arn" +AWS_LAMBDA_FUNCTION_NAME: Final = "aws.lambda.function.name" + def get_as_zip_file(file_name, content): zip_output = io.BytesIO() @@ -126,6 +133,18 @@ def _create_lambda_function(self, function_name: str, function_code: str): Publish=True, ) + def _create_sqs_queue_and_get_arn(self) -> str: + """Helper method to create SQS queue and return ARN""" + session = botocore.session.get_session() + session.set_credentials( + access_key="access-key", secret_key="secret-key" + ) + sqs_client = session.create_client("sqs", region_name=self.region) + sqs_client.create_queue( + QueueName="MyTestQueue.fifo", Attributes={"FifoQueue": "true"} + ) + return f"arn:aws:sqs:{self.region}:123456789012:MyTestQueue.fifo" + @mark.skip(reason="Docker error, unblocking builds for now.") @mark.skipif( sys.platform == "win32", @@ -185,3 +204,91 @@ def test_invoke_parse_arn(self): self.assertEqual( function_name, attributes[SpanAttributes.FAAS_INVOKED_NAME] ) + + @mock_aws + def test_get_function(self): + function_name = "lambda-function-name-foo" + self._create_lambda_function( + function_name, return_headers_lambda_str() + ) + + self.memory_exporter.clear() + self.client.get_function(FunctionName=function_name) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(1, len(spans)) + span = spans[0] + self.assertEqual( + "GetFunction", span.attributes[SpanAttributes.RPC_METHOD] + ) + self.assertEqual( + "lambda-function-name-foo", + span.attributes[AWS_LAMBDA_FUNCTION_NAME], + ) + + function_arn = span.attributes.get(AWS_LAMBDA_FUNCTION_ARN) + self.assertIsNotNone(function_arn) + self.assertIn("lambda-function-name-foo", function_arn) + + @mock_aws + def test_create_event_source_mapping(self): + function_name = "MyLambdaFnFoo" + self._create_lambda_function( + function_name, return_headers_lambda_str() + ) + + queue_arn = self._create_sqs_queue_and_get_arn() + self.memory_exporter.clear() + response = self.client.create_event_source_mapping( + EventSourceArn=queue_arn, FunctionName=function_name, BatchSize=10 + ) + expected_uuid = response["UUID"] + self.assertIsNotNone(expected_uuid) + self.assertTrue(expected_uuid) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(1, len(spans)) + span = spans[0] + self.assertEqual( + "CreateEventSourceMapping", + span.attributes[SpanAttributes.RPC_METHOD], + ) + self.assertEqual( + "MyLambdaFnFoo", span.attributes[AWS_LAMBDA_FUNCTION_NAME] + ) + + uuid = span.attributes.get(AWS_LAMBDA_RESOURCE_MAPPING_ID) + self.assertIsNotNone(uuid) + self.assertEqual(expected_uuid, uuid) + + @mock_aws + def test_get_event_source_mapping(self): + function_name = "MyLambdaFnBar" + self._create_lambda_function( + function_name, return_headers_lambda_str() + ) + + queue_arn = self._create_sqs_queue_and_get_arn() + + # Create event source mapping first + create_response = self.client.create_event_source_mapping( + EventSourceArn=queue_arn, FunctionName=function_name, BatchSize=10 + ) + mapping_uuid = create_response["UUID"] + + self.memory_exporter.clear() + response = self.client.get_event_source_mapping(UUID=mapping_uuid) + expected_uuid = response["UUID"] + self.assertIsNotNone(expected_uuid) + self.assertTrue(expected_uuid) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(1, len(spans)) + span = spans[0] + self.assertEqual( + "GetEventSourceMapping", + span.attributes[SpanAttributes.RPC_METHOD], + ) + + uuid = span.attributes.get(AWS_LAMBDA_RESOURCE_MAPPING_ID) + self.assertIsNotNone(uuid) + self.assertEqual(expected_uuid, uuid) From 2ef3c8b7c553cbb809ef4704a58eed26220b815a Mon Sep 17 00:00:00 2001 From: Luke Zhang Date: Tue, 30 Sep 2025 15:50:47 -0700 Subject: [PATCH 2/3] add changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8349f4b942..e383f1401b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3765](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3765)) - Add `rstcheck` to pre-commit to stop introducing invalid RST ([#3777](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3777)) +- `opentelemetry-instrumentation-botocore`: botocore: Add AWS_LAMBDA_RESOURCE_MAPPING_ID Semantic Convention Support for AWS Lambda SDK + ([#3800](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3800)) - `opentelemetry-exporter-credential-provider-gcp`: create this package which provides support for supplying your machine's Application Default Credentials (https://cloud.google.com/docs/authentication/application-default-credentials) to the OTLP Exporters created automatically by OpenTelemetry Python's auto instrumentation. These credentials authorize OTLP traces to be sent to `telemetry.googleapis.com`. [#3766](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3766). From 83747fb3b563c7406ccaea6ecd0e8319643b6fbe Mon Sep 17 00:00:00 2001 From: Luke Zhang Date: Tue, 30 Sep 2025 16:45:26 -0700 Subject: [PATCH 3/3] address cr feedback. --- .../tests/test_botocore_lambda.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_lambda.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_lambda.py index 3e2c2dadae..a1677e64f1 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_lambda.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_lambda.py @@ -228,6 +228,7 @@ def test_get_function(self): function_arn = span.attributes.get(AWS_LAMBDA_FUNCTION_ARN) self.assertIsNotNone(function_arn) self.assertIn("lambda-function-name-foo", function_arn) + self.assertIsNone(span.attributes.get(AWS_LAMBDA_RESOURCE_MAPPING_ID)) @mock_aws def test_create_event_source_mapping(self): @@ -292,3 +293,5 @@ def test_get_event_source_mapping(self): uuid = span.attributes.get(AWS_LAMBDA_RESOURCE_MAPPING_ID) self.assertIsNotNone(uuid) self.assertEqual(expected_uuid, uuid) + self.assertIsNone(span.attributes.get(AWS_LAMBDA_FUNCTION_ARN)) + self.assertIsNone(span.attributes.get(AWS_LAMBDA_FUNCTION_NAME))