diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-metric-attribute-generator.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-metric-attribute-generator.ts index fe546e27..c717fe7b 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-metric-attribute-generator.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-metric-attribute-generator.ts @@ -33,6 +33,7 @@ import { SERVICE_METRIC, } from './metric-attribute-generator'; import { SqsUrlParser } from './sqs-url-parser'; +import { LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT } from './aws-opentelemetry-configurator'; // Does not exist in @opentelemetry/semantic-conventions const _SERVER_SOCKET_ADDRESS: string = 'server.socket.address'; @@ -50,6 +51,9 @@ const _GRAPHQL_OPERATION_TYPE: string = 'graphql.operation.type'; // Special DEPENDENCY attribute value if GRAPHQL_OPERATION_TYPE attribute key is present. const GRAPHQL: string = 'graphql'; +// Constants for Lambda operations +const LAMBDA_INVOKE_OPERATION: string = 'Invoke'; + // Normalized remote service names for supported AWS services const NORMALIZED_DYNAMO_DB_SERVICE_NAME: string = 'AWS::DynamoDB'; const NORMALIZED_KINESIS_SERVICE_NAME: string = 'AWS::Kinesis'; @@ -109,6 +113,7 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator { AwsMetricAttributeGenerator.setEgressOperation(span, attributes); AwsMetricAttributeGenerator.setRemoteServiceAndOperation(span, attributes); AwsMetricAttributeGenerator.setRemoteResourceTypeAndIdentifier(span, attributes); + AwsMetricAttributeGenerator.setRemoteEnvironment(span, attributes); AwsMetricAttributeGenerator.setSpanKindForDependency(span, attributes); AwsMetricAttributeGenerator.setRemoteDbUser(span, attributes); @@ -336,7 +341,18 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator { BedrockRuntime: NORMALIZED_BEDROCK_RUNTIME_SERVICE_NAME, SecretsManager: NORMALIZED_SECRETSMANAGER_SERVICE_NAME, SFN: NORMALIZED_STEPFUNCTIONS_SERVICE_NAME, + Lambda: NORMALIZED_LAMBDA_SERVICE_NAME, }; + + // Special handling for Lambda invoke operations + if (AwsMetricAttributeGenerator.isLambdaInvokeOperation(span)) { + const lambdaFunctionName = span.attributes[AWS_ATTRIBUTE_KEYS.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 lambdaFunctionName ? String(lambdaFunctionName) : AwsSpanProcessingUtil.UNKNOWN_REMOTE_SERVICE; + } + return awsSdkServiceMapping[serviceName] || 'AWS::' + serviceName; } return serviceName; @@ -410,24 +426,9 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator { ); cloudFormationIdentifier = AwsMetricAttributeGenerator.escapeDelimiters(activityArn); } else if (AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.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 allow users to override the default value if needed. - if (AwsMetricAttributeGenerator.getRemoteOperation(span, SEMATTRS_RPC_METHOD) === 'Invoke') { - attributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE] = AwsMetricAttributeGenerator.escapeDelimiters( - span.attributes[AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_NAME] - ); - - attributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_ENVIRONMENT] = `lambda:${ - process.env.LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT || 'default' - }`; - } else { + // For non-Invoke Lambda operations, treat Lambda as a resource, + // see normalizeRemoteServiceName for more information. + if (!AwsMetricAttributeGenerator.isLambdaInvokeOperation(span)) { remoteResourceType = NORMALIZED_LAMBDA_SERVICE_NAME + '::Function'; remoteResourceIdentifier = AwsMetricAttributeGenerator.escapeDelimiters( span.attributes[AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_NAME] @@ -491,17 +492,33 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator { remoteResourceIdentifier = AwsMetricAttributeGenerator.getDbConnection(span); } + if (cloudFormationIdentifier === undefined) { + cloudFormationIdentifier = remoteResourceIdentifier; + } + if (remoteResourceType !== undefined && remoteResourceIdentifier !== undefined) { attributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_TYPE] = remoteResourceType; attributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_IDENTIFIER] = remoteResourceIdentifier; + attributes[AWS_ATTRIBUTE_KEYS.AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER] = cloudFormationIdentifier; + } + } - if (AwsSpanProcessingUtil.isAwsSDKSpan(span)) { - if (cloudFormationIdentifier === undefined) { - cloudFormationIdentifier = remoteResourceIdentifier; - } - - attributes[AWS_ATTRIBUTE_KEYS.AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER] = cloudFormationIdentifier; + /** + * 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. + */ + private static setRemoteEnvironment(span: ReadableSpan, attributes: Attributes): void { + // 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 (AwsMetricAttributeGenerator.isLambdaInvokeOperation(span)) { + let remoteEnvironment = process.env[LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT]?.trim(); + if (!remoteEnvironment) { + remoteEnvironment = 'default'; } + attributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_ENVIRONMENT] = `lambda:${remoteEnvironment}`; } } @@ -620,6 +637,18 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator { return input.split('^').join('^^').split('|').join('^|'); } + /** + * Check if the span represents a Lambda Invoke operation. + */ + private static isLambdaInvokeOperation(span: ReadableSpan): boolean { + if (!AwsSpanProcessingUtil.isAwsSDKSpan(span)) { + return false; + } + + const rpcService = AwsMetricAttributeGenerator.getRemoteService(span, SEMATTRS_RPC_SERVICE); + return rpcService === 'Lambda' && span.attributes[SEMATTRS_RPC_METHOD] === LAMBDA_INVOKE_OPERATION; + } + // Extracts the name of the resource from an arn private static extractResourceNameFromArn(attribute: AttributeValue | undefined): string | undefined { if (typeof attribute === 'string' && attribute.startsWith('arn:aws:')) { diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts index 4d4938fe..dac8e45d 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts @@ -75,6 +75,7 @@ const FORMAT_OTEL_UNSAMPLED_TRACES_BINARY_PREFIX = 'T1U'; // Follow Python SDK Impl to set the max span batch size // which will reduce the chance of UDP package size is larger than 64KB const LAMBDA_SPAN_EXPORT_BATCH_SIZE = 10; +export const LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT: string = 'LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT'; /** * Aws Application Signals Config Provider creates a configuration object that can be provided to diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts b/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts index c51c03ec..f860e270 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts @@ -756,6 +756,15 @@ describe('AwsMetricAttributeGeneratorTest', () => { mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_SQS_QUEUE_URL, undefined); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_SQS_QUEUE_NAME, undefined); + // Validate behaviour of AWS_SQS_QUEUE_URL attribute without queue name, then remove it. + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_SQS_QUEUE_URL, 'https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue'); + validateRemoteResourceAttributes( + 'AWS::SQS::Queue', + 'MyQueue', + 'https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue' + ); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_SQS_QUEUE_URL, undefined); + // Validate behaviour of AWS_KINESIS_STREAM_NAME attribute, then remove it. mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_KINESIS_STREAM_NAME, 'AWS_KINESIS_STREAM_NAME'); validateRemoteResourceAttributes('AWS::Kinesis::Stream', 'AWS_KINESIS_STREAM_NAME'); @@ -763,7 +772,7 @@ describe('AwsMetricAttributeGeneratorTest', () => { // Validate behaviour of AWS_SNS_TOPIC_ARN attribute then remove it. mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_SNS_TOPIC_ARN, 'arn:aws:sns:us-east-1:123456789012:testTopic'); - validateRemoteResourceAttributes('AWS::SNS::Topic', 'testTopic'); + validateRemoteResourceAttributes('AWS::SNS::Topic', 'testTopic', 'arn:aws:sns:us-east-1:123456789012:testTopic'); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_SNS_TOPIC_ARN, undefined); // Validate behaviour of AWS_SECRETSMANAGER_SECRET_ARN attributes then remove it. @@ -771,7 +780,11 @@ describe('AwsMetricAttributeGeneratorTest', () => { AWS_ATTRIBUTE_KEYS.AWS_SECRETSMANAGER_SECRET_ARN, 'arn:aws:secretsmanager:us-east-1:123456789123:secret:testSecret' ); - validateRemoteResourceAttributes('AWS::SecretsManager::Secret', 'testSecret'); + validateRemoteResourceAttributes( + 'AWS::SecretsManager::Secret', + 'testSecret', + 'arn:aws:secretsmanager:us-east-1:123456789123:secret:testSecret' + ); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_SECRETSMANAGER_SECRET_ARN, undefined); // Validate behaviour of AWS_LAMBDA_FUNCTION_NAME and AWS_LAMBDA_FUNCTION_ARN @@ -780,7 +793,11 @@ describe('AwsMetricAttributeGeneratorTest', () => { AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_ARN, 'arn:aws:lambda:us-east-1:123456789012:function:aws_lambda_function_name' ); - validateRemoteResourceAttributes('AWS::Lambda::Function', 'aws_lambda_function_name'); + validateRemoteResourceAttributes( + 'AWS::Lambda::Function', + 'aws_lambda_function_name', + 'arn:aws:lambda:us-east-1:123456789012:function:aws_lambda_function_name' + ); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_NAME, undefined); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_ARN, undefined); @@ -794,14 +811,22 @@ describe('AwsMetricAttributeGeneratorTest', () => { AWS_ATTRIBUTE_KEYS.AWS_STEPFUNCTIONS_STATEMACHINE_ARN, 'arn:aws:states:us-east-1:123456789123:stateMachine:testStateMachine' ); - validateRemoteResourceAttributes('AWS::StepFunctions::StateMachine', 'testStateMachine'); + validateRemoteResourceAttributes( + 'AWS::StepFunctions::StateMachine', + 'testStateMachine', + 'arn:aws:states:us-east-1:123456789123:stateMachine:testStateMachine' + ); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_STEPFUNCTIONS_STATEMACHINE_ARN, undefined); mockAttribute( AWS_ATTRIBUTE_KEYS.AWS_STEPFUNCTIONS_ACTIVITY_ARN, 'arn:aws:states:us-east-1:123456789123:activity:testActivity' ); - validateRemoteResourceAttributes('AWS::StepFunctions::Activity', 'testActivity'); + validateRemoteResourceAttributes( + 'AWS::StepFunctions::Activity', + 'testActivity', + 'arn:aws:states:us-east-1:123456789123:activity:testActivity' + ); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_STEPFUNCTIONS_ACTIVITY_ARN, undefined); // Validate behaviour of AWS_TABLE_NAMES attribute with one table name, then remove it. @@ -841,23 +866,49 @@ describe('AwsMetricAttributeGeneratorTest', () => { // Validate behaviour of AWS_BEDROCK_DATA_SOURCE_ID attribute, then remove it. mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_DATA_SOURCE_ID, 'test_datasource_id'); - validateRemoteResourceAttributes('AWS::Bedrock::DataSource', 'test_datasource_id'); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_KNOWLEDGE_BASE_ID, 'test_kb_id'); + validateRemoteResourceAttributes('AWS::Bedrock::DataSource', 'test_datasource_id', 'test_kb_id|test_datasource_id'); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_DATA_SOURCE_ID, undefined); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_KNOWLEDGE_BASE_ID, undefined); // Validate behaviour of AWS_BEDROCK_DATA_SOURCE_ID attribute with special chars(^), then remove it. mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_DATA_SOURCE_ID, 'test_datasource_^id'); - validateRemoteResourceAttributes('AWS::Bedrock::DataSource', 'test_datasource_^^id'); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_KNOWLEDGE_BASE_ID, 'test_kb_^id'); + validateRemoteResourceAttributes( + 'AWS::Bedrock::DataSource', + 'test_datasource_^^id', + 'test_kb_^^id|test_datasource_^^id' + ); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_DATA_SOURCE_ID, undefined); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_KNOWLEDGE_BASE_ID, undefined); // Validate behaviour of AWS_BEDROCK_GUARDRAIL_ID attribute, then remove it. mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_GUARDRAIL_ID, 'test_guardrail_id'); - validateRemoteResourceAttributes('AWS::Bedrock::Guardrail', 'test_guardrail_id'); + mockAttribute( + AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_GUARDRAIL_ARN, + 'arn:aws:bedrock:us-east-1:123456789012:guardrail/test_guardrail_id' + ); + validateRemoteResourceAttributes( + 'AWS::Bedrock::Guardrail', + 'test_guardrail_id', + 'arn:aws:bedrock:us-east-1:123456789012:guardrail/test_guardrail_id' + ); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_GUARDRAIL_ID, undefined); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_GUARDRAIL_ARN, undefined); // Validate behaviour of AWS_BEDROCK_GUARDRAIL_ID attribute with special chars(^), then remove it. mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_GUARDRAIL_ID, 'test_guardrail_^id'); - validateRemoteResourceAttributes('AWS::Bedrock::Guardrail', 'test_guardrail_^^id'); + mockAttribute( + AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_GUARDRAIL_ARN, + 'arn:aws:bedrock:us-east-1:123456789012:guardrail/test_guardrail_^id' + ); + validateRemoteResourceAttributes( + 'AWS::Bedrock::Guardrail', + 'test_guardrail_^^id', + 'arn:aws:bedrock:us-east-1:123456789012:guardrail/test_guardrail_^^id' + ); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_GUARDRAIL_ID, undefined); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_GUARDRAIL_ARN, undefined); // Validate behaviour of AWS_BEDROCK_KNOWLEDGE_BASE_ID attribute, then remove it. mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_KNOWLEDGE_BASE_ID, 'test_knowledgeBase_id'); @@ -1114,7 +1165,16 @@ describe('AwsMetricAttributeGeneratorTest', () => { mockAttribute(SEMATTRS_PEER_SERVICE, undefined); } - function validateRemoteResourceAttributes(type: string | undefined, identifier: string | undefined): void { + function validateRemoteResourceAttributes( + type: string | undefined, + identifier: string | undefined, + cfnPrimaryId: string | undefined = undefined + ): void { + // If cfnPrimaryId is not provided, it defaults to identifier + if (cfnPrimaryId === undefined) { + cfnPrimaryId = identifier; + } + // Client, Producer and Consumer spans should generate the expected remote resource attributes (spanDataMock as any).kind = SpanKind.CLIENT; let actualAttributes: Attributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[ @@ -1122,22 +1182,26 @@ describe('AwsMetricAttributeGeneratorTest', () => { ]; expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_TYPE]).toEqual(type); expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_IDENTIFIER]).toEqual(identifier); + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER]).toEqual(cfnPrimaryId); (spanDataMock as any).kind = SpanKind.PRODUCER; actualAttributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[DEPENDENCY_METRIC]; expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_TYPE]).toEqual(type); expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_IDENTIFIER]).toEqual(identifier); + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER]).toEqual(cfnPrimaryId); (spanDataMock as any).kind = SpanKind.CONSUMER; actualAttributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[DEPENDENCY_METRIC]; expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_TYPE]).toEqual(type); expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_IDENTIFIER]).toEqual(identifier); + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER]).toEqual(cfnPrimaryId); // Server span should not generate remote resource attributes (spanDataMock as any).kind = SpanKind.SERVER; actualAttributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[SERVICE_METRIC]; expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_TYPE]).toEqual(undefined); expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_IDENTIFIER]).toEqual(undefined); + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER]).toEqual(undefined); } it('testDBUserAttribute', () => { @@ -1261,6 +1325,129 @@ describe('AwsMetricAttributeGeneratorTest', () => { expect(attributeMap[DEPENDENCY_METRIC]).toEqual(dependencyAttributes); }); + it('testCloudformationPrimaryIdentifierFallbackToRemoteResourceIdentifier', () => { + // Test that when cloudformationPrimaryIdentifier is not explicitly set, + // it falls back to use the same value as remoteResourceIdentifier. + mockAttribute(SEMATTRS_RPC_SYSTEM, 'aws-api'); + (spanDataMock as any).kind = SpanKind.CLIENT; + + // Test case 1: S3 Bucket (no ARN available, should use bucket name for both) + mockAttribute(SEMATTRS_RPC_SERVICE, 'S3'); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_S3_BUCKET, 'my-test-bucket'); + validateRemoteResourceAttributes('AWS::S3::Bucket', 'my-test-bucket'); + + // Test S3 bucket with special characters + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_S3_BUCKET, 'my-test|bucket^name'); + validateRemoteResourceAttributes('AWS::S3::Bucket', 'my-test^|bucket^^name'); + mockAttribute(SEMATTRS_RPC_SERVICE, undefined); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_S3_BUCKET, undefined); + + // Test case 2: SQS Queue by name (no ARN, should use queue name for both) + mockAttribute(SEMATTRS_RPC_SERVICE, 'SQS'); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_SQS_QUEUE_NAME, 'my-test-queue'); + validateRemoteResourceAttributes('AWS::SQS::Queue', 'my-test-queue'); + + // Test SQS queue with special characters + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_SQS_QUEUE_NAME, 'my^queue|name'); + validateRemoteResourceAttributes('AWS::SQS::Queue', 'my^^queue^|name'); + mockAttribute(SEMATTRS_RPC_SERVICE, undefined); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_SQS_QUEUE_NAME, undefined); + + // Test case 3: DynamoDB Table (no ARN, should use table name for both) + mockAttribute(SEMATTRS_RPC_SERVICE, 'DynamoDB'); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_DYNAMODB_TABLE_NAMES, ['my-test-table']); + validateRemoteResourceAttributes('AWS::DynamoDB::Table', 'my-test-table'); + + // Test DynamoDB table with special characters + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_DYNAMODB_TABLE_NAMES, ['my|test^table']); + validateRemoteResourceAttributes('AWS::DynamoDB::Table', 'my^|test^^table'); + mockAttribute(SEMATTRS_RPC_SERVICE, undefined); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_DYNAMODB_TABLE_NAMES, undefined); + + // Test case 4: Kinesis Stream + mockAttribute(SEMATTRS_RPC_SERVICE, 'Kinesis'); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_KINESIS_STREAM_NAME, 'my-test-stream'); + validateRemoteResourceAttributes('AWS::Kinesis::Stream', 'my-test-stream'); + + // Test Kinesis stream with special characters + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_KINESIS_STREAM_NAME, 'my-stream^with|chars'); + validateRemoteResourceAttributes('AWS::Kinesis::Stream', 'my-stream^^with^|chars'); + mockAttribute(SEMATTRS_RPC_SERVICE, undefined); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_KINESIS_STREAM_NAME, undefined); + + // Test case 5: Lambda Function (non-invoke operation, no ARN) + mockAttribute(SEMATTRS_RPC_SERVICE, 'Lambda'); + mockAttribute(SEMATTRS_RPC_METHOD, 'GetFunction'); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_NAME, 'my-test-function'); + validateRemoteResourceAttributes('AWS::Lambda::Function', 'my-test-function'); + + // Test Lambda function with special characters + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_NAME, 'my-function|with^chars'); + validateRemoteResourceAttributes('AWS::Lambda::Function', 'my-function^|with^^chars'); + mockAttribute(SEMATTRS_RPC_SERVICE, undefined); + mockAttribute(SEMATTRS_RPC_METHOD, undefined); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_NAME, undefined); + + mockAttribute(SEMATTRS_RPC_SYSTEM, undefined); + }); + + it('testSetRemoteEnvironment', () => { + // Test remote environment setting for Lambda invoke operations. + + // Test 1: Setting remote environment when all relevant attributes are present + (spanDataMock as any).kind = SpanKind.CLIENT; + mockAttribute(SEMATTRS_RPC_SYSTEM, 'aws-api'); + mockAttribute(SEMATTRS_RPC_SERVICE, 'Lambda'); + mockAttribute(SEMATTRS_RPC_METHOD, 'Invoke'); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_NAME, 'testFunction'); + + let actualAttributes: Attributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[ + DEPENDENCY_METRIC + ]; + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_ENVIRONMENT]).toEqual('lambda:default'); + + // Test 2: NOT setting it when RPC_SYSTEM is missing + mockAttribute(SEMATTRS_RPC_SYSTEM, undefined); + actualAttributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[DEPENDENCY_METRIC]; + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_ENVIRONMENT]).toBeUndefined(); + mockAttribute(SEMATTRS_RPC_SYSTEM, 'aws-api'); + + // Test 3: NOT setting it when RPC_METHOD is missing + mockAttribute(SEMATTRS_RPC_METHOD, undefined); + actualAttributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[DEPENDENCY_METRIC]; + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_ENVIRONMENT]).toBeUndefined(); + mockAttribute(SEMATTRS_RPC_METHOD, 'Invoke'); + + // 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 + mockAttribute(SEMATTRS_RPC_SYSTEM, 'aws-api'); + mockAttribute(SEMATTRS_RPC_SERVICE, 'Lambda'); + mockAttribute(SEMATTRS_RPC_METHOD, 'Invoke'); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_NAME, undefined); + + actualAttributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[DEPENDENCY_METRIC]; + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_ENVIRONMENT]).toEqual('lambda:default'); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_NAME, 'testFunction'); + + // Test 5: NOT setting it for non-Lambda services + mockAttribute(SEMATTRS_RPC_SERVICE, 'S3'); + mockAttribute(SEMATTRS_RPC_METHOD, 'GetObject'); + actualAttributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[DEPENDENCY_METRIC]; + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_ENVIRONMENT]).toBeUndefined(); + + // Test 6: NOT setting it for Lambda non-Invoke operations + mockAttribute(SEMATTRS_RPC_SERVICE, 'Lambda'); + mockAttribute(SEMATTRS_RPC_METHOD, 'GetFunction'); + actualAttributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[DEPENDENCY_METRIC]; + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_ENVIRONMENT]).toBeUndefined(); + + // Clean up + mockAttribute(SEMATTRS_RPC_SYSTEM, undefined); + mockAttribute(SEMATTRS_RPC_SERVICE, undefined); + mockAttribute(SEMATTRS_RPC_METHOD, undefined); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_NAME, undefined); + }); + it('testJdbcDbConnectionString', () => { mockAttribute(SEMATTRS_DB_SYSTEM, 'mysql');