boto3sqs: Make propagation compatible with other instrumentations and add 'messaging.url' span attribute (#1234)

* boto3sqs: Fix various issues

* do not use 'otel' prefix for propagation keys to make propagation
  compatible with other SQS instrumentations like Node.Js
  Inject propergator.fields keys into the MessageAttributeNames argument
  for 'receive_message' calls to retreive the corresponding message attributes
* add 'messaging.url' span attribute to SQS spans
* add boto3sqs instrumentation to tox.ini to run tests in CI
* add some basic unit tests

* changelog

* fix linting issues

* unset instrumented flag on uninstrument
This commit is contained in:
Mario Jonke
2022-08-23 09:29:58 +02:00
committed by GitHub
parent 7625b82dff
commit f48b3136c4
5 changed files with 330 additions and 143 deletions

View File

@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Fixed
- `opentelemetry-instrumentation-boto3sqs` Make propagation compatible with other SQS instrumentations, add 'messaging.url' span attribute, and fix missing package dependencies.
([#1234](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1234))
## [1.12.0-0.33b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0-0.33b0) - 2022-08-08 ## [1.12.0-0.33b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0-0.33b0) - 2022-08-08
- Adding multiple db connections support for django-instrumentation's sqlcommenter - Adding multiple db connections support for django-instrumentation's sqlcommenter

View File

@ -46,10 +46,13 @@ package_dir=
packages=find_namespace: packages=find_namespace:
install_requires = install_requires =
opentelemetry-api ~= 1.12 opentelemetry-api ~= 1.12
opentelemetry-semantic-conventions == 0.33b0
opentelemetry-instrumentation == 0.33b0
wrapt >= 1.0.0, < 2.0.0 wrapt >= 1.0.0, < 2.0.0
[options.extras_require] [options.extras_require]
test = test =
opentelemetry-test-utils == 0.33b0
[options.packages.find] [options.packages.find]
where = src where = src

View File

@ -29,7 +29,7 @@ Usage
Boto3SQSInstrumentor().instrument() Boto3SQSInstrumentor().instrument()
""" """
import logging import logging
from typing import Any, Collection, Dict, Generator, List, Optional from typing import Any, Collection, Dict, Generator, List, Mapping, Optional
import boto3 import boto3
import botocore.client import botocore.client
@ -53,33 +53,31 @@ from .package import _instruments
from .version import __version__ from .version import __version__
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
# We use this prefix so we can request all instrumentation MessageAttributeNames with a wildcard, without harming
# existing filters _IS_SQS_INSTRUMENTED_ATTRIBUTE = "_otel_boto3sqs_instrumented"
_OPENTELEMETRY_ATTRIBUTE_IDENTIFIER: str = "otel."
_OTEL_IDENTIFIER_LENGTH = len(_OPENTELEMETRY_ATTRIBUTE_IDENTIFIER)
class Boto3SQSGetter(Getter[CarrierT]): class Boto3SQSGetter(Getter[CarrierT]):
def get(self, carrier: CarrierT, key: str) -> Optional[List[str]]: def get(self, carrier: CarrierT, key: str) -> Optional[List[str]]:
value = carrier.get(f"{_OPENTELEMETRY_ATTRIBUTE_IDENTIFIER}{key}", {}) msg_attr = carrier.get(key)
if not value: if not isinstance(msg_attr, Mapping):
return None return None
return [value.get("StringValue")]
value = msg_attr.get("StringValue")
if value is None:
return None
return [value]
def keys(self, carrier: CarrierT) -> List[str]: def keys(self, carrier: CarrierT) -> List[str]:
return [ return list(carrier.keys())
key[_OTEL_IDENTIFIER_LENGTH:]
if key.startswith(_OPENTELEMETRY_ATTRIBUTE_IDENTIFIER)
else key
for key in carrier.keys()
]
class Boto3SQSSetter(Setter[CarrierT]): class Boto3SQSSetter(Setter[CarrierT]):
def set(self, carrier: CarrierT, key: str, value: str) -> None: def set(self, carrier: CarrierT, key: str, value: str) -> None:
# This is a limitation defined by AWS for SQS MessageAttributes size # This is a limitation defined by AWS for SQS MessageAttributes size
if len(carrier.items()) < 10: if len(carrier.items()) < 10:
carrier[f"{_OPENTELEMETRY_ATTRIBUTE_IDENTIFIER}{key}"] = { carrier[key] = {
"StringValue": value, "StringValue": value,
"DataType": "String", "DataType": "String",
} }
@ -145,6 +143,7 @@ class Boto3SQSInstrumentor(BaseInstrumentor):
def _enrich_span( def _enrich_span(
span: Span, span: Span,
queue_name: str, queue_name: str,
queue_url: str,
conversation_id: Optional[str] = None, conversation_id: Optional[str] = None,
operation: Optional[MessagingOperationValues] = None, operation: Optional[MessagingOperationValues] = None,
message_id: Optional[str] = None, message_id: Optional[str] = None,
@ -157,12 +156,12 @@ class Boto3SQSInstrumentor(BaseInstrumentor):
SpanAttributes.MESSAGING_DESTINATION_KIND, SpanAttributes.MESSAGING_DESTINATION_KIND,
MessagingDestinationKindValues.QUEUE.value, MessagingDestinationKindValues.QUEUE.value,
) )
span.set_attribute(SpanAttributes.MESSAGING_URL, queue_url)
if operation: if operation:
span.set_attribute( span.set_attribute(
SpanAttributes.MESSAGING_OPERATION, operation.value SpanAttributes.MESSAGING_OPERATION, operation.value
) )
else:
span.set_attribute(SpanAttributes.MESSAGING_TEMP_DESTINATION, True)
if conversation_id: if conversation_id:
span.set_attribute( span.set_attribute(
SpanAttributes.MESSAGING_CONVERSATION_ID, conversation_id SpanAttributes.MESSAGING_CONVERSATION_ID, conversation_id
@ -190,15 +189,19 @@ class Boto3SQSInstrumentor(BaseInstrumentor):
return queue_url.split("/")[-1] return queue_url.split("/")[-1]
def _create_processing_span( def _create_processing_span(
self, queue_name: str, receipt_handle: str, message: Dict[str, Any] self,
queue_name: str,
queue_url: str,
receipt_handle: str,
message: Dict[str, Any],
) -> None: ) -> None:
message_attributes = message.get("MessageAttributes", {}) message_attributes = message.get("MessageAttributes", {})
links = [] links = []
ctx = propagate.extract(message_attributes, getter=boto3sqs_getter) ctx = propagate.extract(message_attributes, getter=boto3sqs_getter)
if ctx: parent_span_ctx = trace.get_current_span(ctx).get_span_context()
for item in ctx.values(): if parent_span_ctx.is_valid:
if hasattr(item, "get_span_context"): links.append(Link(context=parent_span_ctx))
links.append(Link(context=item.get_span_context()))
span = self._tracer.start_span( span = self._tracer.start_span(
name=f"{queue_name} process", links=links, kind=SpanKind.CONSUMER name=f"{queue_name} process", links=links, kind=SpanKind.CONSUMER
) )
@ -208,11 +211,12 @@ class Boto3SQSInstrumentor(BaseInstrumentor):
Boto3SQSInstrumentor._enrich_span( Boto3SQSInstrumentor._enrich_span(
span, span,
queue_name, queue_name,
queue_url,
message_id=message_id, message_id=message_id,
operation=MessagingOperationValues.PROCESS, operation=MessagingOperationValues.PROCESS,
) )
def _wrap_send_message(self) -> None: def _wrap_send_message(self, sqs_class: type) -> None:
def send_wrapper(wrapped, instance, args, kwargs): def send_wrapper(wrapped, instance, args, kwargs):
if context.get_value(_SUPPRESS_INSTRUMENTATION_KEY): if context.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
return wrapped(*args, **kwargs) return wrapped(*args, **kwargs)
@ -227,7 +231,7 @@ class Boto3SQSInstrumentor(BaseInstrumentor):
kind=SpanKind.PRODUCER, kind=SpanKind.PRODUCER,
end_on_exit=True, end_on_exit=True,
) as span: ) as span:
Boto3SQSInstrumentor._enrich_span(span, queue_name) Boto3SQSInstrumentor._enrich_span(span, queue_name, queue_url)
attributes = kwargs.pop("MessageAttributes", {}) attributes = kwargs.pop("MessageAttributes", {})
propagate.inject(attributes, setter=boto3sqs_setter) propagate.inject(attributes, setter=boto3sqs_setter)
retval = wrapped(*args, MessageAttributes=attributes, **kwargs) retval = wrapped(*args, MessageAttributes=attributes, **kwargs)
@ -239,9 +243,9 @@ class Boto3SQSInstrumentor(BaseInstrumentor):
) )
return retval return retval
wrap_function_wrapper(self._sqs_class, "send_message", send_wrapper) wrap_function_wrapper(sqs_class, "send_message", send_wrapper)
def _wrap_send_message_batch(self) -> None: def _wrap_send_message_batch(self, sqs_class: type) -> None:
def send_batch_wrapper(wrapped, instance, args, kwargs): def send_batch_wrapper(wrapped, instance, args, kwargs):
queue_url = kwargs.get("QueueUrl") queue_url = kwargs.get("QueueUrl")
entries = kwargs.get("Entries") entries = kwargs.get("Entries")
@ -260,12 +264,11 @@ class Boto3SQSInstrumentor(BaseInstrumentor):
for entry in entries: for entry in entries:
entry_id = entry["Id"] entry_id = entry["Id"]
span = self._tracer.start_span( span = self._tracer.start_span(
name=f"{queue_name} send", name=f"{queue_name} send", kind=SpanKind.PRODUCER
kind=SpanKind.PRODUCER,
) )
ids_to_spans[entry_id] = span ids_to_spans[entry_id] = span
Boto3SQSInstrumentor._enrich_span( Boto3SQSInstrumentor._enrich_span(
span, queue_name, conversation_id=entry_id span, queue_name, queue_url, conversation_id=entry_id
) )
with trace.use_span(span): with trace.use_span(span):
if "MessageAttributes" not in entry: if "MessageAttributes" not in entry:
@ -288,15 +291,15 @@ class Boto3SQSInstrumentor(BaseInstrumentor):
return retval return retval
wrap_function_wrapper( wrap_function_wrapper(
self._sqs_class, "send_message_batch", send_batch_wrapper sqs_class, "send_message_batch", send_batch_wrapper
) )
def _wrap_receive_message(self) -> None: def _wrap_receive_message(self, sqs_class: type) -> None:
def receive_message_wrapper(wrapped, instance, args, kwargs): def receive_message_wrapper(wrapped, instance, args, kwargs):
queue_url = kwargs.get("QueueUrl") queue_url = kwargs.get("QueueUrl")
message_attribute_names = kwargs.pop("MessageAttributeNames", []) message_attribute_names = kwargs.pop("MessageAttributeNames", [])
message_attribute_names.append( message_attribute_names.extend(
f"{_OPENTELEMETRY_ATTRIBUTE_IDENTIFIER}*" propagate.get_global_textmap().fields
) )
queue_name = Boto3SQSInstrumentor._extract_queue_name_from_url( queue_name = Boto3SQSInstrumentor._extract_queue_name_from_url(
queue_url queue_url
@ -309,6 +312,7 @@ class Boto3SQSInstrumentor(BaseInstrumentor):
Boto3SQSInstrumentor._enrich_span( Boto3SQSInstrumentor._enrich_span(
span, span,
queue_name, queue_name,
queue_url,
operation=MessagingOperationValues.RECEIVE, operation=MessagingOperationValues.RECEIVE,
) )
retval = wrapped( retval = wrapped(
@ -327,7 +331,7 @@ class Boto3SQSInstrumentor(BaseInstrumentor):
receipt_handle receipt_handle
) )
self._create_processing_span( self._create_processing_span(
queue_name, receipt_handle, message queue_name, queue_url, receipt_handle, message
) )
retval["Messages"] = Boto3SQSInstrumentor.ContextableList( retval["Messages"] = Boto3SQSInstrumentor.ContextableList(
messages messages
@ -335,10 +339,11 @@ class Boto3SQSInstrumentor(BaseInstrumentor):
return retval return retval
wrap_function_wrapper( wrap_function_wrapper(
self._sqs_class, "receive_message", receive_message_wrapper sqs_class, "receive_message", receive_message_wrapper
) )
def _wrap_delete_message(self) -> None: @staticmethod
def _wrap_delete_message(sqs_class: type) -> None:
def delete_message_wrapper(wrapped, instance, args, kwargs): def delete_message_wrapper(wrapped, instance, args, kwargs):
receipt_handle = kwargs.get("ReceiptHandle") receipt_handle = kwargs.get("ReceiptHandle")
if receipt_handle: if receipt_handle:
@ -346,10 +351,11 @@ class Boto3SQSInstrumentor(BaseInstrumentor):
return wrapped(*args, **kwargs) return wrapped(*args, **kwargs)
wrap_function_wrapper( wrap_function_wrapper(
self._sqs_class, "delete_message", delete_message_wrapper sqs_class, "delete_message", delete_message_wrapper
) )
def _wrap_delete_message_batch(self) -> None: @staticmethod
def _wrap_delete_message_batch(sqs_class: type) -> None:
def delete_message_wrapper_batch(wrapped, instance, args, kwargs): def delete_message_wrapper_batch(wrapped, instance, args, kwargs):
entries = kwargs.get("Entries") entries = kwargs.get("Entries")
for entry in entries: for entry in entries:
@ -361,9 +367,7 @@ class Boto3SQSInstrumentor(BaseInstrumentor):
return wrapped(*args, **kwargs) return wrapped(*args, **kwargs)
wrap_function_wrapper( wrap_function_wrapper(
self._sqs_class, sqs_class, "delete_message_batch", delete_message_wrapper_batch
"delete_message_batch",
delete_message_wrapper_batch,
) )
def _wrap_client_creation(self) -> None: def _wrap_client_creation(self) -> None:
@ -375,43 +379,45 @@ class Boto3SQSInstrumentor(BaseInstrumentor):
def client_wrapper(wrapped, instance, args, kwargs): def client_wrapper(wrapped, instance, args, kwargs):
retval = wrapped(*args, **kwargs) retval = wrapped(*args, **kwargs)
if not self._did_decorate: self._decorate_sqs(type(retval))
self._decorate_sqs()
return retval return retval
wrap_function_wrapper(boto3, "client", client_wrapper) wrap_function_wrapper(boto3, "client", client_wrapper)
def _decorate_sqs(self) -> None: def _decorate_sqs(self, sqs_class: type) -> None:
""" """
Since botocore creates classes on the fly using schemas, we try to find the class that inherits from the base Since botocore creates classes on the fly using schemas, we try to find the class that inherits from the base
class and is SQS to wrap. class and is SQS to wrap.
""" """
# We define SQS client as the only client that implements send_message_batch # We define SQS client as the only client that implements send_message_batch
sqs_class = [ if not hasattr(sqs_class, "send_message_batch"):
cls return
for cls in botocore.client.BaseClient.__subclasses__()
if hasattr(cls, "send_message_batch")
]
if sqs_class:
self._sqs_class = sqs_class[0]
self._did_decorate = True
self._wrap_send_message()
self._wrap_send_message_batch()
self._wrap_receive_message()
self._wrap_delete_message()
self._wrap_delete_message_batch()
def _un_decorate_sqs(self) -> None: if getattr(sqs_class, _IS_SQS_INSTRUMENTED_ATTRIBUTE, False):
if self._did_decorate: return
unwrap(self._sqs_class, "send_message")
unwrap(self._sqs_class, "send_message_batch") setattr(sqs_class, _IS_SQS_INSTRUMENTED_ATTRIBUTE, True)
unwrap(self._sqs_class, "receive_message")
unwrap(self._sqs_class, "delete_message") self._wrap_send_message(sqs_class)
unwrap(self._sqs_class, "delete_message_batch") self._wrap_send_message_batch(sqs_class)
self._did_decorate = False self._wrap_receive_message(sqs_class)
self._wrap_delete_message(sqs_class)
self._wrap_delete_message_batch(sqs_class)
@staticmethod
def _un_decorate_sqs(sqs_class: type) -> None:
if not getattr(sqs_class, _IS_SQS_INSTRUMENTED_ATTRIBUTE, False):
return
unwrap(sqs_class, "send_message")
unwrap(sqs_class, "send_message_batch")
unwrap(sqs_class, "receive_message")
unwrap(sqs_class, "delete_message")
unwrap(sqs_class, "delete_message_batch")
setattr(sqs_class, _IS_SQS_INSTRUMENTED_ATTRIBUTE, False)
def _instrument(self, **kwargs: Dict[str, Any]) -> None: def _instrument(self, **kwargs: Dict[str, Any]) -> None:
self._did_decorate: bool = False
self._tracer_provider: Optional[TracerProvider] = kwargs.get( self._tracer_provider: Optional[TracerProvider] = kwargs.get(
"tracer_provider" "tracer_provider"
) )
@ -419,8 +425,12 @@ class Boto3SQSInstrumentor(BaseInstrumentor):
__name__, __version__, self._tracer_provider __name__, __version__, self._tracer_provider
) )
self._wrap_client_creation() self._wrap_client_creation()
self._decorate_sqs()
for client_cls in botocore.client.BaseClient.__subclasses__():
self._decorate_sqs(client_cls)
def _uninstrument(self, **kwargs: Dict[str, Any]) -> None: def _uninstrument(self, **kwargs: Dict[str, Any]) -> None:
unwrap(boto3, "client") unwrap(boto3, "client")
self._un_decorate_sqs()
for client_cls in botocore.client.BaseClient.__subclasses__():
self._un_decorate_sqs(client_cls)

View File

@ -14,79 +14,72 @@
# pylint: disable=no-name-in-module # pylint: disable=no-name-in-module
from unittest import TestCase from contextlib import contextmanager
from typing import Any, Dict
from unittest import TestCase, mock
import boto3 import boto3
import botocore.client from botocore.awsrequest import AWSResponse
from wrapt import BoundFunctionWrapper, FunctionWrapper from wrapt import BoundFunctionWrapper, FunctionWrapper
from opentelemetry.instrumentation.boto3sqs import ( from opentelemetry.instrumentation.boto3sqs import (
_OPENTELEMETRY_ATTRIBUTE_IDENTIFIER,
Boto3SQSGetter, Boto3SQSGetter,
Boto3SQSInstrumentor, Boto3SQSInstrumentor,
Boto3SQSSetter, Boto3SQSSetter,
) )
from opentelemetry.semconv.trace import (
MessagingDestinationKindValues,
MessagingOperationValues,
SpanAttributes,
)
from opentelemetry.test.test_base import TestBase
from opentelemetry.trace import SpanKind
from opentelemetry.trace.span import Span, format_span_id, format_trace_id
def _make_sqs_client():
return boto3.client(
"sqs",
region_name="us-east-1",
aws_access_key_id="dummy",
aws_secret_access_key="dummy",
)
# pylint: disable=attribute-defined-outside-init
class TestBoto3SQSInstrumentor(TestCase): class TestBoto3SQSInstrumentor(TestCase):
def define_sqs_mock(self) -> None: def _assert_instrumented(self, client):
# pylint: disable=R0201 self.assertIsInstance(boto3.client, FunctionWrapper)
class SQSClientMock(botocore.client.BaseClient): self.assertIsInstance(client.send_message, BoundFunctionWrapper)
def send_message(self, *args, **kwargs): self.assertIsInstance(client.send_message_batch, BoundFunctionWrapper)
... self.assertIsInstance(client.receive_message, BoundFunctionWrapper)
self.assertIsInstance(client.delete_message, BoundFunctionWrapper)
self.assertIsInstance(
client.delete_message_batch, BoundFunctionWrapper
)
def send_message_batch(self, *args, **kwargs): @staticmethod
... @contextmanager
def _active_instrumentor():
def receive_message(self, *args, **kwargs): Boto3SQSInstrumentor().instrument()
... try:
yield
def delete_message(self, *args, **kwargs): finally:
... Boto3SQSInstrumentor().uninstrument()
def delete_message_batch(self, *args, **kwargs):
...
self._boto_sqs_mock = SQSClientMock
def test_instrument_api_before_client_init(self) -> None: def test_instrument_api_before_client_init(self) -> None:
instrumentation = Boto3SQSInstrumentor() with self._active_instrumentor():
client = _make_sqs_client()
instrumentation.instrument() self._assert_instrumented(client)
self.assertTrue(isinstance(boto3.client, FunctionWrapper))
instrumentation.uninstrument()
def test_instrument_api_after_client_init(self) -> None: def test_instrument_api_after_client_init(self) -> None:
self.define_sqs_mock() client = _make_sqs_client()
instrumentation = Boto3SQSInstrumentor() with self._active_instrumentor():
self._assert_instrumented(client)
instrumentation.instrument() def test_instrument_multiple_clients(self):
self.assertTrue(isinstance(boto3.client, FunctionWrapper)) with self._active_instrumentor():
self.assertTrue( self._assert_instrumented(_make_sqs_client())
isinstance(self._boto_sqs_mock.send_message, BoundFunctionWrapper) self._assert_instrumented(_make_sqs_client())
)
self.assertTrue(
isinstance(
self._boto_sqs_mock.send_message_batch, BoundFunctionWrapper
)
)
self.assertTrue(
isinstance(
self._boto_sqs_mock.receive_message, BoundFunctionWrapper
)
)
self.assertTrue(
isinstance(
self._boto_sqs_mock.delete_message, BoundFunctionWrapper
)
)
self.assertTrue(
isinstance(
self._boto_sqs_mock.delete_message_batch, BoundFunctionWrapper
)
)
instrumentation.uninstrument()
class TestBoto3SQSGetter(TestCase): class TestBoto3SQSGetter(TestCase):
@ -101,29 +94,17 @@ class TestBoto3SQSGetter(TestCase):
def test_get_value(self) -> None: def test_get_value(self) -> None:
key = "test" key = "test"
value = "value" value = "value"
carrier = { carrier = {key: {"StringValue": value, "DataType": "String"}}
f"{_OPENTELEMETRY_ATTRIBUTE_IDENTIFIER}{key}": {
"StringValue": value,
"DataType": "String",
}
}
val = self.getter.get(carrier, key) val = self.getter.get(carrier, key)
self.assertEqual(val, [value]) self.assertEqual(val, [value])
def test_keys(self): def test_keys(self):
key1 = "test1"
value1 = "value1"
key2 = "test2"
value2 = "value2"
carrier = { carrier = {
f"{_OPENTELEMETRY_ATTRIBUTE_IDENTIFIER}{key1}": { "test1": {"StringValue": "value1", "DataType": "String"},
"StringValue": value1, "test2": {"StringValue": "value2", "DataType": "String"},
"DataType": "String",
},
key2: {"StringValue": value2, "DataType": "String"},
} }
keys = self.getter.keys(carrier) keys = self.getter.keys(carrier)
self.assertEqual(keys, [key1, key2]) self.assertEqual(keys, list(carrier.keys()))
def test_keys_empty(self): def test_keys_empty(self):
keys = self.getter.keys({}) keys = self.getter.keys({})
@ -145,8 +126,188 @@ class TestBoto3SQSSetter(TestCase):
for dict_key, dict_val in carrier[original_key].items(): for dict_key, dict_val in carrier[original_key].items():
self.assertEqual(original_value[dict_key], dict_val) self.assertEqual(original_value[dict_key], dict_val)
# Ensure the new key is added well # Ensure the new key is added well
self.assertIn( self.assertEqual(carrier[key]["StringValue"], value)
f"{_OPENTELEMETRY_ATTRIBUTE_IDENTIFIER}{key}", carrier.keys()
class TestBoto3SQSInstrumentation(TestBase):
def setUp(self):
super().setUp()
self._reset_instrumentor()
Boto3SQSInstrumentor().instrument()
self._client = _make_sqs_client()
self._queue_name = "MyQueue"
self._queue_url = f"https://sqs.us-east-1.amazonaws.com/123456789012/{self._queue_name}"
def tearDown(self):
super().tearDown()
Boto3SQSInstrumentor().uninstrument()
self._reset_instrumentor()
@staticmethod
def _reset_instrumentor():
Boto3SQSInstrumentor.received_messages_spans.clear()
Boto3SQSInstrumentor.current_span_related_to_token = None
Boto3SQSInstrumentor.current_context_token = None
@staticmethod
def _make_aws_response_func(response):
def _response_func(*args, **kwargs):
return AWSResponse("http://127.0.0.1", 200, {}, "{}"), response
return _response_func
@contextmanager
def _mocked_endpoint(self, response):
response_func = self._make_aws_response_func(response)
with mock.patch(
"botocore.endpoint.Endpoint.make_request", new=response_func
):
yield
def _assert_injected_span(self, msg_attrs: Dict[str, Any], span: Span):
trace_parent = msg_attrs["traceparent"]["StringValue"]
ctx = span.get_span_context()
self.assertEqual(
self._to_trace_parent(ctx.trace_id, ctx.span_id),
trace_parent.lower(),
) )
new_value = carrier[f"{_OPENTELEMETRY_ATTRIBUTE_IDENTIFIER}{key}"]
self.assertEqual(new_value["StringValue"], value) def _default_span_attrs(self):
return {
SpanAttributes.MESSAGING_SYSTEM: "aws.sqs",
SpanAttributes.MESSAGING_DESTINATION: self._queue_name,
SpanAttributes.MESSAGING_DESTINATION_KIND: MessagingDestinationKindValues.QUEUE.value,
SpanAttributes.MESSAGING_URL: self._queue_url,
}
@staticmethod
def _to_trace_parent(trace_id: int, span_id: int) -> str:
return f"00-{format_trace_id(trace_id)}-{format_span_id(span_id)}-01".lower()
def _get_only_span(self):
spans = self.get_finished_spans()
self.assertEqual(1, len(spans))
return spans[0]
@staticmethod
def _make_message(message_id: str, body: str, receipt: str):
return {
"MessageId": message_id,
"ReceiptHandle": receipt,
"MD5OfBody": "777",
"Body": body,
"Attributes": {},
"MD5OfMessageAttributes": "111",
"MessageAttributes": {},
}
def _add_trace_parent(
self, message: Dict[str, Any], trace_id: int, span_id: int
):
message["MessageAttributes"]["traceparent"] = {
"StringValue": self._to_trace_parent(trace_id, span_id),
"DataType": "String",
}
def test_send_message(self):
message_id = "123456789"
mock_response = {
"MD5OfMessageBody": "1234",
"MD5OfMessageAttributes": "5678",
"MD5OfMessageSystemAttributes": "9012",
"MessageId": message_id,
"SequenceNumber": "0",
}
message_attrs = {}
with self._mocked_endpoint(mock_response):
self._client.send_message(
QueueUrl=self._queue_url,
MessageBody="hello msg",
MessageAttributes=message_attrs,
)
span = self._get_only_span()
self.assertEqual(f"{self._queue_name} send", span.name)
self.assertEqual(SpanKind.PRODUCER, span.kind)
self.assertEqual(
{
SpanAttributes.MESSAGING_MESSAGE_ID: message_id,
**self._default_span_attrs(),
},
span.attributes,
)
self._assert_injected_span(message_attrs, span)
def test_receive_message(self):
msg_def = {
"1": {"receipt": "01", "trace_id": 10, "span_id": 1},
"2": {"receipt": "02", "trace_id": 20, "span_id": 2},
}
mock_response = {"Messages": []}
for msg_id, attrs in msg_def.items():
message = self._make_message(
msg_id, f"hello {msg_id}", attrs["receipt"]
)
self._add_trace_parent(
message, attrs["trace_id"], attrs["span_id"]
)
mock_response["Messages"].append(message)
message_attr_names = []
with self._mocked_endpoint(mock_response):
response = self._client.receive_message(
QueueUrl=self._queue_url,
MessageAttributeNames=message_attr_names,
)
self.assertIn("traceparent", message_attr_names)
# receive span
span = self._get_only_span()
self.assertEqual(f"{self._queue_name} receive", span.name)
self.assertEqual(SpanKind.CONSUMER, span.kind)
self.assertEqual(
{
SpanAttributes.MESSAGING_OPERATION: MessagingOperationValues.RECEIVE.value,
**self._default_span_attrs(),
},
span.attributes,
)
self.memory_exporter.clear()
# processing spans
self.assertEqual(2, len(response["Messages"]))
for msg in response["Messages"]:
msg_id = msg["MessageId"]
attrs = msg_def[msg_id]
with self._mocked_endpoint(None):
self._client.delete_message(
QueueUrl=self._queue_url, ReceiptHandle=attrs["receipt"]
)
span = self._get_only_span()
self.assertEqual(f"{self._queue_name} process", span.name)
# processing span attributes
self.assertEqual(
{
SpanAttributes.MESSAGING_MESSAGE_ID: msg_id,
SpanAttributes.MESSAGING_OPERATION: MessagingOperationValues.PROCESS.value,
**self._default_span_attrs(),
},
span.attributes,
)
# processing span links
self.assertEqual(1, len(span.links))
link = span.links[0]
self.assertEqual(attrs["trace_id"], link.context.trace_id)
self.assertEqual(attrs["span_id"], link.context.span_id)
self.memory_exporter.clear()

View File

@ -32,6 +32,10 @@ envlist =
py3{6,7,8,9,10}-test-instrumentation-botocore py3{6,7,8,9,10}-test-instrumentation-botocore
pypy3-test-instrumentation-botocore pypy3-test-instrumentation-botocore
; opentelemetry-instrumentation-boto3sqs
py3{6,7,8,9,10}-test-instrumentation-boto3sqs
pypy3-test-instrumentation-boto3sqs
; opentelemetry-instrumentation-django ; opentelemetry-instrumentation-django
; Only officially supported Python versions are tested for each Django ; Only officially supported Python versions are tested for each Django
; major release. Updated list can be found at: ; major release. Updated list can be found at:
@ -259,6 +263,7 @@ changedir =
test-instrumentation-aws-lambda: instrumentation/opentelemetry-instrumentation-aws-lambda/tests test-instrumentation-aws-lambda: instrumentation/opentelemetry-instrumentation-aws-lambda/tests
test-instrumentation-boto: instrumentation/opentelemetry-instrumentation-boto/tests test-instrumentation-boto: instrumentation/opentelemetry-instrumentation-boto/tests
test-instrumentation-botocore: instrumentation/opentelemetry-instrumentation-botocore/tests test-instrumentation-botocore: instrumentation/opentelemetry-instrumentation-botocore/tests
test-instrumentation-boto3sqs: instrumentation/opentelemetry-instrumentation-boto3sqs/tests
test-instrumentation-celery: instrumentation/opentelemetry-instrumentation-celery/tests test-instrumentation-celery: instrumentation/opentelemetry-instrumentation-celery/tests
test-instrumentation-dbapi: instrumentation/opentelemetry-instrumentation-dbapi/tests test-instrumentation-dbapi: instrumentation/opentelemetry-instrumentation-dbapi/tests
test-instrumentation-django{1,2,3,4}: instrumentation/opentelemetry-instrumentation-django/tests test-instrumentation-django{1,2,3,4}: instrumentation/opentelemetry-instrumentation-django/tests
@ -328,6 +333,8 @@ commands_pre =
boto: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore[test] boto: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore[test]
boto: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-boto[test] boto: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-boto[test]
boto3sqs: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-boto3sqs[test]
falcon{1,2,3}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon[test] falcon{1,2,3}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon[test]
flask: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-flask[test] flask: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-flask[test]
@ -445,6 +452,7 @@ commands_pre =
python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi[test]
python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi[test]
python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore[test]
python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-boto3sqs[test]
python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-django[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-django[test]
python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-starlette[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-starlette[test]
python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc[test]