Skip to content

Commit 79a5a32

Browse files
Generate cross-account metric attributes
1 parent 1becaf1 commit 79a5a32

File tree

6 files changed

+532
-84
lines changed

6 files changed

+532
-84
lines changed

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@
77
from urllib.parse import ParseResult, urlparse
88

99
from amazon.opentelemetry.distro._aws_attribute_keys import (
10+
AWS_AUTH_ACCESS_KEY,
11+
AWS_AUTH_REGION,
1012
AWS_BEDROCK_AGENT_ID,
1113
AWS_BEDROCK_DATA_SOURCE_ID,
1214
AWS_BEDROCK_GUARDRAIL_ARN,
1315
AWS_BEDROCK_GUARDRAIL_ID,
1416
AWS_BEDROCK_KNOWLEDGE_BASE_ID,
1517
AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER,
18+
AWS_DYNAMODB_TABLE_ARN,
19+
AWS_KINESIS_STREAM_ARN,
1620
AWS_KINESIS_STREAM_NAME,
1721
AWS_LAMBDA_FUNCTION_ARN,
1822
AWS_LAMBDA_FUNCTION_NAME,
@@ -22,7 +26,10 @@
2226
AWS_REMOTE_DB_USER,
2327
AWS_REMOTE_ENVIRONMENT,
2428
AWS_REMOTE_OPERATION,
29+
AWS_REMOTE_RESOURCE_ACCESS_KEY,
30+
AWS_REMOTE_RESOURCE_ACCOUNT_ID,
2531
AWS_REMOTE_RESOURCE_IDENTIFIER,
32+
AWS_REMOTE_RESOURCE_REGION,
2633
AWS_REMOTE_RESOURCE_TYPE,
2734
AWS_REMOTE_SERVICE,
2835
AWS_SECRETSMANAGER_SECRET_ARN,
@@ -56,6 +63,7 @@
5663
SERVICE_METRIC,
5764
MetricAttributeGenerator,
5865
)
66+
from amazon.opentelemetry.distro.regional_resource_arn_parser import RegionalResourceArnParser
5967
from amazon.opentelemetry.distro.sqs_url_parser import SqsUrlParser
6068
from opentelemetry.sdk.resources import Resource
6169
from opentelemetry.sdk.trace import BoundedAttributes, ReadableSpan
@@ -148,7 +156,11 @@ def _generate_dependency_metric_attributes(span: ReadableSpan, resource: Resourc
148156
_set_service(resource, span, attributes)
149157
_set_egress_operation(span, attributes)
150158
_set_remote_service_and_operation(span, attributes)
151-
_set_remote_type_and_identifier(span, attributes)
159+
is_remote_identifier_present = _set_remote_type_and_identifier(span, attributes)
160+
if is_remote_identifier_present:
161+
is_remote_account_id_present = _set_remote_account_id_and_region(span, attributes)
162+
if not is_remote_account_id_present:
163+
_set_remote_access_key_and_region(span, attributes)
152164
_set_remote_environment(span, attributes)
153165
_set_remote_db_user(span, attributes)
154166
_set_span_kind_for_dependency(span, attributes)
@@ -383,7 +395,7 @@ def _generate_remote_operation(span: ReadableSpan) -> str:
383395

384396

385397
# pylint: disable=too-many-branches,too-many-statements
386-
def _set_remote_type_and_identifier(span: ReadableSpan, attributes: BoundedAttributes) -> None:
398+
def _set_remote_type_and_identifier(span: ReadableSpan, attributes: BoundedAttributes) -> bool:
387399
"""
388400
Remote resource attributes {@link AwsAttributeKeys#AWS_REMOTE_RESOURCE_TYPE} and {@link
389401
AwsAttributeKeys#AWS_REMOTE_RESOURCE_IDENTIFIER} are used to store information about the resource associated with
@@ -403,9 +415,19 @@ def _set_remote_type_and_identifier(span: ReadableSpan, attributes: BoundedAttri
403415
if is_key_present(span, _AWS_TABLE_NAMES) and len(span.attributes.get(_AWS_TABLE_NAMES)) == 1:
404416
remote_resource_type = _NORMALIZED_DYNAMO_DB_SERVICE_NAME + "::Table"
405417
remote_resource_identifier = _escape_delimiters(span.attributes.get(_AWS_TABLE_NAMES)[0])
418+
elif is_key_present(span, AWS_DYNAMODB_TABLE_ARN):
419+
remote_resource_type = _NORMALIZED_DYNAMO_DB_SERVICE_NAME + "::Table"
420+
remote_resource_identifier = (
421+
_escape_delimiters(span.attributes.get(AWS_DYNAMODB_TABLE_ARN)).split(":")[-1].replace("table/", "")
422+
)
406423
elif is_key_present(span, AWS_KINESIS_STREAM_NAME):
407424
remote_resource_type = _NORMALIZED_KINESIS_SERVICE_NAME + "::Stream"
408425
remote_resource_identifier = _escape_delimiters(span.attributes.get(AWS_KINESIS_STREAM_NAME))
426+
elif is_key_present(span, AWS_KINESIS_STREAM_ARN):
427+
remote_resource_type = _NORMALIZED_KINESIS_SERVICE_NAME + "::Stream"
428+
remote_resource_identifier = (
429+
_escape_delimiters(span.attributes.get(AWS_KINESIS_STREAM_ARN)).split(":")[-1].replace("stream/", "")
430+
)
409431
elif is_key_present(span, _AWS_BUCKET_NAME):
410432
remote_resource_type = _NORMALIZED_S3_SERVICE_NAME + "::Bucket"
411433
remote_resource_identifier = _escape_delimiters(span.attributes.get(_AWS_BUCKET_NAME))
@@ -491,6 +513,48 @@ def _set_remote_type_and_identifier(span: ReadableSpan, attributes: BoundedAttri
491513
attributes[AWS_REMOTE_RESOURCE_TYPE] = remote_resource_type
492514
attributes[AWS_REMOTE_RESOURCE_IDENTIFIER] = remote_resource_identifier
493515
attributes[AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER] = cloudformation_primary_identifier
516+
return True
517+
return False
518+
519+
520+
def _set_remote_account_id_and_region(span: ReadableSpan, attributes: BoundedAttributes) -> bool:
521+
ARN_ATTRIBUTES = [
522+
AWS_DYNAMODB_TABLE_ARN,
523+
AWS_KINESIS_STREAM_ARN,
524+
AWS_SNS_TOPIC_ARN,
525+
AWS_SECRETSMANAGER_SECRET_ARN,
526+
AWS_STEPFUNCTIONS_STATEMACHINE_ARN,
527+
AWS_STEPFUNCTIONS_ACTIVITY_ARN,
528+
AWS_BEDROCK_GUARDRAIL_ARN,
529+
AWS_LAMBDA_FUNCTION_ARN,
530+
]
531+
remote_account_id: Optional[str] = None
532+
remote_region: Optional[str] = None
533+
534+
if is_key_present(span, AWS_SQS_QUEUE_URL):
535+
queue_url = _escape_delimiters(span.attributes.get(AWS_SQS_QUEUE_URL))
536+
remote_account_id = SqsUrlParser.get_account_id(queue_url)
537+
remote_region = SqsUrlParser.get_region(queue_url)
538+
else:
539+
for arn_attribute in ARN_ATTRIBUTES:
540+
if is_key_present(span, arn_attribute):
541+
arn = span.attributes.get(arn_attribute)
542+
remote_account_id = RegionalResourceArnParser.get_account_id(arn)
543+
remote_region = RegionalResourceArnParser.get_region(arn)
544+
break
545+
546+
if remote_account_id is not None and remote_region is not None:
547+
attributes[AWS_REMOTE_RESOURCE_ACCOUNT_ID] = remote_account_id
548+
attributes[AWS_REMOTE_RESOURCE_REGION] = remote_region
549+
return True
550+
return False
551+
552+
553+
def _set_remote_access_key_and_region(span: ReadableSpan, attributes: BoundedAttributes) -> None:
554+
if is_key_present(span, AWS_AUTH_ACCESS_KEY):
555+
attributes[AWS_REMOTE_RESOURCE_ACCESS_KEY] = span.attributes.get(AWS_AUTH_ACCESS_KEY)
556+
if is_key_present(span, AWS_AUTH_REGION):
557+
attributes[AWS_REMOTE_RESOURCE_REGION] = span.attributes.get(AWS_AUTH_REGION)
494558

495559

496560
def _set_remote_environment(span: ReadableSpan, attributes: BoundedAttributes) -> None:
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from typing import Optional
4+
5+
6+
class RegionalResourceArnParser:
7+
@staticmethod
8+
def get_account_id(arn: str) -> Optional[str]:
9+
if _is_arn(arn):
10+
return str(arn).split(":")[4]
11+
return None
12+
13+
@staticmethod
14+
def get_region(arn: str) -> Optional[str]:
15+
if _is_arn(arn):
16+
return str(arn).split(":")[3]
17+
return None
18+
19+
20+
def _is_arn(arn: str) -> bool:
21+
# Check if arn follows the format:
22+
# arn:partition:service:region:account-id:resource-type/resource-id or
23+
# arn:partition:service:region:account-id:resource-type:resource-id
24+
if arn is None:
25+
return False
26+
27+
if not str(arn).startswith("arn"):
28+
return False
29+
30+
arn_parts = str(arn).split(":")
31+
return len(arn_parts) >= 6 and _is_account_id(arn_parts[4])
32+
33+
34+
def _is_account_id(input: str) -> bool:
35+
if input is None or len(input) != 12:
36+
return False
37+
38+
if not _check_digits(input):
39+
return False
40+
41+
return True
42+
43+
44+
def _check_digits(string: str) -> bool:
45+
return string.isdigit()

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/sqs_url_parser.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,50 @@ def get_queue_name(url: str) -> Optional[str]:
2323
return split_url[2]
2424
return None
2525

26+
@staticmethod
27+
def get_account_id(url: str) -> Optional[str]:
28+
"""
29+
Extracts the account ID from an SQS URL.
30+
"""
31+
if url is None:
32+
return None
33+
url = url.replace(_HTTP_SCHEMA, "").replace(_HTTPS_SCHEMA, "")
34+
split_url: List[Optional[str]] = url.split("/")
35+
if _is_valid_sqs_url(url):
36+
return split_url[1]
37+
return None
38+
39+
@staticmethod
40+
def get_region(url: str) -> Optional[str]:
41+
"""
42+
Extracts the region from an SQS URL.
43+
"""
44+
if url is None:
45+
return None
46+
url = url.replace(_HTTP_SCHEMA, "").replace(_HTTPS_SCHEMA, "")
47+
split_url: List[Optional[str]] = url.split("/")
48+
if _is_valid_sqs_url(url):
49+
domain: str = split_url[0]
50+
domain_parts: List[str] = domain.split(".")
51+
if len(domain_parts) == 4:
52+
return domain_parts[1]
53+
return None
54+
55+
56+
def _is_valid_sqs_url(url: str) -> bool:
57+
"""
58+
Checks if the URL is a valid SQS URL.
59+
"""
60+
if url is None:
61+
return False
62+
split_url: List[str] = url.split("/")
63+
return (
64+
len(split_url) == 3
65+
and split_url[0].lower().startswith("sqs")
66+
and _is_account_id(split_url[1])
67+
and _is_valid_queue_name(split_url[2])
68+
)
69+
2670

2771
def _is_account_id(input_str: str) -> bool:
2872
if input_str is None or len(input_str) != 12:

0 commit comments

Comments
 (0)