Skip to content

Commit 53601f9

Browse files
committed
feat(integrations): aws boto3 sdk
1 parent 317d6b0 commit 53601f9

13 files changed

Lines changed: 637 additions & 1 deletion

File tree

py/noxfile.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,18 @@ def test_cohere(session, version):
230230
_run_tests(session, f"{INTEGRATION_DIR}/cohere/test_cohere.py", version=version)
231231

232232

233+
BOTO3_VERSIONS = _get_matrix_versions("boto3")
234+
235+
236+
@nox.session()
237+
@nox.parametrize("version", BOTO3_VERSIONS, ids=BOTO3_VERSIONS)
238+
def test_boto3(session, version):
239+
"""Test the native boto3 SDK integration."""
240+
_install_test_deps(session)
241+
_install_matrix_dep(session, "boto3", version)
242+
_run_tests(session, f"{INTEGRATION_DIR}/boto3/test_boto3.py", version=version)
243+
244+
233245
INSTRUCTOR_VERSIONS = _get_matrix_versions("instructor")
234246

235247

py/pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,9 @@ latest = "pytest==9.1.0"
438438
[tool.braintrust.matrix.braintrust-core]
439439
latest = "braintrust-core==0.0.59"
440440

441+
[tool.braintrust.matrix.boto3]
442+
latest = "boto3==1.43.26"
443+
441444
# ---------------------------------------------------------------------------
442445
# Vendor packages — optional third-party packages the SDK can work without.
443446
# Keys are matrix keys; values are Python import names. The noxfile uses this
@@ -461,6 +464,7 @@ agentscope = ["agentscope"]
461464
agno = ["agno"]
462465
autogen = ["autogen-agentchat"]
463466
anthropic = ["anthropic"]
467+
boto3 = ["boto3"]
464468
cohere = ["cohere"]
465469
claude_agent_sdk = ["claude-agent-sdk"]
466470
crewai = ["crewai"]
@@ -485,6 +489,7 @@ agentscope = "agentscope"
485489
autogen-agentchat = "autogen_agentchat"
486490
autogen-ext = "autogen_ext"
487491
anthropic = "anthropic"
492+
boto3 = "boto3"
488493
cohere = "cohere"
489494
autoevals = "autoevals"
490495
braintrust-core = "braintrust_core"

