Skip to content

Commit df0aaa6

Browse files
Generate cross-account metric attributes
1 parent 0c2c969 commit df0aaa6

File tree

6 files changed

+563
-75
lines changed

6 files changed

+563
-75
lines changed

aws-distro-opentelemetry-node-autoinstrumentation/src/aws-metric-attribute-generator.ts

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
MetricAttributeGenerator,
3333
SERVICE_METRIC,
3434
} from './metric-attribute-generator';
35+
import { RegionalResourceArnParser } from './regional-resource-arn-parser';
3536
import { SqsUrlParser } from './sqs-url-parser';
3637
import { LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT } from './aws-opentelemetry-configurator';
3738

@@ -112,8 +113,20 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator {
112113
AwsMetricAttributeGenerator.setService(resource, span, attributes);
113114
AwsMetricAttributeGenerator.setEgressOperation(span, attributes);
114115
AwsMetricAttributeGenerator.setRemoteServiceAndOperation(span, attributes);
115-
AwsMetricAttributeGenerator.setRemoteResourceTypeAndIdentifier(span, attributes);
116+
const isRemoteResourceIdentifierPresent = AwsMetricAttributeGenerator.setRemoteResourceTypeAndIdentifier(
117+
span,
118+
attributes
119+
);
116120
AwsMetricAttributeGenerator.setRemoteEnvironment(span, attributes);
121+
if (isRemoteResourceIdentifierPresent) {
122+
const isAccountIdAndRegionPresent = AwsMetricAttributeGenerator.setRemoteResourceAccountIdAndRegion(
123+
span,
124+
attributes
125+
);
126+
if (!isAccountIdAndRegionPresent) {
127+
AwsMetricAttributeGenerator.setRemoteResourceAccessKeyAndRegion(span, attributes);
128+
}
129+
}
117130
AwsMetricAttributeGenerator.setSpanKindForDependency(span, attributes);
118131
AwsMetricAttributeGenerator.setRemoteDbUser(span, attributes);
119132

@@ -369,7 +382,7 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator {
369382
* href="https://docs.aws.amazon.com/cloudcontrolapi/latest/userguide/supported-resources.html">AWS
370383
* Cloud Control resource format</a>.
371384
*/
372-
private static setRemoteResourceTypeAndIdentifier(span: ReadableSpan, attributes: Attributes): void {
385+
private static setRemoteResourceTypeAndIdentifier(span: ReadableSpan, attributes: Attributes): boolean {
373386
let remoteResourceType: AttributeValue | undefined;
374387
let remoteResourceIdentifier: AttributeValue | undefined;
375388
let cloudFormationIdentifier: AttributeValue | undefined;
@@ -383,11 +396,27 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator {
383396
) {
384397
remoteResourceType = NORMALIZED_DYNAMO_DB_SERVICE_NAME + '::Table';
385398
remoteResourceIdentifier = AwsMetricAttributeGenerator.escapeDelimiters(awsTableNames[0]);
399+
} else if (AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_DYNAMODB_TABLE_ARN)) {
400+
remoteResourceType = NORMALIZED_DYNAMO_DB_SERVICE_NAME + '::Table';
401+
remoteResourceIdentifier = AwsMetricAttributeGenerator.escapeDelimiters(
402+
this.extractResourceNameFromArn(span.attributes[AWS_ATTRIBUTE_KEYS.AWS_DYNAMODB_TABLE_ARN])?.replace(
403+
'table/',
404+
''
405+
)
406+
);
386407
} else if (AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_KINESIS_STREAM_NAME)) {
387408
remoteResourceType = NORMALIZED_KINESIS_SERVICE_NAME + '::Stream';
388409
remoteResourceIdentifier = AwsMetricAttributeGenerator.escapeDelimiters(
389410
span.attributes[AWS_ATTRIBUTE_KEYS.AWS_KINESIS_STREAM_NAME]
390411
);
412+
} else if (AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_KINESIS_STREAM_ARN)) {
413+
remoteResourceType = NORMALIZED_KINESIS_SERVICE_NAME + '::Stream';
414+
remoteResourceIdentifier = AwsMetricAttributeGenerator.escapeDelimiters(
415+
this.extractResourceNameFromArn(span.attributes[AWS_ATTRIBUTE_KEYS.AWS_KINESIS_STREAM_ARN])?.replace(
416+
'stream/',
417+
''
418+
)
419+
);
391420
} else if (AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_S3_BUCKET)) {
392421
remoteResourceType = NORMALIZED_S3_SERVICE_NAME + '::Bucket';
393422
remoteResourceIdentifier = AwsMetricAttributeGenerator.escapeDelimiters(
@@ -500,7 +529,10 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator {
500529
attributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_TYPE] = remoteResourceType;
501530
attributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_IDENTIFIER] = remoteResourceIdentifier;
502531
attributes[AWS_ATTRIBUTE_KEYS.AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER] = cloudFormationIdentifier;
532+
return true;
503533
}
534+
535+
return false;
504536
}
505537

