Skip to content

Commit 81bfc3c

Browse files
Extract resource arn and remote resource access key for cross-account support (#224)
1 parent f6b9564 commit 81bfc3c

File tree

16 files changed

+722
-78
lines changed

16 files changed

+722
-78
lines changed

src/AWS.Distro.OpenTelemetry.AutoInstrumentation/AwsAttributeKeys.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ internal sealed class AwsAttributeKeys
2020
internal static readonly string AttributeAWSSdkDescendant = "aws.sdk.descendant";
2121
internal static readonly string AttributeAWSConsumerParentSpanKind = "aws.consumer.parent.span.kind";
2222

23+
// Cross-account support
24+
internal static readonly string AttributeAWSAuthAccessKey = "aws.auth.account.access_key";
25+
internal static readonly string AttributeAWSAuthRegion = "aws.auth.region";
26+
internal static readonly string AttributeAWSRemoteResourceAccessKey = "aws.remote.resource.account.access_key";
27+
internal static readonly string AttributeAWSRemoteResourceAccountId = "aws.remote.resource.account.id";
28+
internal static readonly string AttributeAWSRemoteResourceRegion = "aws.remote.resource.region";
29+
2330
// This was copied over from AWSSemanticConventions from the here:
2431
// https://github.com/open-telemetry/opentelemetry-dotnet-contrib/blob/main/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSSemanticConventions.cs
2532
// TODO: add any other attributes keys after auto instrumentation.
@@ -39,6 +46,7 @@ internal sealed class AwsAttributeKeys
3946

4047
// internal static readonly string AttributeAWSSQSQueueUrl = "aws.sqs.queue_url";
4148
internal static readonly string AttributeAWSSQSQueueName = "aws.sqs.queue_name";
49+
internal static readonly string AttributeAWSKinesisStreamArn = "aws.kinesis.stream.arn";
4250
internal static readonly string AttributeAWSKinesisStreamName = "aws.kinesis.stream_name";
4351

4452
// This attribute is being used here:
@@ -47,6 +55,7 @@ internal sealed class AwsAttributeKeys
4755
// public const string AttributeAwsDynamodbTableNames = "aws.dynamodb.table_names"
4856
// Going to use the below one because of the manual instrumentation.
4957
// TODO: update/remove attribute according to auto instrumentation.
58+
internal static readonly string AttributeAWSDynamoTableArn = "aws.dynamodb.table.arn";
5059
internal static readonly string AttributeAWSDynamoTableName = "aws.table_name";
5160
internal static readonly string AttributeAWSSQSQueueUrl = "aws.queue_url";
5261

src/AWS.Distro.OpenTelemetry.AutoInstrumentation/AwsMetricAttributeGenerator.cs

Lines changed: 98 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using OpenTelemetry.Trace;
1010
using static AWS.Distro.OpenTelemetry.AutoInstrumentation.AwsAttributeKeys;
1111
using static AWS.Distro.OpenTelemetry.AutoInstrumentation.AwsSpanProcessingUtil;
12+
using static AWS.Distro.OpenTelemetry.AutoInstrumentation.RegionalResourceArnParser;
1213
using static AWS.Distro.OpenTelemetry.AutoInstrumentation.SqsUrlParser;
1314
using static OpenTelemetry.Trace.TraceSemanticConventions;
1415

@@ -94,7 +95,16 @@ private ActivityTagsCollection GenerateDependencyMetricAttributes(Activity span,
9495
SetEgressOperation(span, attributes);
9596
SetRemoteEnvironment(span, attributes);
9697
SetRemoteServiceAndOperation(span, attributes);
97-
SetRemoteResourceTypeAndIdentifier(span, attributes);
98+
bool isRemoteResourceIdentifierPresent = SetRemoteResourceTypeAndIdentifier(span, attributes);
99+
if (isRemoteResourceIdentifierPresent)
100+
{
101+
bool isAccountIdAndRegionPresent = SetRemoteResourceAccountIdAndRegion(span, attributes);
102+
if (!isAccountIdAndRegionPresent)
103+
{
104+
SetRemoteResourceAccessKeyAndRegion(span, attributes);
105+
}
106+
}
107+
98108
SetSpanKindForDependency(span, attributes);
99109
SetRemoteDbUser(span, attributes);
100110

@@ -413,8 +423,9 @@ private static string NormalizeRemoteServiceName(Activity span, string serviceNa
413423

414424
// This function is used to check for AWS specific attributes and set the RemoteResourceType
415425
// and RemoteResourceIdentifier accordingly. Right now, this sets it for DDB, S3, Kinesis,
416-
// and SQS (using QueueName or QueueURL)
417-
private static void SetRemoteResourceTypeAndIdentifier(Activity span, ActivityTagsCollection attributes)
426+
// and SQS (using QueueName or QueueURL). Returns true if remote resource type and identifier
427+
// are successfully set, false otherwise.
428+
private static bool SetRemoteResourceTypeAndIdentifier(Activity span, ActivityTagsCollection attributes)
418429
{
419430
string? remoteResourceType = null;
420431
string? remoteResourceIdentifier = null;
@@ -426,11 +437,21 @@ private static void SetRemoteResourceTypeAndIdentifier(Activity span, ActivityTa
426437
remoteResourceType = NormalizedDynamoDBServiceName + "::Table";
427438
remoteResourceIdentifier = EscapeDelimiters((string?)span.GetTagItem(AttributeAWSDynamoTableName));
428439
}
440+
else if (IsKeyPresent(span, AttributeAWSDynamoTableArn))
441+
{
442+
remoteResourceType = NormalizedDynamoDBServiceName + "::Table";
443+
remoteResourceIdentifier = EscapeDelimiters(RegionalResourceArnParser.ExtractDynamoDbTableNameFromArn((string?)span.GetTagItem(AttributeAWSDynamoTableArn)));
444+
}
429445
else if (IsKeyPresent(span, AttributeAWSKinesisStreamName))
430446
{
431447
remoteResourceType = NormalizedKinesisServiceName + "::Stream";
432448
remoteResourceIdentifier = EscapeDelimiters((string?)span.GetTagItem(AttributeAWSKinesisStreamName));
433449
}
450+
else if (IsKeyPresent(span, AttributeAWSKinesisStreamArn))
451+
{
452+
remoteResourceType = NormalizedKinesisServiceName + "::Stream";
453+
remoteResourceIdentifier = EscapeDelimiters(RegionalResourceArnParser.ExtractKinesisStreamNameFromArn((string?)span.GetTagItem(AttributeAWSKinesisStreamArn)));
454+
}
434455
else if (IsKeyPresent(span, AttributeAWSLambdaFunctionName))
435456
{
436457
// For non-invoke Lambda operations, treat Lambda as a resource.
@@ -455,13 +476,13 @@ private static void SetRemoteResourceTypeAndIdentifier(Activity span, ActivityTa
455476
else if (IsKeyPresent(span, AttributeAWSSecretsManagerSecretArn))
456477
{
457478
remoteResourceType = NormalizedSecretsManagerServiceName + "::Secret";
458-
remoteResourceIdentifier = EscapeDelimiters((string?)span.GetTagItem(AttributeAWSSecretsManagerSecretArn))?.Split(':').Last();
479+
remoteResourceIdentifier = EscapeDelimiters(RegionalResourceArnParser.ExtractResourceNameFromArn((string?)span.GetTagItem(AttributeAWSSecretsManagerSecretArn)));
459480
cloudformationPrimaryIdentifier = EscapeDelimiters((string?)span.GetTagItem(AttributeAWSSecretsManagerSecretArn));
460481
}
461482
else if (IsKeyPresent(span, AttributeAWSSNSTopicArn))
462483
{
463484
remoteResourceType = NormalizedSNSServiceName + "::Topic";
464-
remoteResourceIdentifier = EscapeDelimiters((string?)span.GetTagItem(AttributeAWSSNSTopicArn))?.Split(':').Last();
485+
remoteResourceIdentifier = EscapeDelimiters(RegionalResourceArnParser.ExtractResourceNameFromArn((string?)span.GetTagItem(AttributeAWSSNSTopicArn)));
465486
cloudformationPrimaryIdentifier = EscapeDelimiters((string?)span.GetTagItem(AttributeAWSSNSTopicArn));
466487
}
467488
else if (IsKeyPresent(span, AttributeAWSSQSQueueName))
@@ -479,13 +500,16 @@ private static void SetRemoteResourceTypeAndIdentifier(Activity span, ActivityTa
479500
else if (IsKeyPresent(span, AttributeAWSStepFunctionsActivityArn))
480501
{
481502
remoteResourceType = NormalizedStepFunctionsName + "::Activity";
482-
remoteResourceIdentifier = EscapeDelimiters((string?)span.GetTagItem(AttributeAWSStepFunctionsActivityArn))?.Split(':').Last();
503+
remoteResourceIdentifier =
504+
EscapeDelimiters(
505+
RegionalResourceArnParser.ExtractResourceNameFromArn(
506+
(string?)span.GetTagItem(AttributeAWSStepFunctionsActivityArn)));
483507
cloudformationPrimaryIdentifier = EscapeDelimiters((string?)span.GetTagItem(AttributeAWSStepFunctionsActivityArn));
484508
}
485509
else if (IsKeyPresent(span, AttributeAWSStepFunctionsStateMachineArn))
486510
{
487511
remoteResourceType = NormalizedStepFunctionsName + "::StateMachine";
488-
remoteResourceIdentifier = EscapeDelimiters((string?)span.GetTagItem(AttributeAWSStepFunctionsStateMachineArn))?.Split(':').Last();
512+
remoteResourceIdentifier = EscapeDelimiters(RegionalResourceArnParser.ExtractResourceNameFromArn((string?)span.GetTagItem(AttributeAWSStepFunctionsStateMachineArn)));
489513
cloudformationPrimaryIdentifier = EscapeDelimiters((string?)span.GetTagItem(AttributeAWSStepFunctionsStateMachineArn));
490514
}
491515
else if (IsKeyPresent(span, AttributeAWSBedrockGuardrailId))
@@ -535,6 +559,73 @@ private static void SetRemoteResourceTypeAndIdentifier(Activity span, ActivityTa
535559
attributes.Add(AttributeAWSRemoteResourceType, remoteResourceType);
536560
attributes.Add(AttributeAWSRemoteResourceIdentifier, remoteResourceIdentifier);
537561
attributes.Add(AttributeAWSCloudformationPrimaryIdentifier, cloudformationPrimaryIdentifier);
562+
return true;
563+
}
564+
565+
return false;
566+
}
567+
568+
// Extracts and sets the remote resource account ID and region from either an SQS queue URL or various AWS ARN attributes.
569+
private static bool SetRemoteResourceAccountIdAndRegion(Activity span, ActivityTagsCollection attributes)
570+
{
571+
string[] arnAttributes = new[]
572+
{
573+
AttributeAWSDynamoTableArn,
574+
AttributeAWSKinesisStreamArn,
575+
AttributeAWSSNSTopicArn,
576+
AttributeAWSSecretsManagerSecretArn,
577+
AttributeAWSStepFunctionsActivityArn,
578+
AttributeAWSStepFunctionsStateMachineArn,
579+
AttributeAWSBedrockGuardrailArn,
580+
AttributeAWSLambdaFunctionArn,
581+
};
582+
583+
string? remoteResourceAccountId = null;
584+
string? remoteResourceRegion = null;
585+
586+
if (IsKeyPresent(span, AttributeAWSSQSQueueUrl))
587+
{
588+
string? url = EscapeDelimiters((string?)span.GetTagItem(AttributeAWSSQSQueueUrl));
589+
remoteResourceAccountId = SqsUrlParser.GetAccountId(url);
590+
remoteResourceRegion = SqsUrlParser.GetRegion(url);
591+
}
592+
else
593+
{
594+
foreach (var attributeKey in arnAttributes)
595+
{
596+
if (IsKeyPresent(span, attributeKey))
597+
{
598+
string? arn = (string?)span.GetTagItem(attributeKey);
599+
remoteResourceAccountId = RegionalResourceArnParser.GetAccountId(arn);
600+
remoteResourceRegion = RegionalResourceArnParser.GetRegion(arn);
601+
break;
602+
}
603+
}
604+
}
605+
606+
if (remoteResourceAccountId != null && remoteResourceRegion != null)
607+
{
608+
attributes.Add(AttributeAWSRemoteResourceAccountId, remoteResourceAccountId);
609+
attributes.Add(AttributeAWSRemoteResourceRegion, remoteResourceRegion);
610+
return true;
611+
}
612+
613+
return false;
614+
}
615+
616+
// Extracts and sets the remote resource account access key id and region from STS credentials.
617+
private static void SetRemoteResourceAccessKeyAndRegion(Activity span, ActivityTagsCollection attributes)
618+
{
619+
if (IsKeyPresent(span, AttributeAWSAuthAccessKey))
620+
{
621+
string? remoteResourceAccessKey = (string?)span.GetTagItem(AttributeAWSAuthAccessKey);
622+
attributes.Add(AttributeAWSRemoteResourceAccessKey, remoteResourceAccessKey);
623+
}
624+
625+
if (IsKeyPresent(span, AttributeAWSAuthRegion))
626+
{
627+
string? remoteResourceRegion = (string?)span.GetTagItem(AttributeAWSAuthRegion);
628+
attributes.Add(AttributeAWSRemoteResourceRegion, remoteResourceRegion);
538629
}
539630
}
540631

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
namespace AWS.Distro.OpenTelemetry.AutoInstrumentation;
5+
6+
public class RegionalResourceArnParser
7+
{
8+
public static string? GetAccountId(string? arn) => ParseArn(arn)?[4];
9+
10+
public static string? GetRegion(string? arn) => ParseArn(arn)?[3];
11+
12+
public static string? ExtractKinesisStreamNameFromArn(string? arn) =>
13+
ExtractResourceNameFromArn(arn)?.Replace("stream/", string.Empty);
14+
15+
public static string? ExtractDynamoDbTableNameFromArn(string? arn) =>
16+
ExtractResourceNameFromArn(arn)?.Replace("table/", string.Empty);
17+
18+
public static string? ExtractResourceNameFromArn(string? arn) =>
19+
ParseArn(arn) is var parts && parts != null ? parts[parts.Length - 1] : null;
20+
21+
/// <summary>
22+
/// Parses ARN with formats:
23+
/// arn:partition:service:region:account-id:resource-type/resource-id or
24+
/// arn:partition:service:region:account-id:resource-type:resource-id
25+
/// </summary>
26+
private static string[] ? ParseArn(string? arn)
27+
{
28+
if (arn == null || !arn.StartsWith("arn:"))
29+
{
30+
return null;
31+
}
32+
33+
var parts = arn.Split(':');
34+
return parts.Length >= 6 && IsAccountId(parts[4]) ? parts : null;
35+
}
36+
37+
private static bool IsAccountId(string input)
38+
{
39+
return long.TryParse(input, out _);
40+
}
41+
}

src/AWS.Distro.OpenTelemetry.AutoInstrumentation/SqlUrlParser.cs

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
using System.Text;
5-
64
namespace AWS.Distro.OpenTelemetry.AutoInstrumentation;
75

86
/// <summary>
@@ -17,8 +15,11 @@ public class SqsUrlParser
1715
/// Best-effort logic to extract queue name from an HTTP url. This method should only be used with
1816
/// a string that is, with reasonably high confidence, an SQS queue URL. Handles new/legacy/some
1917
/// custom URLs. Essentially, we require that the URL should have exactly three parts, delimited by
20-
/// /'s (excluding schema), the second part should be a 12-digit account id, and the third part
18+
/// /'s (excluding schema), the second part should be an account id consisting of digits, and the third part
2119
/// should be a valid queue name, per SQS naming conventions.
20+
///
21+
/// Unlike ParseUrl which only handles new URLs and their queuename parsing, this
22+
/// implements its own queue name parsing logic to support multiple URL formats.
2223
/// </summary>
2324
/// <param name="url"><see cref="string"/>Url to get the remote target from</param>
2425
/// <returns>parsed remote target</returns>
@@ -29,8 +30,8 @@ public class SqsUrlParser
2930
return null;
3031
}
3132

32-
url = url.Replace(HttpSchema, string.Empty).Replace(HttpsSchema, string.Empty);
33-
string[] splitUrl = url.Split('/');
33+
string urlWithoutProtocol = url.Replace(HttpSchema, string.Empty).Replace(HttpsSchema, string.Empty);
34+
string[] splitUrl = urlWithoutProtocol.Split('/');
3435
if (splitUrl.Length == 3 && IsAccountId(splitUrl[1]) && IsValidQueueName(splitUrl[2]))
3536
{
3637
return splitUrl[2];
@@ -39,23 +40,46 @@ public class SqsUrlParser
3940
return null;
4041
}
4142

42-
private static bool IsAccountId(string input)
43+
public static string? GetAccountId(string? url) => ParseUrl(url).accountId;
44+
45+
public static string? GetRegion(string? url) => ParseUrl(url).region;
46+
47+
/// <summary>
48+
/// Parses new SQS URLs https://sqs.region.amazonaws.com/accountI/queueName;
49+
/// </summary>
50+
/// <param name="url">SQS URL to parse</param>
51+
/// <returns>Tuple containing queue name, account ID, and region</returns>
52+
public static (string? QueueName, string? accountId, string? region) ParseUrl(string? url)
4353
{
44-
if (input == null || input.Length != 12)
54+
if (url == null)
4555
{
46-
return false;
56+
return (null, null, null);
4757
}
4858

49-
try
50-
{
51-
long.Parse(input);
52-
}
53-
catch (Exception)
59+
string urlWithoutProtocol = url.Replace(HttpSchema, string.Empty).Replace(HttpsSchema, string.Empty);
60+
string[] splitUrl = urlWithoutProtocol.Split('/');
61+
62+
if (
63+
splitUrl.Length != 3 ||
64+
!splitUrl[0].StartsWith("sqs", StringComparison.OrdinalIgnoreCase) ||
65+
!IsAccountId(splitUrl[1]) ||
66+
!IsValidQueueName(splitUrl[2]))
5467
{
55-
return false;
68+
return (null, null, null);
5669
}
5770

58-
return true;
71+
string domain = splitUrl[0];
72+
string[] domainParts = domain.Split('.');
73+
74+
return (
75+
splitUrl[2],
76+
splitUrl[1],
77+
domainParts.Length == 4 ? domainParts[1] : null);
78+
}
79+
80+
private static bool IsAccountId(string input)
81+
{
82+
return long.TryParse(input, out _);
5983
}
6084

6185
private static bool IsValidQueueName(string input)

src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSSemanticConventions.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@ internal static class AWSSemanticConventions
99
public const string AttributeAWSOperationName = "aws.operation";
1010
public const string AttributeAWSRegion = "aws.region";
1111
public const string AttributeAWSRequestId = "aws.requestId";
12+
public const string AttributeAWSAuthAccessKey = "aws.auth.account.access_key";
13+
public const string AttributeAWSAuthRegion = "aws.auth.region";
1214

15+
public const string AttributeAWSDynamoTableArn = "aws.dynamodb.table.arn";
1316
public const string AttributeAWSDynamoTableName = "aws.table_name";
1417
public const string AttributeAWSSQSQueueUrl = "aws.queue_url";
1518
public const string AttributeAWSSQSQueueName = "aws.sqs.queue_name";
1619
public const string AttributeAWSS3BucketName = "aws.s3.bucket";
20+
public const string AttributeAWSKinesisStreamArn = "aws.kinesis.stream.arn";
1721
public const string AttributeAWSKinesisStreamName = "aws.kinesis.stream_name";
1822
public const string AttributeAWSLambdaFunctionArn = "aws.lambda.function.arn";
1923
public const string AttributeAWSLambdaFunctionName = "aws.lambda.function.name";

src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSServiceHelper.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ internal class AWSServiceHelper
1212
{ AWSServiceType.DynamoDbService, new List<string> { "TableName" } },
1313
{ AWSServiceType.SQSService, new List<string> { "QueueUrl", "QueueName" } },
1414
{ AWSServiceType.S3Service, new List<string> { "BucketName" } },
15-
{ AWSServiceType.KinesisService, new List<string> { "StreamName" } },
15+
{ AWSServiceType.KinesisService, new List<string> { "StreamName", "StreamARN" } },
1616
{ AWSServiceType.LambdaService, new List<string> { "UUID", "FunctionName" } },
1717
{ AWSServiceType.SecretsManagerService, new List<string> { "SecretId" } },
1818
{ AWSServiceType.SNSService, new List<string> { "TopicArn" } },
@@ -32,11 +32,13 @@ internal class AWSServiceHelper
3232

3333
internal static IReadOnlyDictionary<string, string> ParameterAttributeMap = new Dictionary<string, string>()
3434
{
35+
{ "TableArn", AWSSemanticConventions.AttributeAWSDynamoTableArn },
3536
{ "TableName", AWSSemanticConventions.AttributeAWSDynamoTableName },
3637
{ "QueueUrl", AWSSemanticConventions.AttributeAWSSQSQueueUrl },
3738
{ "QueueName", AWSSemanticConventions.AttributeAWSSQSQueueName },
3839
{ "BucketName", AWSSemanticConventions.AttributeAWSS3BucketName },
3940
{ "StreamName", AWSSemanticConventions.AttributeAWSKinesisStreamName },
41+
{ "StreamARN", AWSSemanticConventions.AttributeAWSKinesisStreamArn },
4042
{ "TopicArn", AWSSemanticConventions.AttributeAWSSNSTopicArn },
4143
{ "ARN", AWSSemanticConventions.AttributeAWSSecretsManagerSecretArn },
4244
{ "SecretId", AWSSemanticConventions.AttributeAWSSecretsManagerSecretArn },

0 commit comments

Comments
 (0)