feat(boto3): Add `ServiceExtension` class (#7536) · intertermux-code/sentry-python@014769c · GitHub
Skip to content

Commit 014769c

Browse files
feat(boto3): Add ServiceExtension class (getsentry#7536)
### Description Adds service-extension support to `Boto3Integration`. This keeps the generic Boto3 call lifecycle independent of individual AWS services. - Adds `_ServiceExtension` interface for service-specific behavior through `_get_span_config()`, `_get_request_attributes()`, and `_get_response_attributes()`. This allows for custom span operations/origin, request attributes, and response/error attributes. A future method can also inject trace context into service-specific payload, e.g. SQS message attributes. - Extensions are lazily imported (not sure whether this actually a good thing). Service modules are only imported on first use and falls back to generic behavior when service is unknown or broken. - Generic attributes and errors remain authoritative over service-specific ones. - `sentry.kind` is added to every boto3 span and defaults to `sentry.kind="client"`; service extensions can override this later, e.g. [SQS](https://opentelemetry.io/docs/specs/semconv/messaging/sqs/#send-span-producer)/[SNS](https://opentelemetry.io/docs/specs/semconv/messaging/sns/#aws-sns-spans) send `sentry.kind="producer"` or SQS receive use [`sentry.kind="consumer"`](https://opentelemetry.io/docs/specs/semconv/messaging/sqs/#process-span) / [`sentry.kind="client"`](https://opentelemetry.io/docs/specs/semconv/messaging/sqs/#receive-span) #### Issues Resolves getsentry#7477 --------- Co-authored-by: Erica Pisani <hey@ericapisani.dev>
1 parent e899330 commit 014769c

8 files changed

Lines changed: 308 additions & 304 deletions

File tree

‎sentry_sdk/consts.py‎

Lines changed: 6 additions & 0 deletions

‎sentry_sdk/integrations/boto3/_client.py‎

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,22 @@
1414
_set_span_attributes,
1515
_start_client_span,
1616
)
17+
from sentry_sdk.integrations.boto3._services.registry import (
18+
_resolve_service,
19+
)
1720
from sentry_sdk.integrations.boto3.consts import IDENTIFIER
1821
from sentry_sdk.traces import NoOpStreamedSpan, StreamedSpan
1922
from sentry_sdk.utils import capture_internal_exceptions
2023

2124
if TYPE_CHECKING:
22-
from typing import Any, Iterator, Optional, Union
25+
from typing import Any, Dict, Iterator, Optional, Union
2326

27+
from sentry_sdk._types import Attributes
2428
from sentry_sdk.tracing import Span
2529

2630
try:
2731
from botocore.client import BaseClient
32+
from botocore.exceptions import ClientError
2833
except ImportError:
2934
raise DidNotEnable("botocore not installed")
3035

@@ -34,12 +39,12 @@ def _activate_client_span(
3439
span: "Union[Span, StreamedSpan]",
3540
) -> "Iterator[Union[Span, StreamedSpan]]":
3641
"""
37-
Activate the boto span temporarily during `_make_api_call()` without ending it.
42+
Activate the client span temporarily during `_make_api_call()` without ending it.
3843
3944
Botocore returns a `StreamingBody` before its bytes are consumed. Using the
4045
context manager would finish it as soon as `_make_api_call()` returns, so
4146
restore the caller's span here and let the `StreamingBody` wrapper finish
42-
the boto span when body is consumed/closed.
47+
the client span when the body is consumed or closed.
4348
4449
faulty: desired:
4550
boto3 [_make_api_call] boto3 [_make_api_call------]
@@ -74,13 +79,13 @@ def _patch_botocore_client() -> None:
7479

7580
def sentry_patched_init(self: "BaseClient", *args: "Any", **kwargs: "Any") -> None:
7681
orig_init(self, *args, **kwargs)
77-
meta = self.meta
78-
meta.events.register("request-created", _sentry_request_created)
79-
# run after other `before-sign` handlers so existing baggage is preserved.
80-
meta.events.register_last("before-sign", _sentry_before_sign)
82+
with capture_internal_exceptions():
83+
self.meta.events.register("request-created", _sentry_request_created)
84+
# run after other `before-sign` handlers so existing baggage is preserved.
85+
self.meta.events.register_last("before-sign", _sentry_before_sign)
8186

8287
def sentry_patched_make_api_call(
83-
self: "BaseClient", operation_name: str, api_params: "Any"
88+
self: "BaseClient", operation_name: str, api_params: "Dict[str, Any]"
8489
) -> "Any":
8590
"""
8691
Track a single API call, including retries, serialization, and endpoint
@@ -93,32 +98,46 @@ def sentry_patched_make_api_call(
9398
if client.get_integration(IDENTIFIER) is None:
9499
return orig_make_api_call(self, operation_name, api_params)
95100

96-
ctx = AwsCallContext(operation_name, api_params)
97-
98-
# add optional metadata to context.
99-
with capture_internal_exceptions():
100-
ctx.add_metadata(self)
101-
102101
span: "Optional[Union[Span, StreamedSpan]]" = None
103102
with capture_internal_exceptions():
104-
span = _start_client_span(ctx)
103+
ctx = AwsCallContext(operation_name, api_params)
104+
with capture_internal_exceptions():
105+
# add optional metadata to the context, e.g. service-name, region-name, etc.
106+
ctx.add_metadata(self)
107+
service_ext = _resolve_service(ctx.service_name)
108+
span = _start_client_span(ctx, service_ext)
105109

106110
if span is None:
107111
return orig_make_api_call(self, operation_name, api_params)
108112

109113
# activate without finishing; a streaming response may outlive the call.
110114
span_ctx = _activate_client_span(span)
111115

116+
attributes: "Attributes" = {}
112117
try:
113118
with span_ctx:
114119
try:
115120
parsed = orig_make_api_call(self, operation_name, api_params)
116121
except BaseException as error:
122+
if service_ext is not None and isinstance(error, ClientError):
123+
with capture_internal_exceptions():
124+
attributes.update(
125+
service_ext.get_response_attributes(ctx, error.response)
126+
)
127+
with capture_internal_exceptions():
128+
attributes.update(_get_error_attributes(error))
117129
with capture_internal_exceptions():
118-
_set_span_attributes(span, _get_error_attributes(error))
130+
_set_span_attributes(span, attributes)
119131
raise
132+
if service_ext is not None:
133+
with capture_internal_exceptions():
134+
attributes.update(
135+
service_ext.get_response_attributes(ctx, parsed)
136+
)
137+
with capture_internal_exceptions():
138+
attributes.update(_get_response_attributes(parsed))
120139
with capture_internal_exceptions():
121-
_set_span_attributes(span, _get_response_attributes(parsed))
140+
_set_span_attributes(span, attributes)
122141
except BaseException as error:
123142
_finish_span(span, error)
124143
raise

‎sentry_sdk/integrations/boto3/_context.py‎

Lines changed: 17 additions & 30 deletions

0 commit comments

Comments
 (0)