py/src/braintrust/auto.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
AgnoIntegration,
1414
AnthropicIntegration,
1515
AutoGenIntegration,
16+
Boto3Integration,
1617
ClaudeAgentSDKIntegration,
1718
CohereIntegration,
1819
CrewAIIntegration,
@@ -76,6 +77,7 @@ def auto_instrument(
7677
strands: bool = True,
7778
temporal: bool = True,
7879
livekit_agents: bool = True,
80+
boto3: bool = True,
7981
) -> dict[str, bool]:
8082
"""
8183
Auto-instrument supported AI/ML libraries for Braintrust tracing.
@@ -203,7 +205,8 @@ def auto_instrument(
203205
results["temporal"] = _instrument_integration(TemporalIntegration)
204206
if livekit_agents:
205207
results["livekit_agents"] = _instrument_integration(LiveKitAgentsIntegration)
206-
208+
if boto3:
209+
results["boto3"] = _instrument_integration(Boto3Integration)
207210
return results
208211

209212

py/src/braintrust/integrations/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from .agno import AgnoIntegration
44
from .anthropic import AnthropicIntegration
55
from .autogen import AutoGenIntegration
6+
from .boto3 import Boto3Integration
67
from .claude_agent_sdk import ClaudeAgentSDKIntegration
78
from .cohere import CohereIntegration
89
from .crewai import CrewAIIntegration
@@ -29,6 +30,7 @@
2930
"AgnoIntegration",
3031
"AnthropicIntegration",
3132
"AutoGenIntegration",
33+
"Boto3Integration",
3234
"ClaudeAgentSDKIntegration",
3335
"CohereIntegration",
3436
"CrewAIIntegration",
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import os
2+
3+
import boto3
4+
from braintrust.auto import auto_instrument
5+
from braintrust.integrations.boto3.patchers import Boto3ConversePatcher
6+
from braintrust.integrations.test_utils import autoinstrument_test_context
7+
8+
9+
def is_patched(target, patcher):
10+
return bool(getattr(target, patcher.nested_patch_marker_attr(), False))
11+
12+
13+
# verify not patched initially
14+
original_client = boto3.client(
15+
"bedrock-runtime",
16+
aws_access_key_id="test-key",
17+
aws_secret_access_key="test-access-key",
18+
aws_session_token="test-session-key",
19+
region_name="us-east-1",
20+
)
21+
22+
assert is_patched(original_client, Boto3ConversePatcher) is False
23+
24+
# instrument
25+
results = auto_instrument()
26+
assert results.get("boto3") is True
27+
28+
# patch
29+
patched_client = boto3.client(
30+
"bedrock-runtime",
31+
aws_access_key_id="",
32+
aws_secret_access_key="",
33+
aws_session_token="",
34+
region_name="us-east-1",
35+
)
36+
37+
# idempotent
38+
results = auto_instrument()
39+
assert results.get("boto3") is True
40+
41+
42+
assert is_patched(patched_client, Boto3ConversePatcher) is True
43+
44+
# make api call
45+
with autoinstrument_test_context("test_auto_boto3", integration="boto3") as memory_logger:
46+
client = boto3.client(
47+
"bedrock-runtime",
48+
region_name="us-east-1",
49+
aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID"),
50+
aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY"),
51+
aws_session_token=os.getenv("AWS_SESSION_TOKEN"),
52+
)
53+
54+
response = response = client.converse(
55+
modelId="anthropic.claude-3-haiku-20240307-v1:0",
56+
messages=[{"role": "user", "content": [{"text": "what's 2+2"}]}],
57+
system=[{"text": "answer only in single integer"}],
58+
)
59+
60+
spans = memory_logger.pop()
61+
assert len(spans) == 1, f"Expected 1 span, got {len(spans)}"
62+
63+
span = spans[0]
64+
assert span["metadata"]["model_provider"] == "anthropic"
65+
assert "claude" in span["metadata"]["model"]

py/src/braintrust/integrations/base.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,30 @@ def _wrapper(cls, wrapped: Any, instance: Any, args: Any, kwargs: Any) -> Any:
437437
return cls.wrapper(wrapped, instance, args, kwargs)
438438

439439

440+
class NestedFunctionWrapperPatcher(FunctionWrapperPatcher):
441+
"""Add Wrapping for target attribute when the object is formed; useful for dynamic sdk builders such as boto3"""
442+
443+
target_attribute: ClassVar[str]
444+
nested_wrapper: ClassVar[Any]
445+
wrapper: ClassVar[Any] = staticmethod(_call_wrapped)
446+
447+
@classmethod
448+
def nested_patch_marker_attr(cls) -> str:
449+
"""Return the sentinel attribute used to mark an instance as set up."""
450+
suffix = re.sub(r"\W+", "_", cls.name).strip("_")
451+
return f"__braintrust_nested_patched_{suffix}__"
452+
453+
@classmethod
454+
def _wrapper(cls, wrapped: Any, instance: Any, args: Any, kwargs: Any) -> Any:
455+
obj = cls.wrapper(wrapped, instance, args, kwargs)
456+
if not getattr(obj, cls.target_attribute, False):
457+
return
458+
marker = cls.nested_patch_marker_attr()
459+
wrap_function_wrapper(obj, cls.target_attribute, cls.nested_wrapper)
460+
setattr(obj, marker, True)
461+
return obj
462+
463+
440464
class ClassReplacementPatcher(BasePatcher):
441465
"""Base patcher for replacing an exported class with a tracing wrapper class.
442466
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import logging
2+
3+
from braintrust.logger import NOOP_SPAN, current_span, init_logger
4+
5+
from .integration import Boto3Integration
6+
7+
8+
logger = logging.getLogger(__name__)
9+
10+
__all__ = ["Boto3Integration", "setup_boto3"]
11+
12+
13+
def setup_boto3(
14+
api_key: str | None = None,
15+
project_id: str | None = None,
16+
project_name: str | None = None,
17+
) -> bool:
18+
span = current_span()
19+
20+
if span == NOOP_SPAN:
21+
init_logger(project=project_name, api_key=api_key, project_id=project_id)
22+
23+
return Boto3Integration.setup()
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
interactions:
2+
- request:
3+
body: '{"messages": [{"role": "user", "content": [{"text": "what''s 2+2"}]}],
4+
"system": [{"text": "answer only in single integer"}]}'
5+
headers:
6+
Content-Length:
7+
- '124'
8+
Content-Type:
9+
- !!binary |
10+
YXBwbGljYXRpb24vanNvbg==
11+
User-Agent:
12+
- !!binary |
13+
Qm90bzMvMS40My4yNiBtZC9Cb3RvY29yZSMxLjQzLjI4IHVhLzIuMSBvcy9saW51eCM2LjguMC0x
14+
MDUyLWF6dXJlIG1kL2FyY2gjeDg2XzY0IGxhbmcvcHl0aG9uIzMuMTIuMSBtZC9weWltcGwjQ1B5
15+
dGhvbiBtL1osYixELGUgY2ZnL3JldHJ5LW1vZGUjbGVnYWN5IEJvdG9jb3JlLzEuNDMuMjg=
16+
X-Amz-Date:
17+
- !!binary |
18+
MjAyNjA2MTJUMDkwMjE5Wg==
19+
X-Amz-Security-Token:
20+
- !!binary |
21+
SVFvSmIzSnBaMmx1WDJWakVFWWFDWFZ6TFhkbGMzUXRNaUpITUVVQ0lFSlVDZDFxQlZncWhMd3BM
22+
cFluTW92aWxVbnRZOVFTNm1HWitkT2lGdWxBQWlFQXFzYzJ0bXloWjk0N2J2ZXJsVW9yV3BkU0xK
23+
UURmMEdlK0pIUHo4U0dhNWdxZ3dNSUR4QUFHZ3czTWpjMk5EWTBOelE0TkRVaURGWWh1dm52blpJ
24+
bjJtMjYvaXJnQXZiNVIvV2FzOFFSeHBEQXozQ0ppM1hNcUFjR2JlU1FCS0psZklMUDBJWjQ2cW90
25+
d1VNeGRRR0hkbnVvQk9XcnJGTzNGdVlrVkxqYXZHd0tDeWRhSkFlcC9iODR6QzRlbGhWZVNQNUNx
26+
L1hNeGl5SHZaSHROZFdvMEVsN0Fma2dQNVhSUnNYM3ZRM0VER1Y4VW9JL0tSa3JHcGhwbWxtWWQv
27+
dG9JdzJzeTRTYWJoNXpBL0NDRUJWRC9ETlJUaDkxR0MrQ1kvR0YrTk9JUk9lVFY1VmtzWndyOVpz
28+
Tmh2NExqcURmeVdHVXoxbERwZmRzRUhPNHpRbjVBVy9oRGNuc04rYlFuR1NtSThaTjk1VTJsdVlr
29+
elBPOGJUUzJobFJNeFN0KzZvbE1NZ09CUkFkbW1OamlYVGNaK3FkNnBCM2paWDcwVWpOZURIdCtJ
30+
SDRoNUozVGxmVXR5ZG01TlBXdzZsb1l2WitPV1M2dUwyTVg1ZlhtWTlJMVBlZlV1M21KQTRFNlhN
31+
aHNncmlYaWQ5THY1UFJoMU1ybTUvN2pOZTVkLzhuSTdJYTZlLytwTVJXYXEreHVvYzIxQWdob2JF
32+
Q3lWUU1XcWZKSWZrclcxMGpMU3FPYlMwd2tMbXUwUVk2bVFHdmZCaitadTN6TnNpL0hRRUIzRjB6
33+
QWtmRENHYmhYbXJrTzVjRkJPNG0zdFdjNmJoWGlST2o0QVRmVVRzSGVvOFlzOExhTEdsSGtSbmdt
34+
WWc4Y3dPVG5FSTBUNldUQjVEYm41L2IybUN6dHdJNHdCTFBUUUhkcVRxVDJ2M05JbC8zTitpd0dQ
35+
UytGRGRnYldPZXVjZEV6TWluWnNNRXJ4RUExK1dZMVNtMkp6alN6VmFOc0NZTDZXQ3A5dlNneUE3
36+
U001aDFQVjlLRCtnPQ==
37+
amz-sdk-invocation-id:
38+
- !!binary |
39+
MzkwYzg5YjktMjM4Ni00NWI1LTkzZTYtYjgyMjc1YWM2OWY2
40+
amz-sdk-request:
41+
- !!binary |
42+
YXR0ZW1wdD0x
43+
method: POST
44+
uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-3-haiku-20240307-v1%3A0/converse
45+
response:
46+
body:
47+
string: '{"metrics":{"latencyMs":269},"output":{"message":{"content":[{"text":"4"}],"role":"assistant"}},"stopReason":"end_turn","usage":{"inputTokens":18,"outputTokens":5,"serverToolUsage":{},"totalTokens":23}}'
48+
headers:
49+
Connection:
50+
- keep-alive
51+
Content-Length:
52+
- '202'
53+
Content-Type:
54+
- application/json
55+
Date:
56+
- Fri, 12 Jun 2026 09:02:20 GMT
57+
x-amzn-RequestId:
58+
- 20c1e6b5-f545-444a-8de6-a47e4c4c295a
59+
status:
60+
code: 200
61+
message: OK
62+
version: 1
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
interactions:
2+
- request:
3+
body: '{"messages": [{"role": "user", "content": [{"text": "what''s 2+2"}]}],
4+
"system": [{"text": "answer only in single integer"}]}'
5+
headers:
6+
Content-Length:
7+
- '124'
8+
Content-Type:
9+
- !!binary |
10+
YXBwbGljYXRpb24vanNvbg==
11+
User-Agent:
12+
- !!binary |
13+
Qm90bzMvMS40My4yNiBtZC9Cb3RvY29yZSMxLjQzLjI4IHVhLzIuMSBvcy9saW51eCM2LjguMC0x
14+
MDUyLWF6dXJlIG1kL2FyY2gjeDg2XzY0IGxhbmcvcHl0aG9uIzMuMTIuMSBtZC9weWltcGwjQ1B5
15+
dGhvbiBtL2UsYixaLEQgY2ZnL3JldHJ5LW1vZGUjbGVnYWN5IEJvdG9jb3JlLzEuNDMuMjg=
16+
X-Amz-Date:
17+
- !!binary |
18+
MjAyNjA2MTJUMDYxMzAxWg==
19+
X-Amz-Security-Token:
20+
- !!binary |
21+
SVFvSmIzSnBaMmx1WDJWakVFWWFDWFZ6TFhkbGMzUXRNaUpITUVVQ0lFSlVDZDFxQlZncWhMd3BM
22+
cFluTW92aWxVbnRZOVFTNm1HWitkT2lGdWxBQWlFQXFzYzJ0bXloWjk0N2J2ZXJsVW9yV3BkU0xK
23+
UURmMEdlK0pIUHo4U0dhNWdxZ3dNSUR4QUFHZ3czTWpjMk5EWTBOelE0TkRVaURGWWh1dm52blpJ
24+
bjJtMjYvaXJnQXZiNVIvV2FzOFFSeHBEQXozQ0ppM1hNcUFjR2JlU1FCS0psZklMUDBJWjQ2cW90
25+
d1VNeGRRR0hkbnVvQk9XcnJGTzNGdVlrVkxqYXZHd0tDeWRhSkFlcC9iODR6QzRlbGhWZVNQNUNx
26+
L1hNeGl5SHZaSHROZFdvMEVsN0Fma2dQNVhSUnNYM3ZRM0VER1Y4VW9JL0tSa3JHcGhwbWxtWWQv
27+
dG9JdzJzeTRTYWJoNXpBL0NDRUJWRC9ETlJUaDkxR0MrQ1kvR0YrTk9JUk9lVFY1VmtzWndyOVpz
28+
Tmh2NExqcURmeVdHVXoxbERwZmRzRUhPNHpRbjVBVy9oRGNuc04rYlFuR1NtSThaTjk1VTJsdVlr
29+
elBPOGJUUzJobFJNeFN0KzZvbE1NZ09CUkFkbW1OamlYVGNaK3FkNnBCM2paWDcwVWpOZURIdCtJ
30+
SDRoNUozVGxmVXR5ZG01TlBXdzZsb1l2WitPV1M2dUwyTVg1ZlhtWTlJMVBlZlV1M21KQTRFNlhN
31+
aHNncmlYaWQ5THY1UFJoMU1ybTUvN2pOZTVkLzhuSTdJYTZlLytwTVJXYXEreHVvYzIxQWdob2JF
32+
Q3lWUU1XcWZKSWZrclcxMGpMU3FPYlMwd2tMbXUwUVk2bVFHdmZCaitadTN6TnNpL0hRRUIzRjB6
33+
QWtmRENHYmhYbXJrTzVjRkJPNG0zdFdjNmJoWGlST2o0QVRmVVRzSGVvOFlzOExhTEdsSGtSbmdt
34+
WWc4Y3dPVG5FSTBUNldUQjVEYm41L2IybUN6dHdJNHdCTFBUUUhkcVRxVDJ2M05JbC8zTitpd0dQ
35+
UytGRGRnYldPZXVjZEV6TWluWnNNRXJ4RUExK1dZMVNtMkp6alN6VmFOc0NZTDZXQ3A5dlNneUE3
36+
U001aDFQVjlLRCtnPQ==
37+
amz-sdk-invocation-id:
38+
- !!binary |
39+
MzBlZjQ1MzktMmFlNS00M2IwLTkwMjUtMWQwZDU3MDkzMjQ5
40+
amz-sdk-request:
41+
- !!binary |
42+
YXR0ZW1wdD0x
43+
method: POST
44+
uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-3-haiku-20240307-v1%3A0/converse
45+
response:
46+
body:
47+
string: '{"metrics":{"latencyMs":257},"output":{"message":{"content":[{"text":"4"}],"role":"assistant"}},"stopReason":"end_turn","usage":{"inputTokens":18,"outputTokens":5,"serverToolUsage":{},"totalTokens":23}}'
48+
headers:
49+
Connection:
50+
- keep-alive
51+
Content-Length:
52+
- '202'
53+
Content-Type:
54+
- application/json
55+
Date:
56+
- Fri, 12 Jun 2026 06:13:02 GMT
57+
x-amzn-RequestId:
58+
- f9de30ea-4863-4786-9de1-a01f0232e54c
59+
status:
60+
code: 200
61+
message: OK
62+
version: 1
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from braintrust.integrations.base import BaseIntegration
2+
3+
from .patchers import Boto3ConversePatcher
4+
5+
6+
class Boto3Integration(BaseIntegration):
7+
name = "boto3_integration"
8+
patchers = (Boto3ConversePatcher,)
9+
import_names = ("botocore",)

0 commit comments

Comments
 (0)