opentelemetry-instrumentation-botocore 0.49b2__py3-none-any.whl → 0.51b0__py3-none-any.whl

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.
@@ -188,11 +188,15 @@ class BotocoreInstrumentor(BaseInstrumentor):
188
188
  }
189
189
 
190
190
  _safe_invoke(extension.extract_attributes, attributes)
191
+ end_span_on_exit = extension.should_end_span_on_exit()
191
192
 
192
193
  with self._tracer.start_as_current_span(
193
194
  call_context.span_name,
194
195
  kind=call_context.span_kind,
195
196
  attributes=attributes,
197
+ # tracing streaming services require to close the span manually
198
+ # at a later time after the stream has been consumed
199
+ end_on_exit=end_span_on_exit,
196
200
  ) as span:
197
201
  _safe_invoke(extension.before_service_call, span)
198
202
  self._call_request_hook(span, call_context)
@@ -0,0 +1,3 @@
1
+ OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = (
2
+ "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"
3
+ )
@@ -32,6 +32,7 @@ def _lazy_load(module, cls):
32
32
 
33
33
 
34
34
  _KNOWN_EXTENSIONS = {
35
+ "bedrock-runtime": _lazy_load(".bedrock", "_BedrockRuntimeExtension"),
35
36
  "dynamodb": _lazy_load(".dynamodb", "_DynamoDbExtension"),
36
37
  "lambda": _lazy_load(".lmbd", "_LambdaExtension"),
37
38
  "sns": _lazy_load(".sns", "_SnsExtension"),
@@ -0,0 +1,396 @@
1
+ # Copyright The OpenTelemetry Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # Includes work from:
16
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
17
+ # SPDX-License-Identifier: Apache-2.0
18
+
19
+ from __future__ import annotations
20
+
21
+ import io
22
+ import json
23
+ import logging
24
+ from typing import Any
25
+
26
+ from botocore.eventstream import EventStream
27
+ from botocore.response import StreamingBody
28
+
29
+ from opentelemetry.instrumentation.botocore.extensions.bedrock_utils import (
30
+ ConverseStreamWrapper,
31
+ InvokeModelWithResponseStreamWrapper,
32
+ )
33
+ from opentelemetry.instrumentation.botocore.extensions.types import (
34
+ _AttributeMapT,
35
+ _AwsSdkExtension,
36
+ _BotoClientErrorT,
37
+ )
38
+ from opentelemetry.semconv._incubating.attributes.error_attributes import (
39
+ ERROR_TYPE,
40
+ )
41
+ from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import (
42
+ GEN_AI_OPERATION_NAME,
43
+ GEN_AI_REQUEST_MAX_TOKENS,
44
+ GEN_AI_REQUEST_MODEL,
45
+ GEN_AI_REQUEST_STOP_SEQUENCES,
46
+ GEN_AI_REQUEST_TEMPERATURE,
47
+ GEN_AI_REQUEST_TOP_P,
48
+ GEN_AI_RESPONSE_FINISH_REASONS,
49
+ GEN_AI_SYSTEM,
50
+ GEN_AI_USAGE_INPUT_TOKENS,
51
+ GEN_AI_USAGE_OUTPUT_TOKENS,
52
+ GenAiOperationNameValues,
53
+ GenAiSystemValues,
54
+ )
55
+ from opentelemetry.trace.span import Span
56
+ from opentelemetry.trace.status import Status, StatusCode
57
+
58
+ _logger = logging.getLogger(__name__)
59
+
60
+ _MODEL_ID_KEY: str = "modelId"
61
+
62
+
63
+ class _BedrockRuntimeExtension(_AwsSdkExtension):
64
+ """
65
+ This class is an extension for <a
66
+ href="https://docs.aws.amazon.com/bedrock/latest/APIReference/API_Operations_Amazon_Bedrock_Runtime.html">
67
+ Amazon Bedrock Runtime</a>.
68
+ """
69
+
70
+ _HANDLED_OPERATIONS = {
71
+ "Converse",
72
+ "ConverseStream",
73
+ "InvokeModel",
74
+ "InvokeModelWithResponseStream",
75
+ }
76
+ _DONT_CLOSE_SPAN_ON_END_OPERATIONS = {
77
+ "ConverseStream",
78
+ "InvokeModelWithResponseStream",
79
+ }
80
+
81
+ def should_end_span_on_exit(self):
82
+ return (
83
+ self._call_context.operation
84
+ not in self._DONT_CLOSE_SPAN_ON_END_OPERATIONS
85
+ )
86
+
87
+ def extract_attributes(self, attributes: _AttributeMapT):
88
+ if self._call_context.operation not in self._HANDLED_OPERATIONS:
89
+ return
90
+
91
+ attributes[GEN_AI_SYSTEM] = GenAiSystemValues.AWS_BEDROCK.value
92
+
93
+ model_id = self._call_context.params.get(_MODEL_ID_KEY)
94
+ if model_id:
95
+ attributes[GEN_AI_REQUEST_MODEL] = model_id
96
+ attributes[GEN_AI_OPERATION_NAME] = (
97
+ GenAiOperationNameValues.CHAT.value
98
+ )
99
+
100
+ # Converse / ConverseStream
101
+ if inference_config := self._call_context.params.get(
102
+ "inferenceConfig"
103
+ ):
104
+ self._set_if_not_none(
105
+ attributes,
106
+ GEN_AI_REQUEST_TEMPERATURE,
107
+ inference_config.get("temperature"),
108
+ )
109
+ self._set_if_not_none(
110
+ attributes,
111
+ GEN_AI_REQUEST_TOP_P,
112
+ inference_config.get("topP"),
113
+ )
114
+ self._set_if_not_none(
115
+ attributes,
116
+ GEN_AI_REQUEST_MAX_TOKENS,
117
+ inference_config.get("maxTokens"),
118
+ )
119
+ self._set_if_not_none(
120
+ attributes,
121
+ GEN_AI_REQUEST_STOP_SEQUENCES,
122
+ inference_config.get("stopSequences"),
123
+ )
124
+
125
+ # InvokeModel
126
+ # Get the request body if it exists
127
+ body = self._call_context.params.get("body")
128
+ if body:
129
+ try:
130
+ request_body = json.loads(body)
131
+
132
+ if "amazon.titan" in model_id:
133
+ # titan interface is a text completion one
134
+ attributes[GEN_AI_OPERATION_NAME] = (
135
+ GenAiOperationNameValues.TEXT_COMPLETION.value
136
+ )
137
+ self._extract_titan_attributes(
138
+ attributes, request_body
139
+ )
140
+ elif "amazon.nova" in model_id:
141
+ self._extract_nova_attributes(attributes, request_body)
142
+ elif "anthropic.claude" in model_id:
143
+ self._extract_claude_attributes(
144
+ attributes, request_body
145
+ )
146
+ except json.JSONDecodeError:
147
+ _logger.debug("Error: Unable to parse the body as JSON")
148
+
149
+ def _extract_titan_attributes(self, attributes, request_body):
150
+ config = request_body.get("textGenerationConfig", {})
151
+ self._set_if_not_none(
152
+ attributes, GEN_AI_REQUEST_TEMPERATURE, config.get("temperature")
153
+ )
154
+ self._set_if_not_none(
155
+ attributes, GEN_AI_REQUEST_TOP_P, config.get("topP")
156
+ )
157
+ self._set_if_not_none(
158
+ attributes, GEN_AI_REQUEST_MAX_TOKENS, config.get("maxTokenCount")
159
+ )
160
+ self._set_if_not_none(
161
+ attributes,
162
+ GEN_AI_REQUEST_STOP_SEQUENCES,
163
+ config.get("stopSequences"),
164
+ )
165
+
166
+ def _extract_nova_attributes(self, attributes, request_body):
167
+ config = request_body.get("inferenceConfig", {})
168
+ self._set_if_not_none(
169
+ attributes, GEN_AI_REQUEST_TEMPERATURE, config.get("temperature")
170
+ )
171
+ self._set_if_not_none(
172
+ attributes, GEN_AI_REQUEST_TOP_P, config.get("topP")
173
+ )
174
+ self._set_if_not_none(
175
+ attributes, GEN_AI_REQUEST_MAX_TOKENS, config.get("max_new_tokens")
176
+ )
177
+ self._set_if_not_none(
178
+ attributes,
179
+ GEN_AI_REQUEST_STOP_SEQUENCES,
180
+ config.get("stopSequences"),
181
+ )
182
+
183
+ def _extract_claude_attributes(self, attributes, request_body):
184
+ self._set_if_not_none(
185
+ attributes,
186
+ GEN_AI_REQUEST_MAX_TOKENS,
187
+ request_body.get("max_tokens"),
188
+ )
189
+ self._set_if_not_none(
190
+ attributes,
191
+ GEN_AI_REQUEST_TEMPERATURE,
192
+ request_body.get("temperature"),
193
+ )
194
+ self._set_if_not_none(
195
+ attributes, GEN_AI_REQUEST_TOP_P, request_body.get("top_p")
196
+ )
197
+ self._set_if_not_none(
198
+ attributes,
199
+ GEN_AI_REQUEST_STOP_SEQUENCES,
200
+ request_body.get("stop_sequences"),
201
+ )
202
+
203
+ @staticmethod
204
+ def _set_if_not_none(attributes, key, value):
205
+ if value is not None:
206
+ attributes[key] = value
207
+
208
+ def before_service_call(self, span: Span):
209
+ if self._call_context.operation not in self._HANDLED_OPERATIONS:
210
+ return
211
+
212
+ if not span.is_recording():
213
+ return
214
+
215
+ operation_name = span.attributes.get(GEN_AI_OPERATION_NAME, "")
216
+ request_model = span.attributes.get(GEN_AI_REQUEST_MODEL, "")
217
+ # avoid setting to an empty string if are not available
218
+ if operation_name and request_model:
219
+ span.update_name(f"{operation_name} {request_model}")
220
+
221
+ # pylint: disable=no-self-use
222
+ def _converse_on_success(self, span: Span, result: dict[str, Any]):
223
+ if usage := result.get("usage"):
224
+ if input_tokens := usage.get("inputTokens"):
225
+ span.set_attribute(
226
+ GEN_AI_USAGE_INPUT_TOKENS,
227
+ input_tokens,
228
+ )
229
+ if output_tokens := usage.get("outputTokens"):
230
+ span.set_attribute(
231
+ GEN_AI_USAGE_OUTPUT_TOKENS,
232
+ output_tokens,
233
+ )
234
+
235
+ if stop_reason := result.get("stopReason"):
236
+ span.set_attribute(
237
+ GEN_AI_RESPONSE_FINISH_REASONS,
238
+ [stop_reason],
239
+ )
240
+
241
+ def _invoke_model_on_success(
242
+ self, span: Span, result: dict[str, Any], model_id: str
243
+ ):
244
+ original_body = None
245
+ try:
246
+ original_body = result["body"]
247
+ body_content = original_body.read()
248
+
249
+ # Replenish stream for downstream application use
250
+ new_stream = io.BytesIO(body_content)
251
+ result["body"] = StreamingBody(new_stream, len(body_content))
252
+
253
+ response_body = json.loads(body_content.decode("utf-8"))
254
+ if "amazon.titan" in model_id:
255
+ self._handle_amazon_titan_response(span, response_body)
256
+ elif "amazon.nova" in model_id:
257
+ self._handle_amazon_nova_response(span, response_body)
258
+ elif "anthropic.claude" in model_id:
259
+ self._handle_anthropic_claude_response(span, response_body)
260
+
261
+ except json.JSONDecodeError:
262
+ _logger.debug("Error: Unable to parse the response body as JSON")
263
+ except Exception as exc: # pylint: disable=broad-exception-caught
264
+ _logger.debug("Error processing response: %s", exc)
265
+ finally:
266
+ if original_body is not None:
267
+ original_body.close()
268
+
269
+ def _on_stream_error_callback(self, span: Span, exception):
270
+ span.set_status(Status(StatusCode.ERROR, str(exception)))
271
+ if span.is_recording():
272
+ span.set_attribute(ERROR_TYPE, type(exception).__qualname__)
273
+ span.end()
274
+
275
+ def on_success(self, span: Span, result: dict[str, Any]):
276
+ if self._call_context.operation not in self._HANDLED_OPERATIONS:
277
+ return
278
+
279
+ if not span.is_recording():
280
+ if not self.should_end_span_on_exit():
281
+ span.end()
282
+ return
283
+
284
+ # ConverseStream
285
+ if "stream" in result and isinstance(result["stream"], EventStream):
286
+
287
+ def stream_done_callback(response):
288
+ self._converse_on_success(span, response)
289
+ span.end()
290
+
291
+ def stream_error_callback(exception):
292
+ self._on_stream_error_callback(span, exception)
293
+
294
+ result["stream"] = ConverseStreamWrapper(
295
+ result["stream"], stream_done_callback, stream_error_callback
296
+ )
297
+ return
298
+
299
+ # Converse
300
+ self._converse_on_success(span, result)
301
+
302
+ model_id = self._call_context.params.get(_MODEL_ID_KEY)
303
+ if not model_id:
304
+ return
305
+
306
+ # InvokeModel
307
+ if "body" in result and isinstance(result["body"], StreamingBody):
308
+ self._invoke_model_on_success(span, result, model_id)
309
+ return
310
+
311
+ # InvokeModelWithResponseStream
312
+ if "body" in result and isinstance(result["body"], EventStream):
313
+
314
+ def invoke_model_stream_done_callback(response):
315
+ # the callback gets data formatted as the simpler converse API
316
+ self._converse_on_success(span, response)
317
+ span.end()
318
+
319
+ def invoke_model_stream_error_callback(exception):
320
+ self._on_stream_error_callback(span, exception)
321
+
322
+ result["body"] = InvokeModelWithResponseStreamWrapper(
323
+ result["body"],
324
+ invoke_model_stream_done_callback,
325
+ invoke_model_stream_error_callback,
326
+ model_id,
327
+ )
328
+ return
329
+
330
+ # pylint: disable=no-self-use
331
+ def _handle_amazon_titan_response(
332
+ self, span: Span, response_body: dict[str, Any]
333
+ ):
334
+ if "inputTextTokenCount" in response_body:
335
+ span.set_attribute(
336
+ GEN_AI_USAGE_INPUT_TOKENS, response_body["inputTextTokenCount"]
337
+ )
338
+ if "results" in response_body and response_body["results"]:
339
+ result = response_body["results"][0]
340
+ if "tokenCount" in result:
341
+ span.set_attribute(
342
+ GEN_AI_USAGE_OUTPUT_TOKENS, result["tokenCount"]
343
+ )
344
+ if "completionReason" in result:
345
+ span.set_attribute(
346
+ GEN_AI_RESPONSE_FINISH_REASONS,
347
+ [result["completionReason"]],
348
+ )
349
+
350
+ # pylint: disable=no-self-use
351
+ def _handle_amazon_nova_response(
352
+ self, span: Span, response_body: dict[str, Any]
353
+ ):
354
+ if "usage" in response_body:
355
+ usage = response_body["usage"]
356
+ if "inputTokens" in usage:
357
+ span.set_attribute(
358
+ GEN_AI_USAGE_INPUT_TOKENS, usage["inputTokens"]
359
+ )
360
+ if "outputTokens" in usage:
361
+ span.set_attribute(
362
+ GEN_AI_USAGE_OUTPUT_TOKENS, usage["outputTokens"]
363
+ )
364
+ if "stopReason" in response_body:
365
+ span.set_attribute(
366
+ GEN_AI_RESPONSE_FINISH_REASONS, [response_body["stopReason"]]
367
+ )
368
+
369
+ # pylint: disable=no-self-use
370
+ def _handle_anthropic_claude_response(
371
+ self, span: Span, response_body: dict[str, Any]
372
+ ):
373
+ if usage := response_body.get("usage"):
374
+ if "input_tokens" in usage:
375
+ span.set_attribute(
376
+ GEN_AI_USAGE_INPUT_TOKENS, usage["input_tokens"]
377
+ )
378
+ if "output_tokens" in usage:
379
+ span.set_attribute(
380
+ GEN_AI_USAGE_OUTPUT_TOKENS, usage["output_tokens"]
381
+ )
382
+ if "stop_reason" in response_body:
383
+ span.set_attribute(
384
+ GEN_AI_RESPONSE_FINISH_REASONS, [response_body["stop_reason"]]
385
+ )
386
+
387
+ def on_error(self, span: Span, exception: _BotoClientErrorT):
388
+ if self._call_context.operation not in self._HANDLED_OPERATIONS:
389
+ return
390
+
391
+ span.set_status(Status(StatusCode.ERROR, str(exception)))
392
+ if span.is_recording():
393
+ span.set_attribute(ERROR_TYPE, type(exception).__qualname__)
394
+
395
+ if not self.should_end_span_on_exit():
396
+ span.end()
@@ -0,0 +1,222 @@
1
+ # Copyright The OpenTelemetry Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ from typing import Callable, Dict, Union
19
+
20
+ from botocore.eventstream import EventStream, EventStreamError
21
+ from wrapt import ObjectProxy
22
+
23
+ _StreamDoneCallableT = Callable[[Dict[str, Union[int, str]]], None]
24
+ _StreamErrorCallableT = Callable[[Exception], None]
25
+
26
+
27
+ # pylint: disable=abstract-method
28
+ class ConverseStreamWrapper(ObjectProxy):
29
+ """Wrapper for botocore.eventstream.EventStream"""
30
+
31
+ def __init__(
32
+ self,
33
+ stream: EventStream,
34
+ stream_done_callback: _StreamDoneCallableT,
35
+ stream_error_callback: _StreamErrorCallableT,
36
+ ):
37
+ super().__init__(stream)
38
+
39
+ self._stream_done_callback = stream_done_callback
40
+ self._stream_error_callback = stream_error_callback
41
+ # accumulating things in the same shape of non-streaming version
42
+ # {"usage": {"inputTokens": 0, "outputTokens": 0}, "stopReason": "finish"}
43
+ self._response = {}
44
+
45
+ def __iter__(self):
46
+ try:
47
+ for event in self.__wrapped__:
48
+ self._process_event(event)
49
+ yield event
50
+ except EventStreamError as exc:
51
+ self._stream_error_callback(exc)
52
+ raise
53
+
54
+ def _process_event(self, event):
55
+ if "messageStart" in event:
56
+ # {'messageStart': {'role': 'assistant'}}
57
+ return
58
+
59
+ if "contentBlockDelta" in event:
60
+ # {'contentBlockDelta': {'delta': {'text': "Hello"}, 'contentBlockIndex': 0}}
61
+ return
62
+
63
+ if "contentBlockStop" in event:
64
+ # {'contentBlockStop': {'contentBlockIndex': 0}}
65
+ return
66
+
67
+ if "messageStop" in event:
68
+ # {'messageStop': {'stopReason': 'end_turn'}}
69
+ if stop_reason := event["messageStop"].get("stopReason"):
70
+ self._response["stopReason"] = stop_reason
71
+ return
72
+
73
+ if "metadata" in event:
74
+ # {'metadata': {'usage': {'inputTokens': 12, 'outputTokens': 15, 'totalTokens': 27}, 'metrics': {'latencyMs': 2980}}}
75
+ if usage := event["metadata"].get("usage"):
76
+ self._response["usage"] = {}
77
+ if input_tokens := usage.get("inputTokens"):
78
+ self._response["usage"]["inputTokens"] = input_tokens
79
+
80
+ if output_tokens := usage.get("outputTokens"):
81
+ self._response["usage"]["outputTokens"] = output_tokens
82
+
83
+ self._stream_done_callback(self._response)
84
+ return
85
+
86
+
87
+ # pylint: disable=abstract-method
88
+ class InvokeModelWithResponseStreamWrapper(ObjectProxy):
89
+ """Wrapper for botocore.eventstream.EventStream"""
90
+
91
+ def __init__(
92
+ self,
93
+ stream: EventStream,
94
+ stream_done_callback: _StreamDoneCallableT,
95
+ stream_error_callback: _StreamErrorCallableT,
96
+ model_id: str,
97
+ ):
98
+ super().__init__(stream)
99
+
100
+ self._stream_done_callback = stream_done_callback
101
+ self._stream_error_callback = stream_error_callback
102
+ self._model_id = model_id
103
+
104
+ # accumulating things in the same shape of the Converse API
105
+ # {"usage": {"inputTokens": 0, "outputTokens": 0}, "stopReason": "finish"}
106
+ self._response = {}
107
+
108
+ def __iter__(self):
109
+ try:
110
+ for event in self.__wrapped__:
111
+ self._process_event(event)
112
+ yield event
113
+ except EventStreamError as exc:
114
+ self._stream_error_callback(exc)
115
+ raise
116
+
117
+ def _process_event(self, event):
118
+ if "chunk" not in event:
119
+ return
120
+
121
+ json_bytes = event["chunk"].get("bytes", b"")
122
+ decoded = json_bytes.decode("utf-8")
123
+ try:
124
+ chunk = json.loads(decoded)
125
+ except json.JSONDecodeError:
126
+ return
127
+
128
+ if "amazon.titan" in self._model_id:
129
+ self._process_amazon_titan_chunk(chunk)
130
+ elif "amazon.nova" in self._model_id:
131
+ self._process_amazon_nova_chunk(chunk)
132
+ elif "anthropic.claude" in self._model_id:
133
+ self._process_anthropic_claude_chunk(chunk)
134
+
135
+ def _process_invocation_metrics(self, invocation_metrics):
136
+ self._response["usage"] = {}
137
+ if input_tokens := invocation_metrics.get("inputTokenCount"):
138
+ self._response["usage"]["inputTokens"] = input_tokens
139
+
140
+ if output_tokens := invocation_metrics.get("outputTokenCount"):
141
+ self._response["usage"]["outputTokens"] = output_tokens
142
+
143
+ def _process_amazon_titan_chunk(self, chunk):
144
+ if (stop_reason := chunk.get("completionReason")) is not None:
145
+ self._response["stopReason"] = stop_reason
146
+
147
+ if invocation_metrics := chunk.get("amazon-bedrock-invocationMetrics"):
148
+ # "amazon-bedrock-invocationMetrics":{
149
+ # "inputTokenCount":9,"outputTokenCount":128,"invocationLatency":3569,"firstByteLatency":2180
150
+ # }
151
+ self._process_invocation_metrics(invocation_metrics)
152
+ self._stream_done_callback(self._response)
153
+
154
+ def _process_amazon_nova_chunk(self, chunk):
155
+ if "messageStart" in chunk:
156
+ # {'messageStart': {'role': 'assistant'}}
157
+ return
158
+
159
+ if "contentBlockDelta" in chunk:
160
+ # {'contentBlockDelta': {'delta': {'text': "Hello"}, 'contentBlockIndex': 0}}
161
+ return
162
+
163
+ if "contentBlockStop" in chunk:
164
+ # {'contentBlockStop': {'contentBlockIndex': 0}}
165
+ return
166
+
167
+ if "messageStop" in chunk:
168
+ # {'messageStop': {'stopReason': 'end_turn'}}
169
+ if stop_reason := chunk["messageStop"].get("stopReason"):
170
+ self._response["stopReason"] = stop_reason
171
+ return
172
+
173
+ if "metadata" in chunk:
174
+ # {'metadata': {'usage': {'inputTokens': 8, 'outputTokens': 117}, 'metrics': {}, 'trace': {}}}
175
+ if usage := chunk["metadata"].get("usage"):
176
+ self._response["usage"] = {}
177
+ if input_tokens := usage.get("inputTokens"):
178
+ self._response["usage"]["inputTokens"] = input_tokens
179
+
180
+ if output_tokens := usage.get("outputTokens"):
181
+ self._response["usage"]["outputTokens"] = output_tokens
182
+
183
+ self._stream_done_callback(self._response)
184
+ return
185
+
186
+ def _process_anthropic_claude_chunk(self, chunk):
187
+ # pylint: disable=too-many-return-statements
188
+ if not (message_type := chunk.get("type")):
189
+ return
190
+
191
+ if message_type == "message_start":
192
+ # {'type': 'message_start', 'message': {'id': 'id', 'type': 'message', 'role': 'assistant', 'model': 'claude-2.0', 'content': [], 'stop_reason': None, 'stop_sequence': None, 'usage': {'input_tokens': 18, 'output_tokens': 1}}}
193
+ return
194
+
195
+ if message_type == "content_block_start":
196
+ # {'type': 'content_block_start', 'index': 0, 'content_block': {'type': 'text', 'text': ''}}
197
+ return
198
+
199
+ if message_type == "content_block_delta":
200
+ # {'type': 'content_block_delta', 'index': 0, 'delta': {'type': 'text_delta', 'text': 'Here'}}
201
+ return
202
+
203
+ if message_type == "content_block_stop":
204
+ # {'type': 'content_block_stop', 'index': 0}
205
+ return
206
+
207
+ if message_type == "message_delta":
208
+ # {'type': 'message_delta', 'delta': {'stop_reason': 'end_turn', 'stop_sequence': None}, 'usage': {'output_tokens': 123}}
209
+ if (
210
+ stop_reason := chunk.get("delta", {}).get("stop_reason")
211
+ ) is not None:
212
+ self._response["stopReason"] = stop_reason
213
+ return
214
+
215
+ if message_type == "message_stop":
216
+ # {'type': 'message_stop', 'amazon-bedrock-invocationMetrics': {'inputTokenCount': 18, 'outputTokenCount': 123, 'invocationLatency': 5250, 'firstByteLatency': 290}}
217
+ if invocation_metrics := chunk.get(
218
+ "amazon-bedrock-invocationMetrics"
219
+ ):
220
+ self._process_invocation_metrics(invocation_metrics)
221
+ self._stream_done_callback(self._response)
222
+ return
@@ -101,6 +101,14 @@ class _AwsSdkExtension:
101
101
  """
102
102
  return True
103
103
 
104
+ def should_end_span_on_exit(self) -> bool: # pylint:disable=no-self-use
105
+ """Returns if the span should be closed automatically on exit
106
+
107
+ Extensions might override this function to disable automatic closing
108
+ of the span if they need to close it at a later time themselves.
109
+ """
110
+ return True
111
+
104
112
  def extract_attributes(self, attributes: _AttributeMapT):
105
113
  """Callback which gets invoked before the span is created.
106
114
 
@@ -12,4 +12,4 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- __version__ = "0.49b2"
15
+ __version__ = "0.51b0"
@@ -1,10 +1,12 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: opentelemetry-instrumentation-botocore
3
- Version: 0.49b2
3
+ Version: 0.51b0
4
4
  Summary: OpenTelemetry Botocore instrumentation
5
5
  Project-URL: Homepage, https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-botocore
6
+ Project-URL: Repository, https://github.com/open-telemetry/opentelemetry-python-contrib
6
7
  Author-email: OpenTelemetry Authors <cncf-opentelemetry-contributors@lists.cncf.io>
7
- License: Apache-2.0
8
+ License-Expression: Apache-2.0
9
+ License-File: LICENSE
8
10
  Classifier: Development Status :: 4 - Beta
9
11
  Classifier: Intended Audience :: Developers
10
12
  Classifier: License :: OSI Approved :: Apache Software License
@@ -15,11 +17,12 @@ Classifier: Programming Language :: Python :: 3.9
15
17
  Classifier: Programming Language :: Python :: 3.10
16
18
  Classifier: Programming Language :: Python :: 3.11
17
19
  Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
18
21
  Requires-Python: >=3.8
19
22
  Requires-Dist: opentelemetry-api~=1.12
20
- Requires-Dist: opentelemetry-instrumentation==0.49b2
23
+ Requires-Dist: opentelemetry-instrumentation==0.51b0
21
24
  Requires-Dist: opentelemetry-propagator-aws-xray~=1.0
22
- Requires-Dist: opentelemetry-semantic-conventions==0.49b2
25
+ Requires-Dist: opentelemetry-semantic-conventions==0.51b0
23
26
  Provides-Extra: instruments
24
27
  Requires-Dist: botocore~=1.0; extra == 'instruments'
25
28
  Description-Content-Type: text/x-rst
@@ -0,0 +1,18 @@
1
+ opentelemetry/instrumentation/botocore/__init__.py,sha256=ln81OVC1dMnXvEubhw9Ye-ABBPAVhwRTnWM63h6s8aA,10469
2
+ opentelemetry/instrumentation/botocore/environment_variables.py,sha256=c1lrIEj5wwxZwLd5ppJsfGADBfQLnb_HuxXDLv7ul6s,114
3
+ opentelemetry/instrumentation/botocore/package.py,sha256=6xvfRpU_C3wlSO6pto7MhGtkPoCHDEiRO_Fh4DiC_50,622
4
+ opentelemetry/instrumentation/botocore/version.py,sha256=xU54GoF0aIm8WYGdGIfcg45s1dMdG2m5bpBCICAKNEo,608
5
+ opentelemetry/instrumentation/botocore/extensions/__init__.py,sha256=KhGIZFpvOCOMEtVDSP0BhRpHbrkfpJz81B772JFu0u8,1740
6
+ opentelemetry/instrumentation/botocore/extensions/_messaging.py,sha256=ca2Uwyb1vxWu5qUkKTlfn9KJFN6k8HOTrrBYvwX4WzA,1636
7
+ opentelemetry/instrumentation/botocore/extensions/bedrock.py,sha256=HvxltbsCGrwi_fUdO4lUoDJ59Pc959L2kdqgdvJQkao,14284
8
+ opentelemetry/instrumentation/botocore/extensions/bedrock_utils.py,sha256=ApoIE0oNPHivq3VCek4sz5y_pxO2ZXl3TDyWkcaGXXw,8639
9
+ opentelemetry/instrumentation/botocore/extensions/dynamodb.py,sha256=BA-zoY-Cr878t5Q3pYS3r1PKAd4FFBtdTCXZLoC3tLE,13554
10
+ opentelemetry/instrumentation/botocore/extensions/lmbd.py,sha256=Fm4_Pq9Wl2BPsHBo6iTJtof4QZTvG1XRBLrf6t-JoWM,4153
11
+ opentelemetry/instrumentation/botocore/extensions/sns.py,sha256=Y0P3r_9msd33UFUFeitS01QBF4Aivqo4yPIDDy-qeLw,5360
12
+ opentelemetry/instrumentation/botocore/extensions/sqs.py,sha256=2-ifTjtMQXayKQ-fEdxS_R7ckPXl7Givq3JESFz-Ou0,2791
13
+ opentelemetry/instrumentation/botocore/extensions/types.py,sha256=hkPNjqAJBeWtYGKunIKuIEVELi0QxMrKe8Vadd27Gp0,5188
14
+ opentelemetry_instrumentation_botocore-0.51b0.dist-info/METADATA,sha256=tqczEhMpt0XdsI3BEkgtR-tFOBjsTzp97RPI6vN5v8Y,2122
15
+ opentelemetry_instrumentation_botocore-0.51b0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ opentelemetry_instrumentation_botocore-0.51b0.dist-info/entry_points.txt,sha256=v5hzQbZMJ61JuhBNq5jHYAapvv3C_486h9CTqxlkUTM,100
17
+ opentelemetry_instrumentation_botocore-0.51b0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
18
+ opentelemetry_instrumentation_botocore-0.51b0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.26.3
2
+ Generator: hatchling 1.27.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,15 +0,0 @@
1
- opentelemetry/instrumentation/botocore/__init__.py,sha256=GWNEAzPLwvBUhXGw0B6iAWKBgC9MjM5zHdHGquvEYcU,10223
2
- opentelemetry/instrumentation/botocore/package.py,sha256=6xvfRpU_C3wlSO6pto7MhGtkPoCHDEiRO_Fh4DiC_50,622
3
- opentelemetry/instrumentation/botocore/version.py,sha256=Axuk6SG7SQR82yhUQ-qxl0W3DkT1XdtfwpB4A3PI6pM,608
4
- opentelemetry/instrumentation/botocore/extensions/__init__.py,sha256=nQGZ4Clq4d2eIeDK6v0IqQVJ6SIIBpzm57DJGXgL9TA,1665
5
- opentelemetry/instrumentation/botocore/extensions/_messaging.py,sha256=ca2Uwyb1vxWu5qUkKTlfn9KJFN6k8HOTrrBYvwX4WzA,1636
6
- opentelemetry/instrumentation/botocore/extensions/dynamodb.py,sha256=BA-zoY-Cr878t5Q3pYS3r1PKAd4FFBtdTCXZLoC3tLE,13554
7
- opentelemetry/instrumentation/botocore/extensions/lmbd.py,sha256=Fm4_Pq9Wl2BPsHBo6iTJtof4QZTvG1XRBLrf6t-JoWM,4153
8
- opentelemetry/instrumentation/botocore/extensions/sns.py,sha256=Y0P3r_9msd33UFUFeitS01QBF4Aivqo4yPIDDy-qeLw,5360
9
- opentelemetry/instrumentation/botocore/extensions/sqs.py,sha256=2-ifTjtMQXayKQ-fEdxS_R7ckPXl7Givq3JESFz-Ou0,2791
10
- opentelemetry/instrumentation/botocore/extensions/types.py,sha256=JWSXOP3KC6WRXbWG4EQczDDjaYhpq2HzTisUZn9e0m4,4857
11
- opentelemetry_instrumentation_botocore-0.49b2.dist-info/METADATA,sha256=G2Ksm5EyHKkwEzR5XGjbA3AHIt2lh5Drf7rFClzVVck,1950
12
- opentelemetry_instrumentation_botocore-0.49b2.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
13
- opentelemetry_instrumentation_botocore-0.49b2.dist-info/entry_points.txt,sha256=v5hzQbZMJ61JuhBNq5jHYAapvv3C_486h9CTqxlkUTM,100
14
- opentelemetry_instrumentation_botocore-0.49b2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
15
- opentelemetry_instrumentation_botocore-0.49b2.dist-info/RECORD,,