Skip to content

fix(boto3): Trace the complete botocore client-call lifecycle - #7538

Open
pabloDeputter wants to merge 7 commits into
pablo/harden-boto3-streaming-bodyfrom
pablo/trace-boto3-client-call-lifecycle
Open

pabloDeputter wants to merge 7 commits into
pablo/harden-boto3-streaming-bodyfrom
pablo/trace-boto3-client-call-lifecycle

Conversation

@pabloDeputter

@pabloDeputter pabloDeputter commented Sep 18, 2026

Copy link
Copy Markdown
Member

Description

Move boto3 span creation from individual HTTP request attempts to full botocore client-call lifecycle.

Previously, the client span was created from the botocore request-created event; since botocore creates a new AwsRequest event for every retry, these spans represent individual retries rather than the full event. Following OTel (https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/#rpc-client-span) the span should cover the entire call lifecycle including all retries. Wrapping _make_api_call() covers all retries performed by botocore, including serialization, endpoint resolution, the final response or failures (https://github.com/boto/botocore/blob/develop/botocore/client.py:999).

  • patch BaseClient._make_api_call() so one span represents a single boto operation; this span is also kept active across every retry attempt.
  • request-created is kept for breadcrumbs, HTTP request attributes, and trace propagation.
  • client spans that fail will preserve the original exception.
  • responses that return a StreamingBody are kept open until body is consumed or closed.
  • fix the correct parent/child relationship between boto, HTTP, and streaming spans.

Issues

Resolves #7474

@pabloDeputter
pabloDeputter added this pull request to stack #7539 September 18, 2026 14:23
@github-actions

github-actions Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Codecov Results 📊

129901 passed | ⏭️ 7171 skipped | Total: 137072 | Pass Rate: 94.77% | Execution Time: 454m 54s

📊 Comparison with Base Branch

Metric Change
Total Tests 📈 +546
Passed Tests 📈 +523
Failed Tests
Skipped Tests 📈 +23

All tests are passing successfully.

✅ Patch coverage is 95.51%. Project has 2546 uncovered lines.
✅ Project coverage is 90.27%. Comparing base (ff199b0) to head (f649477).

Files with missing lines (3)
File Patch % Lines
sentry_sdk/integrations/boto3/_instrumentation.py 94.51% ⚠️ 5 Missing and 11 partials
sentry_sdk/integrations/boto3/_client.py 96.23% ⚠️ 2 Missing and 1 partials
sentry_sdk/integrations/boto3/_context.py 96.30% ⚠️ 1 Missing and 2 partials
Coverage diff
@@            Coverage Diff             @@
##        master       #PR       +/-##
==========================================
+ Coverage    90.21%    90.27%    +0.06%
==========================================
  Files          194       198        +4
  Lines        25888     26156      +268
  Branches      9584      9696      +112
==========================================
+ Hits         23352     23610      +258
- Misses        2536      2546       +10
- Partials      1455      1456        +1

Generated by Codecov Action

@pabloDeputter
pabloDeputter force-pushed the pablo/trace-boto3-client-call-lifecycle branch from 53a8717 to 4efce4b Compare September 18, 2026 15:06
@pabloDeputter
pabloDeputter removed this pull request from stack #7539 September 18, 2026 15:08
@pabloDeputter
pabloDeputter changed the base branch from pablo/refactor-boto3-integration to pablo/harden-boto3-streaming-body September 18, 2026 15:08
@pabloDeputter
pabloDeputter added this pull request to stack #7541 September 18, 2026 15:08
@pabloDeputter
pabloDeputter force-pushed the pablo/trace-boto3-client-call-lifecycle branch from 4efce4b to 05501d2 Compare September 18, 2026 15:15
@pabloDeputter
pabloDeputter marked this pull request as ready for review September 18, 2026 15:15
@pabloDeputter
pabloDeputter requested a review from a team as a code owner September 18, 2026 15:15
Comment thread sentry_sdk/integrations/boto3/_client.py Outdated
Comment thread tests/integrations/boto3/test_client.py Outdated

@ericapisani ericapisani left a comment

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.

Minor things but overall looking good. Will take a look at tests on my 2nd pass

from sentry_sdk.integrations.boto3 import Boto3Integration
@contextmanager
def _activate_client_span(span: "StreamedSpan") -> "Iterator[StreamedSpan]":
"""Temporarily activate an inactive boto span without ending it."""

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.

Worth adding context on why we're doing this.

Comment thread sentry_sdk/integrations/boto3/_client.py
# use unknown if `service_id_hyphenized` so span name can still be created.
# e.g. "aws.unkown.GetObject"
service_name = ctx.service_id_hyphenized or "unknown"
span_name = "aws.%s.%s" % (service_name, ctx.operation_name)

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.

f-strings are generally the preferred way to construct strings like this in modern python these days

Suggested change
span_name = "aws.%s.%s" % (service_name, ctx.operation_name)
span_name = f"aws.{service_name}.{ctx.operation_name}"

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.

yupp, I took over the old code, but I usually also use f-strings.

SPANDATA.SENTRY_ORIGIN: span_origin,
}
if ctx.service_id:
attributes[SPANDATA.RPC_METHOD] = "%s/%s" % (

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.

Same f-string comment here

"sentry.op": OP.HTTP_CLIENT_STREAM,
"sentry.origin": Boto3Integration.origin,
SPANDATA.SENTRY_OP: OP.HTTP_CLIENT_STREAM,
SPANDATA.SENTRY_ORIGIN: Boto3Integration.origin,

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.

Nice change👌🏻

Comment on lines +279 to +281
if isinstance(span, StreamedSpan) and (
span.get_attributes().get(SPANDATA.SENTRY_ORIGIN) != Boto3Integration.origin
):

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.

Because of the length of this conditional, I think it'd be a bit cleaner to pull this into a variable

Suggested change
if isinstance(span, StreamedSpan) and (
span.get_attributes().get(SPANDATA.SENTRY_ORIGIN) != Boto3Integration.origin
):
is_span_origin_from_boto = span.get_attributes().get(SPANDATA.SENTRY_ORIGIN) == Boto3Integration.origin
if isinstance(span, StreamedSpan) and is_span_origin_from_boto:

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 agree, but then you get the issue that the legacy span doesn't support get_attributes(); I fixed it like this:

# an ignored streamed span is not activated; avoid enriching its parent.
if isinstance(span, StreamedSpan):
    if not (span.get_attributes().get(SPANDATA.SENTRY_ORIGIN) == ORIGIN):
        return

headers["foo"] = "old"
headers["foo"] = "new"
produces two fields: {"foo": "old", "foo": "new"}. So delete existing
fields before assigning replacement.

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.

Great comment, thanks for adding this 👍🏻 🙏🏻

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.

yeah it's really weird behavior 😆

Comment thread sentry_sdk/integrations/stdlib.py Outdated
},
# boto3 integration owns span's lifecycle; keep child inactive so it
# can't restore boto3 span later on.
active=not is_inactive_boto3_span,

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.

