Skip to content

Commit 73b4f68

Browse files
authored
Add AwsSpanProcessingUtil (aws-observability#15)
AwsSpanProcessingUtil is a utility module designed to support shared logic across AWS Span Processors. This is a roughly-carbon-copy file of https://github.com/aws-observability/aws-otel-java-instrumentation/blob/main/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsSpanProcessingUtil.java By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
2 parents 5f7989a + 1337fb7 commit 73b4f68

File tree

2 files changed

+170
-0
lines changed

2 files changed

+170
-0
lines changed
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Utility module designed to support shared logic across AWS Span Processors."""
4+
from amazon.opentelemetry.distro._aws_attribute_keys import AwsAttributeKeys
5+
from opentelemetry.sdk.trace import InstrumentationScope, ReadableSpan
6+
from opentelemetry.semconv.trace import MessagingOperationValues, SpanAttributes
7+
from opentelemetry.trace import SpanKind
8+
9+
# Default attribute values if no valid span attribute value is identified
10+
UNKNOWN_SERVICE: str = "UnknownService"
11+
UNKNOWN_OPERATION: str = "UnknownOperation"
12+
UNKNOWN_REMOTE_SERVICE: str = "UnknownRemoteService"
13+
UNKNOWN_REMOTE_OPERATION: str = "UnknownRemoteOperation"
14+
INTERNAL_OPERATION: str = "InternalOperation"
15+
LOCAL_ROOT: str = "LOCAL_ROOT"
16+
17+
# Useful constants
18+
_SQS_RECEIVE_MESSAGE_SPAN_NAME: str = "Sqs.ReceiveMessage"
19+
_AWS_SDK_INSTRUMENTATION_SCOPE_PREFIX: str = "io.opentelemetry.aws-sdk-"
20+
21+
22+
def get_ingress_operation(__, span: ReadableSpan) -> str:
23+
"""
24+
Ingress operation (i.e. operation for Server and Consumer spans) will be generated from "http.method + http.target/
25+
with the first API path parameter" if the default span name is None, UnknownOperation or http.method value.
26+
"""
27+
operation: str = span.name
28+
if should_use_internal_operation(span):
29+
operation = INTERNAL_OPERATION
30+
elif not _is_valid_operation(span, operation):
31+
operation = _generate_ingress_operation(span)
32+
return operation
33+
34+
35+
def get_egress_operation(span: ReadableSpan) -> str:
36+
if should_use_internal_operation(span):
37+
return INTERNAL_OPERATION
38+
return span.attributes.get(AwsAttributeKeys.AWS_LOCAL_OPERATION)
39+
40+
41+
def extract_api_path_value(http_target: str) -> str:
42+
"""Extract the first part from API http target if it exists
43+
44+
Args
45+
http_target - http request target string value. Eg, /payment/1234
46+
Returns
47+
the first part from the http target. Eg, /payment
48+
:return:
49+
"""
50+
if http_target is None or len(http_target) == 0:
51+
return "/"
52+
paths: [str] = http_target.split("/")
53+
if len(paths) > 1:
54+
return "/" + paths[1]
55+
return "/"
56+
57+
58+
def is_key_present(span: ReadableSpan, key: str) -> bool:
59+
return span.attributes.get(key) is not None
60+
61+
62+
def is_aws_sdk_span(span: ReadableSpan) -> bool:
63+
# https://opentelemetry.io/docs/specs/otel/trace/semantic_conventions/instrumentation/aws-sdk/#common-attributes
64+
return "aws-api" == span.attributes.get(SpanAttributes.RPC_SYSTEM)
65+
66+
67+
def should_generate_service_metric_attributes(span: ReadableSpan) -> bool:
68+
return (is_local_root(span) and not _is_sqs_receive_message_consumer_span(span)) or SpanKind.SERVER == span.kind
69+
70+
71+
def should_generate_dependency_metric_attributes(span: ReadableSpan) -> bool:
72+
return (
73+
SpanKind.CLIENT == span.kind
74+
or SpanKind.PRODUCER == span.kind
75+
or (_is_dependency_consumer_span(span) and not _is_sqs_receive_message_consumer_span(span))
76+
)
77+
78+
79+
def is_consumer_process_span(span: ReadableSpan) -> bool:
80+
messaging_operation: str = span.attributes.get(SpanAttributes.MESSAGING_OPERATION)
81+
return SpanKind.CONSUMER == span.kind and MessagingOperationValues.PROCESS == messaging_operation
82+
83+
84+
def should_use_internal_operation(span: ReadableSpan) -> bool:
85+
"""
86+
Any spans that are Local Roots and also not SERVER should have aws.local.operation renamed toInternalOperation.
87+
"""
88+
return is_local_root(span) and not SpanKind.SERVER == span.kind
89+
90+
91+
def is_local_root(span: ReadableSpan) -> bool:
92+
"""
93+
A span is a local root if it has no parent or if the parent is remote. This function checks the parent context
94+
and returns true if it is a local root.
95+
"""
96+
return span.parent is None or not span.parent.is_valid or span.parent.is_remote
97+
98+
99+
def _is_sqs_receive_message_consumer_span(span: ReadableSpan) -> bool:
100+
"""To identify the SQS consumer spans produced by AWS SDK instrumentation"""
101+
messaging_operation: str = span.attributes.get(SpanAttributes.MESSAGING_OPERATION)
102+
instrumentation_scope: InstrumentationScope = span.instrumentation_scope
103+
104+
return (
105+
_SQS_RECEIVE_MESSAGE_SPAN_NAME.casefold() == span.name.casefold()
106+
and SpanKind.CONSUMER == span.kind
107+
and instrumentation_scope is not None
108+
and instrumentation_scope.name.startswith(_AWS_SDK_INSTRUMENTATION_SCOPE_PREFIX)
109+
and (messaging_operation is None or messaging_operation == MessagingOperationValues.PROCESS)
110+
)
111+
112+
113+
def _is_dependency_consumer_span(span: ReadableSpan) -> bool:
114+
if SpanKind.CONSUMER != span.kind:
115+
return False
116+
117+
if is_consumer_process_span(span):
118+
if is_local_root(span):
119+
return True
120+
parent_span_kind: str = span.attributes.get(AwsAttributeKeys.AWS_CONSUMER_PARENT_SPAN_KIND)
121+
return SpanKind.CONSUMER != parent_span_kind
122+
123+
return True
124+
125+
126+
def _is_valid_operation(span: ReadableSpan, operation: str) -> bool:
127+
"""
128+
When Span name is null, UnknownOperation or HttpMethod value, it will be treated as invalid local operation value
129+
that needs to be further processed
130+
"""
131+
if operation is None or operation == UNKNOWN_OPERATION:
132+
return False
133+
134+
if is_key_present(span, SpanAttributes.HTTP_METHOD):
135+
http_method: str = span.attributes.get(SpanAttributes.HTTP_METHOD)
136+
return operation != http_method
137+
138+
return True
139+
140+
141+
def _generate_ingress_operation(span: ReadableSpan) -> str:
142+
"""
143+
When span name is not meaningful(null, unknown or http_method value) as operation name for http use cases. Will try
144+
to extract the operation name from http target string
145+
"""
146+
operation: str = UNKNOWN_OPERATION
147+
if is_key_present(span, SpanAttributes.HTTP_TARGET):
148+
http_target: str = span.attributes.get(SpanAttributes.HTTP_TARGET)
149+
# get the first part from API path string as operation value
150+
# the more levels/parts we get from API path the higher chance for getting high cardinality data
151+
if http_target is not None:
152+
operation = extract_api_path_value(http_target)
153+
if is_key_present(span, SpanAttributes.HTTP_METHOD):
154+
http_method: str = span.attributes.get(SpanAttributes.HTTP_METHOD)
155+
if http_method is not None:
156+
operation = http_method + " " + operation
157+
158+
return operation
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from unittest import TestCase
4+
5+
from amazon.opentelemetry.distro._aws_span_processing_util import is_key_present
6+
from opentelemetry.sdk.trace import ReadableSpan
7+
8+
9+
class TestAwsSpanProcessingUtil(TestCase):
10+
def test_basic(self):
11+
span: ReadableSpan = ReadableSpan(name="test")
12+
self.assertFalse(is_key_present(span, "test"))

0 commit comments

Comments
 (0)