telemetry-dev-bedrock 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,165 @@
1
+ Metadata-Version: 2.4
2
+ Name: telemetry-dev-bedrock
3
+ Version: 0.1.0
4
+ Summary: AWS Bedrock integration for telemetry.dev Python SDK
5
+ Keywords: telemetry,opentelemetry,bedrock,aws,boto3,genai,tracing
6
+ Author: telemetry.dev
7
+ License-Expression: MIT
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Typing :: Typed
16
+ Requires-Dist: telemetry-dev>=0.1.0
17
+ Requires-Dist: boto3>=1.36
18
+ Requires-Python: >=3.10
19
+ Project-URL: Homepage, https://telemetry.dev
20
+ Project-URL: Repository, https://github.com/telemetry-dev/telemetry.dev
21
+ Description-Content-Type: text/markdown
22
+
23
+ # telemetry-dev-bedrock
24
+
25
+ AWS Bedrock instrumentation for the telemetry.dev Python SDK.
26
+
27
+ This package instruments boto3/botocore clients for:
28
+
29
+ - `bedrock-runtime`
30
+ - `Converse`
31
+ - `ConverseStream`
32
+ - `InvokeModel`
33
+ - `InvokeModelWithResponseStream`
34
+ - `ApplyGuardrail`
35
+ - `bedrock-agent-runtime`
36
+ - `InvokeAgent`
37
+ - `InvokeInlineAgent`
38
+ - `Retrieve`
39
+ - `RetrieveAndGenerate`
40
+ - `RetrieveAndGenerateStream`
41
+ - `InvokeFlow`
42
+
43
+ Telemetry is emitted through `telemetry_dev.start_span`. No AWS request parameters are mutated.
44
+
45
+ ## Install
46
+
47
+ ```sh
48
+ uv add telemetry-dev telemetry-dev-bedrock boto3
49
+ ```
50
+
51
+ or with pip:
52
+
53
+ ```sh
54
+ pip install telemetry-dev telemetry-dev-bedrock boto3
55
+ ```
56
+
57
+ ## Quickstart
58
+
59
+ ```py
60
+ import os
61
+ import boto3
62
+ import telemetry_dev
63
+ from telemetry_dev_bedrock import wrap_bedrock
64
+
65
+ telemetry_dev.init(
66
+ api_key=os.getenv("TELEMETRY_DEV_API_KEY"),
67
+ service_name="bedrock-app",
68
+ environment="production",
69
+ )
70
+
71
+ bedrock = wrap_bedrock(boto3.client("bedrock-runtime", region_name="us-east-1"))
72
+
73
+ bedrock.converse(
74
+ modelId="anthropic.claude-3-5-haiku-20241022-v1:0",
75
+ messages=[{"role": "user", "content": [{"text": "Hello"}]}],
76
+ )
77
+
78
+ telemetry_dev.shutdown()
79
+ ```
80
+
81
+ `wrap_bedrock` accepts either a `bedrock-runtime` or `bedrock-agent-runtime` boto3 client. It patches the operation methods on that instance and is idempotent.
82
+
83
+ ## Global instrumentation
84
+
85
+ ```py
86
+ from telemetry_dev_bedrock import instrument_bedrock, uninstrument_bedrock
87
+
88
+ instrument_bedrock()
89
+ # bedrock-runtime and bedrock-agent-runtime clients created before or after this point are covered.
90
+ uninstrument_bedrock()
91
+ ```
92
+
93
+ Global instrumentation wraps `botocore.client.BaseClient._make_api_call` and filters by service name, so unrelated boto3 clients pass through untouched. Per-client `wrap_bedrock()` clients are skipped by the global wrapper to avoid double spans.
94
+
95
+ ## Options
96
+
97
+ | Option | Default | Applies to | Notes |
98
+ | --- | --- | --- | --- |
99
+ | `capture_agent_trace` | `False` | Agent Runtime streams | Aggregates trace usage and counts by default. When enabled, also attaches raw trace events as `td.metadata.agent_trace` after SDK masking/truncation. |
100
+
101
+ ```py
102
+ agent = wrap_bedrock(
103
+ boto3.client("bedrock-agent-runtime"),
104
+ capture_agent_trace=True,
105
+ )
106
+ ```
107
+
108
+ ## Signal coverage
109
+
110
+ All instrumented AWS responses/errors include `gen_ai.response.id` from the AWS request id, plus `aws.http.status_code`, `aws.request.attempts` when retries occurred, and `aws.request.total_retry_delay_ms` when the SDK exposes them.
111
+
112
+ | Operation | Span type | Span name | Captured fields |
113
+ | --- | --- | --- | --- |
114
+ | `Converse` | `generation` | `chat {modelId}` | normalized input/output messages, system instructions, sampling params, usage/cache usage, finish reason, request id, retry attempts, prompt-router response model, server latency, guardrail metadata |
115
+ | `ConverseStream` | `generation` | `chat {modelId}` | pull-through stream output, time-to-first-chunk, usage/latency from `metadata`, partial output on early break/error |
116
+ | `InvokeModel` | `generation` or `embedding` | `chat {modelId}` / `embeddings {modelId}` | native JSON request/response bodies, provider-native sampling, provider-native usage, embedding output type, HTTP-header token fallback |
117
+ | `InvokeModelWithResponseStream` | `generation` or `embedding` | `chat {modelId}` / `embeddings {modelId}` | provider-native chunk text where known, optional final `amazon-bedrock-invocationMetrics` usage |
118
+ | `ApplyGuardrail` | `span` | `apply_guardrail {guardrailIdentifier}` | guardrail input/output, action, action reason, request id |
119
+ | `InvokeAgent` / `InvokeInlineAgent` | `agent` | `invoke_agent {agentId}` / `invoke_agent {agentName}` | user input, streamed answer, session/memory ids, alias id, trace usage aggregation, trace event count, return-control output |
120
+ | `Retrieve` | `span` | `retrieve {knowledgeBaseId}` | query, retrieval results, guardrail action, result count metadata |
121
+ | `RetrieveAndGenerate` / stream | `generation` | `retrieve_and_generate {modelArn basename}` | input text, generated text, citations count, session id, guardrail action |
122
+ | `InvokeFlow` | `agent` | `invoke_flow {flowIdentifier}` | inputs, flow output events, completion reason |
123
+
124
+ ## StreamingBody behavior
125
+
126
+ `InvokeModel` returns a botocore `StreamingBody`. The integration reads it once to capture output and token usage, then replaces it with a new `StreamingBody` over the same bytes. Caller code can still call `response["body"].read()` normally.
127
+
128
+ Event streams are wrapped lazily. The wrapper does not pre-read events, preserves backpressure, forwards unknown attributes to the inner stream, and calls `close()` on the inner stream when closed.
129
+
130
+ ## Message normalization
131
+
132
+ Converse messages follow the OpenTelemetry GenAI non-normative LLM-call examples:
133
+
134
+ - text blocks become `{ "type": "text", "content": ... }`
135
+ - `toolUse` becomes `{ "type": "tool_call", "id", "name", "arguments" }`
136
+ - `toolResult` becomes `{ "type": "tool_call_response", "id", "response" }`
137
+ - reasoning text becomes `{ "type": "reasoning", "content" }`
138
+ - image/document/video/audio bytes become blob parts without byte content
139
+ - S3/URI sources become `{ "type": "uri", "uri", "modality" }`
140
+
141
+ InvokeModel captures provider-native JSON bodies verbatim. Non-JSON bodies are not captured.
142
+
143
+ ## Semantics and guarantees
144
+
145
+ - Provider is emitted as `amazon-bedrock`. This intentionally differs from the OpenTelemetry registry value `aws.bedrock` so telemetry.dev pricing keys match Bedrock model IDs exactly. The raw caller `modelId`, `modelArn`, or `foundationModel` is emitted as the model.
146
+ - Instrumentation is fail-open. Normalization, span updates, and span endings are guarded so telemetry failures do not break caller code.
147
+ - Requests are never modified.
148
+ - Streams end spans exactly once. Exhaustion records full output and finish reason; early `break`, `close()`, `GeneratorExit`, or stream errors record partial output.
149
+ - boto3 has no async client. `aiobotocore` is a separate package and is out of scope.
150
+
151
+ ## Coverage gaps
152
+
153
+ The following calls pass through unless a supported operation above is used:
154
+
155
+ - `CountTokens`
156
+ - `InvokeGuardrailChecks`
157
+ - `InvokeModelWithBidirectionalStream`
158
+ - `StartAsyncInvoke`, `GetAsyncInvoke`, `ListAsyncInvokes`
159
+ - Agent Runtime session CRUD
160
+ - `Rerank`
161
+ - `GenerateQuery`
162
+ - `OptimizePrompt`
163
+ - `AgenticRetrieveStream`
164
+
165
+ Provider-native `InvokeModel` usage is best-effort. Anthropic Claude, Amazon Titan/Nova, Meta Llama, and Titan embeddings expose known token fields. Cohere and Mistral text models do not consistently include usage in the JSON body; spans still capture request/response bodies and finish reasons where present.
@@ -0,0 +1,143 @@
1
+ # telemetry-dev-bedrock
2
+
3
+ AWS Bedrock instrumentation for the telemetry.dev Python SDK.
4
+
5
+ This package instruments boto3/botocore clients for:
6
+
7
+ - `bedrock-runtime`
8
+ - `Converse`
9
+ - `ConverseStream`
10
+ - `InvokeModel`
11
+ - `InvokeModelWithResponseStream`
12
+ - `ApplyGuardrail`
13
+ - `bedrock-agent-runtime`
14
+ - `InvokeAgent`
15
+ - `InvokeInlineAgent`
16
+ - `Retrieve`
17
+ - `RetrieveAndGenerate`
18
+ - `RetrieveAndGenerateStream`
19
+ - `InvokeFlow`
20
+
21
+ Telemetry is emitted through `telemetry_dev.start_span`. No AWS request parameters are mutated.
22
+
23
+ ## Install
24
+
25
+ ```sh
26
+ uv add telemetry-dev telemetry-dev-bedrock boto3
27
+ ```
28
+
29
+ or with pip:
30
+
31
+ ```sh
32
+ pip install telemetry-dev telemetry-dev-bedrock boto3
33
+ ```
34
+
35
+ ## Quickstart
36
+
37
+ ```py
38
+ import os
39
+ import boto3
40
+ import telemetry_dev
41
+ from telemetry_dev_bedrock import wrap_bedrock
42
+
43
+ telemetry_dev.init(
44
+ api_key=os.getenv("TELEMETRY_DEV_API_KEY"),
45
+ service_name="bedrock-app",
46
+ environment="production",
47
+ )
48
+
49
+ bedrock = wrap_bedrock(boto3.client("bedrock-runtime", region_name="us-east-1"))
50
+
51
+ bedrock.converse(
52
+ modelId="anthropic.claude-3-5-haiku-20241022-v1:0",
53
+ messages=[{"role": "user", "content": [{"text": "Hello"}]}],
54
+ )
55
+
56
+ telemetry_dev.shutdown()
57
+ ```
58
+
59
+ `wrap_bedrock` accepts either a `bedrock-runtime` or `bedrock-agent-runtime` boto3 client. It patches the operation methods on that instance and is idempotent.
60
+
61
+ ## Global instrumentation
62
+
63
+ ```py
64
+ from telemetry_dev_bedrock import instrument_bedrock, uninstrument_bedrock
65
+
66
+ instrument_bedrock()
67
+ # bedrock-runtime and bedrock-agent-runtime clients created before or after this point are covered.
68
+ uninstrument_bedrock()
69
+ ```
70
+
71
+ Global instrumentation wraps `botocore.client.BaseClient._make_api_call` and filters by service name, so unrelated boto3 clients pass through untouched. Per-client `wrap_bedrock()` clients are skipped by the global wrapper to avoid double spans.
72
+
73
+ ## Options
74
+
75
+ | Option | Default | Applies to | Notes |
76
+ | --- | --- | --- | --- |
77
+ | `capture_agent_trace` | `False` | Agent Runtime streams | Aggregates trace usage and counts by default. When enabled, also attaches raw trace events as `td.metadata.agent_trace` after SDK masking/truncation. |
78
+
79
+ ```py
80
+ agent = wrap_bedrock(
81
+ boto3.client("bedrock-agent-runtime"),
82
+ capture_agent_trace=True,
83
+ )
84
+ ```
85
+
86
+ ## Signal coverage
87
+
88
+ All instrumented AWS responses/errors include `gen_ai.response.id` from the AWS request id, plus `aws.http.status_code`, `aws.request.attempts` when retries occurred, and `aws.request.total_retry_delay_ms` when the SDK exposes them.
89
+
90
+ | Operation | Span type | Span name | Captured fields |
91
+ | --- | --- | --- | --- |
92
+ | `Converse` | `generation` | `chat {modelId}` | normalized input/output messages, system instructions, sampling params, usage/cache usage, finish reason, request id, retry attempts, prompt-router response model, server latency, guardrail metadata |
93
+ | `ConverseStream` | `generation` | `chat {modelId}` | pull-through stream output, time-to-first-chunk, usage/latency from `metadata`, partial output on early break/error |
94
+ | `InvokeModel` | `generation` or `embedding` | `chat {modelId}` / `embeddings {modelId}` | native JSON request/response bodies, provider-native sampling, provider-native usage, embedding output type, HTTP-header token fallback |
95
+ | `InvokeModelWithResponseStream` | `generation` or `embedding` | `chat {modelId}` / `embeddings {modelId}` | provider-native chunk text where known, optional final `amazon-bedrock-invocationMetrics` usage |
96
+ | `ApplyGuardrail` | `span` | `apply_guardrail {guardrailIdentifier}` | guardrail input/output, action, action reason, request id |
97
+ | `InvokeAgent` / `InvokeInlineAgent` | `agent` | `invoke_agent {agentId}` / `invoke_agent {agentName}` | user input, streamed answer, session/memory ids, alias id, trace usage aggregation, trace event count, return-control output |
98
+ | `Retrieve` | `span` | `retrieve {knowledgeBaseId}` | query, retrieval results, guardrail action, result count metadata |
99
+ | `RetrieveAndGenerate` / stream | `generation` | `retrieve_and_generate {modelArn basename}` | input text, generated text, citations count, session id, guardrail action |
100
+ | `InvokeFlow` | `agent` | `invoke_flow {flowIdentifier}` | inputs, flow output events, completion reason |
101
+
102
+ ## StreamingBody behavior
103
+
104
+ `InvokeModel` returns a botocore `StreamingBody`. The integration reads it once to capture output and token usage, then replaces it with a new `StreamingBody` over the same bytes. Caller code can still call `response["body"].read()` normally.
105
+
106
+ Event streams are wrapped lazily. The wrapper does not pre-read events, preserves backpressure, forwards unknown attributes to the inner stream, and calls `close()` on the inner stream when closed.
107
+
108
+ ## Message normalization
109
+
110
+ Converse messages follow the OpenTelemetry GenAI non-normative LLM-call examples:
111
+
112
+ - text blocks become `{ "type": "text", "content": ... }`
113
+ - `toolUse` becomes `{ "type": "tool_call", "id", "name", "arguments" }`
114
+ - `toolResult` becomes `{ "type": "tool_call_response", "id", "response" }`
115
+ - reasoning text becomes `{ "type": "reasoning", "content" }`
116
+ - image/document/video/audio bytes become blob parts without byte content
117
+ - S3/URI sources become `{ "type": "uri", "uri", "modality" }`
118
+
119
+ InvokeModel captures provider-native JSON bodies verbatim. Non-JSON bodies are not captured.
120
+
121
+ ## Semantics and guarantees
122
+
123
+ - Provider is emitted as `amazon-bedrock`. This intentionally differs from the OpenTelemetry registry value `aws.bedrock` so telemetry.dev pricing keys match Bedrock model IDs exactly. The raw caller `modelId`, `modelArn`, or `foundationModel` is emitted as the model.
124
+ - Instrumentation is fail-open. Normalization, span updates, and span endings are guarded so telemetry failures do not break caller code.
125
+ - Requests are never modified.
126
+ - Streams end spans exactly once. Exhaustion records full output and finish reason; early `break`, `close()`, `GeneratorExit`, or stream errors record partial output.
127
+ - boto3 has no async client. `aiobotocore` is a separate package and is out of scope.
128
+
129
+ ## Coverage gaps
130
+
131
+ The following calls pass through unless a supported operation above is used:
132
+
133
+ - `CountTokens`
134
+ - `InvokeGuardrailChecks`
135
+ - `InvokeModelWithBidirectionalStream`
136
+ - `StartAsyncInvoke`, `GetAsyncInvoke`, `ListAsyncInvokes`
137
+ - Agent Runtime session CRUD
138
+ - `Rerank`
139
+ - `GenerateQuery`
140
+ - `OptimizePrompt`
141
+ - `AgenticRetrieveStream`
142
+
143
+ Provider-native `InvokeModel` usage is best-effort. Anthropic Claude, Amazon Titan/Nova, Meta Llama, and Titan embeddings expose known token fields. Cohere and Mistral text models do not consistently include usage in the JSON body; spans still capture request/response bodies and finish reasons where present.
@@ -0,0 +1,67 @@
1
+ [project]
2
+ name = "telemetry-dev-bedrock"
3
+ version = "0.1.0"
4
+ description = "AWS Bedrock integration for telemetry.dev Python SDK"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.10"
8
+ authors = [{ name = "telemetry.dev" }]
9
+ keywords = ["telemetry", "opentelemetry", "bedrock", "aws", "boto3", "genai", "tracing"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Intended Audience :: Developers",
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.10",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3.13",
18
+ "Typing :: Typed",
19
+ ]
20
+ dependencies = [
21
+ "telemetry-dev>=0.1.0",
22
+ "boto3>=1.36",
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://telemetry.dev"
27
+ Repository = "https://github.com/telemetry-dev/telemetry.dev"
28
+
29
+ [tool.uv.sources]
30
+ telemetry-dev = { path = "../python", editable = true }
31
+
32
+ [dependency-groups]
33
+ dev = [
34
+ "pytest>=8.3",
35
+ "ruff>=0.9",
36
+ "pyright>=1.1.390",
37
+ ]
38
+
39
+ [build-system]
40
+ requires = ["uv_build>=0.9.0,<0.10.0"]
41
+ build-backend = "uv_build"
42
+
43
+ [tool.pytest.ini_options]
44
+ testpaths = ["tests"]
45
+
46
+ [tool.ruff]
47
+ line-length = 100
48
+ target-version = "py310"
49
+
50
+ [tool.ruff.lint]
51
+ select = ["E", "F", "I", "UP", "B", "RUF"]
52
+
53
+ [tool.pyright]
54
+ include = ["src", "tests"]
55
+ typeCheckingMode = "strict"
56
+ pythonVersion = "3.10"
57
+ reportMissingTypeStubs = false
58
+ reportPrivateUsage = false
59
+ reportUnknownArgumentType = false
60
+ reportUnknownLambdaType = false
61
+ reportUnknownMemberType = false
62
+ reportUnknownParameterType = false
63
+ reportUnknownVariableType = false
64
+ reportArgumentType = false
65
+ reportAssignmentType = false
66
+ reportOptionalMemberAccess = false
67
+ reportOptionalSubscript = false
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from ._instrument import instrument_bedrock, uninstrument_bedrock, wrap_bedrock
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ __all__ = ["__version__", "instrument_bedrock", "uninstrument_bedrock", "wrap_bedrock"]
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ PROVIDER = "amazon-bedrock"
7
+
8
+
9
+ def clean(fields: dict[str, Any]) -> dict[str, Any]:
10
+ return {key: value for key, value in fields.items() if value is not None}
11
+
12
+
13
+ def metadata_fields(response: dict[str, Any] | BaseException | None) -> dict[str, Any]:
14
+ metadata: dict[str, Any] | None = None
15
+ if isinstance(response, dict) and isinstance(response.get("ResponseMetadata"), dict):
16
+ metadata = response["ResponseMetadata"]
17
+ else:
18
+ error_response = getattr(response, "response", None)
19
+ if isinstance(error_response, dict) and isinstance(
20
+ error_response.get("ResponseMetadata"), dict
21
+ ):
22
+ metadata = error_response["ResponseMetadata"]
23
+ if metadata is None:
24
+ return {}
25
+ retry_attempts = metadata.get("RetryAttempts")
26
+ attempts = retry_attempts + 1 if isinstance(retry_attempts, int) else None
27
+ total_retry_delay = metadata.get("TotalRetryDelay")
28
+ if total_retry_delay is None:
29
+ total_retry_delay = metadata.get("totalRetryDelay")
30
+ attributes = clean(
31
+ {
32
+ "aws.http.status_code": metadata.get("HTTPStatusCode"),
33
+ "aws.request.attempts": attempts if attempts and attempts > 1 else None,
34
+ "aws.request.total_retry_delay_ms": total_retry_delay,
35
+ }
36
+ )
37
+ return clean(
38
+ {
39
+ "response_id": metadata.get("RequestId"),
40
+ "attributes": attributes or None,
41
+ }
42
+ )
43
+
44
+
45
+ def error_fields(error: BaseException) -> dict[str, Any]:
46
+ fields = metadata_fields(error)
47
+ response = getattr(error, "response", None)
48
+ code = None
49
+ if isinstance(response, dict) and isinstance(response.get("Error"), dict):
50
+ code = response["Error"].get("Code")
51
+ fields["error"] = error
52
+ if code:
53
+ attrs = dict(fields.get("attributes") or {})
54
+ attrs["aws.error.code"] = code
55
+ fields["attributes"] = attrs
56
+ return fields
57
+
58
+
59
+ def usage_from_converse(value: Any) -> dict[str, int] | None:
60
+ if not isinstance(value, dict):
61
+ return None
62
+ usage = clean(
63
+ {
64
+ "input_tokens": value.get("inputTokens"),
65
+ "output_tokens": value.get("outputTokens"),
66
+ "total_tokens": value.get("totalTokens"),
67
+ "cache_read_input_tokens": value.get("cacheReadInputTokens"),
68
+ "cache_creation_input_tokens": value.get("cacheWriteInputTokens"),
69
+ }
70
+ )
71
+ return usage or None
72
+
73
+
74
+ def metadata_attr_value(value: Any) -> Any:
75
+ if value is None or isinstance(value, str):
76
+ return value
77
+ return json.dumps(value, default=repr, ensure_ascii=False)
78
+
79
+
80
+ def merge_fields(*parts: dict[str, Any]) -> dict[str, Any]:
81
+ merged: dict[str, Any] = {}
82
+ for part in parts:
83
+ metadata = part.get("metadata")
84
+ attributes = part.get("attributes")
85
+ usage = part.get("usage")
86
+ for key, value in part.items():
87
+ if key not in {"metadata", "attributes", "usage"} and value is not None:
88
+ merged[key] = value
89
+ if isinstance(metadata, dict):
90
+ merged["metadata"] = {
91
+ **merged.get("metadata", {}),
92
+ **{
93
+ key: normalized
94
+ for key, value in metadata.items()
95
+ if (normalized := metadata_attr_value(value)) is not None
96
+ },
97
+ }
98
+ if isinstance(attributes, dict):
99
+ merged["attributes"] = {**merged.get("attributes", {}), **attributes}
100
+ if isinstance(usage, dict):
101
+ merged["usage"] = {**merged.get("usage", {}), **usage}
102
+ return merged