diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-attribute-keys.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-attribute-keys.ts index 54bd517a..39f3282e 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-attribute-keys.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-attribute-keys.ts @@ -5,12 +5,17 @@ import { SEMATTRS_AWS_DYNAMODB_TABLE_NAMES } from '@opentelemetry/semantic-conve // Utility class holding attribute keys with special meaning to AWS components export const AWS_ATTRIBUTE_KEYS: { [key: string]: string } = { + AWS_AUTH_ACCESS_KEY: 'aws.auth.access_key', + AWS_AUTH_REGION: 'aws.auth.region', AWS_SPAN_KIND: 'aws.span.kind', AWS_LOCAL_SERVICE: 'aws.local.service', AWS_LOCAL_OPERATION: 'aws.local.operation', AWS_REMOTE_SERVICE: 'aws.remote.service', AWS_REMOTE_ENVIRONMENT: 'aws.remote.environment', AWS_REMOTE_OPERATION: 'aws.remote.operation', + AWS_REMOTE_RESOURCE_ACCESS_KEY: 'aws.remote.resource.account.access_key', + AWS_REMOTE_RESOURCE_ACCOUNT_ID: 'aws.remote.resource.account.id', + AWS_REMOTE_RESOURCE_REGION: 'aws.remote.resource.region', AWS_REMOTE_RESOURCE_TYPE: 'aws.remote.resource.type', AWS_REMOTE_RESOURCE_IDENTIFIER: 'aws.remote.resource.identifier', AWS_SDK_DESCENDANT: 'aws.sdk.descendant', @@ -31,7 +36,9 @@ export const AWS_ATTRIBUTE_KEYS: { [key: string]: string } = { AWS_S3_BUCKET: 'aws.s3.bucket', AWS_SQS_QUEUE_URL: 'aws.sqs.queue.url', AWS_SQS_QUEUE_NAME: 'aws.sqs.queue.name', + AWS_KINESIS_STREAM_ARN: 'aws.kinesis.stream.arn', AWS_KINESIS_STREAM_NAME: 'aws.kinesis.stream.name', + AWS_DYNAMODB_TABLE_ARN: 'aws.dynamodb.table.arn', AWS_DYNAMODB_TABLE_NAMES: SEMATTRS_AWS_DYNAMODB_TABLE_NAMES, AWS_BEDROCK_DATA_SOURCE_ID: 'aws.bedrock.data_source.id', AWS_BEDROCK_KNOWLEDGE_BASE_ID: 'aws.bedrock.knowledge_base.id', 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..977d7057 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 @@ -32,6 +32,7 @@ import { MetricAttributeGenerator, SERVICE_METRIC, } from './metric-attribute-generator'; +import { RegionalResourceArnParser } from './regional-resource-arn-parser'; import { SqsUrlParser } from './sqs-url-parser'; // Does not exist in @opentelemetry/semantic-conventions @@ -69,6 +70,16 @@ const DB_CONNECTION_RESOURCE_TYPE: string = 'DB::Connection'; // - `defaultServiceName()` returns `unknown_service:${process.argv0}` const OTEL_UNKNOWN_SERVICE: string = defaultServiceName(); +const ARN_ATTRIBUTES: string[] = [ + AWS_ATTRIBUTE_KEYS.AWS_DYNAMODB_TABLE_ARN, + AWS_ATTRIBUTE_KEYS.AWS_KINESIS_STREAM_ARN, + AWS_ATTRIBUTE_KEYS.AWS_SNS_TOPIC_ARN, + AWS_ATTRIBUTE_KEYS.AWS_SECRETSMANAGER_SECRET_ARN, + AWS_ATTRIBUTE_KEYS.AWS_STEPFUNCTIONS_STATEMACHINE_ARN, + AWS_ATTRIBUTE_KEYS.AWS_STEPFUNCTIONS_ACTIVITY_ARN, + AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_ARN, + AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_GUARDRAIL_ARN, +]; /** * AwsMetricAttributeGenerator generates very specific metric attributes based on low-cardinality * span and resource attributes. If such attributes are not present, we fallback to default values. @@ -108,7 +119,19 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator { AwsMetricAttributeGenerator.setService(resource, span, attributes); AwsMetricAttributeGenerator.setEgressOperation(span, attributes); AwsMetricAttributeGenerator.setRemoteServiceAndOperation(span, attributes); - AwsMetricAttributeGenerator.setRemoteResourceTypeAndIdentifier(span, attributes); + const isRemoteResourceIdentifierPresent = AwsMetricAttributeGenerator.setRemoteResourceTypeAndIdentifier( + span, + attributes + ); + if (isRemoteResourceIdentifierPresent) { + const isAccountIdAndRegionPresent = AwsMetricAttributeGenerator.setRemoteResourceAccountIdAndRegion( + span, + attributes + ); + if (!isAccountIdAndRegionPresent) { + AwsMetricAttributeGenerator.setRemoteResourceAccessKeyAndRegion(span, attributes); + } + } AwsMetricAttributeGenerator.setSpanKindForDependency(span, attributes); AwsMetricAttributeGenerator.setRemoteDbUser(span, attributes); @@ -353,7 +376,7 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator { * href="https://docs.aws.amazon.com/cloudcontrolapi/latest/userguide/supported-resources.html">AWS * Cloud Control resource format. */ - private static setRemoteResourceTypeAndIdentifier(span: ReadableSpan, attributes: Attributes): void { + private static setRemoteResourceTypeAndIdentifier(span: ReadableSpan, attributes: Attributes): boolean { let remoteResourceType: AttributeValue | undefined; let remoteResourceIdentifier: AttributeValue | undefined; let cloudFormationIdentifier: AttributeValue | undefined; @@ -367,11 +390,27 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator { ) { remoteResourceType = NORMALIZED_DYNAMO_DB_SERVICE_NAME + '::Table'; remoteResourceIdentifier = AwsMetricAttributeGenerator.escapeDelimiters(awsTableNames[0]); + } else if (AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_DYNAMODB_TABLE_ARN)) { + remoteResourceType = NORMALIZED_DYNAMO_DB_SERVICE_NAME + '::Table'; + remoteResourceIdentifier = AwsMetricAttributeGenerator.escapeDelimiters( + this.extractResourceNameFromArn(span.attributes[AWS_ATTRIBUTE_KEYS.AWS_DYNAMODB_TABLE_ARN])?.replace( + 'table/', + '' + ) + ); } else if (AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_KINESIS_STREAM_NAME)) { remoteResourceType = NORMALIZED_KINESIS_SERVICE_NAME + '::Stream'; remoteResourceIdentifier = AwsMetricAttributeGenerator.escapeDelimiters( span.attributes[AWS_ATTRIBUTE_KEYS.AWS_KINESIS_STREAM_NAME] ); + } else if (AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_KINESIS_STREAM_ARN)) { + remoteResourceType = NORMALIZED_KINESIS_SERVICE_NAME + '::Stream'; + remoteResourceIdentifier = AwsMetricAttributeGenerator.escapeDelimiters( + this.extractResourceNameFromArn(span.attributes[AWS_ATTRIBUTE_KEYS.AWS_KINESIS_STREAM_ARN])?.replace( + 'stream/', + '' + ) + ); } else if (AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_S3_BUCKET)) { remoteResourceType = NORMALIZED_S3_SERVICE_NAME + '::Bucket'; remoteResourceIdentifier = AwsMetricAttributeGenerator.escapeDelimiters( @@ -502,6 +541,48 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator { attributes[AWS_ATTRIBUTE_KEYS.AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER] = cloudFormationIdentifier; } + return true; + } + return false; + } + + private static setRemoteResourceAccountIdAndRegion(span: ReadableSpan, attributes: Attributes): boolean { + let remoteResourceAccountId: string | undefined = undefined; + let remoteResourceRegion: string | undefined = undefined; + + if (AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_SQS_QUEUE_URL)) { + const sqsQueueUrl = AwsMetricAttributeGenerator.escapeDelimiters( + span.attributes[AWS_ATTRIBUTE_KEYS.AWS_SQS_QUEUE_URL] + ); + remoteResourceAccountId = SqsUrlParser.getAccountId(sqsQueueUrl); + remoteResourceRegion = SqsUrlParser.getRegion(sqsQueueUrl); + } else { + for (const attributeKey of ARN_ATTRIBUTES) { + if (AwsSpanProcessingUtil.isKeyPresent(span, attributeKey)) { + const arn = span.attributes[attributeKey]; + remoteResourceAccountId = RegionalResourceArnParser.getAccountId(arn); + remoteResourceRegion = RegionalResourceArnParser.getRegion(arn); + break; + } + } + } + + if (remoteResourceAccountId !== undefined && remoteResourceRegion !== undefined) { + attributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_ACCOUNT_ID] = remoteResourceAccountId; + attributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_REGION] = remoteResourceRegion; + return true; + } + + return false; + } + + private static setRemoteResourceAccessKeyAndRegion(span: ReadableSpan, attributes: Attributes): void { + if (AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_AUTH_ACCESS_KEY)) { + attributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_ACCESS_KEY] = + span.attributes[AWS_ATTRIBUTE_KEYS.AWS_AUTH_ACCESS_KEY]; + } + if (AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_AUTH_REGION)) { + attributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_REGION] = span.attributes[AWS_ATTRIBUTE_KEYS.AWS_AUTH_REGION]; } } diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/patches/instrumentation-patch.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/patches/instrumentation-patch.ts index 18f2b226..84c03904 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/patches/instrumentation-patch.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/patches/instrumentation-patch.ts @@ -80,6 +80,8 @@ export function applyInstrumentationPatches(instrumentations: Instrumentation[]) patchSqsServiceExtension(services.get('SQS')); patchSnsServiceExtension(services.get('SNS')); patchLambdaServiceExtension(services.get('Lambda')); + patchKinesisServiceExtension(services.get('Kinesis')); + patchDynamoDbServiceExtension(services.get('DynamoDB')); } } else if (instrumentation.instrumentationName === '@opentelemetry/instrumentation-aws-lambda') { diag.debug('Patching aws lambda instrumentation'); @@ -189,6 +191,68 @@ function patchSnsServiceExtension(snsServiceExtension: any): void { } } +/* + * This patch extends the existing upstream extension for Kinesis. Extensions allow for custom logic for adding + * service-specific information to spans, such as attributes. Specifically, we are adding logic to add + * `aws.kinesis.stream.arn` attribute, to be used to generate RemoteTarget and achieve parity with the Java/Python instrumentation. + * + * + * @param kinesisServiceExtension Kinesis Service Extension obtained the service extension list from the AWS SDK OTel Instrumentation + */ +function patchKinesisServiceExtension(kinesisServiceExtension: any): void { + if (kinesisServiceExtension) { + const requestPreSpanHook = kinesisServiceExtension.requestPreSpanHook; + kinesisServiceExtension._requestPreSpanHook = requestPreSpanHook; + + const patchedRequestPreSpanHook = ( + request: NormalizedRequest, + _config: AwsSdkInstrumentationConfig + ): RequestMetadata => { + const requestMetadata: RequestMetadata = kinesisServiceExtension._requestPreSpanHook(request, _config); + if (requestMetadata.spanAttributes) { + const streamArn = request.commandInput?.StreamARN; + if (streamArn) { + requestMetadata.spanAttributes[AWS_ATTRIBUTE_KEYS.AWS_KINESIS_STREAM_ARN] = streamArn; + } + } + return requestMetadata; + }; + + kinesisServiceExtension.requestPreSpanHook = patchedRequestPreSpanHook; + } +} + +/* + * This patch extends the existing upstream extension for DynamoDB. Extensions allow for custom logic for adding + * service-specific information to spans, such as attributes. Specifically, we are adding logic to add + * `aws.dynamodb.table.arn` attribute, to be used to generate RemoteTarget and achieve parity with the Java/Python instrumentation. + * + * + * @param dynamoDbServiceExtension DynamoDB Service Extension obtained the service extension list from the AWS SDK OTel Instrumentation + */ +function patchDynamoDbServiceExtension(dynamoDbServiceExtension: any): void { + if (dynamoDbServiceExtension) { + if (typeof dynamoDbServiceExtension.responseHook === 'function') { + const originalResponseHook = dynamoDbServiceExtension.responseHook; + dynamoDbServiceExtension.responseHook = ( + response: NormalizedResponse, + span: Span, + tracer: Tracer, + config: AwsSdkInstrumentationConfig + ): void => { + originalResponseHook.call(dynamoDbServiceExtension, response, span, tracer, config); + + if (response.data && response.data.Table) { + const tableArn = response.data.Table.TableArn; + if (tableArn) { + span.setAttribute(AWS_ATTRIBUTE_KEYS.AWS_DYNAMODB_TABLE_ARN, tableArn); + } + } + }; + } + } +} + /* * This patch extends the existing upstream extension for Lambda. Extensions allow for custom logic for adding * service-specific information to spans, such as attributes. Specifically, we are adding logic to add @@ -293,7 +357,7 @@ function patchAwsLambdaInstrumentation(instrumentation: Instrumentation): void { } } -// Override the upstream private _getV3SmithyClientSendPatch method to add middleware to inject X-Ray Trace Context into HTTP Headers +// Override the upstream private _getV3SmithyClientSendPatch method to add middlewares to inject X-Ray Trace Context into HTTP Headers and to extract account access key id and region for cross-account support // https://github.com/open-telemetry/opentelemetry-js-contrib/blob/instrumentation-aws-sdk-v0.48.0/plugins/node/opentelemetry-instrumentation-aws-sdk/src/aws-sdk.ts#L373-L384 const awsXrayPropagator = new AWSXRayPropagator(); const V3_CLIENT_CONFIG_KEY = Symbol('opentelemetry.instrumentation.aws-sdk.client.config'); @@ -306,6 +370,38 @@ function patchAwsSdkInstrumentation(instrumentation: Instrumentation): void { original: (...args: unknown[]) => Promise ) { return function send(this: any, command: V3PluginCommand, ...args: unknown[]): Promise { + this.middlewareStack?.add( + (next: any, context: any) => async (middlewareArgs: any) => { + const activeContext = otelContext.active(); + const span = trace.getSpan(activeContext); + + try { + const credsProvider = this.config.credentials; + if (credsProvider && span) { + const credentials = await credsProvider(); + if (credentials.accessKeyId) { + span.setAttribute(AWS_ATTRIBUTE_KEYS.AWS_AUTH_ACCESS_KEY, credentials.accessKeyId); + } + } + if (span && this.config.region) { + const region = await this.config.region(); + if (region) { + span.setAttribute(AWS_ATTRIBUTE_KEYS.AWS_AUTH_REGION, region); + } + } + } catch (err) { + diag.debug('Fail to get auth account access key and region.'); + } + + return await next(middlewareArgs); + }, + { + step: 'build', + name: '_extractSignerCredentials', + override: true, + } + ); + this.middlewareStack?.add( (next: any, context: any) => async (middlewareArgs: any) => { awsXrayPropagator.inject(otelContext.active(), middlewareArgs.request.headers, defaultTextMapSetter); diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/regional-resource-arn-parser.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/regional-resource-arn-parser.ts new file mode 100644 index 00000000..91394469 --- /dev/null +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/regional-resource-arn-parser.ts @@ -0,0 +1,52 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { AttributeValue } from '@opentelemetry/api'; + +export class RegionalResourceArnParser { + public static getAccountId(arn: AttributeValue | undefined): string | undefined { + if (this.isArn(arn)) { + return (arn! as string).split(':')[4]; + } + return undefined; + } + + public static getRegion(arn: AttributeValue | undefined): string | undefined { + if (this.isArn(arn)) { + return (arn! as string).split(':')[3]; + } + return undefined; + } + + public static isArn(arn: AttributeValue | undefined): boolean { + // Check if arn follows the format: + // arn:partition:service:region:account-id:resource-type/resource-id or + // arn:partition:service:region:account-id:resource-type:resource-id + if (!arn || typeof arn !== 'string') { + return false; + } + + if (!arn.startsWith('arn')) { + return false; + } + + const arnParts = arn.split(':'); + return arnParts.length >= 6 && this.isAccountId(arnParts[4]); + } + + private static isAccountId(input: string): boolean { + if (input == null || input.length !== 12) { + return false; + } + + if (!this._checkDigits(input)) { + return false; + } + + return true; + } + + private static _checkDigits(str: string): boolean { + return /^\d+$/.test(str); + } +} diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/sqs-url-parser.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/sqs-url-parser.ts index 37d0a1bc..fcb1fa30 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/sqs-url-parser.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/sqs-url-parser.ts @@ -30,6 +30,57 @@ export class SqsUrlParser { return undefined; } + /** + * Extracts the account ID from an SQS URL. + */ + public static getAccountId(url: AttributeValue | undefined): string | undefined { + if (typeof url !== 'string') { + return undefined; + } + + url = url.replace(HTTP_SCHEMA, '').replace(HTTPS_SCHEMA, ''); + if (this.isValidSqsUrl(url)) { + const splitUrl: string[] = url.split('/'); + return splitUrl[1]; + } + + return undefined; + } + + /** + * Extracts the region from an SQS URL. + */ + public static getRegion(url: AttributeValue | undefined): string | undefined { + if (typeof url !== 'string') { + return undefined; + } + + url = url.replace(HTTP_SCHEMA, '').replace(HTTPS_SCHEMA, ''); + if (this.isValidSqsUrl(url)) { + const splitUrl: string[] = url.split('/'); + const domain: string = splitUrl[0]; + const domainParts: string[] = domain.split('.'); + if (domainParts.length === 4) { + return domainParts[1]; + } + } + + return undefined; + } + + /** + * Checks if the URL is a valid SQS URL. + */ + private static isValidSqsUrl(url: string): boolean { + const splitUrl: string[] = url.split('/'); + return ( + splitUrl.length === 3 && + splitUrl[0].toLowerCase().startsWith('sqs') && + this.isAccountId(splitUrl[1]) && + this.isValidQueueName(splitUrl[2]) + ); + } + private static isAccountId(input: string): boolean { if (input == null || input.length !== 12) { return false; 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..eab1190b 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 @@ -61,6 +61,8 @@ const UNKNOWN_REMOTE_SERVICE: string = 'UnknownRemoteService'; const UNKNOWN_REMOTE_OPERATION: string = 'UnknownRemoteOperation'; const INTERNAL_OPERATION: string = 'InternalOperation'; const LOCAL_ROOT: string = 'LOCAL_ROOT'; +const AWS_REMOTE_RESOURCE_REGION: string = 'us-east-1'; +const AWS_REMOTE_RESOURCE_ACCESS_KEY: string = 'AWS access key'; const GENERATOR: AwsMetricAttributeGenerator = new AwsMetricAttributeGenerator(); @@ -731,39 +733,73 @@ describe('AwsMetricAttributeGeneratorTest', () => { it('testSdkClientSpanWithRemoteResourceAttributes', () => { mockAttribute(SEMATTRS_RPC_SYSTEM, 'aws-api'); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_AUTH_ACCESS_KEY, AWS_REMOTE_RESOURCE_ACCESS_KEY); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_AUTH_REGION, AWS_REMOTE_RESOURCE_REGION); // Validate behaviour of aws bucket name attribute, then remove it. mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_S3_BUCKET, 'aws_s3_bucket_name'); - validateRemoteResourceAttributes('AWS::S3::Bucket', 'aws_s3_bucket_name'); + validateRemoteResourceAttributes( + 'AWS::S3::Bucket', + 'aws_s3_bucket_name', + AWS_REMOTE_RESOURCE_REGION, + undefined, + AWS_REMOTE_RESOURCE_ACCESS_KEY + ); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_S3_BUCKET, undefined); // Validate behaviour of AWS_SQS_QUEUE_NAME attribute, then remove it. mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_SQS_QUEUE_NAME, 'aws_queue_name'); - validateRemoteResourceAttributes('AWS::SQS::Queue', 'aws_queue_name'); + validateRemoteResourceAttributes( + 'AWS::SQS::Queue', + 'aws_queue_name', + AWS_REMOTE_RESOURCE_REGION, + undefined, + AWS_REMOTE_RESOURCE_ACCESS_KEY + ); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_SQS_QUEUE_NAME, undefined); // Validate behaviour of having both AWS_SQS_QUEUE_NAME and AWS_SQS_QUEUE_URL attribute, then remove // them. Queue name is more reliable than queue URL, so we prefer to use name over URL. mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_SQS_QUEUE_URL, 'https://sqs.us-east-2.amazonaws.com/123456789012/Queue'); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_SQS_QUEUE_NAME, 'aws_queue_name'); - validateRemoteResourceAttributes('AWS::SQS::Queue', 'aws_queue_name'); + validateRemoteResourceAttributes('AWS::SQS::Queue', 'aws_queue_name', 'us-east-2', '123456789012', undefined); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_SQS_QUEUE_URL, undefined); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_SQS_QUEUE_NAME, undefined); // Valid queue name with invalid queue URL, we should default to using the queue name. mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_SQS_QUEUE_URL, 'invalidUrl'); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_SQS_QUEUE_NAME, 'aws_queue_name'); - validateRemoteResourceAttributes('AWS::SQS::Queue', 'aws_queue_name'); + validateRemoteResourceAttributes( + 'AWS::SQS::Queue', + 'aws_queue_name', + AWS_REMOTE_RESOURCE_REGION, + undefined, + AWS_REMOTE_RESOURCE_ACCESS_KEY + ); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_SQS_QUEUE_URL, undefined); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_SQS_QUEUE_NAME, 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'); + validateRemoteResourceAttributes( + 'AWS::Kinesis::Stream', + 'AWS_KINESIS_STREAM_NAME', + AWS_REMOTE_RESOURCE_REGION, + undefined, + AWS_REMOTE_RESOURCE_ACCESS_KEY + ); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_KINESIS_STREAM_NAME, undefined); + // Validate behaviour of AWS_DYNAMODB_STREAM_ARN attribute, then remove it. + mockAttribute( + AWS_ATTRIBUTE_KEYS.AWS_KINESIS_STREAM_ARN, + 'arn:aws:kinesis:us-west-2:123456789012:stream/aws_stream_name' + ); + validateRemoteResourceAttributes('AWS::Kinesis::Stream', 'aws_stream_name', 'us-west-2', '123456789012', undefined); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_KINESIS_STREAM_ARN, undefined); + // 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', 'us-east-1', '123456789012', undefined); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_SNS_TOPIC_ARN, undefined); // Validate behaviour of AWS_SECRETSMANAGER_SECRET_ARN attributes then remove it. @@ -771,7 +807,13 @@ 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', + 'us-east-1', + '123456789123', + undefined + ); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_SECRETSMANAGER_SECRET_ARN, undefined); // Validate behaviour of AWS_LAMBDA_FUNCTION_NAME and AWS_LAMBDA_FUNCTION_ARN @@ -780,13 +822,25 @@ 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', + 'us-east-1', + '123456789012', + undefined + ); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_NAME, undefined); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_ARN, undefined); // Validate behaviour of AWS_LAMBDA_RESOURCE_MAPPING_ID attribute then remove it. mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_RESOURCE_MAPPING_ID, 'aws_lambda_resource_mapping_id'); - validateRemoteResourceAttributes('AWS::Lambda::EventSourceMapping', 'aws_lambda_resource_mapping_id'); + validateRemoteResourceAttributes( + 'AWS::Lambda::EventSourceMapping', + 'aws_lambda_resource_mapping_id', + AWS_REMOTE_RESOURCE_REGION, + undefined, + AWS_REMOTE_RESOURCE_ACCESS_KEY + ); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_RESOURCE_MAPPING_ID, undefined); // Validate behaviour of AWS_STEPFUNCTIONS_STATEMACHINE_ARN and AWS_STEPFUNCTIONS_ACTIVITY_ARN attributes then remove them. @@ -794,19 +848,37 @@ 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', + 'us-east-1', + '123456789123', + undefined + ); 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', + 'us-east-1', + '123456789123', + undefined + ); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_STEPFUNCTIONS_ACTIVITY_ARN, undefined); // Validate behaviour of AWS_TABLE_NAMES attribute with one table name, then remove it. mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_DYNAMODB_TABLE_NAMES, ['aws_table_name']); - validateRemoteResourceAttributes('AWS::DynamoDB::Table', 'aws_table_name'); + validateRemoteResourceAttributes( + 'AWS::DynamoDB::Table', + 'aws_table_name', + AWS_REMOTE_RESOURCE_REGION, + undefined, + AWS_REMOTE_RESOURCE_ACCESS_KEY + ); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_DYNAMODB_TABLE_NAMES, undefined); // Validate behaviour of AWS_TABLE_NAMES attribute with no table name, then remove it. @@ -821,63 +893,201 @@ describe('AwsMetricAttributeGeneratorTest', () => { // Validate behaviour of AWS_TABLE_NAMES attribute with special chars(|), then remove it. mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_DYNAMODB_TABLE_NAMES, ['aws_table|name']); - validateRemoteResourceAttributes('AWS::DynamoDB::Table', 'aws_table^|name'); + validateRemoteResourceAttributes( + 'AWS::DynamoDB::Table', + 'aws_table^|name', + AWS_REMOTE_RESOURCE_REGION, + undefined, + AWS_REMOTE_RESOURCE_ACCESS_KEY + ); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_DYNAMODB_TABLE_NAMES, undefined); // Validate behaviour of AWS_TABLE_NAMES attribute with special chars(^), then remove it. mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_DYNAMODB_TABLE_NAMES, ['aws_table^name']); - validateRemoteResourceAttributes('AWS::DynamoDB::Table', 'aws_table^^name'); + validateRemoteResourceAttributes( + 'AWS::DynamoDB::Table', + 'aws_table^^name', + AWS_REMOTE_RESOURCE_REGION, + undefined, + AWS_REMOTE_RESOURCE_ACCESS_KEY + ); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_DYNAMODB_TABLE_NAMES, undefined); + mockAttribute( + AWS_ATTRIBUTE_KEYS.AWS_DYNAMODB_TABLE_ARN, + 'arn:aws:dynamodb:us-west-2:123456789012:table/aws_table_name' + ); + validateRemoteResourceAttributes('AWS::DynamoDB::Table', 'aws_table_name', 'us-west-2', '123456789012', undefined); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_DYNAMODB_TABLE_ARN, undefined); + // Validate behaviour of AWS_BEDROCK_AGENT_ID attribute, then remove it. mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_AGENT_ID, 'test_agent_id'); - validateRemoteResourceAttributes('AWS::Bedrock::Agent', 'test_agent_id'); + validateRemoteResourceAttributes( + 'AWS::Bedrock::Agent', + 'test_agent_id', + AWS_REMOTE_RESOURCE_REGION, + undefined, + AWS_REMOTE_RESOURCE_ACCESS_KEY + ); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_DYNAMODB_TABLE_NAMES, undefined); // Validate behaviour of AWS_BEDROCK_AGENT_ID attribute with special chars(^), then remove it. mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_AGENT_ID, 'test_agent_^id'); - validateRemoteResourceAttributes('AWS::Bedrock::Agent', 'test_agent_^^id'); + validateRemoteResourceAttributes( + 'AWS::Bedrock::Agent', + 'test_agent_^^id', + AWS_REMOTE_RESOURCE_REGION, + undefined, + AWS_REMOTE_RESOURCE_ACCESS_KEY + ); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_AGENT_ID, undefined); // 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'); + validateRemoteResourceAttributes( + 'AWS::Bedrock::DataSource', + 'test_datasource_id', + AWS_REMOTE_RESOURCE_REGION, + undefined, + AWS_REMOTE_RESOURCE_ACCESS_KEY + ); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_DATA_SOURCE_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'); + validateRemoteResourceAttributes( + 'AWS::Bedrock::DataSource', + 'test_datasource_^^id', + AWS_REMOTE_RESOURCE_REGION, + undefined, + AWS_REMOTE_RESOURCE_ACCESS_KEY + ); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_DATA_SOURCE_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-west-2:123456789012:guardrail/aws_guardrail_arn' + ); + validateRemoteResourceAttributes( + 'AWS::Bedrock::Guardrail', + 'test_guardrail_id', + 'us-west-2', + '123456789012', + undefined + ); 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'); + validateRemoteResourceAttributes( + 'AWS::Bedrock::Guardrail', + 'test_guardrail_^^id', + AWS_REMOTE_RESOURCE_REGION, + undefined, + AWS_REMOTE_RESOURCE_ACCESS_KEY + ); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_GUARDRAIL_ID, 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'); - validateRemoteResourceAttributes('AWS::Bedrock::KnowledgeBase', 'test_knowledgeBase_id'); + validateRemoteResourceAttributes( + 'AWS::Bedrock::KnowledgeBase', + 'test_knowledgeBase_id', + AWS_REMOTE_RESOURCE_REGION, + undefined, + AWS_REMOTE_RESOURCE_ACCESS_KEY + ); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_KNOWLEDGE_BASE_ID, undefined); // Validate behaviour of AWS_BEDROCK_KNOWLEDGE_BASE_ID attribute with special chars(^), then remove it. mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_KNOWLEDGE_BASE_ID, 'test_knowledgeBase_^id'); - validateRemoteResourceAttributes('AWS::Bedrock::KnowledgeBase', 'test_knowledgeBase_^^id'); + validateRemoteResourceAttributes( + 'AWS::Bedrock::KnowledgeBase', + 'test_knowledgeBase_^^id', + AWS_REMOTE_RESOURCE_REGION, + undefined, + AWS_REMOTE_RESOURCE_ACCESS_KEY + ); mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_KNOWLEDGE_BASE_ID, undefined); // Validate behaviour of GEN_AI_REQUEST_MODEL attribute, then remove it. mockAttribute(AwsSpanProcessingUtil.GEN_AI_REQUEST_MODEL, 'test.service_id'); - validateRemoteResourceAttributes('AWS::Bedrock::Model', 'test.service_id'); + validateRemoteResourceAttributes( + 'AWS::Bedrock::Model', + 'test.service_id', + AWS_REMOTE_RESOURCE_REGION, + undefined, + AWS_REMOTE_RESOURCE_ACCESS_KEY + ); mockAttribute(AwsSpanProcessingUtil.GEN_AI_REQUEST_MODEL, undefined); // Validate behaviour of GEN_AI_REQUEST_MODEL attribute with special chars(^), then remove it. mockAttribute(AwsSpanProcessingUtil.GEN_AI_REQUEST_MODEL, 'test.service_^id'); - validateRemoteResourceAttributes('AWS::Bedrock::Model', 'test.service_^^id'); + validateRemoteResourceAttributes( + 'AWS::Bedrock::Model', + 'test.service_^^id', + AWS_REMOTE_RESOURCE_REGION, + undefined, + AWS_REMOTE_RESOURCE_ACCESS_KEY + ); mockAttribute(AwsSpanProcessingUtil.GEN_AI_REQUEST_MODEL, undefined); + + // Cross account support + // Invalid arn but account access key is available + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_STEPFUNCTIONS_STATEMACHINE_ARN, 'invalid_arn'); + validateRemoteResourceAttributes(undefined, undefined); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_STEPFUNCTIONS_STATEMACHINE_ARN, undefined); + + // Invalid arn and no account access key + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_AUTH_ACCESS_KEY, undefined); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_AUTH_REGION, undefined); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_STEPFUNCTIONS_STATEMACHINE_ARN, 'invalid_arn'); + validateRemoteResourceAttributes(undefined, undefined); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_STEPFUNCTIONS_STATEMACHINE_ARN, undefined); + + // Both account access key and account id are not available + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_S3_BUCKET, 'aws_s3_bucket_name'); + validateRemoteResourceAttributes('AWS::S3::Bucket', 'aws_s3_bucket_name'); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_S3_BUCKET, undefined); + + // Account access key is not available + mockAttribute( + AWS_ATTRIBUTE_KEYS.AWS_STEPFUNCTIONS_STATEMACHINE_ARN, + 'arn:aws:states:us-east-1:123456789123:stateMachine:testStateMachine' + ); + validateRemoteResourceAttributes( + 'AWS::StepFunctions::StateMachine', + 'testStateMachine', + 'us-east-1', + '123456789123', + undefined + ); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_STEPFUNCTIONS_STATEMACHINE_ARN, undefined); + + // Arn with invalid account id + mockAttribute( + AWS_ATTRIBUTE_KEYS.AWS_STEPFUNCTIONS_STATEMACHINE_ARN, + 'arn:aws:states:us-east-1:invalid_account_id:stateMachine:testStateMachine' + ); + validateRemoteResourceAttributes('AWS::StepFunctions::StateMachine', 'testStateMachine'); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_STEPFUNCTIONS_STATEMACHINE_ARN, undefined); + + // Arn with invalid region + mockAttribute( + AWS_ATTRIBUTE_KEYS.AWS_STEPFUNCTIONS_STATEMACHINE_ARN, + 'arn:aws:states:invalid_region:123456789123:stateMachine:testStateMachine' + ); + validateRemoteResourceAttributes( + 'AWS::StepFunctions::StateMachine', + 'testStateMachine', + 'invalid_region', + '123456789123', + undefined + ); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_STEPFUNCTIONS_STATEMACHINE_ARN, undefined); }); it('testDBClientSpanWithRemoteResourceAttributes', () => { @@ -1114,30 +1324,54 @@ describe('AwsMetricAttributeGeneratorTest', () => { mockAttribute(SEMATTRS_PEER_SERVICE, undefined); } - function validateRemoteResourceAttributes(type: string | undefined, identifier: string | undefined): void { + function validateRemoteResourceAttributes( + type: string | undefined, + identifier: string | undefined, + region: string | undefined = undefined, + accountId: string | undefined = undefined, + accessKey: string | undefined = undefined + ): void { // 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)[ - DEPENDENCY_METRIC - ]; - expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_TYPE]).toEqual(type); - expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_IDENTIFIER]).toEqual(identifier); + const spanKinds = [SpanKind.CLIENT, SpanKind.PRODUCER, SpanKind.CONSUMER]; + + for (const kind of spanKinds) { + (spanDataMock as any).kind = kind; + const actualAttributes: Attributes = 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); + + // Cross account support + if (region !== undefined) { + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_REGION]).toEqual(region); + } else { + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_REGION]).toEqual(undefined); + } - (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); + if (accountId !== undefined) { + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_ACCOUNT_ID]).toEqual(accountId); + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_ACCESS_KEY]).toEqual(undefined); + } else { + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_ACCOUNT_ID]).toEqual(undefined); + } - (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); + if (accessKey !== undefined) { + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_ACCESS_KEY]).toEqual(accessKey); + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_ACCOUNT_ID]).toEqual(undefined); + } else { + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_ACCESS_KEY]).toEqual(undefined); + } + } // Server span should not generate remote resource attributes (spanDataMock as any).kind = SpanKind.SERVER; - actualAttributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[SERVICE_METRIC]; + const 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_REMOTE_RESOURCE_REGION]).toEqual(undefined); + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_ACCOUNT_ID]).toEqual(undefined); + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_ACCESS_KEY]).toEqual(undefined); } it('testDBUserAttribute', () => { diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/test/patches/instrumentation-patch.test.ts b/aws-distro-opentelemetry-node-autoinstrumentation/test/patches/instrumentation-patch.test.ts index 502ba84c..fc2e5ab0 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/test/patches/instrumentation-patch.test.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/patches/instrumentation-patch.test.ts @@ -57,6 +57,8 @@ const _BEDROCK_GUARDRAIL_ARN: string = 'arn:aws:bedrock:us-east-1:123456789012:g const _BEDROCK_KNOWLEDGEBASE_ID: string = 'KnowledgeBaseId'; const _GEN_AI_SYSTEM: string = 'aws.bedrock'; const _GEN_AI_REQUEST_MODEL: string = 'genAiReuqestModelId'; +const _STREAM_ARN: string = 'arn:aws:kinesis:us-west-2:123456789012:stream/testStream'; +const _TABLE_ARN: string = 'arn:aws:dynamodb:us-west-2:123456789012:table/testTable'; const mockHeaders = { 'x-test-header': 'test-value', @@ -89,6 +91,8 @@ describe('InstrumentationPatchTest', () => { expect(services.get('Lambda').requestPreSpanHook).toBeTruthy(); expect(services.get('SQS')._requestPreSpanHook).toBeFalsy(); expect(services.get('SQS').requestPreSpanHook).toBeTruthy(); + expect(services.get('Kinesis')._requestPreSpanHook).toBeFalsy(); + expect(services.get('Kinesis').requestPreSpanHook).toBeTruthy(); expect(services.has('Bedrock')).toBeFalsy(); expect(services.has('BedrockAgent')).toBeFalsy(); expect(services.get('BedrockAgentRuntime')).toBeFalsy(); @@ -118,6 +122,8 @@ describe('InstrumentationPatchTest', () => { expect(services.get('Lambda').requestPreSpanHook).toBeTruthy(); expect(services.get('SQS')._requestPreSpanHook).toBeTruthy(); expect(services.get('SQS').requestPreSpanHook).toBeTruthy(); + expect(services.get('Kinesis')._requestPreSpanHook).toBeTruthy(); + expect(services.get('Kinesis').requestPreSpanHook).toBeTruthy(); expect(services.has('Bedrock')).toBeTruthy(); expect(services.has('BedrockAgent')).toBeTruthy(); expect(services.get('BedrockAgentRuntime')).toBeTruthy(); @@ -178,6 +184,14 @@ describe('InstrumentationPatchTest', () => { expect(() => doExtractBedrockAttributes(services, 'Bedrock')).toThrow(); }); + it('Kinesis without patching', () => { + const unpatchedAwsSdkInstrumentation: AwsInstrumentation = extractAwsSdkInstrumentation(UNPATCHED_INSTRUMENTATIONS); + const services: Map = extractServicesFromAwsSdkInstrumentation(unpatchedAwsSdkInstrumentation); + expect(() => doExtractKinesisAttributes(services)).not.toThrow(); + const kinesisAttributes: Attributes = doExtractKinesisAttributes(services); + expect(kinesisAttributes[AWS_ATTRIBUTE_KEYS.AWS_KINESIS_STREAM_ARN]).toBeUndefined(); + }); + it('SNS with patching', () => { const patchedAwsSdkInstrumentation: AwsInstrumentation = extractAwsSdkInstrumentation(PATCHED_INSTRUMENTATIONS); const services: Map = extractServicesFromAwsSdkInstrumentation(patchedAwsSdkInstrumentation); @@ -232,6 +246,20 @@ describe('InstrumentationPatchTest', () => { expect(responseHookSecretsManagerAttributes[AWS_ATTRIBUTE_KEYS.AWS_SECRETSMANAGER_SECRET_ARN]).toBe(_SECRETS_ARN); }); + it('Kinesis with patching', () => { + const patchedAwsSdkInstrumentation: AwsInstrumentation = extractAwsSdkInstrumentation(PATCHED_INSTRUMENTATIONS); + const services: Map = extractServicesFromAwsSdkInstrumentation(patchedAwsSdkInstrumentation); + const requestKinesisAttributes: Attributes = doExtractKinesisAttributes(services); + expect(requestKinesisAttributes[AWS_ATTRIBUTE_KEYS.AWS_KINESIS_STREAM_ARN]).toEqual(_STREAM_ARN); + }); + + it('DynamoDB with patching', () => { + const patchedAwsSdkInstrumentation: AwsInstrumentation = extractAwsSdkInstrumentation(PATCHED_INSTRUMENTATIONS); + const services: Map = extractServicesFromAwsSdkInstrumentation(patchedAwsSdkInstrumentation); + const responseDynamoDbAttributes: Attributes = doResponseHookDynamoDb(services); + expect(responseDynamoDbAttributes[AWS_ATTRIBUTE_KEYS.AWS_DYNAMODB_TABLE_ARN]).toEqual(_TABLE_ARN); + }); + it('Bedrock with patching', () => { const patchedAwsSdkInstrumentation: AwsInstrumentation = extractAwsSdkInstrumentation(PATCHED_INSTRUMENTATIONS); const services: Map = extractServicesFromAwsSdkInstrumentation(patchedAwsSdkInstrumentation); @@ -437,6 +465,18 @@ describe('InstrumentationPatchTest', () => { return doExtractAttributes(services, serviceName, params); } + function doExtractKinesisAttributes(services: Map): Attributes { + const serviceName: string = 'Kinesis'; + const params: NormalizedRequest = { + serviceName: serviceName, + commandName: 'mockCommandName', + commandInput: { + StreamARN: _STREAM_ARN, + }, + }; + return doExtractAttributes(services, serviceName, params); + } + function doExtractAttributes( services: Map, serviceName: string, @@ -483,6 +523,23 @@ describe('InstrumentationPatchTest', () => { return doResponseHook(services, 'Lambda', results as NormalizedResponse); } + function doResponseHookDynamoDb(services: Map): Attributes { + const results: Partial = { + data: { + Table: { + TableArn: _TABLE_ARN, + }, + }, + request: { + commandInput: {}, + commandName: 'dummy_operation', + serviceName: 'DynamoDB', + }, + }; + + return doResponseHook(services, 'DynamoDB', results as NormalizedResponse); + } + function doResponseHookBedrock( services: Map, serviceName: string, @@ -554,9 +611,13 @@ describe('InstrumentationPatchTest', () => { mockedMiddlewareStack = { add: (arg1: any, arg2: any) => mockedMiddlewareStackInternal.push([arg1, arg2]), }; + const mockConfig = { + credentials: () => Promise.resolve({ accessKeyId: 'test-access-key' }), + region: () => Promise.resolve('us-west-2'), + }; const send = extractAwsSdkInstrumentation(PATCHED_INSTRUMENTATIONS) ['_getV3SmithyClientSendPatch']((...args: unknown[]) => Promise.resolve()) - .bind({ middlewareStack: mockedMiddlewareStack }); + .bind({ middlewareStack: mockedMiddlewareStack, config: mockConfig }); middlewareArgsHeader = { request: { @@ -580,14 +641,14 @@ describe('InstrumentationPatchTest', () => { (carrier as any)['isCarrierModified'] = 'carrierIsModified'; (carrier as any)[AWSXRAY_TRACE_ID_HEADER] = testXrayTraceHeader; }); - await mockedMiddlewareStackInternal[0][0]((arg: any) => Promise.resolve(), null)(middlewareArgsHeader); + await mockedMiddlewareStackInternal[1][0]((arg: any) => Promise.resolve(), null)(middlewareArgsHeader); expect(middlewareArgsHeader.request.headers['isCarrierModified']).toEqual('carrierIsModified'); expect(middlewareArgsHeader.request.headers).not.toHaveProperty(AWSXRAY_TRACE_ID_HEADER); expect(middlewareArgsHeader.request.headers).toHaveProperty(AWSXRAY_TRACE_ID_HEADER_CAPITALIZED); expect(middlewareArgsHeader.request.headers[AWSXRAY_TRACE_ID_HEADER_CAPITALIZED]).toEqual(testXrayTraceHeader); - expect(mockedMiddlewareStackInternal[0][1].name).toEqual('_adotInjectXrayContextMiddleware'); + expect(mockedMiddlewareStackInternal[1][1].name).toEqual('_adotInjectXrayContextMiddleware'); }); it('Does not set trace header when AWSXRayPropagator does not inject trace header', async () => { @@ -601,12 +662,24 @@ describe('InstrumentationPatchTest', () => { sinon.stub(context, 'active').returns(invalidContext); - await mockedMiddlewareStackInternal[0][0]((arg: any) => Promise.resolve(), null)(middlewareArgsHeader); + await mockedMiddlewareStackInternal[1][0]((arg: any) => Promise.resolve(), null)(middlewareArgsHeader); expect(middlewareArgsHeader.request.headers).not.toHaveProperty(AWSXRAY_TRACE_ID_HEADER); expect(middlewareArgsHeader.request.headers).not.toHaveProperty(AWSXRAY_TRACE_ID_HEADER_CAPITALIZED); - expect(mockedMiddlewareStackInternal[0][1].name).toEqual('_adotInjectXrayContextMiddleware'); + expect(mockedMiddlewareStackInternal[1][1].name).toEqual('_adotInjectXrayContextMiddleware'); + }); + + it('Add cross account information span attributes from STS credentials', async () => { + const mockSpan = { setAttribute: sinon.stub() }; + sinon.stub(trace, 'getSpan').returns(mockSpan as unknown as Span); + const credentialsMiddlewareArgs: any = {}; + await mockedMiddlewareStackInternal[0][0]((arg: any) => Promise.resolve(arg), null)(credentialsMiddlewareArgs); + expect(mockedMiddlewareStackInternal[0][1].name).toEqual('_extractSignerCredentials'); + expect( + mockSpan.setAttribute.calledWith(AWS_ATTRIBUTE_KEYS.AWS_AUTH_ACCESS_KEY, 'test-access-key') + ).toBeTruthy(); + expect(mockSpan.setAttribute.calledWith(AWS_ATTRIBUTE_KEYS.AWS_AUTH_REGION, 'us-west-2')).toBeTruthy(); }); }); diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/test/regional-resource-arn-parser.test.ts b/aws-distro-opentelemetry-node-autoinstrumentation/test/regional-resource-arn-parser.test.ts new file mode 100644 index 00000000..fc0bd6b5 --- /dev/null +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/regional-resource-arn-parser.test.ts @@ -0,0 +1,43 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { expect } from 'expect'; +import { RegionalResourceArnParser } from '../src/regional-resource-arn-parser'; + +describe('RegionalResourceArnParserTest', () => { + it('testGetAccountId', () => { + validateAccountId(undefined, undefined); + validateAccountId('', undefined); + validateAccountId(' ', undefined); + validateAccountId(':', undefined); + validateAccountId('::::::', undefined); + validateAccountId('not:an:arn:string', undefined); + validateAccountId('arn:aws:ec2:us-west-2:123456', undefined); + validateAccountId('arn:aws:ec2:us-west-2:1234567xxxxx', undefined); + validateAccountId('arn:aws:ec2:us-west-2:123456789012', undefined); + validateAccountId('arn:aws:dynamodb:us-west-2:123456789012:table/test_table', '123456789012'); + validateAccountId('arn:aws:acm:us-east-1:123456789012:certificate:abc-123', '123456789012'); + }); + + it('testGetRegion', () => { + validateRegion(undefined, undefined); + validateRegion('', undefined); + validateRegion(' ', undefined); + validateRegion(':', undefined); + validateRegion('::::::', undefined); + validateRegion('not:an:arn:string', undefined); + validateRegion('arn:aws:ec2:us-west-2:123456', undefined); + validateRegion('arn:aws:ec2:us-west-2:1234567xxxxx', undefined); + validateRegion('arn:aws:ec2:us-west-2:123456789012', undefined); + validateRegion('arn:aws:dynamodb:us-west-2:123456789012:table/test_table', 'us-west-2'); + validateRegion('arn:aws:acm:us-east-1:123456789012:certificate:abc-123', 'us-east-1'); + }); +}); + +function validateAccountId(arn: string | undefined, expectedAccountId: string | undefined): void { + expect(RegionalResourceArnParser.getAccountId(arn)).toEqual(expectedAccountId); +} + +function validateRegion(arn: string | undefined, expectedName: string | undefined): void { + expect(RegionalResourceArnParser.getRegion(arn)).toEqual(expectedName); +} diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/test/sqs-url-parser.test.ts b/aws-distro-opentelemetry-node-autoinstrumentation/test/sqs-url-parser.test.ts index 53f6e296..9a6b9223 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/test/sqs-url-parser.test.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/sqs-url-parser.test.ts @@ -6,53 +6,95 @@ import { SqsUrlParser } from '../src/sqs-url-parser'; describe('SqsUrlParserTest', () => { it('testSqsClientSpanBasicUrls', async () => { - validate('https://sqs.us-east-1.amazonaws.com/123412341234/Q_Name-5', 'Q_Name-5'); - validate('https://sqs.af-south-1.amazonaws.com/999999999999/-_ThisIsValid', '-_ThisIsValid'); - validate('http://sqs.eu-west-3.amazonaws.com/000000000000/FirstQueue', 'FirstQueue'); - validate('sqs.sa-east-1.amazonaws.com/123456781234/SecondQueue', 'SecondQueue'); + validateGetQueueName('https://sqs.us-east-1.amazonaws.com/123412341234/Q_Name-5', 'Q_Name-5'); + validateGetQueueName('https://sqs.af-south-1.amazonaws.com/999999999999/-_ThisIsValid', '-_ThisIsValid'); + validateGetQueueName('http://sqs.eu-west-3.amazonaws.com/000000000000/FirstQueue', 'FirstQueue'); + validateGetQueueName('sqs.sa-east-1.amazonaws.com/123456781234/SecondQueue', 'SecondQueue'); }); it('testSqsClientSpanLegacyFormatUrls', () => { - validate('https://ap-northeast-2.queue.amazonaws.com/123456789012/MyQueue', 'MyQueue'); - validate('http://cn-northwest-1.queue.amazonaws.com/123456789012/MyQueue', 'MyQueue'); - validate('http://cn-north-1.queue.amazonaws.com/123456789012/MyQueue', 'MyQueue'); - validate('ap-south-1.queue.amazonaws.com/123412341234/MyLongerQueueNameHere', 'MyLongerQueueNameHere'); - validate('https://queue.amazonaws.com/123456789012/MyQueue', 'MyQueue'); + validateGetQueueName('https://ap-northeast-2.queue.amazonaws.com/123456789012/MyQueue', 'MyQueue'); + validateGetQueueName('http://cn-northwest-1.queue.amazonaws.com/123456789012/MyQueue', 'MyQueue'); + validateGetQueueName('http://cn-north-1.queue.amazonaws.com/123456789012/MyQueue', 'MyQueue'); + validateGetQueueName('ap-south-1.queue.amazonaws.com/123412341234/MyLongerQueueNameHere', 'MyLongerQueueNameHere'); + validateGetQueueName('https://queue.amazonaws.com/123456789012/MyQueue', 'MyQueue'); }); it('testSqsClientSpanCustomUrls', () => { - validate('http://127.0.0.1:1212/123456789012/MyQueue', 'MyQueue'); - validate('https://127.0.0.1:1212/123412341234/RRR', 'RRR'); - validate('127.0.0.1:1212/123412341234/QQ', 'QQ'); - validate('https://amazon.com/123412341234/BB', 'BB'); + validateGetQueueName('http://127.0.0.1:1212/123456789012/MyQueue', 'MyQueue'); + validateGetQueueName('https://127.0.0.1:1212/123412341234/RRR', 'RRR'); + validateGetQueueName('127.0.0.1:1212/123412341234/QQ', 'QQ'); + validateGetQueueName('https://amazon.com/123412341234/BB', 'BB'); }); it('testSqsClientSpanLongUrls', () => { const queueName: string = 'a'.repeat(80); - validate('http://127.0.0.1:1212/123456789012/' + queueName, queueName); + validateGetQueueName('http://127.0.0.1:1212/123456789012/' + queueName, queueName); const queueNameTooLong: string = 'a'.repeat(81); - validate('http://127.0.0.1:1212/123456789012/' + queueNameTooLong, undefined); + validateGetQueueName('http://127.0.0.1:1212/123456789012/' + queueNameTooLong, undefined); }); it('testClientSpanSqsInvalidOrEmptyUrls', () => { - validate(undefined, undefined); - validate('', undefined); - validate(' ', undefined); - validate('/', undefined); - validate('//', undefined); - validate('///', undefined); - validate('//asdf', undefined); - validate('/123412341234/as&df', undefined); - validate('invalidUrl', undefined); - validate('https://www.amazon.com', undefined); - validate('https://sqs.us-east-1.amazonaws.com/123412341234/.', undefined); - validate('https://sqs.us-east-1.amazonaws.com/12/Queue', undefined); - validate('https://sqs.us-east-1.amazonaws.com/A/A', undefined); - validate('https://sqs.us-east-1.amazonaws.com/123412341234/A/ThisShouldNotBeHere', undefined); + validateGetQueueName(undefined, undefined); + validateGetQueueName('', undefined); + validateGetQueueName(' ', undefined); + validateGetQueueName('/', undefined); + validateGetQueueName('//', undefined); + validateGetQueueName('///', undefined); + validateGetQueueName('//asdf', undefined); + validateGetQueueName('/123412341234/as&df', undefined); + validateGetQueueName('invalidUrl', undefined); + validateGetQueueName('https://www.amazon.com', undefined); + validateGetQueueName('https://sqs.us-east-1.amazonaws.com/123412341234/.', undefined); + validateGetQueueName('https://sqs.us-east-1.amazonaws.com/12/Queue', undefined); + validateGetQueueName('https://sqs.us-east-1.amazonaws.com/A/A', undefined); + validateGetQueueName('https://sqs.us-east-1.amazonaws.com/123412341234/A/ThisShouldNotBeHere', undefined); + }); + + it('testGetAccountId', () => { + validateGetAccountId(undefined, undefined); + validateGetAccountId('', undefined); + validateGetAccountId(' ', undefined); + validateGetAccountId('/', undefined); + validateGetAccountId('//', undefined); + validateGetAccountId('///', undefined); + validateGetAccountId('//asdf', undefined); + validateGetAccountId('/123412341234/as&df', undefined); + validateGetAccountId('invalidUrl', undefined); + validateGetAccountId('https://www.amazon.com', undefined); + validateGetAccountId('https://sqs.us-east-1.amazonaws.com/12341234/Queue', undefined); + validateGetAccountId('https://sqs.us-east-1.amazonaws.com/1234123412xx/Queue', undefined); + validateGetAccountId('https://sqs.us-east-1.amazonaws.com/1234123412xx', undefined); + validateGetAccountId('https://sqs.us-east-1.amazonaws.com/123412341234/Q_Namez -5', '123412341234'); + }); + + it('testGetRegion', () => { + validateGetRegion(undefined, undefined); + validateGetRegion('', undefined); + validateGetRegion(' ', undefined); + validateGetRegion('/', undefined); + validateGetRegion('//', undefined); + validateGetRegion('///', undefined); + validateGetRegion('//asdf', undefined); + validateGetRegion('/123412341234/as&df', undefined); + validateGetRegion('invalidUrl', undefined); + validateGetRegion('https://www.amazon.com', undefined); + validateGetRegion('https://sqs.us-east-1.amazonaws.com/12341234/Queue', undefined); + validateGetRegion('https://sqs.us-east-1.amazonaws.com/1234123412xx/Queue', undefined); + validateGetRegion('https://sqs.us-east-1.amazonaws.com/1234123412xx', undefined); + validateGetRegion('https://sqs.us-east-1.amazonaws.com/123412341234/Q_Namez -5', 'us-east-1'); }); }); -function validate(url: string | undefined, expectedName: string | undefined): void { +function validateGetRegion(url: string | undefined, expectedRegion: string | undefined): void { + expect(SqsUrlParser.getRegion(url)).toEqual(expectedRegion); +} + +function validateGetAccountId(url: string | undefined, expectedAccountId: string | undefined): void { + expect(SqsUrlParser.getAccountId(url)).toEqual(expectedAccountId); +} + +function validateGetQueueName(url: string | undefined, expectedName: string | undefined): void { expect(SqsUrlParser.getQueueName(url)).toEqual(expectedName); } diff --git a/contract-tests/images/applications/aws-sdk/server.js b/contract-tests/images/applications/aws-sdk/server.js index 288bb967..7a73fffb 100644 --- a/contract-tests/images/applications/aws-sdk/server.js +++ b/contract-tests/images/applications/aws-sdk/server.js @@ -9,9 +9,9 @@ const fetch = require('node-fetch'); const JSZip = require('jszip'); const { S3Client, CreateBucketCommand, PutObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3'); -const { DynamoDBClient, CreateTableCommand, PutItemCommand } = require('@aws-sdk/client-dynamodb'); +const { DynamoDBClient, CreateTableCommand, PutItemCommand, DescribeTableCommand } = require('@aws-sdk/client-dynamodb'); const { SQSClient, CreateQueueCommand, SendMessageCommand, ReceiveMessageCommand } = require('@aws-sdk/client-sqs'); -const { KinesisClient, CreateStreamCommand, PutRecordCommand } = require('@aws-sdk/client-kinesis'); +const { KinesisClient, CreateStreamCommand, PutRecordCommand, DescribeStreamCommand } = require('@aws-sdk/client-kinesis'); const { BedrockClient, GetGuardrailCommand } = require('@aws-sdk/client-bedrock'); const { BedrockAgentClient, GetKnowledgeBaseCommand, GetDataSourceCommand, GetAgentCommand } = require('@aws-sdk/client-bedrock-agent'); const { BedrockRuntimeClient, InvokeModelCommand } = require('@aws-sdk/client-bedrock-runtime'); @@ -194,6 +194,8 @@ async function handleGetRequest(req, res, path) { await handleSnsRequest(req, res, path); } else if (path.includes('lambda')) { await handleLambdaRequest(req, res, path); + } else if (path.includes('cross-account')) { + await handleCrossAccountRequest(req, res, path); } else { res.writeHead(404); res.end(); @@ -383,6 +385,19 @@ async function handleDdbRequest(req, res, path) { res.statusCode = 500; } res.end(); + } else if (path.includes('describe/some-table')) { + try { + const response = await ddbClient.send( + new DescribeTableCommand({ + TableName: 'put_test_table' + }) + ); + res.statusCode = 200; + } catch (err) { + console.log('Error describing the table', err); + res.statusCode = 500; + } + res.end(); } else { res.statusCode = 404; res.end(); @@ -532,6 +547,29 @@ async function handleKinesisRequest(req, res, path) { res.statusCode = 500; } res.end(); + } else if (path.includes('describe/my-stream')) { + try { + await withInjected200Success( + kinesisClient, + ['DescribeStreamCommand'], + { StreamName: 'test_stream', + StreamARN: 'arn:aws:kinesis:us-west-2:000000000000:stream/test_stream' + }, + async () => { + await kinesisClient.send( + new DescribeStreamCommand ({ + StreamName: 'test_stream', + StreamARN: 'arn:aws:kinesis:us-west-2:000000000000:stream/test_stream' + }) + ); + } + ); + res.statusCode = 200; + } catch (err) { + console.log('Error describing the stream', err); + res.statusCode = 500; + } + res.end(); } else { res.statusCode = 404; res.end(); @@ -1041,6 +1079,35 @@ async function handleLambdaRequest(req, res, path) { res.end(); } +async function handleCrossAccountRequest(req, res, path) { + const s3Client = new S3Client({ + endpoint: _AWS_SDK_S3_ENDPOINT, + forcePathStyle: true, + region: "eu-central-1", + credentials: { + accessKeyId: "account_b_access_key_id", + secretAccessKey: "account_b_secret_access_key", + sessionToken: "account_b_token" + } + }); + + if (path.includes('createbucket/account_b')) { + try { + await s3Client.send( + new CreateBucketCommand({ + Bucket: "cross-account-bucket", + CreateBucketConfiguration: { LocationConstraint: "eu-central-1" } + }) + ); + res.statusCode = 200; + } catch (err) { + console.log('Error creating a bucket in another account', err); + res.statusCode = 500; + } + res.end(); + } +} + async function setupLambda() { const lambdaClient = new LambdaClient({ endpoint: _AWS_SDK_ENDPOINT, diff --git a/contract-tests/tests/test/amazon/aws-sdk/aws_sdk_test.py b/contract-tests/tests/test/amazon/aws-sdk/aws_sdk_test.py index 005e286b..2ba55419 100644 --- a/contract-tests/tests/test/amazon/aws-sdk/aws_sdk_test.py +++ b/contract-tests/tests/test/amazon/aws-sdk/aws_sdk_test.py @@ -19,7 +19,10 @@ AWS_REMOTE_RESOURCE_TYPE, AWS_REMOTE_SERVICE, AWS_SPAN_KIND, - AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER + AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER, + AWS_REMOTE_RESOURCE_ACCESS_KEY, + AWS_REMOTE_RESOURCE_ACCOUNT_ID, + AWS_REMOTE_RESOURCE_REGION ) from opentelemetry.proto.common.v1.common_pb2 import AnyValue, KeyValue from opentelemetry.proto.metrics.v1.metrics_pb2 import ExponentialHistogramDataPoint, Metric @@ -31,6 +34,7 @@ _AWS_SQS_QUEUE_URL: str = "aws.sqs.queue.url" _AWS_SQS_QUEUE_NAME: str = "aws.sqs.queue.name" +_AWS_KINESIS_STREAM_ARN: str = "aws.kinesis.stream.arn" _AWS_KINESIS_STREAM_NAME: str = "aws.kinesis.stream.name" _AWS_BEDROCK_AGENT_ID: str = "aws.bedrock.agent.id" _AWS_BEDROCK_GUARDRAIL_ID: str = "aws.bedrock.guardrail.id" @@ -49,6 +53,7 @@ _GEN_AI_RESPONSE_FINISH_REASONS: str = "gen_ai.response.finish_reasons" _GEN_AI_USAGE_INPUT_TOKENS: str = 'gen_ai.usage.input_tokens' _GEN_AI_USAGE_OUTPUT_TOKENS: str = 'gen_ai.usage.output_tokens' +_AWS_DYNAMODB_TABLE_ARN: str = "aws.dynamodb.table.arn" # pylint: disable=too-many-public-methods class AWSSDKTest(ContractTestBase): @@ -236,6 +241,30 @@ def test_dynamodb_put_item(self): span_name="DynamoDB.PutItem", ) + def test_dynamodb_describe_table(self): + self.do_test_requests( + "ddb/describe/some-table", + "GET", + 200, + 0, + 0, + local_operation="GET /ddb", + remote_service="AWS::DynamoDB", + remote_operation="DescribeTable", + remote_resource_type="AWS::DynamoDB::Table", + remote_resource_identifier="put_test_table", + cloudformation_primary_identifier="put_test_table", + remote_resource_account_id="000000000000", + remote_resource_region="us-west-2", + request_specific_attributes={ + SpanAttributes.AWS_DYNAMODB_TABLE_NAMES: ["put_test_table"], + }, + response_specific_attributes={ + _AWS_DYNAMODB_TABLE_ARN: r"arn:aws:dynamodb:us-west-2:000000000000:table/put_test_table", + }, + span_name="DynamoDB.DescribeTable", + ) + def test_dynamodb_error(self): self.do_test_requests( "ddb/error", @@ -397,6 +426,28 @@ def test_kinesis_put_record(self): }, span_name="Kinesis.PutRecord", ) + + def test_kinesis_describe_stream(self): + self.do_test_requests( + "kinesis/describe/my-stream", + "GET", + 200, + 0, + 0, + local_operation="GET /kinesis", + remote_service="AWS::Kinesis", + remote_operation="DescribeStream", + remote_resource_type="AWS::Kinesis::Stream", + remote_resource_identifier="test_stream", + cloudformation_primary_identifier="test_stream", + remote_resource_account_id="000000000000", + remote_resource_region="us-west-2", + request_specific_attributes={ + _AWS_KINESIS_STREAM_NAME: "test_stream", + _AWS_KINESIS_STREAM_ARN: "arn:aws:kinesis:us-west-2:000000000000:stream/test_stream" + }, + span_name="Kinesis.DescribeStream", + ) def test_kinesis_error(self): self.do_test_requests( @@ -1034,6 +1085,27 @@ def test_lambda_error(self): span_name="Lambda.GetEventSourceMapping", ) + def test_cross_account(self): + self.do_test_requests( + "cross-account/createbucket/account_b", + "GET", + 200, + 0, + 0, + local_operation="GET /cross-account", + remote_service="AWS::S3", + remote_operation="CreateBucket", + remote_resource_type="AWS::S3::Bucket", + remote_resource_identifier="cross-account-bucket", + cloudformation_primary_identifier="cross-account-bucket", + request_specific_attributes={ + SpanAttributes.AWS_S3_BUCKET: "cross-account-bucket", + }, + remote_resource_access_key="account_b_access_key_id", + remote_resource_region="eu-central-1", + span_name="S3.CreateBucket", + ) + #TODO: Need to add test_lambda_get_event_source_mapping once workaround is figured out for storing UUID between tests @override @@ -1062,6 +1134,9 @@ def _assert_aws_span_attributes(self, resource_scope_spans: List[ResourceScopeSp kwargs.get("remote_resource_type", "None"), kwargs.get("remote_resource_identifier", "None"), kwargs.get("cloudformation_primary_identifier", "None"), + kwargs.get("remote_resource_account_id", "None"), + kwargs.get("remote_resource_region", "None"), + kwargs.get("remote_resource_account_access_key", "None"), ) def _assert_aws_attributes( @@ -1073,7 +1148,10 @@ def _assert_aws_attributes( span_kind: str, remote_resource_type: str, remote_resource_identifier: str, - cloudformation_primary_identifier: str + cloudformation_primary_identifier: str, + remote_resource_account_id: str, + remote_resource_region: str, + remote_resource_account_access_key: str ) -> None: attributes_dict: Dict[str, AnyValue] = self._get_attributes_dict(attributes_list) self._assert_str_attribute(attributes_dict, AWS_LOCAL_SERVICE, self.get_application_otel_service_name()) @@ -1086,7 +1164,17 @@ def _assert_aws_attributes( self._assert_attribute(attributes_dict, AWS_REMOTE_RESOURCE_IDENTIFIER, remote_resource_identifier) if cloudformation_primary_identifier != "None": self._assert_attribute(attributes_dict, AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER, cloudformation_primary_identifier) - + if remote_resource_account_id != "None": + assert remote_resource_identifier != "None" + self._assert_str_attribute(attributes_dict, AWS_REMOTE_RESOURCE_ACCOUNT_ID, remote_resource_account_id) + self._assert_str_attribute(attributes_dict, AWS_REMOTE_RESOURCE_REGION, remote_resource_region) + self.assertIsNone(attributes_dict.get(AWS_REMOTE_RESOURCE_ACCESS_KEY)) + if remote_resource_account_access_key != "None": + assert remote_resource_identifier != "None" + self._assert_str_attribute(attributes_dict, AWS_REMOTE_RESOURCE_ACCESS_KEY, remote_resource_account_access_key) + self._assert_str_attribute(attributes_dict, AWS_REMOTE_RESOURCE_REGION, remote_resource_region) + self.assertIsNone(attributes_dict.get(AWS_REMOTE_RESOURCE_ACCOUNT_ID)) + self._assert_str_attribute(attributes_dict, AWS_SPAN_KIND, span_kind) @override @@ -1187,10 +1275,23 @@ def _assert_metric_attributes( self._assert_attribute(attribute_dict, AWS_SPAN_KIND, kwargs.get("dependency_metric_span_kind") or "CLIENT") remote_resource_type = kwargs.get("remote_resource_type", "None") remote_resource_identifier = kwargs.get("remote_resource_identifier", "None") + remote_resource_account_id = kwargs.get("remote_resource_account_id", "None") + remote_resource_account_access_key = kwargs.get("remote_resource_account_access_key", "None") + remote_resource_region = kwargs.get("remote_resource_region", "None") if remote_resource_type != "None": self._assert_attribute(attribute_dict, AWS_REMOTE_RESOURCE_TYPE, remote_resource_type) if remote_resource_identifier != "None": self._assert_attribute(attribute_dict, AWS_REMOTE_RESOURCE_IDENTIFIER, remote_resource_identifier) + if remote_resource_account_id != "None": + assert remote_resource_identifier != "None" + self._assert_str_attribute(attribute_dict, AWS_REMOTE_RESOURCE_ACCOUNT_ID, remote_resource_account_id) + self.assertIsNone(attribute_dict.get(AWS_REMOTE_RESOURCE_ACCESS_KEY)) + if remote_resource_account_access_key != "None": + assert remote_resource_identifier != "None" + self._assert_str_attribute(attribute_dict, AWS_REMOTE_RESOURCE_ACCESS_KEY, remote_resource_account_access_key) + self.assertIsNone(attribute_dict.get(AWS_REMOTE_RESOURCE_ACCOUNT_ID)) + if remote_resource_region != "None": + self._assert_str_attribute(attribute_dict, AWS_REMOTE_RESOURCE_REGION, remote_resource_region) self.check_sum(metric_name, dependency_dp.sum, expected_sum) attribute_dict: Dict[str, AnyValue] = self._get_attributes_dict(service_dp.attributes) @@ -1212,10 +1313,23 @@ def _assert_metric_attributes( self._assert_str_attribute(attribute_dict, AWS_SPAN_KIND, kwargs.get("dependency_metric_span_kind") or "CLIENT") remote_resource_type = kwargs.get("remote_resource_type", "None") remote_resource_identifier = kwargs.get("remote_resource_identifier", "None") + remote_resource_account_id = kwargs.get("remote_resource_account_id", "None") + remote_resource_account_access_key = kwargs.get("remote_resource_account_access_key", "None") + remote_resource_region = kwargs.get("remote_resource_region", "None") if remote_resource_type != "None": self._assert_attribute(attribute_dict, AWS_REMOTE_RESOURCE_TYPE, remote_resource_type) if remote_resource_identifier != "None": self._assert_attribute(attribute_dict, AWS_REMOTE_RESOURCE_IDENTIFIER, remote_resource_identifier) + if remote_resource_account_id != "None": + assert remote_resource_identifier != "None" + self._assert_str_attribute(attribute_dict, AWS_REMOTE_RESOURCE_ACCOUNT_ID, remote_resource_account_id) + self.assertIsNone(attribute_dict.get(AWS_REMOTE_RESOURCE_ACCESS_KEY)) + if remote_resource_account_access_key != "None": + assert remote_resource_identifier != "None" + self._assert_str_attribute(attribute_dict, AWS_REMOTE_RESOURCE_ACCESS_KEY, remote_resource_account_access_key) + self.assertIsNone(attribute_dict.get(AWS_REMOTE_RESOURCE_ACCOUNT_ID)) + if remote_resource_region != "None": + self._assert_str_attribute(attribute_dict, AWS_REMOTE_RESOURCE_REGION, remote_resource_region) self.check_sum(metric_name, dependency_dp.sum, expected_sum) attribute_dict_service: Dict[str, AnyValue] = self._get_attributes_dict(service_dp.attributes) diff --git a/contract-tests/tests/test/amazon/utils/application_signals_constants.py b/contract-tests/tests/test/amazon/utils/application_signals_constants.py index 14b602e8..59a5920d 100644 --- a/contract-tests/tests/test/amazon/utils/application_signals_constants.py +++ b/contract-tests/tests/test/amazon/utils/application_signals_constants.py @@ -19,3 +19,6 @@ AWS_REMOTE_RESOURCE_IDENTIFIER: str = "aws.remote.resource.identifier" AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER: str = 'aws.remote.resource.cfn.primary.identifier' AWS_SPAN_KIND: str = "aws.span.kind" +AWS_REMOTE_RESOURCE_ACCESS_KEY: str = "aws.remote.resource.account.access_key" +AWS_REMOTE_RESOURCE_ACCOUNT_ID: str = "aws.remote.resource.account.id" +AWS_REMOTE_RESOURCE_REGION: str = "aws.remote.resource.region" \ No newline at end of file