|
| 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 |
0 commit comments