Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions sentry_sdk/integrations/boto3/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version
from sentry_sdk.integrations.boto3._client import _patch_botocore_client
Comment thread
cursor[bot] marked this conversation as resolved.
from sentry_sdk.integrations.boto3.consts import IDENTIFIER, ORIGIN
from sentry_sdk.utils import parse_version

try:
from botocore import __version__ as BOTOCORE_VERSION
except ImportError:
raise DidNotEnable("botocore is not installed")


class Boto3Integration(Integration):
identifier = IDENTIFIER
origin = ORIGIN

@staticmethod
def setup_once() -> None:
version = parse_version(BOTOCORE_VERSION)
_check_minimum_version(Boto3Integration, version, "botocore")

_patch_botocore_client()
44 changes: 44 additions & 0 deletions sentry_sdk/integrations/boto3/_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from functools import partial
from typing import TYPE_CHECKING

from sentry_sdk.integrations import DidNotEnable, _check_minimum_version
from sentry_sdk.integrations.boto3._instrumentation import (
_sentry_after_call,
_sentry_after_call_error,
_sentry_before_sign,
_sentry_request_created,
)
from sentry_sdk.utils import parse_version

if TYPE_CHECKING:
from typing import Any

try:
from botocore import __version__ as BOTOCORE_VERSION
from botocore.client import BaseClient
except ImportError:
raise DidNotEnable("botocore is not installed")


def _patch_botocore_client() -> None:
from sentry_sdk.integrations.boto3 import Boto3Integration

version = parse_version(BOTOCORE_VERSION)
_check_minimum_version(Boto3Integration, version, "botocore")

orig_init = BaseClient.__init__

def sentry_patched_init(self: "BaseClient", *args: "Any", **kwargs: "Any") -> None:
orig_init(self, *args, **kwargs)
meta = self.meta
service_id = meta.service_model.service_id
meta.events.register(
"request-created",
partial(_sentry_request_created, service_id=service_id),
)
# run after other `before-sign` handlers, allowing it to see and preserve existing baggage.
meta.events.register_last("before-sign", _sentry_before_sign)
meta.events.register("after-call", _sentry_after_call)
meta.events.register("after-call-error", _sentry_after_call_error)

BaseClient.__init__ = sentry_patched_init # type: ignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from functools import partial
from typing import TYPE_CHECKING

import sentry_sdk
from sentry_sdk.consts import OP, SPANDATA
from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version
from sentry_sdk.integrations import DidNotEnable
from sentry_sdk.integrations.boto3.consts import ORIGIN
from sentry_sdk.traces import StreamedSpan
from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, Span
from sentry_sdk.tracing_utils import (
Expand All @@ -16,7 +16,6 @@
from sentry_sdk.utils import (
capture_internal_exceptions,
parse_url,
parse_version,
)

if TYPE_CHECKING:
Expand All @@ -26,46 +25,17 @@


try:
from botocore import __version__ as BOTOCORE_VERSION
from botocore.awsrequest import AWSRequest
from botocore.client import BaseClient
from botocore.response import StreamingBody
except ImportError:
raise DidNotEnable("botocore is not installed")


class Boto3Integration(Integration):
identifier = "boto3"
origin = f"auto.http.{identifier}"

@staticmethod
def setup_once() -> None:
version = parse_version(BOTOCORE_VERSION)
_check_minimum_version(Boto3Integration, version, "botocore")

orig_init = BaseClient.__init__

def sentry_patched_init(
self: "BaseClient", *args: "Any", **kwargs: "Any"
) -> None:
orig_init(self, *args, **kwargs)
meta = self.meta
service_id = meta.service_model.service_id
meta.events.register(
"request-created",
partial(_sentry_request_created, service_id=service_id),
)
# run after other `before-sign` handlers, allowing it to see and preserve existing baggage.
meta.events.register_last("before-sign", _sentry_before_sign)
meta.events.register("after-call", _sentry_after_call)
meta.events.register("after-call-error", _sentry_after_call_error)

BaseClient.__init__ = sentry_patched_init # type: ignore


def _sentry_request_created(
service_id: "ServiceId", request: "AWSRequest", operation_name: str, **kwargs: "Any"
) -> None:
from sentry_sdk.integrations.boto3 import Boto3Integration

description = "aws.%s.%s" % (service_id.hyphenize(), operation_name)

client = sentry_sdk.get_client()
Expand Down Expand Up @@ -93,7 +63,7 @@ def _sentry_request_created(
name=description,
attributes={
"sentry.op": OP.HTTP_CLIENT,
"sentry.origin": Boto3Integration.origin,
"sentry.origin": ORIGIN,
SPANDATA.RPC_METHOD: f"{service_id}/{operation_name}",
},
)
Expand All @@ -105,7 +75,7 @@ def _sentry_request_created(
span = sentry_sdk.start_span(
op=OP.HTTP_CLIENT,
name=description,
origin=Boto3Integration.origin,
origin=ORIGIN,
)

if parsed_url:
Expand Down Expand Up @@ -141,6 +111,8 @@ def _sentry_request_created(
def _sentry_before_sign(
request: "AWSRequest", signature_version: "Any", **kwargs: "Any"
) -> None:
from sentry_sdk.integrations.boto3 import Boto3Integration

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A fun thing that you can do to try and get around the need for this dynamic import - you can provide the string rather than the class to client.get_integration.

So I think lines 117 and 118 below would look like this:

if client.get_integration("Boto3Integration") is None:
        return

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay good to know, I didn't knew this was possible. Then we'll probably use client.get_integration("boto3") instead, right? Since we use boto3 as identifier for the integration.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do this probably in a later PR, cause I don't want to do merge hell once again 😭


client = sentry_sdk.get_client()
if client.get_integration(Boto3Integration) is None:
return
Expand Down Expand Up @@ -214,14 +186,14 @@ def _sentry_after_call(
parent_span=span,
attributes={
"sentry.op": OP.HTTP_CLIENT_STREAM,
"sentry.origin": Boto3Integration.origin,
"sentry.origin": ORIGIN,
},
)
else:
streaming_span = span.start_child(
op=OP.HTTP_CLIENT_STREAM,
name=span.description,
origin=Boto3Integration.origin,
origin=ORIGIN,
)

orig_read = body.read
Expand Down
2 changes: 2 additions & 0 deletions sentry_sdk/integrations/boto3/consts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
IDENTIFIER = "boto3"
ORIGIN = f"auto.http.{IDENTIFIER}"
6 changes: 6 additions & 0 deletions tests/integrations/boto3/test_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from sentry_sdk.integrations.boto3 import Boto3Integration


def test_public_api():
assert Boto3Integration.__module__ == "sentry_sdk.integrations.boto3"
assert Boto3Integration.identifier == "boto3"
4 changes: 2 additions & 2 deletions tests/integrations/boto3/test_s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ def test_omit_url_data_if_parsing_fails(
items = capture_items("span")

with mock.patch(
"sentry_sdk.integrations.boto3.parse_url",
"sentry_sdk.integrations.boto3._instrumentation.parse_url",
side_effect=ValueError,
):
with sentry_sdk.traces.start_span(
Expand Down Expand Up @@ -294,7 +294,7 @@ def test_omit_url_data_if_parsing_fails(
events = capture_events()

with mock.patch(
"sentry_sdk.integrations.boto3.parse_url",
"sentry_sdk.integrations.boto3._instrumentation.parse_url",
side_effect=ValueError,
):
with sentry_sdk.start_transaction() as transaction, MockResponse(
Expand Down
Loading