diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py index 5610539a3..ec5b693ed 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py @@ -103,6 +103,10 @@ _NORMALIZED_LAMBDA_SERVICE_NAME: str = "AWS::Lambda" _DB_CONNECTION_STRING_TYPE: str = "DB::Connection" +# Constants for Lambda operations +_LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT: str = "LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT" +_LAMBDA_INVOKE_OPERATION: str = "Invoke" + # Special DEPENDENCY attribute value if GRAPHQL_OPERATION_TYPE attribute key is present. _GRAPHQL: str = "graphql" @@ -145,6 +149,7 @@ def _generate_dependency_metric_attributes(span: ReadableSpan, resource: Resourc _set_egress_operation(span, attributes) _set_remote_service_and_operation(span, attributes) _set_remote_type_and_identifier(span, attributes) + _set_remote_environment(span, attributes) _set_remote_db_user(span, attributes) _set_span_kind_for_dependency(span, attributes) return attributes @@ -317,7 +322,17 @@ def _normalize_remote_service_name(span: ReadableSpan, service_name: str) -> str "Secrets Manager": _NORMALIZED_SECRETSMANAGER_SERVICE_NAME, "SNS": _NORMALIZED_SNS_SERVICE_NAME, "SFN": _NORMALIZED_STEPFUNCTIONS_SERVICE_NAME, + "Lambda": _NORMALIZED_LAMBDA_SERVICE_NAME, } + + # Special handling for Lambda invoke operations + if _is_lambda_invoke_operation(span): + lambda_function_name = span.attributes.get(AWS_LAMBDA_FUNCTION_NAME) + # If Lambda name is not present, use UnknownRemoteService + # This is intentional - we want to clearly indicate when the Lambda function name + # is missing rather than falling back to a generic service name + return lambda_function_name if lambda_function_name else UNKNOWN_REMOTE_SERVICE + return aws_sdk_service_mapping.get(service_name, "AWS::" + service_name) return service_name @@ -450,22 +465,9 @@ def _set_remote_type_and_identifier(span: ReadableSpan, attributes: BoundedAttri )[-1] cloudformation_primary_identifier = _escape_delimiters(span.attributes.get(AWS_STEPFUNCTIONS_ACTIVITY_ARN)) elif is_key_present(span, AWS_LAMBDA_FUNCTION_NAME): - # Handling downstream Lambda as a service vs. an AWS resource: - # - If the method call is "Invoke", we treat downstream Lambda as a service. - # - Otherwise, we treat it as an AWS resource. - # - # This addresses a Lambda topology issue in Application Signals. - # More context in PR: https://github.com/aws-observability/aws-otel-python-instrumentation/pull/319 - # - # NOTE: The env var LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT was introduced as part of this fix. - # It is optional and allows users to override the default value if needed. - if span.attributes.get(_RPC_METHOD) == "Invoke": - attributes[AWS_REMOTE_SERVICE] = _escape_delimiters(span.attributes.get(AWS_LAMBDA_FUNCTION_NAME)) - - attributes[AWS_REMOTE_ENVIRONMENT] = ( - f'lambda:{os.environ.get("LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT", "default")}' - ) - else: + # For non-Invoke Lambda operations, treat Lambda as a resource, + # see normalize_remote_service_name for more information. + if not _is_lambda_invoke_operation(span): remote_resource_type = _NORMALIZED_LAMBDA_SERVICE_NAME + "::Function" remote_resource_identifier = _escape_delimiters(span.attributes.get(AWS_LAMBDA_FUNCTION_NAME)) cloudformation_primary_identifier = _escape_delimiters(span.attributes.get(AWS_LAMBDA_FUNCTION_ARN)) @@ -491,6 +493,32 @@ def _set_remote_type_and_identifier(span: ReadableSpan, attributes: BoundedAttri attributes[AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER] = cloudformation_primary_identifier +def _set_remote_environment(span: ReadableSpan, attributes: BoundedAttributes) -> None: + """ + Remote environment is used to identify the environment of downstream services. Currently only + set to "lambda:default" for Lambda Invoke operations when aws-api system is detected. + """ + # We want to treat downstream Lambdas as a service rather than a resource because + # Application Signals topology map gets disconnected due to conflicting Lambda Entity + # definitions + # Additional context can be found in + # https://github.com/aws-observability/aws-otel-python-instrumentation/pull/319 + if _is_lambda_invoke_operation(span): + remote_environment = os.environ.get(_LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT, "").strip() + if not remote_environment: + remote_environment = "default" + attributes[AWS_REMOTE_ENVIRONMENT] = f"lambda:{remote_environment}" + + +def _is_lambda_invoke_operation(span: ReadableSpan) -> bool: + """Check if the span represents a Lambda Invoke operation.""" + if not is_aws_sdk_span(span): + return False + + rpc_service = _get_remote_service(span, _RPC_SERVICE) + return rpc_service == "Lambda" and span.attributes.get(_RPC_METHOD) == _LAMBDA_INVOKE_OPERATION + + def _get_db_connection(span: ReadableSpan) -> None: """ RemoteResourceIdentifier is populated with rule: diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py index 6e4e76cc5..d122519cf 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py @@ -11,6 +11,7 @@ from amazon.opentelemetry.distro._aws_attribute_keys import ( AWS_BEDROCK_AGENT_ID, AWS_BEDROCK_DATA_SOURCE_ID, + AWS_BEDROCK_GUARDRAIL_ARN, AWS_BEDROCK_GUARDRAIL_ID, AWS_BEDROCK_KNOWLEDGE_BASE_ID, AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER, @@ -891,6 +892,40 @@ def test_normalize_remote_service_name_aws_sdk(self): self.validate_aws_sdk_service_normalization("SNS", "AWS::SNS") self.validate_aws_sdk_service_normalization("SFN", "AWS::StepFunctions") + # AWS SDK Lambda tests - non-Invoke operations + self.validate_aws_sdk_service_normalization("Lambda", "AWS::Lambda") + + # Lambda Invoke with function name + self._mock_attribute( + [ + SpanAttributes.RPC_SYSTEM, + SpanAttributes.RPC_SERVICE, + SpanAttributes.RPC_METHOD, + AWS_LAMBDA_FUNCTION_NAME, + ], + ["aws-api", "Lambda", "Invoke", "testFunction"], + ) + self.span_mock.kind = SpanKind.CLIENT + actual_attributes = _GENERATOR.generate_metric_attributes_dict_from_span(self.span_mock, self.resource).get( + DEPENDENCY_METRIC + ) + self.assertEqual(actual_attributes.get(AWS_REMOTE_SERVICE), "testFunction") + + # Lambda Invoke without AWS_LAMBDA_NAME - should fall back to UnknownRemoteService + self._mock_attribute( + [ + SpanAttributes.RPC_SYSTEM, + SpanAttributes.RPC_SERVICE, + SpanAttributes.RPC_METHOD, + AWS_LAMBDA_FUNCTION_NAME, + ], + ["aws-api", "Lambda", "Invoke", None], + ) + actual_attributes = _GENERATOR.generate_metric_attributes_dict_from_span(self.span_mock, self.resource).get( + DEPENDENCY_METRIC + ) + self.assertEqual(actual_attributes.get(AWS_REMOTE_SERVICE), "UnknownRemoteService") + def validate_aws_sdk_service_normalization(self, service_name: str, expected_remote_service: str): self._mock_attribute([SpanAttributes.RPC_SYSTEM, SpanAttributes.RPC_SERVICE], ["aws-api", service_name]) self.span_mock.kind = SpanKind.CLIENT @@ -1006,12 +1041,14 @@ def test_sdk_client_span_with_remote_resource_attributes(self): keys, values, ) - self._validate_remote_resource_attributes("AWS::SQS::Queue", "aws_queue_name") + self._validate_remote_resource_attributes( + "AWS::SQS::Queue", "aws_queue_name", "https://sqs.us-east-2.amazonaws.com/123456789012/Queue" + ) self._mock_attribute([AWS_SQS_QUEUE_URL, AWS_SQS_QUEUE_NAME], [None, None]) # Valid queue name with invalid queue URL, we should default to using the queue name. self._mock_attribute([AWS_SQS_QUEUE_URL, AWS_SQS_QUEUE_NAME], ["invalidUrl", "aws_queue_name"], keys, values) - self._validate_remote_resource_attributes("AWS::SQS::Queue", "aws_queue_name") + self._validate_remote_resource_attributes("AWS::SQS::Queue", "aws_queue_name", "invalidUrl") self._mock_attribute([AWS_SQS_QUEUE_URL, AWS_SQS_QUEUE_NAME], [None, None]) # Validate behaviour of AWS_KINESIS_STREAM_NAME attribute, then remove it. @@ -1063,7 +1100,9 @@ def test_sdk_client_span_with_remote_resource_attributes(self): keys, values, ) - self._validate_remote_resource_attributes("AWS::Bedrock::DataSource", "test_datasource_id") + self._validate_remote_resource_attributes( + "AWS::Bedrock::DataSource", "test_datasource_id", "test_knowledge_base_id|test_datasource_id" + ) self._mock_attribute([AWS_BEDROCK_DATA_SOURCE_ID, AWS_BEDROCK_KNOWLEDGE_BASE_ID], [None, None]) # Validate behaviour of AWS_BEDROCK_DATA_SOURCE_ID attribute with special chars(^), then remove it. @@ -1073,18 +1112,38 @@ def test_sdk_client_span_with_remote_resource_attributes(self): keys, values, ) - self._validate_remote_resource_attributes("AWS::Bedrock::DataSource", "test_datasource_^^id") + self._validate_remote_resource_attributes( + "AWS::Bedrock::DataSource", "test_datasource_^^id", "test_knowledge_base_^^id|test_datasource_^^id" + ) self._mock_attribute([AWS_BEDROCK_DATA_SOURCE_ID, AWS_BEDROCK_KNOWLEDGE_BASE_ID], [None, None]) # Validate behaviour of AWS_BEDROCK_GUARDRAIL_ID attribute, then remove it. - self._mock_attribute([AWS_BEDROCK_GUARDRAIL_ID], ["test_guardrail_id"], keys, values) - self._validate_remote_resource_attributes("AWS::Bedrock::Guardrail", "test_guardrail_id") - self._mock_attribute([AWS_BEDROCK_GUARDRAIL_ID], [None]) + self._mock_attribute( + [AWS_BEDROCK_GUARDRAIL_ID, AWS_BEDROCK_GUARDRAIL_ARN], + ["test_guardrail_id", "arn:aws:bedrock:us-east-1:123456789012:guardrail/test_guardrail_id"], + keys, + values, + ) + self._validate_remote_resource_attributes( + "AWS::Bedrock::Guardrail", + "test_guardrail_id", + "arn:aws:bedrock:us-east-1:123456789012:guardrail/test_guardrail_id", + ) + self._mock_attribute([AWS_BEDROCK_GUARDRAIL_ID, AWS_BEDROCK_GUARDRAIL_ARN], [None, None]) # Validate behaviour of AWS_BEDROCK_GUARDRAIL_ID attribute with special chars(^), then remove it. - self._mock_attribute([AWS_BEDROCK_GUARDRAIL_ID], ["test_guardrail_^id"], keys, values) - self._validate_remote_resource_attributes("AWS::Bedrock::Guardrail", "test_guardrail_^^id") - self._mock_attribute([AWS_BEDROCK_GUARDRAIL_ID], [None]) + self._mock_attribute( + [AWS_BEDROCK_GUARDRAIL_ID, AWS_BEDROCK_GUARDRAIL_ARN], + ["test_guardrail_^id", "arn:aws:bedrock:us-east-1:123456789012:guardrail/test_guardrail_^id"], + keys, + values, + ) + self._validate_remote_resource_attributes( + "AWS::Bedrock::Guardrail", + "test_guardrail_^^id", + "arn:aws:bedrock:us-east-1:123456789012:guardrail/test_guardrail_^^id", + ) + self._mock_attribute([AWS_BEDROCK_GUARDRAIL_ID, AWS_BEDROCK_GUARDRAIL_ARN], [None, None]) # Validate behaviour of AWS_BEDROCK_KNOWLEDGE_BASE_ID attribute, then remove it. self._mock_attribute([AWS_BEDROCK_KNOWLEDGE_BASE_ID], ["test_knowledgeBase_id"], keys, values) @@ -1113,12 +1172,18 @@ def test_sdk_client_span_with_remote_resource_attributes(self): keys, values, ) - self._validate_remote_resource_attributes("AWS::SecretsManager::Secret", "secret_name-lERW9H") + self._validate_remote_resource_attributes( + "AWS::SecretsManager::Secret", + "secret_name-lERW9H", + "arn:aws:secretsmanager:us-east-1:123456789012:secret:secret_name-lERW9H", + ) self._mock_attribute([AWS_SECRETSMANAGER_SECRET_ARN], [None]) # Validate behaviour of AWS_SNS_TOPIC_ARN attribute, then remove it. self._mock_attribute([AWS_SNS_TOPIC_ARN], ["arn:aws:sns:us-west-2:012345678901:test_topic"], keys, values) - self._validate_remote_resource_attributes("AWS::SNS::Topic", "test_topic") + self._validate_remote_resource_attributes( + "AWS::SNS::Topic", "test_topic", "arn:aws:sns:us-west-2:012345678901:test_topic" + ) self._mock_attribute([AWS_SNS_TOPIC_ARN], [None]) # Validate behaviour of AWS_STEPFUNCTIONS_STATEMACHINE_ARN attribute, then remove it. @@ -1128,7 +1193,11 @@ def test_sdk_client_span_with_remote_resource_attributes(self): keys, values, ) - self._validate_remote_resource_attributes("AWS::StepFunctions::StateMachine", "test_state_machine") + self._validate_remote_resource_attributes( + "AWS::StepFunctions::StateMachine", + "test_state_machine", + "arn:aws:states:us-east-1:123456789012:stateMachine:test_state_machine", + ) self._mock_attribute([AWS_STEPFUNCTIONS_STATEMACHINE_ARN], [None]) # Validate behaviour of AWS_STEPFUNCTIONS_ACTIVITY_ARN attribute, then remove it. @@ -1138,7 +1207,11 @@ def test_sdk_client_span_with_remote_resource_attributes(self): keys, values, ) - self._validate_remote_resource_attributes("AWS::StepFunctions::Activity", "testActivity") + self._validate_remote_resource_attributes( + "AWS::StepFunctions::Activity", + "testActivity", + "arn:aws:states:us-east-1:007003123456789012:activity:testActivity", + ) self._mock_attribute([AWS_STEPFUNCTIONS_ACTIVITY_ARN], [None]) # Validate behaviour of AWS_LAMBDA_RESOURCEMAPPING_ID attribute, then remove it. @@ -1160,8 +1233,8 @@ def test_sdk_client_span_with_remote_resource_attributes(self): # Test AWS Lambda Invoke scenario with default lambda remote environment self.span_mock.kind = SpanKind.CLIENT self._mock_attribute( - [AWS_LAMBDA_FUNCTION_NAME, SpanAttributes.RPC_METHOD], - ["test_downstream_lambda1", "Invoke"], + [AWS_LAMBDA_FUNCTION_NAME, SpanAttributes.RPC_METHOD, SpanAttributes.RPC_SERVICE], + ["test_downstream_lambda1", "Invoke", "Lambda"], keys, values, ) @@ -1173,14 +1246,16 @@ def test_sdk_client_span_with_remote_resource_attributes(self): self.assertNotIn(AWS_REMOTE_RESOURCE_TYPE, dependency_attributes) self.assertNotIn(AWS_REMOTE_RESOURCE_IDENTIFIER, dependency_attributes) self.assertNotIn(AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER, dependency_attributes) - self._mock_attribute([AWS_LAMBDA_FUNCTION_NAME, SpanAttributes.RPC_METHOD], [None, None]) + self._mock_attribute( + [AWS_LAMBDA_FUNCTION_NAME, SpanAttributes.RPC_METHOD, SpanAttributes.RPC_SERVICE], [None, None, None] + ) # Test AWS Lambda Invoke scenario with user-configured lambda remote environment os.environ["LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT"] = "test" self.span_mock.kind = SpanKind.CLIENT self._mock_attribute( - [AWS_LAMBDA_FUNCTION_NAME, SpanAttributes.RPC_METHOD], - ["testLambdaFunction", "Invoke"], + [AWS_LAMBDA_FUNCTION_NAME, SpanAttributes.RPC_METHOD, SpanAttributes.RPC_SERVICE], + ["testLambdaFunction", "Invoke", "Lambda"], keys, values, ) @@ -1192,7 +1267,9 @@ def test_sdk_client_span_with_remote_resource_attributes(self): self.assertNotIn(AWS_REMOTE_RESOURCE_TYPE, dependency_attributes) self.assertNotIn(AWS_REMOTE_RESOURCE_IDENTIFIER, dependency_attributes) self.assertNotIn(AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER, dependency_attributes) - self._mock_attribute([AWS_LAMBDA_FUNCTION_NAME, SpanAttributes.RPC_METHOD], [None, None]) + self._mock_attribute( + [AWS_LAMBDA_FUNCTION_NAME, SpanAttributes.RPC_METHOD, SpanAttributes.RPC_SERVICE], [None, None, None] + ) os.environ.pop("LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT", None) # Test AWS Lambda non-Invoke scenario @@ -1204,11 +1281,67 @@ def test_sdk_client_span_with_remote_resource_attributes(self): keys, values, ) - self._validate_remote_resource_attributes("AWS::Lambda::Function", "testLambdaFunction") + self._validate_remote_resource_attributes("AWS::Lambda::Function", "testLambdaFunction", lambda_arn) self._mock_attribute( [AWS_LAMBDA_FUNCTION_NAME, AWS_LAMBDA_FUNCTION_ARN, SpanAttributes.RPC_METHOD], [None, None, None] ) + # Validate behaviour of AWS_LAMBDA_NAME for non-Invoke operations (treated as resource) + self._mock_attribute( + [ + SpanAttributes.RPC_SYSTEM, + SpanAttributes.RPC_SERVICE, + SpanAttributes.RPC_METHOD, + AWS_LAMBDA_FUNCTION_NAME, + AWS_LAMBDA_FUNCTION_ARN, + ], + [ + "aws-api", + "Lambda", + "GetFunction", + "testLambdaName", + "arn:aws:lambda:us-east-1:123456789012:function:testLambdaName", + ], + keys, + values, + ) + self._validate_remote_resource_attributes( + "AWS::Lambda::Function", "testLambdaName", "arn:aws:lambda:us-east-1:123456789012:function:testLambdaName" + ) + self._mock_attribute( + [ + SpanAttributes.RPC_SYSTEM, + SpanAttributes.RPC_SERVICE, + SpanAttributes.RPC_METHOD, + AWS_LAMBDA_FUNCTION_NAME, + AWS_LAMBDA_FUNCTION_ARN, + ], + [None, None, None, None, None], + ) + + # Validate that Lambda Invoke with function name treats Lambda as a service, not a resource + self._mock_attribute( + [ + SpanAttributes.RPC_SYSTEM, + SpanAttributes.RPC_SERVICE, + SpanAttributes.RPC_METHOD, + AWS_LAMBDA_FUNCTION_NAME, + ], + ["aws-api", "Lambda", "Invoke", "testLambdaName"], + keys, + values, + ) + self._validate_remote_resource_attributes(None, None) + self._mock_attribute( + [ + SpanAttributes.RPC_SYSTEM, + SpanAttributes.RPC_SERVICE, + SpanAttributes.RPC_METHOD, + AWS_LAMBDA_FUNCTION_NAME, + ], + [None, None, None, None], + ) + self._mock_attribute([SpanAttributes.RPC_SYSTEM], [None]) def test_client_db_span_with_remote_resource_attributes(self): @@ -1487,7 +1620,13 @@ def test_client_db_span_with_remote_resource_attributes(self): [None], ) - def _validate_remote_resource_attributes(self, expected_type: str, expected_identifier: str) -> None: + def _validate_remote_resource_attributes( + self, expected_type: str, expected_identifier: str, expected_cfn_primary_id: str = None + ) -> None: + # If expected_cfn_primary_id is not provided, it defaults to expected_identifier + if expected_cfn_primary_id is None: + expected_cfn_primary_id = expected_identifier + # Client, Producer, and Consumer spans should generate the expected remote resource attribute self.span_mock.kind = SpanKind.CLIENT actual_attributes = _GENERATOR.generate_metric_attributes_dict_from_span(self.span_mock, self.resource).get( @@ -1495,6 +1634,7 @@ def _validate_remote_resource_attributes(self, expected_type: str, expected_iden ) self.assertEqual(expected_type, actual_attributes.get(AWS_REMOTE_RESOURCE_TYPE)) self.assertEqual(expected_identifier, actual_attributes.get(AWS_REMOTE_RESOURCE_IDENTIFIER)) + self.assertEqual(expected_cfn_primary_id, actual_attributes.get(AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER)) self.span_mock.kind = SpanKind.PRODUCER actual_attributes = _GENERATOR.generate_metric_attributes_dict_from_span(self.span_mock, self.resource).get( @@ -1502,6 +1642,7 @@ def _validate_remote_resource_attributes(self, expected_type: str, expected_iden ) self.assertEqual(expected_type, actual_attributes.get(AWS_REMOTE_RESOURCE_TYPE)) self.assertEqual(expected_identifier, actual_attributes.get(AWS_REMOTE_RESOURCE_IDENTIFIER)) + self.assertEqual(expected_cfn_primary_id, actual_attributes.get(AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER)) self.span_mock.kind = SpanKind.CONSUMER actual_attributes = _GENERATOR.generate_metric_attributes_dict_from_span(self.span_mock, self.resource).get( @@ -1509,6 +1650,7 @@ def _validate_remote_resource_attributes(self, expected_type: str, expected_iden ) self.assertEqual(expected_type, actual_attributes.get(AWS_REMOTE_RESOURCE_TYPE)) self.assertEqual(expected_identifier, actual_attributes.get(AWS_REMOTE_RESOURCE_IDENTIFIER)) + self.assertEqual(expected_cfn_primary_id, actual_attributes.get(AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER)) # Server span should not generate remote resource attribute self.span_mock.kind = SpanKind.SERVER @@ -1517,6 +1659,7 @@ def _validate_remote_resource_attributes(self, expected_type: str, expected_iden ) self.assertNotIn(AWS_REMOTE_RESOURCE_TYPE, actual_attributes) self.assertNotIn(AWS_REMOTE_RESOURCE_IDENTIFIER, actual_attributes) + self.assertNotIn(AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER, actual_attributes) self._mock_attribute([SpanAttributes.DB_SYSTEM], [None]) @@ -1539,3 +1682,173 @@ def _validate_attributes_produced_for_non_local_root_span_of_kind( self.assertIsNone(dependency_attributes) self.assertEqual(len(service_attributes), len(BoundedAttributes(attributes=expected_attributes))) self.assertEqual(service_attributes, BoundedAttributes(attributes=expected_attributes)) + + def test_set_remote_environment(self): + """Test remote environment setting for Lambda invoke operations.""" + keys = [] + values = [] + + # Test 1: Setting remote environment when all relevant attributes are present + self.span_mock.kind = SpanKind.CLIENT + self._mock_attribute( + [ + SpanAttributes.RPC_SYSTEM, + SpanAttributes.RPC_SERVICE, + SpanAttributes.RPC_METHOD, + AWS_LAMBDA_FUNCTION_NAME, + ], + ["aws-api", "Lambda", "Invoke", "testFunction"], + keys, + values, + ) + actual_attributes = _GENERATOR.generate_metric_attributes_dict_from_span(self.span_mock, self.resource).get( + DEPENDENCY_METRIC + ) + self.assertEqual(actual_attributes.get(AWS_REMOTE_ENVIRONMENT), "lambda:default") + + # Test 2: NOT setting it when RPC_SYSTEM is missing + self._mock_attribute([SpanAttributes.RPC_SYSTEM], [None]) + actual_attributes = _GENERATOR.generate_metric_attributes_dict_from_span(self.span_mock, self.resource).get( + DEPENDENCY_METRIC + ) + self.assertIsNone(actual_attributes.get(AWS_REMOTE_ENVIRONMENT)) + self._mock_attribute([SpanAttributes.RPC_SYSTEM], ["aws-api"], keys, values) + + # Test 3: NOT setting it when RPC_METHOD is missing + self._mock_attribute([SpanAttributes.RPC_METHOD], [None]) + actual_attributes = _GENERATOR.generate_metric_attributes_dict_from_span(self.span_mock, self.resource).get( + DEPENDENCY_METRIC + ) + self.assertIsNone(actual_attributes.get(AWS_REMOTE_ENVIRONMENT)) + self._mock_attribute([SpanAttributes.RPC_METHOD], ["Invoke"], keys, values) + + # Test 4: Still setting it to lambda:default when AWS_LAMBDA_FUNCTION_NAME is missing + # Keep the other attributes but remove AWS_LAMBDA_FUNCTION_NAME + self._mock_attribute( + [ + SpanAttributes.RPC_SYSTEM, + SpanAttributes.RPC_SERVICE, + SpanAttributes.RPC_METHOD, + AWS_LAMBDA_FUNCTION_NAME, + ], + ["aws-api", "Lambda", "Invoke", None], + keys, + values, + ) + + actual_attributes = _GENERATOR.generate_metric_attributes_dict_from_span(self.span_mock, self.resource).get( + DEPENDENCY_METRIC + ) + self.assertEqual(actual_attributes.get(AWS_REMOTE_ENVIRONMENT), "lambda:default") + self._mock_attribute([AWS_LAMBDA_FUNCTION_NAME], ["testFunction"], keys, values) + + # Test 5: NOT setting it for non-Lambda services + self._mock_attribute( + [SpanAttributes.RPC_SERVICE, SpanAttributes.RPC_METHOD], + ["S3", "GetObject"], + keys, + values, + ) + actual_attributes = _GENERATOR.generate_metric_attributes_dict_from_span(self.span_mock, self.resource).get( + DEPENDENCY_METRIC + ) + self.assertIsNone(actual_attributes.get(AWS_REMOTE_ENVIRONMENT)) + + # Test 6: NOT setting it for Lambda non-Invoke operations + self._mock_attribute( + [SpanAttributes.RPC_SERVICE, SpanAttributes.RPC_METHOD], + ["Lambda", "GetFunction"], + keys, + values, + ) + actual_attributes = _GENERATOR.generate_metric_attributes_dict_from_span(self.span_mock, self.resource).get( + DEPENDENCY_METRIC + ) + self.assertIsNone(actual_attributes.get(AWS_REMOTE_ENVIRONMENT)) + + def test_cloudformation_primary_identifier_fallback_to_remote_resource_identifier(self): + """Test that when cloudformationPrimaryIdentifier is not explicitly set, + it falls back to use the same value as remoteResourceIdentifier.""" + keys = [] + values = [] + + keys, values = self._mock_attribute([SpanAttributes.RPC_SYSTEM], ["aws-api"], keys, values) + self.span_mock.kind = SpanKind.CLIENT + + # Test case 1: S3 Bucket (no ARN available, should use bucket name for both) + keys, values = self._mock_attribute( + [SpanAttributes.RPC_SERVICE, SpanAttributes.AWS_S3_BUCKET], ["S3", "my-test-bucket"], keys, values + ) + self._validate_remote_resource_attributes("AWS::S3::Bucket", "my-test-bucket") + + # Test S3 bucket with special characters + keys, values = self._mock_attribute([SpanAttributes.AWS_S3_BUCKET], ["my-test|bucket^name"], keys, values) + self._validate_remote_resource_attributes("AWS::S3::Bucket", "my-test^|bucket^^name") + keys, values = self._mock_attribute( + [SpanAttributes.RPC_SERVICE, SpanAttributes.AWS_S3_BUCKET], [None, None], keys, values + ) + + # Test case 2: SQS Queue by name (no ARN, should use queue name for both) + keys, values = self._mock_attribute( + [SpanAttributes.RPC_SERVICE, AWS_SQS_QUEUE_NAME], ["SQS", "my-test-queue"], keys, values + ) + self._validate_remote_resource_attributes("AWS::SQS::Queue", "my-test-queue") + + # Test SQS queue with special characters + keys, values = self._mock_attribute([AWS_SQS_QUEUE_NAME], ["my^queue|name"], keys, values) + self._validate_remote_resource_attributes("AWS::SQS::Queue", "my^^queue^|name") + keys, values = self._mock_attribute( + [SpanAttributes.RPC_SERVICE, AWS_SQS_QUEUE_NAME], [None, None], keys, values + ) + + # Test case 3: DynamoDB Table (no ARN, should use table name for both) + keys, values = self._mock_attribute( + [SpanAttributes.RPC_SERVICE, SpanAttributes.AWS_DYNAMODB_TABLE_NAMES], + ["DynamoDB", ["my-test-table"]], + keys, + values, + ) + self._validate_remote_resource_attributes("AWS::DynamoDB::Table", "my-test-table") + + # Test DynamoDB table with special characters + keys, values = self._mock_attribute( + [SpanAttributes.AWS_DYNAMODB_TABLE_NAMES], [["my|test^table"]], keys, values + ) + self._validate_remote_resource_attributes("AWS::DynamoDB::Table", "my^|test^^table") + keys, values = self._mock_attribute( + [SpanAttributes.RPC_SERVICE, SpanAttributes.AWS_DYNAMODB_TABLE_NAMES], [None, None], keys, values + ) + + # Test case 4: Kinesis Stream + keys, values = self._mock_attribute( + [SpanAttributes.RPC_SERVICE, AWS_KINESIS_STREAM_NAME], ["Kinesis", "my-test-stream"], keys, values + ) + self._validate_remote_resource_attributes("AWS::Kinesis::Stream", "my-test-stream") + + # Test Kinesis stream with special characters + keys, values = self._mock_attribute([AWS_KINESIS_STREAM_NAME], ["my-stream^with|chars"], keys, values) + self._validate_remote_resource_attributes("AWS::Kinesis::Stream", "my-stream^^with^|chars") + keys, values = self._mock_attribute( + [SpanAttributes.RPC_SERVICE, AWS_KINESIS_STREAM_NAME], [None, None], keys, values + ) + + # Test case 5: Lambda Function (non-invoke operation, no ARN) + keys, values = self._mock_attribute( + [SpanAttributes.RPC_SERVICE, SpanAttributes.RPC_METHOD, AWS_LAMBDA_FUNCTION_NAME], + ["Lambda", "GetFunction", "my-test-function"], + keys, + values, + ) + self._validate_remote_resource_attributes("AWS::Lambda::Function", "my-test-function") + + # Test Lambda function with special characters + keys, values = self._mock_attribute([AWS_LAMBDA_FUNCTION_NAME], ["my-function|with^chars"], keys, values) + self._validate_remote_resource_attributes("AWS::Lambda::Function", "my-function^|with^^chars") + keys, values = self._mock_attribute( + [SpanAttributes.RPC_SERVICE, SpanAttributes.RPC_METHOD, AWS_LAMBDA_FUNCTION_NAME], + [None, None, None], + keys, + values, + ) + + keys, values = self._mock_attribute([SpanAttributes.RPC_SYSTEM], [None], keys, values)