Skip to content

Commit 905fcfe

Browse files
committed
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.
1 parent 229d969 commit 905fcfe

File tree

2 files changed

+150
-6
lines changed

2 files changed

+150
-6
lines changed

instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/lmbd.py

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,27 @@
1616
import inspect
1717
import json
1818
import re
19-
from typing import Dict
19+
from typing import Dict, Final
2020

2121
from opentelemetry.instrumentation.botocore.extensions.types import (
2222
_AttributeMapT,
2323
_AwsSdkCallContext,
2424
_AwsSdkExtension,
2525
_BotocoreInstrumentorContext,
26+
_BotoResultT,
2627
)
2728
from opentelemetry.propagate import inject
29+
from opentelemetry.semconv._incubating.attributes.aws_attributes import (
30+
AWS_LAMBDA_RESOURCE_MAPPING_ID,
31+
)
2832
from opentelemetry.semconv.trace import SpanAttributes
2933
from opentelemetry.trace.span import Span
3034

35+
# Work is underway to add these two keys to the SemConv AWS registry, in line with other AWS resources.
36+
# https://github.com/open-telemetry/semantic-conventions/blob/main/docs/registry/attributes/aws.md#amazon-lambda-attributes
37+
AWS_LAMBDA_FUNCTION_ARN: Final = "aws.lambda.function.arn"
38+
AWS_LAMBDA_FUNCTION_NAME: Final = "aws.lambda.function.name"
39+
3140

3241
class _LambdaOperation(abc.ABC):
3342
@classmethod
@@ -68,12 +77,16 @@ def extract_attributes(
6877
)
6978
attributes[SpanAttributes.FAAS_INVOKED_REGION] = call_context.region
7079

71-
@classmethod
72-
def _parse_function_name(cls, call_context: _AwsSdkCallContext):
80+
@staticmethod
81+
def _parse_function_name(call_context: _AwsSdkCallContext):
7382
function_name_or_arn = call_context.params.get("FunctionName")
74-
matches = cls.ARN_LAMBDA_PATTERN.match(function_name_or_arn)
75-
function_name = matches.group(1)
76-
return function_name_or_arn if function_name is None else function_name
83+
if function_name_or_arn is None:
84+
return None
85+
matches = _OpInvoke.ARN_LAMBDA_PATTERN.match(function_name_or_arn)
86+
if matches:
87+
function_name = matches.group(1)
88+
return function_name if function_name else function_name_or_arn
89+
return function_name_or_arn
7790

7891
@classmethod
7992
def before_service_call(cls, call_context: _AwsSdkCallContext, span: Span):
@@ -115,6 +128,13 @@ def __init__(self, call_context: _AwsSdkCallContext):
115128
self._op = _OPERATION_MAPPING.get(call_context.operation)
116129

117130
def extract_attributes(self, attributes: _AttributeMapT):
131+
function_name = _OpInvoke._parse_function_name(self._call_context)
132+
if function_name:
133+
attributes[AWS_LAMBDA_FUNCTION_NAME] = function_name
134+
resource_mapping_id = self._call_context.params.get("UUID")
135+
if resource_mapping_id:
136+
attributes[AWS_LAMBDA_RESOURCE_MAPPING_ID] = resource_mapping_id
137+
118138
if self._op is None:
119139
return
120140

@@ -127,3 +147,20 @@ def before_service_call(
127147
return
128148

129149
self._op.before_service_call(self._call_context, span)
150+
151+
def on_success(
152+
self,
153+
span: Span,
154+
result: _BotoResultT,
155+
instrumentor_context: _BotocoreInstrumentorContext,
156+
):
157+
resource_mapping_id = result.get("UUID")
158+
if resource_mapping_id:
159+
span.set_attribute(
160+
AWS_LAMBDA_RESOURCE_MAPPING_ID, resource_mapping_id
161+
)
162+
163+
lambda_configuration = result.get("Configuration", {})
164+
function_arn = lambda_configuration.get("FunctionArn")
165+
if function_arn:
166+
span.set_attribute(AWS_LAMBDA_FUNCTION_ARN, function_arn)

instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_lambda.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import json
1717
import sys
1818
import zipfile
19+
from typing import Final
1920
from unittest import mock
2021

2122
import botocore.session
@@ -27,11 +28,17 @@
2728
_LambdaExtension,
2829
)
2930
from opentelemetry.propagate import get_global_textmap, set_global_textmap
31+
from opentelemetry.semconv._incubating.attributes.aws_attributes import (
32+
AWS_LAMBDA_RESOURCE_MAPPING_ID,
33+
)
3034
from opentelemetry.semconv.trace import SpanAttributes
3135
from opentelemetry.test.mock_textmap import MockTextMapPropagator
3236
from opentelemetry.test.test_base import TestBase
3337
from opentelemetry.trace.span import Span
3438

39+
AWS_LAMBDA_FUNCTION_ARN: Final = "aws.lambda.function.arn"
40+
AWS_LAMBDA_FUNCTION_NAME: Final = "aws.lambda.function.name"
41+
3542

