Skip to content

Commit c34f139

Browse files
authored
Improve local operation resolution (#113)
*Issue:* In the [`get_ingress_operation()`](https://github.com/aws-observability/aws-otel-python-instrumentation/blob/main/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_span_processing_util.py#L43) method, we try to resolve the `aws.local.operation` by first using the span name. If the span name is not meaningful, we [try to generate the value](https://github.com/aws-observability/aws-otel-python-instrumentation/blob/main/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_span_processing_util.py#L162) by combining `http.method` and `http.target` values from the span. If the `http.target` value is not present, we set the `aws.local.operation` to `UnknownOperation`. In our testing, we found that quite often we get ingress requests where the span name is not meaningful and the `http.target` is also not set by the instrumentation. So we end up getting a lot of `UnknownOperation` for such requests. *Proposal:* If the span name is not meaningful, and the `http.target` is not present, then we try and parse the `http.url` if present to only pick the first part of the url path (to minimize the probability of high cardinality values) and combine it with the `http.method` to get the local operation. Similar logic is employed in [resolving the remote operation](https://github.com/aws-observability/aws-otel-python-instrumentation/blob/566d1629b3ce4d26436c6dcad52f55f2b9e5873a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py#L302-L320). By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 566d162 commit c34f139

File tree

2 files changed

+116
-11
lines changed

2 files changed

+116
-11
lines changed

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

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import json
55
import os
66
from typing import Dict, List
7+
from urllib.parse import ParseResult, urlparse
78

89
from amazon.opentelemetry.distro._aws_attribute_keys import AWS_CONSUMER_PARENT_SPAN_KIND, AWS_LOCAL_OPERATION
910
from opentelemetry.sdk.trace import InstrumentationScope, ReadableSpan
@@ -161,19 +162,25 @@ def _is_valid_operation(span: ReadableSpan, operation: str) -> bool:
161162

162163
def _generate_ingress_operation(span: ReadableSpan) -> str:
163164
"""
164-
When span name is not meaningful(null, unknown or http_method value) as operation name for http use cases. Will try
165-
to extract the operation name from http target string
165+
When span name is not meaningful, this method is invoked to try to extract the operation name from either
166+
`http.target`, if present, or from `http.url`, and combine with `http.method`.
166167
"""
167168
operation: str = UNKNOWN_OPERATION
169+
http_path: str = None
168170
if is_key_present(span, SpanAttributes.HTTP_TARGET):
169-
http_target: str = span.attributes.get(SpanAttributes.HTTP_TARGET)
170-
# get the first part from API path string as operation value
171-
# the more levels/parts we get from API path the higher chance for getting high cardinality data
172-
if http_target is not None:
173-
operation = extract_api_path_value(http_target)
174-
if is_key_present(span, SpanAttributes.HTTP_METHOD):
175-
http_method: str = span.attributes.get(SpanAttributes.HTTP_METHOD)
176-
if http_method is not None:
177-
operation = http_method + " " + operation
171+
http_path = span.attributes.get(SpanAttributes.HTTP_TARGET)
172+
elif is_key_present(span, SpanAttributes.HTTP_URL):
173+
http_url = span.attributes.get(SpanAttributes.HTTP_URL)
174+
url: ParseResult = urlparse(http_url)
175+
http_path = url.path
176+
177+
# get the first part from API path string as operation value
178+
# the more levels/parts we get from API path the higher chance for getting high cardinality data
179+
if http_path is not None:
180+
operation = extract_api_path_value(http_path)
181+
if is_key_present(span, SpanAttributes.HTTP_METHOD):
182+
http_method: str = span.attributes.get(SpanAttributes.HTTP_METHOD)
183+
if http_method is not None:
184+
operation = http_method + " " + operation
178185

179186
return operation

aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
# SPDX-License-Identifier: Apache-2.0
3+
4+
# pylint: disable=too-many-lines
5+
36
from typing import Dict, List, Optional
47
from unittest import TestCase
58
from unittest.mock import MagicMock
@@ -303,6 +306,101 @@ def test_server_span_with_span_name_with_http_target(self):
303306
self._mock_attribute(SpanAttributes.HTTP_METHOD, None)
304307
self._mock_attribute(SpanAttributes.HTTP_TARGET, None)
305308

309+
def test_server_span_with_span_name_with_target_and_url(self):
310+
# when http.target & http.url are present, the local operation should be derived from the http.target
311+
self._update_resource_with_service_name()
312+
self.span_mock.name = "POST"
313+
self._mock_attribute(
314+
[SpanAttributes.HTTP_METHOD, SpanAttributes.HTTP_TARGET, SpanAttributes.HTTP_URL],
315+
["POST", "/my-target/09876", "http://127.0.0.1:8000/payment/123"],
316+
)
317+
318+
expected_attributes: Attributes = {
319+
AWS_SPAN_KIND: SpanKind.SERVER.name,
320+
AWS_LOCAL_SERVICE: _SERVICE_NAME_VALUE,
321+
AWS_LOCAL_OPERATION: "POST /my-target",
322+
}
323+
self._validate_attributes_produced_for_non_local_root_span_of_kind(expected_attributes, SpanKind.SERVER)
324+
self._mock_attribute(SpanAttributes.HTTP_METHOD, None)
325+
self._mock_attribute(SpanAttributes.HTTP_TARGET, None)
326+
self._mock_attribute(SpanAttributes.HTTP_URL, None)
327+
328+
def test_server_span_with_span_name_with_http_url(self):
329+
self._update_resource_with_service_name()
330+
self.span_mock.name = "POST"
331+
self._mock_attribute(
332+
[SpanAttributes.HTTP_METHOD, SpanAttributes.HTTP_URL], ["POST", "http://127.0.0.1:8000/payment/123"]
333+
)
334+
335+
expected_attributes: Attributes = {
336+
AWS_SPAN_KIND: SpanKind.SERVER.name,
337+
AWS_LOCAL_SERVICE: _SERVICE_NAME_VALUE,
338+
AWS_LOCAL_OPERATION: "POST /payment",
339+
}
340+
self._validate_attributes_produced_for_non_local_root_span_of_kind(expected_attributes, SpanKind.SERVER)
341+
self._mock_attribute(SpanAttributes.HTTP_METHOD, None)
342+
self._mock_attribute(SpanAttributes.HTTP_URL, None)
343+
344+
def test_server_span_with_http_url_with_no_path(self):
345+
# http.url with no path should result in local operation to be "POST /"
346+
self._update_resource_with_service_name()
347+
self.span_mock.name = "POST"
348+
self._mock_attribute([SpanAttributes.HTTP_METHOD, SpanAttributes.HTTP_URL], ["POST", "http://www.example.com"])
349+
350+
expected_attributes: Attributes = {
351+
AWS_SPAN_KIND: SpanKind.SERVER.name,
352+
AWS_LOCAL_SERVICE: _SERVICE_NAME_VALUE,
353+
AWS_LOCAL_OPERATION: "POST /",
354+
}
355+
self._validate_attributes_produced_for_non_local_root_span_of_kind(expected_attributes, SpanKind.SERVER)
356+
self._mock_attribute(SpanAttributes.HTTP_METHOD, None)
357+
self._mock_attribute(SpanAttributes.HTTP_URL, None)
358+
359+
def test_server_span_with_http_url_as_none(self):
360+
# if http.url is none, local operation should default to UnknownOperation
361+
self._update_resource_with_service_name()
362+
self.span_mock.name = "POST"
363+
self._mock_attribute([SpanAttributes.HTTP_METHOD, SpanAttributes.HTTP_URL], ["POST", None])
364+
365+
expected_attributes: Attributes = {
366+
AWS_SPAN_KIND: SpanKind.SERVER.name,
367+
AWS_LOCAL_SERVICE: _SERVICE_NAME_VALUE,
368+
AWS_LOCAL_OPERATION: _UNKNOWN_OPERATION,
369+
}
370+
self._validate_attributes_produced_for_non_local_root_span_of_kind(expected_attributes, SpanKind.SERVER)
371+
self._mock_attribute(SpanAttributes.HTTP_METHOD, None)
372+
self._mock_attribute(SpanAttributes.HTTP_URL, None)
373+
374+
def test_server_span_with_http_url_as_empty(self):
375+
# if http.url is empty, local operation should default to "POST /"
376+
self._update_resource_with_service_name()
377+
self.span_mock.name = "POST"
378+
self._mock_attribute([SpanAttributes.HTTP_METHOD, SpanAttributes.HTTP_URL], ["POST", ""])
379+
380+
expected_attributes: Attributes = {
381+
AWS_SPAN_KIND: SpanKind.SERVER.name,
382+
AWS_LOCAL_SERVICE: _SERVICE_NAME_VALUE,
383+
AWS_LOCAL_OPERATION: "POST /",
384+
}
385+
self._validate_attributes_produced_for_non_local_root_span_of_kind(expected_attributes, SpanKind.SERVER)
386+
self._mock_attribute(SpanAttributes.HTTP_METHOD, None)
387+
self._mock_attribute(SpanAttributes.HTTP_URL, None)
388+
389+
def test_server_span_with_http_url_as_invalid(self):
390+
# if http.url is invalid, local operation should default to "POST /"
391+
self._update_resource_with_service_name()
392+
self.span_mock.name = "POST"
393+
self._mock_attribute([SpanAttributes.HTTP_METHOD, SpanAttributes.HTTP_URL], ["POST", "invalid_url"])
394+
395+
expected_attributes: Attributes = {
396+
AWS_SPAN_KIND: SpanKind.SERVER.name,
397+
AWS_LOCAL_SERVICE: _SERVICE_NAME_VALUE,
398+
AWS_LOCAL_OPERATION: "POST /",
399+
}
400+
self._validate_attributes_produced_for_non_local_root_span_of_kind(expected_attributes, SpanKind.SERVER)
401+
self._mock_attribute(SpanAttributes.HTTP_METHOD, None)
402+
self._mock_attribute(SpanAttributes.HTTP_URL, None)
403+
306404
def test_producer_span_with_attributes(self):
307405
self._update_resource_with_service_name()
308406
self._mock_attribute(

0 commit comments

Comments
 (0)