[boto3sqs] Instrument Session and resource (#2161)

* [boto3sqs] Instrument `Session` and `resource`

This commit addresses the following open issues:

- https://github.com/open-telemetry/opentelemetry-python-contrib/issues/1699
- https://github.com/open-telemetry/opentelemetry-python-contrib/issues/1996

There are four ways to access the SQS API via `boto3`:

- `client = boto3.client("sqs")`
- `client = boto3.Session().client("sqs")`
- `sqs = boto3.resource("sqs")`
- `sqs = boto3.Session().resource("sqs")`

The existing wrapper tied into `boto3.client` to wrap a generated `botocore.client.SQS` class.
The change here covers the three missing initialization methods.

* update changelog

* rename duplicate test methods

* implement uninstrument

* [boto3sqs] Reduce number of wrapper targets

There are actually 6 ways to initialize a boto3 API object.

```py
boto3.client()              # Using default global session
boto3.resource()            # Using default global session
boto3.Session().client()    # Using "re-exported" session.Session
boto3.Session().resource()  # Using "re-exported" session.Session

boto3.session.Session().client()    # Using session.Session directly
boto3.session.Session().resource()  # Using session.Session directly
```

We only have to patch `session.Session.client` to catch all the cases.

- b3c158c62a/boto3/session.py (L217-L229)
- b3c158c62a/boto3/session.py (L446-L457)

* Remove unused import

---------

Co-authored-by: Matt Oberle <mattoberle@users.noreply.github.com>
Co-authored-by: Diego Hurtado <ocelotl@users.noreply.github.com>
This commit is contained in:
Matt Oberle
2024-04-22 18:44:31 -04:00
committed by GitHub
parent 4e90498bf3
commit c644f0d7d5
3 changed files with 66 additions and 16 deletions

View File

@ -37,6 +37,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `opentelemetry-instrumentation-grpc` AioClientInterceptor should propagate with a Metadata object - `opentelemetry-instrumentation-grpc` AioClientInterceptor should propagate with a Metadata object
([#2363](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2363)) ([#2363](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2363))
- `opentelemetry-instrumentation-boto3sqs` Instrument Session and resource
([#2161](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2161))
### Added ### Added

View File

@ -31,7 +31,7 @@ Usage
import logging import logging
from typing import Any, Collection, Dict, Generator, List, Mapping, Optional from typing import Any, Collection, Dict, Generator, List, Mapping, Optional
import boto3 import boto3.session
import botocore.client import botocore.client
from wrapt import wrap_function_wrapper from wrapt import wrap_function_wrapper
@ -382,7 +382,7 @@ class Boto3SQSInstrumentor(BaseInstrumentor):
self._decorate_sqs(type(retval)) self._decorate_sqs(type(retval))
return retval return retval
wrap_function_wrapper(boto3, "client", client_wrapper) wrap_function_wrapper(boto3.session.Session, "client", client_wrapper)
def _decorate_sqs(self, sqs_class: type) -> None: def _decorate_sqs(self, sqs_class: type) -> None:
""" """
@ -433,7 +433,7 @@ class Boto3SQSInstrumentor(BaseInstrumentor):
self._decorate_sqs(client_cls) 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.session.Session, "client")
for client_cls in botocore.client.BaseClient.__subclasses__(): for client_cls in botocore.client.BaseClient.__subclasses__():
self._un_decorate_sqs(client_cls) self._un_decorate_sqs(client_cls)

View File

@ -20,7 +20,7 @@ from unittest import TestCase, mock
import boto3 import boto3
from botocore.awsrequest import AWSResponse from botocore.awsrequest import AWSResponse
from wrapt import BoundFunctionWrapper, FunctionWrapper from wrapt import BoundFunctionWrapper
from opentelemetry.instrumentation.boto3sqs import ( from opentelemetry.instrumentation.boto3sqs import (
Boto3SQSGetter, Boto3SQSGetter,
@ -37,8 +37,17 @@ from opentelemetry.trace import SpanKind
from opentelemetry.trace.span import Span, format_span_id, format_trace_id from opentelemetry.trace.span import Span, format_span_id, format_trace_id
def _make_sqs_client(): def _make_sqs_client(*, session=False):
return boto3.client( return (boto3.Session() if session else boto3).client(
"sqs",
region_name="us-east-1",
aws_access_key_id="dummy",
aws_secret_access_key="dummy",
)
def _make_sqs_resource(*, session=False):
return (boto3.Session() if session else boto3).resource(
"sqs", "sqs",
region_name="us-east-1", region_name="us-east-1",
aws_access_key_id="dummy", aws_access_key_id="dummy",
@ -48,7 +57,6 @@ def _make_sqs_client():
class TestBoto3SQSInstrumentor(TestCase): class TestBoto3SQSInstrumentor(TestCase):
def _assert_instrumented(self, client): def _assert_instrumented(self, client):
self.assertIsInstance(boto3.client, FunctionWrapper)
self.assertIsInstance(client.send_message, BoundFunctionWrapper) self.assertIsInstance(client.send_message, BoundFunctionWrapper)
self.assertIsInstance(client.send_message_batch, BoundFunctionWrapper) self.assertIsInstance(client.send_message_batch, BoundFunctionWrapper)
self.assertIsInstance(client.receive_message, BoundFunctionWrapper) self.assertIsInstance(client.receive_message, BoundFunctionWrapper)
@ -57,6 +65,17 @@ class TestBoto3SQSInstrumentor(TestCase):
client.delete_message_batch, BoundFunctionWrapper client.delete_message_batch, BoundFunctionWrapper
) )
def _assert_uninstrumented(self, client):
self.assertNotIsInstance(client.send_message, BoundFunctionWrapper)
self.assertNotIsInstance(
client.send_message_batch, BoundFunctionWrapper
)
self.assertNotIsInstance(client.receive_message, BoundFunctionWrapper)
self.assertNotIsInstance(client.delete_message, BoundFunctionWrapper)
self.assertNotIsInstance(
client.delete_message_batch, BoundFunctionWrapper
)
@staticmethod @staticmethod
@contextmanager @contextmanager
def _active_instrumentor(): def _active_instrumentor():
@ -67,19 +86,48 @@ class TestBoto3SQSInstrumentor(TestCase):
Boto3SQSInstrumentor().uninstrument() Boto3SQSInstrumentor().uninstrument()
def test_instrument_api_before_client_init(self) -> None: def test_instrument_api_before_client_init(self) -> None:
with self._active_instrumentor(): for session in (False, True):
client = _make_sqs_client() with self._active_instrumentor():
self._assert_instrumented(client) client = _make_sqs_client(session=session)
self._assert_instrumented(client)
self._assert_uninstrumented(client)
def test_instrument_api_after_client_init(self) -> None: def test_instrument_api_after_client_init(self) -> None:
client = _make_sqs_client() for session in (False, True):
with self._active_instrumentor(): client = _make_sqs_client(session=session)
self._assert_instrumented(client) with self._active_instrumentor():
self._assert_instrumented(client)
self._assert_uninstrumented(client)
def test_instrument_multiple_clients(self): def test_instrument_multiple_clients(self):
with self._active_instrumentor(): for session in (False, True):
self._assert_instrumented(_make_sqs_client()) with self._active_instrumentor():
self._assert_instrumented(_make_sqs_client()) self._assert_instrumented(_make_sqs_client(session=session))
self._assert_instrumented(_make_sqs_client(session=session))
def test_instrument_api_before_resource_init(self) -> None:
for session in (False, True):
with self._active_instrumentor():
sqs = _make_sqs_resource(session=session)
self._assert_instrumented(sqs.meta.client)
self._assert_uninstrumented(sqs.meta.client)
def test_instrument_api_after_resource_init(self) -> None:
for session in (False, True):
sqs = _make_sqs_resource(session=session)
with self._active_instrumentor():
self._assert_instrumented(sqs.meta.client)
self._assert_uninstrumented(sqs.meta.client)
def test_instrument_multiple_resources(self):
for session in (False, True):
with self._active_instrumentor():
self._assert_instrumented(
_make_sqs_resource(session=session).meta.client
)
self._assert_instrumented(
_make_sqs_resource(session=session).meta.client
)
class TestBoto3SQSGetter(TestCase): class TestBoto3SQSGetter(TestCase):