3643
def get_as_zip_file(file_name, content):
3744
zip_output = io.BytesIO()
@@ -126,6 +133,18 @@ def _create_lambda_function(self, function_name: str, function_code: str):
126133
Publish=True,
127134
)
128135

136+
def _create_sqs_queue_and_get_arn(self) -> str:
137+
"""Helper method to create SQS queue and return ARN"""
138+
session = botocore.session.get_session()
139+
session.set_credentials(
140+
access_key="access-key", secret_key="secret-key"
141+
)
142+
sqs_client = session.create_client("sqs", region_name=self.region)
143+
sqs_client.create_queue(
144+
QueueName="MyTestQueue.fifo", Attributes={"FifoQueue": "true"}
145+
)
146+
return f"arn:aws:sqs:{self.region}:123456789012:MyTestQueue.fifo"
147+
129148
@mark.skip(reason="Docker error, unblocking builds for now.")
130149
@mark.skipif(
131150
sys.platform == "win32",
@@ -185,3 +204,91 @@ def test_invoke_parse_arn(self):
185204
self.assertEqual(
186205
function_name, attributes[SpanAttributes.FAAS_INVOKED_NAME]
187206
)
207+
208+
@mock_aws
209+
def test_get_function(self):
210+
function_name = "lambda-function-name-foo"
211+
self._create_lambda_function(
212+
function_name, return_headers_lambda_str()
213+
)
214+
215+
self.memory_exporter.clear()
216+
self.client.get_function(FunctionName=function_name)
217+
spans = self.memory_exporter.get_finished_spans()
218+
self.assertEqual(1, len(spans))
219+
span = spans[0]
220+
self.assertEqual(
221+
"GetFunction", span.attributes[SpanAttributes.RPC_METHOD]
222+
)
223+
self.assertEqual(
224+
"lambda-function-name-foo",
225+
span.attributes[AWS_LAMBDA_FUNCTION_NAME],
226+
)
227+
228+
function_arn = span.attributes.get(AWS_LAMBDA_FUNCTION_ARN)
229+
self.assertIsNotNone(function_arn)
230+
self.assertIn("lambda-function-name-foo", function_arn)
231+
232+
@mock_aws
233+
def test_create_event_source_mapping(self):
234+
function_name = "MyLambdaFnFoo"
235+
self._create_lambda_function(
236+
function_name, return_headers_lambda_str()
237+
)
238+
239+
queue_arn = self._create_sqs_queue_and_get_arn()
240+
self.memory_exporter.clear()
241+
response = self.client.create_event_source_mapping(
242+
EventSourceArn=queue_arn, FunctionName=function_name, BatchSize=10
243+
)
244+
expected_uuid = response["UUID"]
245+
self.assertIsNotNone(expected_uuid)
246+
self.assertTrue(expected_uuid)
247+
248+
spans = self.memory_exporter.get_finished_spans()
249+
self.assertEqual(1, len(spans))
250+
span = spans[0]
251+
self.assertEqual(
252+
"CreateEventSourceMapping",
253+
span.attributes[SpanAttributes.RPC_METHOD],
254+
)
255+
self.assertEqual(
256+
"MyLambdaFnFoo", span.attributes[AWS_LAMBDA_FUNCTION_NAME]
257+
)
258+
259+
uuid = span.attributes.get(AWS_LAMBDA_RESOURCE_MAPPING_ID)
260+
self.assertIsNotNone(uuid)
261+
self.assertEqual(expected_uuid, uuid)
262+
263+
@mock_aws
264+
def test_get_event_source_mapping(self):
265+
function_name = "MyLambdaFnBar"
266+
self._create_lambda_function(
267+
function_name, return_headers_lambda_str()
268+
)
269+
270+
queue_arn = self._create_sqs_queue_and_get_arn()
271+
272+
# Create event source mapping first
273+
create_response = self.client.create_event_source_mapping(
274+
EventSourceArn=queue_arn, FunctionName=function_name, BatchSize=10
275+
)
276+
mapping_uuid = create_response["UUID"]
277+
278+
self.memory_exporter.clear()
279+
response = self.client.get_event_source_mapping(UUID=mapping_uuid)
280+
expected_uuid = response["UUID"]
281+
self.assertIsNotNone(expected_uuid)
282+
self.assertTrue(expected_uuid)
283+
284+
spans = self.memory_exporter.get_finished_spans()
285+
self.assertEqual(1, len(spans))
286+
span = spans[0]
287+
self.assertEqual(
288+
"GetEventSourceMapping",
289+
span.attributes[SpanAttributes.RPC_METHOD],
290+
)
291+
292+
uuid = span.attributes.get(AWS_LAMBDA_RESOURCE_MAPPING_ID)
293+
self.assertIsNotNone(uuid)
294+
self.assertEqual(expected_uuid, uuid)

0 commit comments

Comments
 (0)