Skip to content

Commit 6f0b98e

Browse files
committed
feat: Add auto-instrumentation support for Lambda
1 parent 525f0ef commit 6f0b98e

File tree

5 files changed

+98
-0
lines changed

5 files changed

+98
-0
lines changed

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_attribute_keys.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,6 @@
2828
AWS_SNS_TOPIC_ARN: str = "aws.sns.topic.arn"
2929
AWS_STEPFUNCTIONS_STATEMACHINE_ARN: str = "aws.stepfunctions.state_machine.arn"
3030
AWS_STEPFUNCTIONS_ACTIVITY_ARN: str = "aws.stepfunctions.activity.arn"
31+
AWS_LAMBDA_FUNCTION_NAME: str = "aws.lambda.function.name"
32+
AWS_LAMBDA_RESOURCEMAPPING_ID: str = "aws.lambda.resource_mapping.id"
33+
AWS_LAMBDA_FUNCTION_ARN: str = "aws.lambda.function.arn"

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER,
1414
AWS_KINESIS_STREAM_CONSUMERNAME,
1515
AWS_KINESIS_STREAM_NAME,
16+
AWS_LAMBDA_FUNCTION_ARN,
17+
AWS_LAMBDA_FUNCTION_NAME,
18+
AWS_LAMBDA_RESOURCEMAPPING_ID,
1619
AWS_LOCAL_OPERATION,
1720
AWS_LOCAL_SERVICE,
1821
AWS_REMOTE_DB_USER,
@@ -96,6 +99,7 @@
9699
_NORMALIZED_SECRETSMANAGER_SERVICE_NAME: str = "AWS::SecretsManager"
97100
_NORMALIZED_SNS_SERVICE_NAME: str = "AWS::SNS"
98101
_NORMALIZED_STEPFUNCTIONS_SERVICE_NAME: str = "AWS::StepFunctions"
102+
_NORMALIZED_LAMBDA_SERVICE_NAME: str = "AWS::Lambda"
99103
_DB_CONNECTION_STRING_TYPE: str = "DB::Connection"
100104