506538
/**
@@ -522,6 +554,56 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator {
522554
}
523555
}
524556

557+
private static setRemoteResourceAccountIdAndRegion(span: ReadableSpan, attributes: Attributes): boolean {
558+
const ARN_ATTRIBUTES: string[] = [
559+
AWS_ATTRIBUTE_KEYS.AWS_DYNAMODB_TABLE_ARN,
560+
AWS_ATTRIBUTE_KEYS.AWS_KINESIS_STREAM_ARN,
561+
AWS_ATTRIBUTE_KEYS.AWS_SNS_TOPIC_ARN,
562+
AWS_ATTRIBUTE_KEYS.AWS_SECRETSMANAGER_SECRET_ARN,
563+
AWS_ATTRIBUTE_KEYS.AWS_STEPFUNCTIONS_STATEMACHINE_ARN,
564+
AWS_ATTRIBUTE_KEYS.AWS_STEPFUNCTIONS_ACTIVITY_ARN,
565+
AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_ARN,
566+
AWS_ATTRIBUTE_KEYS.AWS_BEDROCK_GUARDRAIL_ARN,
567+
];
568+
let remoteResourceAccountId: string | undefined = undefined;
569+
let remoteResourceRegion: string | undefined = undefined;
570+
571+
if (AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_SQS_QUEUE_URL)) {
572+
const sqsQueueUrl = AwsMetricAttributeGenerator.escapeDelimiters(
573+
span.attributes[AWS_ATTRIBUTE_KEYS.AWS_SQS_QUEUE_URL]
574+
);
575+
remoteResourceAccountId = SqsUrlParser.getAccountId(sqsQueueUrl);
576+
remoteResourceRegion = SqsUrlParser.getRegion(sqsQueueUrl);
577+
} else {
578+
for (const attributeKey of ARN_ATTRIBUTES) {
579+
if (AwsSpanProcessingUtil.isKeyPresent(span, attributeKey)) {
580+
const arn = span.attributes[attributeKey];
581+
remoteResourceAccountId = RegionalResourceArnParser.getAccountId(arn);
582+
remoteResourceRegion = RegionalResourceArnParser.getRegion(arn);
583+
break;
584+
}
585+
}
586+
}
587+
588+
if (remoteResourceAccountId !== undefined && remoteResourceRegion !== undefined) {
589+
attributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_ACCOUNT_ID] = remoteResourceAccountId;
590+
attributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_REGION] = remoteResourceRegion;
591+
return true;
592+
}
593+
594+
return false;
595+
}
596+
597+
private static setRemoteResourceAccessKeyAndRegion(span: ReadableSpan, attributes: Attributes): void {
598+
if (AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_AUTH_ACCESS_KEY)) {
599+
attributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_ACCESS_KEY] =
600+
span.attributes[AWS_ATTRIBUTE_KEYS.AWS_AUTH_ACCESS_KEY];
601+
}
602+
if (AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_AUTH_REGION)) {
603+
attributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_REGION] = span.attributes[AWS_ATTRIBUTE_KEYS.AWS_AUTH_REGION];
604+
}
605+
}
606+
525607
/**
526608
* RemoteResourceIdentifier is populated with rule <code>
527609
* ^[{db.name}|]?{address}[|{port}]?
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { AttributeValue } from '@opentelemetry/api';
5+
6+
export class RegionalResourceArnParser {
7+
public static getAccountId(arn: AttributeValue | undefined): string | undefined {
8+
if (this.isArn(arn)) {
9+
return (arn! as string).split(':')[4];
10+
}
11+
return undefined;
12+
}
13+
14+
public static getRegion(arn: AttributeValue | undefined): string | undefined {
15+
if (this.isArn(arn)) {
16+
return (arn! as string).split(':')[3];
17+
}
18+
return undefined;
19+
}
20+
21+
public static isArn(arn: AttributeValue | undefined): boolean {
22+
// Check if arn follows the format:
23+
// arn:partition:service:region:account-id:resource-type/resource-id or
24+
// arn:partition:service:region:account-id:resource-type:resource-id
25+
if (!arn || typeof arn !== 'string') {
26+
return false;
27+
}
28+
29+
if (!arn.startsWith('arn')) {
30+
return false;
31+
}
32+
33+
const arnParts = arn.split(':');
34+
return arnParts.length >= 6 && this.isAccountId(arnParts[4]);
35+
}
36+
37+
private static isAccountId(input: string): boolean {
38+
if (input == null || input.length !== 12) {
39+
return false;
40+
}
41+
42+
if (!this._checkDigits(input)) {
43+
return false;
44+
}
45+
46+
return true;
47+
}
48+
49+
private static _checkDigits(str: string): boolean {
50+
return /^\d+$/.test(str);
51+
}
52+
}

aws-distro-opentelemetry-node-autoinstrumentation/src/sqs-url-parser.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,57 @@ export class SqsUrlParser {
3030
return undefined;
3131
}
3232

33+
/**
34+
* Extracts the account ID from an SQS URL.
35+
*/
36+
public static getAccountId(url: AttributeValue | undefined): string | undefined {
37+
if (typeof url !== 'string') {
38+
return undefined;
39+
}
40+
41+
url = url.replace(HTTP_SCHEMA, '').replace(HTTPS_SCHEMA, '');
42+
if (this.isValidSqsUrl(url)) {
43+
const splitUrl: string[] = url.split('/');
44+
return splitUrl[1];
45+
}
46+
47+
return undefined;
48+
}
49+
50+
/**
51+
* Extracts the region from an SQS URL.
52+
*/
53+
public static getRegion(url: AttributeValue | undefined): string | undefined {
54+
if (typeof url !== 'string') {
55+
return undefined;
56+
}
57+
58+
url = url.replace(HTTP_SCHEMA, '').replace(HTTPS_SCHEMA, '');
59+
if (this.isValidSqsUrl(url)) {
60+
const splitUrl: string[] = url.split('/');
61+
const domain: string = splitUrl[0];
62+
const domainParts: string[] = domain.split('.');
63+
if (domainParts.length === 4) {
64+
return domainParts[1];
65+
}
66+
}
67+
68+
return undefined;
69+
}
70+
71+
/**
72+
* Checks if the URL is a valid SQS URL.
73+
*/
74+
private static isValidSqsUrl(url: string): boolean {
75+
const splitUrl: string[] = url.split('/');
76+
return (
77+
splitUrl.length === 3 &&
78+
splitUrl[0].toLowerCase().startsWith('sqs') &&
79+
this.isAccountId(splitUrl[1]) &&
80+
this.isValidQueueName(splitUrl[2])
81+
);
82+
}
83+
3384
private static isAccountId(input: string): boolean {
3485
if (input == null || input.length !== 12) {
3586
return false;

0 commit comments

Comments
 (0)