Skip to content

Commit 586504e

Browse files
authored
Suppress appsignal metrics and attributes from boto3sqs spans (#127)
*Issue* - Boto3 calls use botocore underneath and OTel offers instrumentation for both libraries - [boto3sqs](https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-boto3sqs) (specifically SQS relates spans) and [botocore](https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-botocore) - For boto3 operations on SQS (send, receive, delete), we generate the following set of spans: - SendMessage: 1 PRODUCER span (from boto3sqs) + 1 CLIENT span (from botocore) per operation - ReceiveMessages: 1 CONSUMER receive span (from boto3sqs) per operation + 1 CONSUMER process span (from boto3sqs) per message received + 1 CLIENT span (from botocore) per operation - DeleteMessage: 1 CLIENT span (from botocore) per operation The above behavior causes the current logic to generate multiple dependency metrics per operation whereas we want to to generate only 1 dependency metrics per operation. *Note: the below image doesn't include the SendMessage operation so there are no spans for it* <img width="1707" alt="before" src="https://github.com/aws-observability/aws-otel-python-instrumentation/assets/50466688/09ad0e1a-d69d-40c3-8e56-d796dd2f54e7"> *Description of changes:* - Identify if the span is generated by the `boto3sqs` instrumentation by checking the instrumentation scope. - If yes, then we don't generate any service and dependency metrics from such span. We also do not populate any `AWS_*` attribute on the span. After making the changes, this is what the dependency metrics looks like. <img width="1812" alt="Screenshot 2024-03-27 at 2 48 59 PM" src="https://github.com/aws-observability/aws-otel-python-instrumentation/assets/50466688/53a95217-3822-49e5-b7be-56208350f4aa"> 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 0bf0163 commit 586504e

File tree

3 files changed

+68
-46
lines changed

3 files changed

+68
-46
lines changed

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

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@
2020
LOCAL_ROOT: str = "LOCAL_ROOT"
2121

2222
# Useful constants
23-
_SQS_RECEIVE_MESSAGE_SPAN_NAME: str = "Sqs.ReceiveMessage"
24-
_AWS_SDK_INSTRUMENTATION_SCOPE_PREFIX: str = "io.opentelemetry.aws-sdk-"
23+
_BOTO3SQS_INSTRUMENTATION_SCOPE: str = "opentelemetry.instrumentation.boto3sqs"
2524

2625
# Max keyword length supported by parsing into remote_operation from DB_STATEMENT
2726
MAX_KEYWORD_LENGTH = 27
@@ -87,14 +86,14 @@ def is_aws_sdk_span(span: ReadableSpan) -> bool:
8786

8887

8988
def should_generate_service_metric_attributes(span: ReadableSpan) -> bool:
90-
return (is_local_root(span) and not _is_sqs_receive_message_consumer_span(span)) or SpanKind.SERVER == span.kind
89+
return (is_local_root(span) and not _is_boto3sqs_span(span)) or SpanKind.SERVER == span.kind
9190

9291

9392
def should_generate_dependency_metric_attributes(span: ReadableSpan) -> bool:
9493
return (
9594
SpanKind.CLIENT == span.kind
96-
or SpanKind.PRODUCER == span.kind
97-
or (_is_dependency_consumer_span(span) and not _is_sqs_receive_message_consumer_span(span))
95+
or (SpanKind.PRODUCER == span.kind and not _is_boto3sqs_span(span))
96+
or (_is_dependency_consumer_span(span) and not _is_boto3sqs_span(span))
9897
)
9998

10099

@@ -118,17 +117,18 @@ def is_local_root(span: ReadableSpan) -> bool:
118117
return span.parent is None or not span.parent.is_valid or span.parent.is_remote
119118

120119

121-
def _is_sqs_receive_message_consumer_span(span: ReadableSpan) -> bool:
122-
"""To identify the SQS consumer spans produced by AWS SDK instrumentation"""
123-
messaging_operation: str = span.attributes.get(SpanAttributes.MESSAGING_OPERATION)
120+
def _is_boto3sqs_span(span: ReadableSpan) -> bool:
121+
"""
122+
To identify if the span produced is from the boto3sqs instrumentation.
123+
We use this to identify the boto3sqs spans and not generate metrics from the since we will generate
124+
the same metrics from botocore spans.
125+
"""
126+
# TODO: Evaluate if we can bring the boto3sqs spans back to generate metrics and not have to suppress them.
124127
instrumentation_scope: InstrumentationScope = span.instrumentation_scope
125-
126128
return (
127-
(span.name is not None and _SQS_RECEIVE_MESSAGE_SPAN_NAME.casefold() == span.name.casefold())
128-
and SpanKind.CONSUMER == span.kind
129-
and instrumentation_scope is not None
130-
and instrumentation_scope.name.startswith(_AWS_SDK_INSTRUMENTATION_SCOPE_PREFIX)
131-
and (messaging_operation is None or messaging_operation == MessagingOperationValues.PROCESS)
129+
instrumentation_scope is not None
130+
and instrumentation_scope.name is not None
131+
and _BOTO3SQS_INSTRUMENTATION_SCOPE.casefold() == instrumentation_scope.name.casefold()
132132
)
133133

134134

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

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,12 @@ class TestAwsMetricAttributeGenerator(TestCase):
5353
def setUp(self):
5454
self.attributes_mock: Attributes = MagicMock()
5555
self.instrumentation_scope_info_mock: InstrumentationScope = MagicMock()
56-
self.instrumentation_scope_info_mock._name = "Scope name"
56+
self.instrumentation_scope_info_mock.name = "Scope name"
5757
self.span_mock: ReadableSpan = MagicMock()
5858
self.span_mock.name = None
5959
self.span_mock.attributes = self.attributes_mock
6060
self.attributes_mock.get.return_value = None
61-
self.span_mock._instrumentation_scope = self.instrumentation_scope_info_mock
61+
self.span_mock.instrumentation_scope = self.instrumentation_scope_info_mock
6262
self.span_mock.get_span_context.return_value = MagicMock()
6363
self.parent_span_context: SpanContext = MagicMock()
6464
self.parent_span_context.is_valid = True
@@ -772,6 +772,39 @@ def test_both_metric_when_local_root_consumer_process(self):
772772
self.assertIsNotNone(service_attributes)
773773
self.assertIsNotNone(dependency_attributes)
774774

775+
def test_local_root_boto3_span(self):
776+
self._update_resource_with_service_name()
777+
self.parent_span_context.is_valid = False
778+
self.span_mock.kind = SpanKind.PRODUCER
779+
self.span_mock.instrumentation_scope.name = "opentelemetry.instrumentation.boto3sqs"
780+
781+
actual_attributes: Attributes = _GENERATOR.generate_metric_attributes_dict_from_span(
782+
self.span_mock, self.resource
783+
)
784+
service_attributes: Attributes = actual_attributes.get(SERVICE_METRIC)
785+
dependency_attributes: Attributes = actual_attributes.get(DEPENDENCY_METRIC)
786+
787+
# boto3sqs spans shouldn't generate aws service attributes even local root
788+
self.assertIsNone(service_attributes)
789+
# boto3sqs spans shouldn't generate aws dependency attributes
790+
self.assertIsNone(dependency_attributes)
791+
792+
def test_non_local_root_boto3_span(self):
793+
self._update_resource_with_service_name()
794+
self.span_mock.kind = SpanKind.CONSUMER
795+
self.span_mock.instrumentation_scope.name = "opentelemetry.instrumentation.boto3sqs"
796+
797+
actual_attributes: Attributes = _GENERATOR.generate_metric_attributes_dict_from_span(
798+
self.span_mock, self.resource
799+
)
800+
service_attributes: Attributes = actual_attributes.get(SERVICE_METRIC)
801+
dependency_attributes: Attributes = actual_attributes.get(DEPENDENCY_METRIC)
802+
803+
# boto3sqs spans shouldn't generate aws service attributes
804+
self.assertIsNone(service_attributes)
805+
# boto3sqs spans shouldn't generate aws dependency attributes
806+
self.assertIsNone(dependency_attributes)
807+
775808
def test_normalize_service_name_non_aws_sdk_span(self):
776809
service_name: str = "non aws service"
777810
self._mock_attribute([SpanAttributes.RPC_SERVICE], [service_name])

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

Lines changed: 19 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -307,45 +307,32 @@ def attributes_get_side_effect(key):
307307

308308
self.assertTrue(is_consumer_process_span(self.span_data_mock))
309309

310-
# check that AWS SDK v1 SQS ReceiveMessage consumer spans metrics are suppressed
311-
def test_no_metric_attributes_for_sqs_consumer_span_aws_sdk_v1(self):
310+
# check that boto3 SQS spans metrics are suppressed
311+
def test_no_metric_attributes_for_boto3sqs_producer_span(self):
312312
instrumentation_scope_mock: InstrumentationScope = MagicMock()
313-
instrumentation_scope_mock.name = "io.opentelemetry.aws-sdk-1.11"
313+
instrumentation_scope_mock.name = "opentelemetry.instrumentation.boto3sqs"
314314
self.span_data_mock.instrumentation_scope = instrumentation_scope_mock
315-
self.span_data_mock.kind = SpanKind.CONSUMER
316-
self.span_data_mock.name = "SQS.ReceiveMessage"
315+
self.span_data_mock.kind = SpanKind.PRODUCER
316+
self.span_data_mock.name = "testQueue send"
317317
self.assertFalse(should_generate_service_metric_attributes(self.span_data_mock))
318318
self.assertFalse(should_generate_dependency_metric_attributes(self.span_data_mock))
319319

320-
# check that AWS SDK v1 SQS ReceiveMessage consumer spans metrics are suppressed
321-
def test_no_metric_attributes_for_sqs_consumer_span_aws_sdk_v2(self):
320+
def test_no_metric_attributes_for_boto3sqs_consumer_span(self):
322321
instrumentation_scope_mock: InstrumentationScope = MagicMock()
323-
instrumentation_scope_mock.name = "io.opentelemetry.aws-sdk-2.2"
322+
instrumentation_scope_mock.name = "opentelemetry.instrumentation.boto3sqs"
324323
self.span_data_mock.instrumentation_scope = instrumentation_scope_mock
325324
self.span_data_mock.kind = SpanKind.CONSUMER
326-
self.span_data_mock.name = "SQS.ReceiveMessage"
325+
self.span_data_mock.name = "testQueue receive"
326+
327327
self.assertFalse(should_generate_service_metric_attributes(self.span_data_mock))
328328
self.assertFalse(should_generate_dependency_metric_attributes(self.span_data_mock))
329329

330-
# check that SQS ReceiveMessage consumer spans metrics are still generated for other instrumentation
331-
def test_metric_attributes_generated_for_other_instrumentation_sqs_consumer_span(self):
330+
def test_no_metric_attributes_for_boto3sqs_process_span(self):
332331
instrumentation_scope_info_mock = MagicMock()
333-
instrumentation_scope_info_mock.name = "my-instrumentation"
332+
instrumentation_scope_info_mock.name = "opentelemetry.instrumentation.boto3sqs"
334333
self.span_data_mock.instrumentation_scope = instrumentation_scope_info_mock
335334
self.span_data_mock.kind = SpanKind.CONSUMER
336-
self.span_data_mock.name = "Sqs.ReceiveMessage"
337-
338-
self.assertTrue(should_generate_service_metric_attributes(self.span_data_mock))
339-
self.assertTrue(should_generate_dependency_metric_attributes(self.span_data_mock))
340-
341-
# check that SQS ReceiveMessage consumer span metrics are suppressed if messaging operation
342-
# is process and not receive
343-
def test_no_metric_attributes_for_aws_sdk_sqs_consumer_process_span(self):
344-
instrumentation_scope_info_mock = MagicMock()
345-
instrumentation_scope_info_mock.name = "io.opentelemetry.aws-sdk-2.2"
346-
self.span_data_mock.instrumentation_scope = instrumentation_scope_info_mock
347-
self.span_data_mock.kind = SpanKind.CONSUMER
348-
self.span_data_mock.name = "Sqs.ReceiveMessage"
335+
self.span_data_mock.name = "testQueue process"
349336

350337
def attributes_get_side_effect_process(key):
351338
if key == SpanAttributes.MESSAGING_OPERATION:
@@ -358,12 +345,14 @@ def attributes_get_side_effect_process(key):
358345
self.assertFalse(should_generate_service_metric_attributes(self.span_data_mock))
359346
self.assertFalse(should_generate_dependency_metric_attributes(self.span_data_mock))
360347

361-
def attributes_get_side_effect_receive(key):
362-
if key == SpanAttributes.MESSAGING_OPERATION:
363-
return MessagingOperationValues.RECEIVE
364-
return None
348+
# check that consumer spans metrics are still generated for other instrumentation
349+
def test_metric_attributes_generated_for_instrumentation_other_than_boto3sqs(self):
350+
instrumentation_scope_info_mock = MagicMock()
351+
instrumentation_scope_info_mock.name = "my-instrumentation"
352+
self.span_data_mock.instrumentation_scope = instrumentation_scope_info_mock
353+
self.span_data_mock.kind = SpanKind.CONSUMER
354+
self.span_data_mock.name = "testQueue receive"
365355

366-
self.attributes_mock.get.side_effect = attributes_get_side_effect_receive
367356
self.assertTrue(should_generate_service_metric_attributes(self.span_data_mock))
368357
self.assertTrue(should_generate_dependency_metric_attributes(self.span_data_mock))
369358

0 commit comments

Comments
 (0)