diff --git a/CHANGELOG.md b/CHANGELOG.md index 95d22a4600..468ef4643e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3777](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3777)) - `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). +- `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)) ## Version 1.37.0/0.58b0 (2025-09-11) 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..a1677e64f1 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,94 @@ 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) + self.assertIsNone(span.attributes.get(AWS_LAMBDA_RESOURCE_MAPPING_ID)) + + @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) + self.assertIsNone(span.attributes.get(AWS_LAMBDA_FUNCTION_ARN)) + self.assertIsNone(span.attributes.get(AWS_LAMBDA_FUNCTION_NAME))