Apologies for this slight nitpick, but can we add a space after the = and before the not?

The "not" almost blends in with the =, especially because the syntax highlighting of Github and IDEs use the same colour for the two. 😭

@pabloDeputter pabloDeputter Sep 21, 2026

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 agree, but the linter doesn't really like that 🤣 I fixed it by adding fmt: off and fmt: on for that specific block; I haven't seen it being used anywhere else in the codebase, so not sure whether this is correct.

@pabloDeputter
pabloDeputter force-pushed the pablo/trace-boto3-client-call-lifecycle branch from 05501d2 to 421da22 Compare September 21, 2026 09:19

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread sentry_sdk/integrations/boto3/_client.py Outdated
Comment thread sentry_sdk/integrations/boto3/_client.py
Comment thread sentry_sdk/integrations/boto3/_instrumentation.py
Comment thread sentry_sdk/integrations/boto3/_instrumentation.py
Comment thread tests/integrations/boto3/test_client.py
@pabloDeputter
pabloDeputter force-pushed the pablo/trace-boto3-client-call-lifecycle branch from c3e8f5b to 66d4665 Compare September 21, 2026 09:38
@pabloDeputter pabloDeputter reopened this Sep 21, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread sentry_sdk/integrations/boto3/_instrumentation.py
@pabloDeputter
pabloDeputter force-pushed the pablo/trace-boto3-client-call-lifecycle branch 2 times, most recently from fe64ae9 to add0913 Compare September 21, 2026 10:54

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 177d277. Configure here.

Comment thread sentry_sdk/integrations/boto3/_client.py
@pabloDeputter
pabloDeputter force-pushed the pablo/trace-boto3-client-call-lifecycle branch from 9091f42 to d473247 Compare September 21, 2026 15:39
@pabloDeputter
pabloDeputter force-pushed the pablo/trace-boto3-client-call-lifecycle branch from d473247 to f649477 Compare September 21, 2026 16:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ref(boto3): Own one logical botocore client-call lifecycle

2 participants