101105
# Special DEPENDENCY attribute value if GRAPHQL_OPERATION_TYPE attribute key is present.
@@ -442,6 +446,13 @@ def _set_remote_type_and_identifier(span: ReadableSpan, attributes: BoundedAttri
442446
elif is_key_present(span, AWS_STEPFUNCTIONS_ACTIVITY_ARN):
443447
remote_resource_type = _NORMALIZED_STEPFUNCTIONS_SERVICE_NAME + "::Activity"
444448
remote_resource_identifier = _escape_delimiters(span.attributes.get(AWS_STEPFUNCTIONS_ACTIVITY_ARN))
449+
elif is_key_present(span, AWS_LAMBDA_RESOURCEMAPPING_ID):
450+
remote_resource_type = _NORMALIZED_LAMBDA_SERVICE_NAME + "::EventSourceMapping"
451+
remote_resource_identifier = _escape_delimiters(span.attributes.get(AWS_LAMBDA_RESOURCEMAPPING_ID))
452+
elif is_key_present(span, AWS_LAMBDA_FUNCTION_NAME):
453+
remote_resource_type = _NORMALIZED_LAMBDA_SERVICE_NAME + "::Function"
454+
remote_resource_identifier = _escape_delimiters(span.attributes.get(AWS_LAMBDA_FUNCTION_NAME))
455+
cloudformation_primary_identifier = _escape_delimiters(span.attributes.get(AWS_LAMBDA_FUNCTION_ARN))
445456
elif is_db_span(span):
446457
remote_resource_type = _DB_CONNECTION_STRING_TYPE
447458
remote_resource_identifier = _get_db_connection(span)

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_botocore_patches.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
from amazon.opentelemetry.distro._aws_attribute_keys import (
77
AWS_KINESIS_STREAM_CONSUMERNAME,
88
AWS_KINESIS_STREAM_NAME,
9+
AWS_LAMBDA_FUNCTION_ARN,
10+
AWS_LAMBDA_FUNCTION_NAME,
11+
AWS_LAMBDA_RESOURCEMAPPING_ID,
912
AWS_SECRETSMANAGER_SECRET_ARN,
1013
AWS_SNS_TOPIC_ARN,
1114
AWS_SQS_QUEUE_NAME,
@@ -20,6 +23,7 @@
2023
_BedrockRuntimeExtension,
2124
)
2225
from opentelemetry.instrumentation.botocore.extensions import _KNOWN_EXTENSIONS
26+
from opentelemetry.instrumentation.botocore.extensions.lmbd import _LambdaExtension
2327
from opentelemetry.instrumentation.botocore.extensions.sns import _SnsExtension
2428
from opentelemetry.instrumentation.botocore.extensions.sqs import _SqsExtension
2529
from opentelemetry.instrumentation.botocore.extensions.types import _AttributeMapT, _AwsSdkExtension, _BotoResultT
@@ -39,6 +43,44 @@ def _apply_botocore_instrumentation_patches() -> None:
3943
_apply_botocore_secretsmanager_patch()
4044
_apply_botocore_sns_patch()
4145
_apply_botocore_stepfunctions_patch()
46+
_apply_botocore_lambda_patch()
47+
48+
49+
def _apply_botocore_lambda_patch() -> None:
50+
"""Botocore instrumentation patch for Lambda
51+
52+
This patch adds an extension to the upstream's list of known extensions for Lambda.
53+
Extensions allow for custom logic for adding service-specific information to spans,
54+
such as attributes. Specifically, we are adding logic to add the
55+
`aws.lambda.function.name` and `aws.lambda.resource_mapping.id` attributes
56+
57+
Sidenote: There exists SpanAttributes.FAAS_INVOKED_NAME for invoke operations
58+
in upstream. However, we want to cover more operations to extract 'FunctionName',
59+
so we define `aws.lambda.function.name` separately. Additionally, this helps
60+
us maintain naming consistency with the other AWS resources.
61+
"""
62+
old_extract_attributes = _LambdaExtension.extract_attributes
63+
64+
def patch_extract_attributes(self, attributes: _AttributeMapT):
65+
old_extract_attributes(self, attributes)
66+
function_name = self._call_context.params.get("FunctionName")
67+
if function_name:
68+
attributes[AWS_LAMBDA_FUNCTION_NAME] = function_name
69+
resource_mapping_id = self._call_context.params.get("UUID")
70+
if resource_mapping_id:
71+
attributes[AWS_LAMBDA_RESOURCEMAPPING_ID] = resource_mapping_id
72+
73+
old_on_success = _LambdaExtension.on_success
74+
75+
def patch_on_success(self, span: Span, result: _BotoResultT):
76+
old_on_success(self, span, result)
77+
lambda_configuration = result.get("Configuration", {})
78+
function_arn = lambda_configuration.get("FunctionArn")
79+
if function_arn:
80+
span.set_attribute(AWS_LAMBDA_FUNCTION_ARN, function_arn)
81+
82+
_LambdaExtension.extract_attributes = patch_extract_attributes
83+
_LambdaExtension.on_success = patch_on_success
4284

4385

4486
def _apply_botocore_stepfunctions_patch() -> None:

aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
AWS_CONSUMER_PARENT_SPAN_KIND,
1616
AWS_KINESIS_STREAM_CONSUMERNAME,
1717
AWS_KINESIS_STREAM_NAME,
18+
AWS_LAMBDA_FUNCTION_NAME,
19+
AWS_LAMBDA_RESOURCEMAPPING_ID,
1820
AWS_LOCAL_OPERATION,
1921
AWS_LOCAL_SERVICE,
2022
AWS_REMOTE_DB_USER,
@@ -1167,6 +1169,27 @@ def test_sdk_client_span_with_remote_resource_attributes(self):
11671169
self._validate_remote_resource_attributes("AWS::Kinesis::Stream", "aws_stream_name")
11681170
self._mock_attribute([AWS_KINESIS_STREAM_NAME, AWS_KINESIS_STREAM_CONSUMERNAME], [None, None])
11691171

1172+
# Validate behaviour of AWS_LAMBDA_FUNCTION_NAME attribute, then remove it.
1173+
self._mock_attribute([AWS_LAMBDA_FUNCTION_NAME], ["aws_lambda_function_name"], keys, values)
1174+
self._validate_remote_resource_attributes("AWS::Lambda::Function", "aws_lambda_function_name")
1175+
self._mock_attribute([AWS_LAMBDA_FUNCTION_NAME], [None])
1176+
1177+
# Validate behaviour of AWS_LAMBDA_RESOURCEMAPPING_ID attribute, then remove it.
1178+
self._mock_attribute([AWS_LAMBDA_RESOURCEMAPPING_ID], ["aws_event_source_mapping_id"], keys, values)
1179+
self._validate_remote_resource_attributes("AWS::Lambda::EventSourceMapping", "aws_event_source_mapping_id")
1180+
self._mock_attribute([AWS_LAMBDA_RESOURCEMAPPING_ID], [None])
1181+
1182+
# Validate behaviour of both AWS_LAMBDA_FUNCTION_NAME and AWS_LAMBDA_RESOURCE_MAPPING_ID,
1183+
# then remove it.
1184+
self._mock_attribute(
1185+
[AWS_LAMBDA_FUNCTION_NAME, AWS_LAMBDA_RESOURCEMAPPING_ID],
1186+
["aws_lambda_function_name", "aws_event_source_mapping_id"],
1187+
keys,
1188+
values
1189+
)
1190+
self._validate_remote_resource_attributes("AWS::Lambda::EventSourceMapping", "aws_event_source_mapping_id")
1191+
self._mock_attribute([AWS_LAMBDA_FUNCTION_NAME, AWS_LAMBDA_RESOURCEMAPPING_ID], [None, None])
1192+
11701193
self._mock_attribute([SpanAttributes.RPC_SYSTEM], [None])
11711194

11721195
def test_client_db_span_with_remote_resource_attributes(self):

aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
_TOPIC_ARN: str = "topicArn"
3232
_STATE_MACHINE_ARN: str = "arn:aws:states:us-west-2:000000000000:stateMachine:testStateMachine"
3333
_ACTIVITY_ARN: str = "arn:aws:states:us-east-1:007003123456789012:activity:testActivity"
34+
_LAMBDA_FUNCTION_NAME: str = "lambdaFunctionName"
35+
_LAMBDA_SOURCE_MAPPING_ID: str = "lambdaEventSourceMappingID"
3436

3537
# Patch names
3638
GET_DISTRIBUTION_PATCH: str = (
@@ -155,6 +157,9 @@ def _test_unpatched_botocore_instrumentation(self):
155157
# StepFunctions
156158
self.assertFalse("stepfunctions" in _KNOWN_EXTENSIONS, "Upstream has added a StepFunctions extension")
157159

160+
# Lambda
161+
self.assertTrue("lambda" in _KNOWN_EXTENSIONS, "Upstream has removed the Lambda extension")
162+
158163
def _test_unpatched_gevent_instrumentation(self):
159164
self.assertFalse(gevent.monkey.is_module_patched("os"), "gevent os module has been patched")
160165
self.assertFalse(gevent.monkey.is_module_patched("thread"), "gevent thread module has been patched")
@@ -239,6 +244,14 @@ def _test_patched_botocore_instrumentation(self):
239244
self.assertTrue("aws.stepfunctions.activity.arn" in stepfunctions_attributes)
240245
self.assertEqual(stepfunctions_attributes["aws.stepfunctions.activity.arn"], _ACTIVITY_ARN)
241246

247+
# Lambda
248+
self.assertTrue("lambda" in _KNOWN_EXTENSIONS)
249+
lambda_attributes: Dict[str, str] = _do_extract_lambda_attributes()
250+
self.assertTrue("aws.lambda.function.name" in lambda_attributes)
251+
self.assertEqual(lambda_attributes["aws.lambda.function.name"], _LAMBDA_FUNCTION_NAME)
252+
self.assertTrue("aws.lambda.resource_mapping.id" in lambda_attributes)
253+
self.assertEqual(lambda_attributes["aws.lambda.resource_mapping.id"], _LAMBDA_SOURCE_MAPPING_ID)
254+
242255
def _test_patched_gevent_os_ssl_instrumentation(self):
243256
# Only ssl and os module should have been patched since the environment variable was set to 'os, ssl'
244257
self.assertTrue(gevent.monkey.is_module_patched("ssl"), "gevent ssl module has not been patched")
@@ -421,6 +434,12 @@ def _do_extract_stepfunctions_attributes() -> Dict[str, str]:
421434
return _do_extract_attributes(service_name, params)
422435

423436

437+
def _do_extract_lambda_attributes() -> Dict[str, str]:
438+
service_name: str = "lambda"
439+
params: Dict[str, str] = {"FunctionName": _LAMBDA_FUNCTION_NAME, "UUID": _LAMBDA_SOURCE_MAPPING_ID}
440+
return _do_extract_attributes(service_name, params)
441+
442+
424443
def _do_extract_attributes(service_name: str, params: Dict[str, Any], operation: str = None) -> Dict[str, str]:
425444
mock_call_context: MagicMock = MagicMock()
426445
mock_call_context.params = params

0 commit comments

Comments
 (0)