Skip to content

Commit 07aad1f

Browse files
authored
Fix: Lambda Topology Issue (#319)
### Description of changes: These changes fix the broken lambda topology in AppSignals console. Before these changes, if lambdaA calls lambdaB, we will have two different nodes to represent the downstream lambda. ![image](https://github.com/user-attachments/assets/ef3473bd-e5a1-4de1-b7e8-5ddd5d214882) ### Test plan: **Configuration:** - lambdaA (AppSignals enabled) - lambdaB (AppSignals enabled) ![image](https://github.com/user-attachments/assets/e63180a3-1aa9-4555-8e30-9eb9b1dd3f1d) **Configuration:** - lambdaA (AppSignals enabled) - lambdaB (AppSignals enabled) - We call both `Invoke` and `GetFunction` from lambdaA to verify that Invoke and non-Invoke calls are handled differently. - `Invoke` calls to downstream lambda will be modeled as Service entity type. - Non-Invoke (`GetFunction` in this case) calls to downstream lambda will be modeled as AWS Resource. ![image](https://github.com/user-attachments/assets/853fff71-1d7e-4069-827d-7062aafb8982) **Configuration:** - lambdaA (AppSignals enabled) - lambdaB (AppSignals enabled) - We modify lambdaB to make a downstream s3 call (`ListBuckets`). We see that the edges are propagated correctly and the entire topology is modeled correctly. ![image](https://github.com/user-attachments/assets/0db07241-19d4-4f57-88f3-fc12934e151c) **Configuration:** - lambdaA (AppSignals enabled) - lambdaB (AppSignals disabled) - We see that lambdaB is correctly modeled as a RemoteService entity type when it does not have AppSignals enabled. ![image](https://github.com/user-attachments/assets/5f781db6-8beb-4452-a163-4b1b8786030b) By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent c96443d commit 07aad1f

File tree

3 files changed

+104
-0
lines changed

3 files changed

+104
-0
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
AWS_REMOTE_DB_USER: str = "aws.remote.db.user"
88
AWS_REMOTE_SERVICE: str = "aws.remote.service"
99
AWS_REMOTE_OPERATION: str = "aws.remote.operation"
10+
AWS_REMOTE_ENVIRONMENT: str = "aws.remote.environment"
1011
AWS_REMOTE_RESOURCE_TYPE: str = "aws.remote.resource.type"
1112
AWS_REMOTE_RESOURCE_IDENTIFIER: str = "aws.remote.resource.identifier"
1213
AWS_SDK_DESCENDANT: str = "aws.sdk.descendant"

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
# SPDX-License-Identifier: Apache-2.0
3+
import os
34
import re
45
from logging import DEBUG, Logger, getLogger
56
from typing import Match, Optional
@@ -13,10 +14,13 @@
1314
AWS_BEDROCK_KNOWLEDGE_BASE_ID,
1415
AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER,
1516
AWS_KINESIS_STREAM_NAME,
17+
AWS_LAMBDA_FUNCTION_ARN,
18+
AWS_LAMBDA_FUNCTION_NAME,
1619
AWS_LAMBDA_RESOURCEMAPPING_ID,
1720
AWS_LOCAL_OPERATION,
1821
AWS_LOCAL_SERVICE,
1922
AWS_REMOTE_DB_USER,
23+
AWS_REMOTE_ENVIRONMENT,
2024
AWS_REMOTE_OPERATION,
2125
AWS_REMOTE_RESOURCE_IDENTIFIER,
2226
AWS_REMOTE_RESOURCE_TYPE,
@@ -445,6 +449,28 @@ def _set_remote_type_and_identifier(span: ReadableSpan, attributes: BoundedAttri
445449
":"
446450
)[-1]
447451
cloudformation_primary_identifier = _escape_delimiters(span.attributes.get(AWS_STEPFUNCTIONS_ACTIVITY_ARN))
452+
elif is_key_present(span, AWS_LAMBDA_FUNCTION_NAME):
453+
# Handling downstream Lambda as a service vs. an AWS resource:
454+
# - If the method call is "Invoke", we treat downstream Lambda as a service.
455+
# - Otherwise, we treat it as an AWS resource.
456+
#
457+
# This addresses a Lambda topology issue in Application Signals.
458+
# More context in PR: https://github.com/aws-observability/aws-otel-python-instrumentation/pull/319
459+
#
460+
# NOTE: The env vars LAMBDA_APPLICATION_SIGNALS_REMOTE_SERVICE and
461+
# LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT were introduced as part of this fix.
462+
# They are optional and allow users to override the default values if needed.
463+
if span.attributes.get(_RPC_METHOD) == "Invoke":
464+
attributes[AWS_REMOTE_SERVICE] = os.environ.get(
465+
"LAMBDA_APPLICATION_SIGNALS_REMOTE_SERVICE", span.attributes.get(AWS_LAMBDA_FUNCTION_NAME)
466+
)
467+
attributes[AWS_REMOTE_ENVIRONMENT] = (
468+
f'lambda:{os.environ.get("LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT", "default")}'
469+
)
470+
else:
471+
remote_resource_type = _NORMALIZED_LAMBDA_SERVICE_NAME + "::Function"
472+
remote_resource_identifier = _escape_delimiters(span.attributes.get(AWS_LAMBDA_FUNCTION_NAME))
473+
cloudformation_primary_identifier = _escape_delimiters(span.attributes.get(AWS_LAMBDA_FUNCTION_ARN))
448474
elif is_key_present(span, AWS_LAMBDA_RESOURCEMAPPING_ID):
449475
remote_resource_type = _NORMALIZED_LAMBDA_SERVICE_NAME + "::EventSourceMapping"
450476
remote_resource_identifier = _escape_delimiters(span.attributes.get(AWS_LAMBDA_RESOURCEMAPPING_ID))

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

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
# pylint: disable=too-many-lines
55

6+
import os
67
from typing import Dict, List, Optional
78
from unittest import TestCase
89
from unittest.mock import MagicMock
@@ -12,12 +13,16 @@
1213
AWS_BEDROCK_DATA_SOURCE_ID,
1314
AWS_BEDROCK_GUARDRAIL_ID,
1415
AWS_BEDROCK_KNOWLEDGE_BASE_ID,
16+
AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER,
1517
AWS_CONSUMER_PARENT_SPAN_KIND,
1618
AWS_KINESIS_STREAM_NAME,
19+
AWS_LAMBDA_FUNCTION_ARN,
20+
AWS_LAMBDA_FUNCTION_NAME,
1721
AWS_LAMBDA_RESOURCEMAPPING_ID,
1822
AWS_LOCAL_OPERATION,
1923
AWS_LOCAL_SERVICE,
2024
AWS_REMOTE_DB_USER,
25+
AWS_REMOTE_ENVIRONMENT,
2126
AWS_REMOTE_OPERATION,
2227
AWS_REMOTE_RESOURCE_IDENTIFIER,
2328
AWS_REMOTE_RESOURCE_TYPE,
@@ -1152,6 +1157,78 @@ def test_sdk_client_span_with_remote_resource_attributes(self):
11521157
self._validate_remote_resource_attributes("AWS::Lambda::EventSourceMapping", "aws_event_source_mapping_id")
11531158
self._mock_attribute([AWS_LAMBDA_RESOURCEMAPPING_ID], [None])
11541159

1160+
# Test AWS Lambda Invoke scenario with default lambda remote environment
1161+
self.span_mock.kind = SpanKind.CLIENT
1162+
self._mock_attribute(
1163+
[AWS_LAMBDA_FUNCTION_NAME, SpanAttributes.RPC_METHOD],
1164+
["test_downstream_lambda1", "Invoke"],
1165+
keys,
1166+
values,
1167+
)
1168+
dependency_attributes = _GENERATOR.generate_metric_attributes_dict_from_span(self.span_mock, self.resource).get(
1169+
DEPENDENCY_METRIC
1170+
)
1171+
self.assertEqual(dependency_attributes.get(AWS_REMOTE_SERVICE), "test_downstream_lambda1")
1172+
self.assertEqual(dependency_attributes.get(AWS_REMOTE_ENVIRONMENT), "lambda:default")
1173+
self.assertNotIn(AWS_REMOTE_RESOURCE_TYPE, dependency_attributes)
1174+
self.assertNotIn(AWS_REMOTE_RESOURCE_IDENTIFIER, dependency_attributes)
1175+
self.assertNotIn(AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER, dependency_attributes)
1176+
self._mock_attribute([AWS_LAMBDA_FUNCTION_NAME, SpanAttributes.RPC_METHOD], [None, None])
1177+
1178+
# Test AWS Lambda Invoke scenario with user-configured lambda remote environment
1179+
os.environ["LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT"] = "test"
1180+
self.span_mock.kind = SpanKind.CLIENT
1181+
self._mock_attribute(
1182+
[AWS_LAMBDA_FUNCTION_NAME, SpanAttributes.RPC_METHOD],
1183+
["testLambdaFunction", "Invoke"],
1184+
keys,
1185+
values,
1186+
)
1187+
dependency_attributes = _GENERATOR.generate_metric_attributes_dict_from_span(self.span_mock, self.resource).get(
1188+
DEPENDENCY_METRIC
1189+
)
1190+
self.assertEqual(dependency_attributes.get(AWS_REMOTE_SERVICE), "testLambdaFunction")
1191+
self.assertEqual(dependency_attributes.get(AWS_REMOTE_ENVIRONMENT), "lambda:test")
1192+
self.assertNotIn(AWS_REMOTE_RESOURCE_TYPE, dependency_attributes)
1193+
self.assertNotIn(AWS_REMOTE_RESOURCE_IDENTIFIER, dependency_attributes)
1194+
self.assertNotIn(AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER, dependency_attributes)
1195+
self._mock_attribute([AWS_LAMBDA_FUNCTION_NAME, SpanAttributes.RPC_METHOD], [None, None])
1196+
os.environ.pop("LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT", None)
1197+
1198+
# Test AWS Lambda Invoke scenario with user-configured lambda remote service
1199+
os.environ["LAMBDA_APPLICATION_SIGNALS_REMOTE_SERVICE"] = "test_downstream_lambda2"
1200+
self.span_mock.kind = SpanKind.CLIENT
1201+
self._mock_attribute(
1202+
[AWS_LAMBDA_FUNCTION_NAME, SpanAttributes.RPC_METHOD],
1203+
["testLambdaFunction", "Invoke"],
1204+
keys,
1205+
values,
1206+
)
1207+
dependency_attributes = _GENERATOR.generate_metric_attributes_dict_from_span(self.span_mock, self.resource).get(
1208+
DEPENDENCY_METRIC
1209+
)
1210+
self.assertEqual(dependency_attributes.get(AWS_REMOTE_SERVICE), "test_downstream_lambda2")
1211+
self.assertEqual(dependency_attributes.get(AWS_REMOTE_ENVIRONMENT), "lambda:default")
1212+
self.assertNotIn(AWS_REMOTE_RESOURCE_TYPE, dependency_attributes)
1213+
self.assertNotIn(AWS_REMOTE_RESOURCE_IDENTIFIER, dependency_attributes)
1214+
self.assertNotIn(AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER, dependency_attributes)
1215+
self._mock_attribute([AWS_LAMBDA_FUNCTION_NAME, SpanAttributes.RPC_METHOD], [None, None])
1216+
os.environ.pop("LAMBDA_APPLICATION_SIGNALS_REMOTE_SERVICE", None)
1217+
1218+
# Test AWS Lambda non-Invoke scenario
1219+
self.span_mock.kind = SpanKind.CLIENT
1220+
lambda_arn = "arn:aws:lambda:us-east-1:123456789012:function:testLambda"
1221+
self._mock_attribute(
1222+
[AWS_LAMBDA_FUNCTION_NAME, AWS_LAMBDA_FUNCTION_ARN, SpanAttributes.RPC_METHOD],
1223+
["testLambdaFunction", lambda_arn, "GetFunction"],
1224+
keys,
1225+
values,
1226+
)
1227+
self._validate_remote_resource_attributes("AWS::Lambda::Function", "testLambdaFunction")
1228+
self._mock_attribute(
1229+
[AWS_LAMBDA_FUNCTION_NAME, AWS_LAMBDA_FUNCTION_ARN, SpanAttributes.RPC_METHOD], [None, None, None]
1230+
)
1231+
11551232
self._mock_attribute([SpanAttributes.RPC_SYSTEM], [None])
11561233

11571234
def test_client_db_span_with_remote_resource_attributes(self):

0 commit comments

Comments
 (0)