opentelemetry-instrumentation-botocore 0.50b0__py3-none-any.whl → 0.52b0__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.
- opentelemetry/instrumentation/botocore/__init__.py +151 -13
- opentelemetry/instrumentation/botocore/environment_variables.py +3 -0
- opentelemetry/instrumentation/botocore/extensions/__init__.py +5 -0
- opentelemetry/instrumentation/botocore/extensions/bedrock.py +756 -0
- opentelemetry/instrumentation/botocore/extensions/bedrock_utils.py +514 -0
- opentelemetry/instrumentation/botocore/extensions/dynamodb.py +10 -2
- opentelemetry/instrumentation/botocore/extensions/lmbd.py +4 -1
- opentelemetry/instrumentation/botocore/extensions/sns.py +15 -13
- opentelemetry/instrumentation/botocore/extensions/sqs.py +7 -1
- opentelemetry/instrumentation/botocore/extensions/types.py +61 -4
- opentelemetry/instrumentation/botocore/version.py +1 -1
- {opentelemetry_instrumentation_botocore-0.50b0.dist-info → opentelemetry_instrumentation_botocore-0.52b0.dist-info}/METADATA +9 -6
- opentelemetry_instrumentation_botocore-0.52b0.dist-info/RECORD +18 -0
- {opentelemetry_instrumentation_botocore-0.50b0.dist-info → opentelemetry_instrumentation_botocore-0.52b0.dist-info}/WHEEL +1 -1
- opentelemetry_instrumentation_botocore-0.50b0.dist-info/RECORD +0 -15
- {opentelemetry_instrumentation_botocore-0.50b0.dist-info → opentelemetry_instrumentation_botocore-0.52b0.dist-info}/entry_points.txt +0 -0
- {opentelemetry_instrumentation_botocore-0.50b0.dist-info → opentelemetry_instrumentation_botocore-0.52b0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,756 @@
|
|
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 timeit import default_timer
|
25
|
+
from typing import Any
|
26
|
+
|
27
|
+
from botocore.eventstream import EventStream
|
28
|
+
from botocore.response import StreamingBody
|
29
|
+
|
30
|
+
from opentelemetry.instrumentation.botocore.extensions.bedrock_utils import (
|
31
|
+
ConverseStreamWrapper,
|
32
|
+
InvokeModelWithResponseStreamWrapper,
|
33
|
+
_Choice,
|
34
|
+
genai_capture_message_content,
|
35
|
+
message_to_event,
|
36
|
+
)
|
37
|
+
from opentelemetry.instrumentation.botocore.extensions.types import (
|
38
|
+
_AttributeMapT,
|
39
|
+
_AwsSdkExtension,
|
40
|
+
_BotoClientErrorT,
|
41
|
+
_BotocoreInstrumentorContext,
|
42
|
+
)
|
43
|
+
from opentelemetry.metrics import Instrument, Meter
|
44
|
+
from opentelemetry.semconv._incubating.attributes.error_attributes import (
|
45
|
+
ERROR_TYPE,
|
46
|
+
)
|
47
|
+
from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import (
|
48
|
+
GEN_AI_OPERATION_NAME,
|
49
|
+
GEN_AI_REQUEST_MAX_TOKENS,
|
50
|
+
GEN_AI_REQUEST_MODEL,
|
51
|
+
GEN_AI_REQUEST_STOP_SEQUENCES,
|
52
|
+
GEN_AI_REQUEST_TEMPERATURE,
|
53
|
+
GEN_AI_REQUEST_TOP_P,
|
54
|
+
GEN_AI_RESPONSE_FINISH_REASONS,
|
55
|
+
GEN_AI_SYSTEM,
|
56
|
+
GEN_AI_TOKEN_TYPE,
|
57
|
+
GEN_AI_USAGE_INPUT_TOKENS,
|
58
|
+
GEN_AI_USAGE_OUTPUT_TOKENS,
|
59
|
+
GenAiOperationNameValues,
|
60
|
+
GenAiSystemValues,
|
61
|
+
GenAiTokenTypeValues,
|
62
|
+
)
|
63
|
+
from opentelemetry.semconv._incubating.metrics.gen_ai_metrics import (
|
64
|
+
GEN_AI_CLIENT_OPERATION_DURATION,
|
65
|
+
GEN_AI_CLIENT_TOKEN_USAGE,
|
66
|
+
)
|
67
|
+
from opentelemetry.trace.span import Span
|
68
|
+
from opentelemetry.trace.status import Status, StatusCode
|
69
|
+
|
70
|
+
_logger = logging.getLogger(__name__)
|
71
|
+
|
72
|
+
_GEN_AI_CLIENT_OPERATION_DURATION_BUCKETS = [
|
73
|
+
0.01,
|
74
|
+
0.02,
|
75
|
+
0.04,
|
76
|
+
0.08,
|
77
|
+
0.16,
|
78
|
+
0.32,
|
79
|
+
0.64,
|
80
|
+
1.28,
|
81
|
+
2.56,
|
82
|
+
5.12,
|
83
|
+
10.24,
|
84
|
+
20.48,
|
85
|
+
40.96,
|
86
|
+
81.92,
|
87
|
+
]
|
88
|
+
|
89
|
+
_GEN_AI_CLIENT_TOKEN_USAGE_BUCKETS = [
|
90
|
+
1,
|
91
|
+
4,
|
92
|
+
16,
|
93
|
+
64,
|
94
|
+
256,
|
95
|
+
1024,
|
96
|
+
4096,
|
97
|
+
16384,
|
98
|
+
65536,
|
99
|
+
262144,
|
100
|
+
1048576,
|
101
|
+
4194304,
|
102
|
+
16777216,
|
103
|
+
67108864,
|
104
|
+
]
|
105
|
+
|
106
|
+
_MODEL_ID_KEY: str = "modelId"
|
107
|
+
|
108
|
+
|
109
|
+
class _BedrockRuntimeExtension(_AwsSdkExtension):
|
110
|
+
"""
|
111
|
+
This class is an extension for <a
|
112
|
+
href="https://docs.aws.amazon.com/bedrock/latest/APIReference/API_Operations_Amazon_Bedrock_Runtime.html">
|
113
|
+
Amazon Bedrock Runtime</a>.
|
114
|
+
"""
|
115
|
+
|
116
|
+
_HANDLED_OPERATIONS = {
|
117
|
+
"Converse",
|
118
|
+
"ConverseStream",
|
119
|
+
"InvokeModel",
|
120
|
+
"InvokeModelWithResponseStream",
|
121
|
+
}
|
122
|
+
_DONT_CLOSE_SPAN_ON_END_OPERATIONS = {
|
123
|
+
"ConverseStream",
|
124
|
+
"InvokeModelWithResponseStream",
|
125
|
+
}
|
126
|
+
|
127
|
+
def should_end_span_on_exit(self):
|
128
|
+
return (
|
129
|
+
self._call_context.operation
|
130
|
+
not in self._DONT_CLOSE_SPAN_ON_END_OPERATIONS
|
131
|
+
)
|
132
|
+
|
133
|
+
def setup_metrics(self, meter: Meter, metrics: dict[str, Instrument]):
|
134
|
+
metrics[GEN_AI_CLIENT_OPERATION_DURATION] = meter.create_histogram(
|
135
|
+
name=GEN_AI_CLIENT_OPERATION_DURATION,
|
136
|
+
description="GenAI operation duration",
|
137
|
+
unit="s",
|
138
|
+
explicit_bucket_boundaries_advisory=_GEN_AI_CLIENT_OPERATION_DURATION_BUCKETS,
|
139
|
+
)
|
140
|
+
metrics[GEN_AI_CLIENT_TOKEN_USAGE] = meter.create_histogram(
|
141
|
+
name=GEN_AI_CLIENT_TOKEN_USAGE,
|
142
|
+
description="Measures number of input and output tokens used",
|
143
|
+
unit="{token}",
|
144
|
+
explicit_bucket_boundaries_advisory=_GEN_AI_CLIENT_TOKEN_USAGE_BUCKETS,
|
145
|
+
)
|
146
|
+
|
147
|
+
def _extract_metrics_attributes(self) -> _AttributeMapT:
|
148
|
+
attributes = {GEN_AI_SYSTEM: GenAiSystemValues.AWS_BEDROCK.value}
|
149
|
+
|
150
|
+
model_id = self._call_context.params.get(_MODEL_ID_KEY)
|
151
|
+
if not model_id:
|
152
|
+
return attributes
|
153
|
+
|
154
|
+
attributes[GEN_AI_REQUEST_MODEL] = model_id
|
155
|
+
|
156
|
+
# titan in invoke model is a text completion one
|
157
|
+
if "body" in self._call_context.params and "amazon.titan" in model_id:
|
158
|
+
attributes[GEN_AI_OPERATION_NAME] = (
|
159
|
+
GenAiOperationNameValues.TEXT_COMPLETION.value
|
160
|
+
)
|
161
|
+
else:
|
162
|
+
attributes[GEN_AI_OPERATION_NAME] = (
|
163
|
+
GenAiOperationNameValues.CHAT.value
|
164
|
+
)
|
165
|
+
return attributes
|
166
|
+
|
167
|
+
def extract_attributes(self, attributes: _AttributeMapT):
|
168
|
+
if self._call_context.operation not in self._HANDLED_OPERATIONS:
|
169
|
+
return
|
170
|
+
|
171
|
+
attributes[GEN_AI_SYSTEM] = GenAiSystemValues.AWS_BEDROCK.value
|
172
|
+
|
173
|
+
model_id = self._call_context.params.get(_MODEL_ID_KEY)
|
174
|
+
if model_id:
|
175
|
+
attributes[GEN_AI_REQUEST_MODEL] = model_id
|
176
|
+
attributes[GEN_AI_OPERATION_NAME] = (
|
177
|
+
GenAiOperationNameValues.CHAT.value
|
178
|
+
)
|
179
|
+
|
180
|
+
# Converse / ConverseStream
|
181
|
+
if inference_config := self._call_context.params.get(
|
182
|
+
"inferenceConfig"
|
183
|
+
):
|
184
|
+
self._set_if_not_none(
|
185
|
+
attributes,
|
186
|
+
GEN_AI_REQUEST_TEMPERATURE,
|
187
|
+
inference_config.get("temperature"),
|
188
|
+
)
|
189
|
+
self._set_if_not_none(
|
190
|
+
attributes,
|
191
|
+
GEN_AI_REQUEST_TOP_P,
|
192
|
+
inference_config.get("topP"),
|
193
|
+
)
|
194
|
+
self._set_if_not_none(
|
195
|
+
attributes,
|
196
|
+
GEN_AI_REQUEST_MAX_TOKENS,
|
197
|
+
inference_config.get("maxTokens"),
|
198
|
+
)
|
199
|
+
self._set_if_not_none(
|
200
|
+
attributes,
|
201
|
+
GEN_AI_REQUEST_STOP_SEQUENCES,
|
202
|
+
inference_config.get("stopSequences"),
|
203
|
+
)
|
204
|
+
|
205
|
+
# InvokeModel
|
206
|
+
# Get the request body if it exists
|
207
|
+
body = self._call_context.params.get("body")
|
208
|
+
if body:
|
209
|
+
try:
|
210
|
+
request_body = json.loads(body)
|
211
|
+
|
212
|
+
if "amazon.titan" in model_id:
|
213
|
+
# titan interface is a text completion one
|
214
|
+
attributes[GEN_AI_OPERATION_NAME] = (
|
215
|
+
GenAiOperationNameValues.TEXT_COMPLETION.value
|
216
|
+
)
|
217
|
+
self._extract_titan_attributes(
|
218
|
+
attributes, request_body
|
219
|
+
)
|
220
|
+
elif "amazon.nova" in model_id:
|
221
|
+
self._extract_nova_attributes(attributes, request_body)
|
222
|
+
elif "anthropic.claude" in model_id:
|
223
|
+
self._extract_claude_attributes(
|
224
|
+
attributes, request_body
|
225
|
+
)
|
226
|
+
except json.JSONDecodeError:
|
227
|
+
_logger.debug("Error: Unable to parse the body as JSON")
|
228
|
+
|
229
|
+
def _extract_titan_attributes(self, attributes, request_body):
|
230
|
+
config = request_body.get("textGenerationConfig", {})
|
231
|
+
self._set_if_not_none(
|
232
|
+
attributes, GEN_AI_REQUEST_TEMPERATURE, config.get("temperature")
|
233
|
+
)
|
234
|
+
self._set_if_not_none(
|
235
|
+
attributes, GEN_AI_REQUEST_TOP_P, config.get("topP")
|
236
|
+
)
|
237
|
+
self._set_if_not_none(
|
238
|
+
attributes, GEN_AI_REQUEST_MAX_TOKENS, config.get("maxTokenCount")
|
239
|
+
)
|
240
|
+
self._set_if_not_none(
|
241
|
+
attributes,
|
242
|
+
GEN_AI_REQUEST_STOP_SEQUENCES,
|
243
|
+
config.get("stopSequences"),
|
244
|
+
)
|
245
|
+
|
246
|
+
def _extract_nova_attributes(self, attributes, request_body):
|
247
|
+
config = request_body.get("inferenceConfig", {})
|
248
|
+
self._set_if_not_none(
|
249
|
+
attributes, GEN_AI_REQUEST_TEMPERATURE, config.get("temperature")
|
250
|
+
)
|
251
|
+
self._set_if_not_none(
|
252
|
+
attributes, GEN_AI_REQUEST_TOP_P, config.get("topP")
|
253
|
+
)
|
254
|
+
self._set_if_not_none(
|
255
|
+
attributes, GEN_AI_REQUEST_MAX_TOKENS, config.get("max_new_tokens")
|
256
|
+
)
|
257
|
+
self._set_if_not_none(
|
258
|
+
attributes,
|
259
|
+
GEN_AI_REQUEST_STOP_SEQUENCES,
|
260
|
+
config.get("stopSequences"),
|
261
|
+
)
|
262
|
+
|
263
|
+
def _extract_claude_attributes(self, attributes, request_body):
|
264
|
+
self._set_if_not_none(
|
265
|
+
attributes,
|
266
|
+
GEN_AI_REQUEST_MAX_TOKENS,
|
267
|
+
request_body.get("max_tokens"),
|
268
|
+
)
|
269
|
+
self._set_if_not_none(
|
270
|
+
attributes,
|
271
|
+
GEN_AI_REQUEST_TEMPERATURE,
|
272
|
+
request_body.get("temperature"),
|
273
|
+
)
|
274
|
+
self._set_if_not_none(
|
275
|
+
attributes, GEN_AI_REQUEST_TOP_P, request_body.get("top_p")
|
276
|
+
)
|
277
|
+
self._set_if_not_none(
|
278
|
+
attributes,
|
279
|
+
GEN_AI_REQUEST_STOP_SEQUENCES,
|
280
|
+
request_body.get("stop_sequences"),
|
281
|
+
)
|
282
|
+
|
283
|
+
@staticmethod
|
284
|
+
def _set_if_not_none(attributes, key, value):
|
285
|
+
if value is not None:
|
286
|
+
attributes[key] = value
|
287
|
+
|
288
|
+
def _get_request_messages(self):
|
289
|
+
"""Extracts and normalize system and user / assistant messages"""
|
290
|
+
input_text = None
|
291
|
+
if system := self._call_context.params.get("system", []):
|
292
|
+
system_messages = [{"role": "system", "content": system}]
|
293
|
+
else:
|
294
|
+
system_messages = []
|
295
|
+
|
296
|
+
if not (messages := self._call_context.params.get("messages", [])):
|
297
|
+
if body := self._call_context.params.get("body"):
|
298
|
+
decoded_body = json.loads(body)
|
299
|
+
if system := decoded_body.get("system"):
|
300
|
+
if isinstance(system, str):
|
301
|
+
content = [{"text": system}]
|
302
|
+
else:
|
303
|
+
content = system
|
304
|
+
system_messages = [{"role": "system", "content": content}]
|
305
|
+
|
306
|
+
messages = decoded_body.get("messages", [])
|
307
|
+
if not messages:
|
308
|
+
# transform old school amazon titan invokeModel api to messages
|
309
|
+
if input_text := decoded_body.get("inputText"):
|
310
|
+
messages = [
|
311
|
+
{"role": "user", "content": [{"text": input_text}]}
|
312
|
+
]
|
313
|
+
|
314
|
+
return system_messages + messages
|
315
|
+
|
316
|
+
def before_service_call(
|
317
|
+
self, span: Span, instrumentor_context: _BotocoreInstrumentorContext
|
318
|
+
):
|
319
|
+
if self._call_context.operation not in self._HANDLED_OPERATIONS:
|
320
|
+
return
|
321
|
+
|
322
|
+
capture_content = genai_capture_message_content()
|
323
|
+
|
324
|
+
messages = self._get_request_messages()
|
325
|
+
for message in messages:
|
326
|
+
event_logger = instrumentor_context.event_logger
|
327
|
+
for event in message_to_event(message, capture_content):
|
328
|
+
event_logger.emit(event)
|
329
|
+
|
330
|
+
if span.is_recording():
|
331
|
+
operation_name = span.attributes.get(GEN_AI_OPERATION_NAME, "")
|
332
|
+
request_model = span.attributes.get(GEN_AI_REQUEST_MODEL, "")
|
333
|
+
# avoid setting to an empty string if are not available
|
334
|
+
if operation_name and request_model:
|
335
|
+
span.update_name(f"{operation_name} {request_model}")
|
336
|
+
|
337
|
+
# this is used to calculate the operation duration metric, duration may be skewed by request_hook
|
338
|
+
# pylint: disable=attribute-defined-outside-init
|
339
|
+
self._operation_start = default_timer()
|
340
|
+
|
341
|
+
# pylint: disable=no-self-use,too-many-locals
|
342
|
+
def _converse_on_success(
|
343
|
+
self,
|
344
|
+
span: Span,
|
345
|
+
result: dict[str, Any],
|
346
|
+
instrumentor_context: _BotocoreInstrumentorContext,
|
347
|
+
capture_content,
|
348
|
+
):
|
349
|
+
if span.is_recording():
|
350
|
+
if usage := result.get("usage"):
|
351
|
+
if input_tokens := usage.get("inputTokens"):
|
352
|
+
span.set_attribute(
|
353
|
+
GEN_AI_USAGE_INPUT_TOKENS,
|
354
|
+
input_tokens,
|
355
|
+
)
|
356
|
+
if output_tokens := usage.get("outputTokens"):
|
357
|
+
span.set_attribute(
|
358
|
+
GEN_AI_USAGE_OUTPUT_TOKENS,
|
359
|
+
output_tokens,
|
360
|
+
)
|
361
|
+
|
362
|
+
if stop_reason := result.get("stopReason"):
|
363
|
+
span.set_attribute(
|
364
|
+
GEN_AI_RESPONSE_FINISH_REASONS,
|
365
|
+
[stop_reason],
|
366
|
+
)
|
367
|
+
|
368
|
+
event_logger = instrumentor_context.event_logger
|
369
|
+
choice = _Choice.from_converse(result, capture_content)
|
370
|
+
# this path is used by streaming apis, in that case we are already out of the span
|
371
|
+
# context so need to add the span context manually
|
372
|
+
span_ctx = span.get_span_context()
|
373
|
+
event_logger.emit(
|
374
|
+
choice.to_choice_event(
|
375
|
+
trace_id=span_ctx.trace_id,
|
376
|
+
span_id=span_ctx.span_id,
|
377
|
+
trace_flags=span_ctx.trace_flags,
|
378
|
+
)
|
379
|
+
)
|
380
|
+
|
381
|
+
metrics = instrumentor_context.metrics
|
382
|
+
metrics_attributes = self._extract_metrics_attributes()
|
383
|
+
if operation_duration_histogram := metrics.get(
|
384
|
+
GEN_AI_CLIENT_OPERATION_DURATION
|
385
|
+
):
|
386
|
+
duration = max((default_timer() - self._operation_start), 0)
|
387
|
+
operation_duration_histogram.record(
|
388
|
+
duration,
|
389
|
+
attributes=metrics_attributes,
|
390
|
+
)
|
391
|
+
|
392
|
+
if token_usage_histogram := metrics.get(GEN_AI_CLIENT_TOKEN_USAGE):
|
393
|
+
if usage := result.get("usage"):
|
394
|
+
if input_tokens := usage.get("inputTokens"):
|
395
|
+
input_attributes = {
|
396
|
+
**metrics_attributes,
|
397
|
+
GEN_AI_TOKEN_TYPE: GenAiTokenTypeValues.INPUT.value,
|
398
|
+
}
|
399
|
+
token_usage_histogram.record(
|
400
|
+
input_tokens, input_attributes
|
401
|
+
)
|
402
|
+
|
403
|
+
if output_tokens := usage.get("outputTokens"):
|
404
|
+
output_attributes = {
|
405
|
+
**metrics_attributes,
|
406
|
+
GEN_AI_TOKEN_TYPE: GenAiTokenTypeValues.COMPLETION.value,
|
407
|
+
}
|
408
|
+
token_usage_histogram.record(
|
409
|
+
output_tokens, output_attributes
|
410
|
+
)
|
411
|
+
|
412
|
+
def _invoke_model_on_success(
|
413
|
+
self,
|
414
|
+
span: Span,
|
415
|
+
result: dict[str, Any],
|
416
|
+
model_id: str,
|
417
|
+
instrumentor_context: _BotocoreInstrumentorContext,
|
418
|
+
capture_content,
|
419
|
+
):
|
420
|
+
original_body = None
|
421
|
+
try:
|
422
|
+
original_body = result["body"]
|
423
|
+
body_content = original_body.read()
|
424
|
+
|
425
|
+
# Replenish stream for downstream application use
|
426
|
+
new_stream = io.BytesIO(body_content)
|
427
|
+
result["body"] = StreamingBody(new_stream, len(body_content))
|
428
|
+
|
429
|
+
response_body = json.loads(body_content.decode("utf-8"))
|
430
|
+
if "amazon.titan" in model_id:
|
431
|
+
self._handle_amazon_titan_response(
|
432
|
+
span, response_body, instrumentor_context, capture_content
|
433
|
+
)
|
434
|
+
elif "amazon.nova" in model_id:
|
435
|
+
self._handle_amazon_nova_response(
|
436
|
+
span, response_body, instrumentor_context, capture_content
|
437
|
+
)
|
438
|
+
elif "anthropic.claude" in model_id:
|
439
|
+
self._handle_anthropic_claude_response(
|
440
|
+
span, response_body, instrumentor_context, capture_content
|
441
|
+
)
|
442
|
+
except json.JSONDecodeError:
|
443
|
+
_logger.debug("Error: Unable to parse the response body as JSON")
|
444
|
+
except Exception as exc: # pylint: disable=broad-exception-caught
|
445
|
+
_logger.debug("Error processing response: %s", exc)
|
446
|
+
finally:
|
447
|
+
if original_body is not None:
|
448
|
+
original_body.close()
|
449
|
+
|
450
|
+
def _on_stream_error_callback(
|
451
|
+
self,
|
452
|
+
span: Span,
|
453
|
+
exception,
|
454
|
+
instrumentor_context: _BotocoreInstrumentorContext,
|
455
|
+
):
|
456
|
+
span.set_status(Status(StatusCode.ERROR, str(exception)))
|
457
|
+
if span.is_recording():
|
458
|
+
span.set_attribute(ERROR_TYPE, type(exception).__qualname__)
|
459
|
+
span.end()
|
460
|
+
|
461
|
+
metrics = instrumentor_context.metrics
|
462
|
+
metrics_attributes = {
|
463
|
+
**self._extract_metrics_attributes(),
|
464
|
+
ERROR_TYPE: type(exception).__qualname__,
|
465
|
+
}
|
466
|
+
if operation_duration_histogram := metrics.get(
|
467
|
+
GEN_AI_CLIENT_OPERATION_DURATION
|
468
|
+
):
|
469
|
+
duration = max((default_timer() - self._operation_start), 0)
|
470
|
+
operation_duration_histogram.record(
|
471
|
+
duration,
|
472
|
+
attributes=metrics_attributes,
|
473
|
+
)
|
474
|
+
|
475
|
+
def on_success(
|
476
|
+
self,
|
477
|
+
span: Span,
|
478
|
+
result: dict[str, Any],
|
479
|
+
instrumentor_context: _BotocoreInstrumentorContext,
|
480
|
+
):
|
481
|
+
if self._call_context.operation not in self._HANDLED_OPERATIONS:
|
482
|
+
return
|
483
|
+
|
484
|
+
capture_content = genai_capture_message_content()
|
485
|
+
|
486
|
+
if self._call_context.operation == "ConverseStream":
|
487
|
+
if "stream" in result and isinstance(
|
488
|
+
result["stream"], EventStream
|
489
|
+
):
|
490
|
+
|
491
|
+
def stream_done_callback(response):
|
492
|
+
self._converse_on_success(
|
493
|
+
span, response, instrumentor_context, capture_content
|
494
|
+
)
|
495
|
+
span.end()
|
496
|
+
|
497
|
+
def stream_error_callback(exception):
|
498
|
+
self._on_stream_error_callback(
|
499
|
+
span, exception, instrumentor_context
|
500
|
+
)
|
501
|
+
|
502
|
+
result["stream"] = ConverseStreamWrapper(
|
503
|
+
result["stream"],
|
504
|
+
stream_done_callback,
|
505
|
+
stream_error_callback,
|
506
|
+
)
|
507
|
+
return
|
508
|
+
elif self._call_context.operation == "Converse":
|
509
|
+
self._converse_on_success(
|
510
|
+
span, result, instrumentor_context, capture_content
|
511
|
+
)
|
512
|
+
|
513
|
+
model_id = self._call_context.params.get(_MODEL_ID_KEY)
|
514
|
+
if not model_id:
|
515
|
+
return
|
516
|
+
|
517
|
+
if self._call_context.operation == "InvokeModel":
|
518
|
+
if "body" in result and isinstance(result["body"], StreamingBody):
|
519
|
+
self._invoke_model_on_success(
|
520
|
+
span,
|
521
|
+
result,
|
522
|
+
model_id,
|
523
|
+
instrumentor_context,
|
524
|
+
capture_content,
|
525
|
+
)
|
526
|
+
return
|
527
|
+
elif self._call_context.operation == "InvokeModelWithResponseStream":
|
528
|
+
if "body" in result and isinstance(result["body"], EventStream):
|
529
|
+
|
530
|
+
def invoke_model_stream_done_callback(response):
|
531
|
+
# the callback gets data formatted as the simpler converse API
|
532
|
+
self._converse_on_success(
|
533
|
+
span, response, instrumentor_context, capture_content
|
534
|
+
)
|
535
|
+
span.end()
|
536
|
+
|
537
|
+
def invoke_model_stream_error_callback(exception):
|
538
|
+
self._on_stream_error_callback(
|
539
|
+
span, exception, instrumentor_context
|
540
|
+
)
|
541
|
+
|
542
|
+
result["body"] = InvokeModelWithResponseStreamWrapper(
|
543
|
+
result["body"],
|
544
|
+
invoke_model_stream_done_callback,
|
545
|
+
invoke_model_stream_error_callback,
|
546
|
+
model_id,
|
547
|
+
)
|
548
|
+
return
|
549
|
+
|
550
|
+
# pylint: disable=no-self-use,too-many-locals
|
551
|
+
def _handle_amazon_titan_response(
|
552
|
+
self,
|
553
|
+
span: Span,
|
554
|
+
response_body: dict[str, Any],
|
555
|
+
instrumentor_context: _BotocoreInstrumentorContext,
|
556
|
+
capture_content: bool,
|
557
|
+
):
|
558
|
+
if "inputTextTokenCount" in response_body:
|
559
|
+
span.set_attribute(
|
560
|
+
GEN_AI_USAGE_INPUT_TOKENS, response_body["inputTextTokenCount"]
|
561
|
+
)
|
562
|
+
if "results" in response_body and response_body["results"]:
|
563
|
+
result = response_body["results"][0]
|
564
|
+
if "tokenCount" in result:
|
565
|
+
span.set_attribute(
|
566
|
+
GEN_AI_USAGE_OUTPUT_TOKENS, result["tokenCount"]
|
567
|
+
)
|
568
|
+
if "completionReason" in result:
|
569
|
+
span.set_attribute(
|
570
|
+
GEN_AI_RESPONSE_FINISH_REASONS,
|
571
|
+
[result["completionReason"]],
|
572
|
+
)
|
573
|
+
|
574
|
+
event_logger = instrumentor_context.event_logger
|
575
|
+
choice = _Choice.from_invoke_amazon_titan(
|
576
|
+
response_body, capture_content
|
577
|
+
)
|
578
|
+
event_logger.emit(choice.to_choice_event())
|
579
|
+
|
580
|
+
metrics = instrumentor_context.metrics
|
581
|
+
metrics_attributes = self._extract_metrics_attributes()
|
582
|
+
if operation_duration_histogram := metrics.get(
|
583
|
+
GEN_AI_CLIENT_OPERATION_DURATION
|
584
|
+
):
|
585
|
+
duration = max((default_timer() - self._operation_start), 0)
|
586
|
+
operation_duration_histogram.record(
|
587
|
+
duration,
|
588
|
+
attributes=metrics_attributes,
|
589
|
+
)
|
590
|
+
|
591
|
+
if token_usage_histogram := metrics.get(GEN_AI_CLIENT_TOKEN_USAGE):
|
592
|
+
if input_tokens := response_body.get("inputTextTokenCount"):
|
593
|
+
input_attributes = {
|
594
|
+
**metrics_attributes,
|
595
|
+
GEN_AI_TOKEN_TYPE: GenAiTokenTypeValues.INPUT.value,
|
596
|
+
}
|
597
|
+
token_usage_histogram.record(
|
598
|
+
input_tokens, input_attributes
|
599
|
+
)
|
600
|
+
|
601
|
+
if results := response_body.get("results"):
|
602
|
+
if output_tokens := results[0].get("tokenCount"):
|
603
|
+
output_attributes = {
|
604
|
+
**metrics_attributes,
|
605
|
+
GEN_AI_TOKEN_TYPE: GenAiTokenTypeValues.COMPLETION.value,
|
606
|
+
}
|
607
|
+
token_usage_histogram.record(
|
608
|
+
output_tokens, output_attributes
|
609
|
+
)
|
610
|
+
|
611
|
+
# pylint: disable=no-self-use,too-many-locals
|
612
|
+
def _handle_amazon_nova_response(
|
613
|
+
self,
|
614
|
+
span: Span,
|
615
|
+
response_body: dict[str, Any],
|
616
|
+
instrumentor_context: _BotocoreInstrumentorContext,
|
617
|
+
capture_content: bool,
|
618
|
+
):
|
619
|
+
if "usage" in response_body:
|
620
|
+
usage = response_body["usage"]
|
621
|
+
if "inputTokens" in usage:
|
622
|
+
span.set_attribute(
|
623
|
+
GEN_AI_USAGE_INPUT_TOKENS, usage["inputTokens"]
|
624
|
+
)
|
625
|
+
if "outputTokens" in usage:
|
626
|
+
span.set_attribute(
|
627
|
+
GEN_AI_USAGE_OUTPUT_TOKENS, usage["outputTokens"]
|
628
|
+
)
|
629
|
+
if "stopReason" in response_body:
|
630
|
+
span.set_attribute(
|
631
|
+
GEN_AI_RESPONSE_FINISH_REASONS, [response_body["stopReason"]]
|
632
|
+
)
|
633
|
+
|
634
|
+
event_logger = instrumentor_context.event_logger
|
635
|
+
choice = _Choice.from_converse(response_body, capture_content)
|
636
|
+
event_logger.emit(choice.to_choice_event())
|
637
|
+
|
638
|
+
metrics = instrumentor_context.metrics
|
639
|
+
metrics_attributes = self._extract_metrics_attributes()
|
640
|
+
if operation_duration_histogram := metrics.get(
|
641
|
+
GEN_AI_CLIENT_OPERATION_DURATION
|
642
|
+
):
|
643
|
+
duration = max((default_timer() - self._operation_start), 0)
|
644
|
+
operation_duration_histogram.record(
|
645
|
+
duration,
|
646
|
+
attributes=metrics_attributes,
|
647
|
+
)
|
648
|
+
|
649
|
+
if token_usage_histogram := metrics.get(GEN_AI_CLIENT_TOKEN_USAGE):
|
650
|
+
if usage := response_body.get("usage"):
|
651
|
+
if input_tokens := usage.get("inputTokens"):
|
652
|
+
input_attributes = {
|
653
|
+
**metrics_attributes,
|
654
|
+
GEN_AI_TOKEN_TYPE: GenAiTokenTypeValues.INPUT.value,
|
655
|
+
}
|
656
|
+
token_usage_histogram.record(
|
657
|
+
input_tokens, input_attributes
|
658
|
+
)
|
659
|
+
|
660
|
+
if output_tokens := usage.get("outputTokens"):
|
661
|
+
output_attributes = {
|
662
|
+
**metrics_attributes,
|
663
|
+
GEN_AI_TOKEN_TYPE: GenAiTokenTypeValues.COMPLETION.value,
|
664
|
+
}
|
665
|
+
token_usage_histogram.record(
|
666
|
+
output_tokens, output_attributes
|
667
|
+
)
|
668
|
+
|
669
|
+
# pylint: disable=no-self-use
|
670
|
+
def _handle_anthropic_claude_response(
|
671
|
+
self,
|
672
|
+
span: Span,
|
673
|
+
response_body: dict[str, Any],
|
674
|
+
instrumentor_context: _BotocoreInstrumentorContext,
|
675
|
+
capture_content: bool,
|
676
|
+
):
|
677
|
+
if usage := response_body.get("usage"):
|
678
|
+
if "input_tokens" in usage:
|
679
|
+
span.set_attribute(
|
680
|
+
GEN_AI_USAGE_INPUT_TOKENS, usage["input_tokens"]
|
681
|
+
)
|
682
|
+
if "output_tokens" in usage:
|
683
|
+
span.set_attribute(
|
684
|
+
GEN_AI_USAGE_OUTPUT_TOKENS, usage["output_tokens"]
|
685
|
+
)
|
686
|
+
if "stop_reason" in response_body:
|
687
|
+
span.set_attribute(
|
688
|
+
GEN_AI_RESPONSE_FINISH_REASONS, [response_body["stop_reason"]]
|
689
|
+
)
|
690
|
+
|
691
|
+
event_logger = instrumentor_context.event_logger
|
692
|
+
choice = _Choice.from_invoke_anthropic_claude(
|
693
|
+
response_body, capture_content
|
694
|
+
)
|
695
|
+
event_logger.emit(choice.to_choice_event())
|
696
|
+
|
697
|
+
metrics = instrumentor_context.metrics
|
698
|
+
metrics_attributes = self._extract_metrics_attributes()
|
699
|
+
if operation_duration_histogram := metrics.get(
|
700
|
+
GEN_AI_CLIENT_OPERATION_DURATION
|
701
|
+
):
|
702
|
+
duration = max((default_timer() - self._operation_start), 0)
|
703
|
+
operation_duration_histogram.record(
|
704
|
+
duration,
|
705
|
+
attributes=metrics_attributes,
|
706
|
+
)
|
707
|
+
|
708
|
+
if token_usage_histogram := metrics.get(GEN_AI_CLIENT_TOKEN_USAGE):
|
709
|
+
if usage := response_body.get("usage"):
|
710
|
+
if input_tokens := usage.get("input_tokens"):
|
711
|
+
input_attributes = {
|
712
|
+
**metrics_attributes,
|
713
|
+
GEN_AI_TOKEN_TYPE: GenAiTokenTypeValues.INPUT.value,
|
714
|
+
}
|
715
|
+
token_usage_histogram.record(
|
716
|
+
input_tokens, input_attributes
|
717
|
+
)
|
718
|
+
|
719
|
+
if output_tokens := usage.get("output_tokens"):
|
720
|
+
output_attributes = {
|
721
|
+
**metrics_attributes,
|
722
|
+
GEN_AI_TOKEN_TYPE: GenAiTokenTypeValues.COMPLETION.value,
|
723
|
+
}
|
724
|
+
token_usage_histogram.record(
|
725
|
+
output_tokens, output_attributes
|
726
|
+
)
|
727
|
+
|
728
|
+
def on_error(
|
729
|
+
self,
|
730
|
+
span: Span,
|
731
|
+
exception: _BotoClientErrorT,
|
732
|
+
instrumentor_context: _BotocoreInstrumentorContext,
|
733
|
+
):
|
734
|
+
if self._call_context.operation not in self._HANDLED_OPERATIONS:
|
735
|
+
return
|
736
|
+
|
737
|
+
span.set_status(Status(StatusCode.ERROR, str(exception)))
|
738
|
+
if span.is_recording():
|
739
|
+
span.set_attribute(ERROR_TYPE, type(exception).__qualname__)
|
740
|
+
|
741
|
+
if not self.should_end_span_on_exit():
|
742
|
+
span.end()
|
743
|
+
|
744
|
+
metrics = instrumentor_context.metrics
|
745
|
+
metrics_attributes = {
|
746
|
+
**self._extract_metrics_attributes(),
|
747
|
+
ERROR_TYPE: type(exception).__qualname__,
|
748
|
+
}
|
749
|
+
if operation_duration_histogram := metrics.get(
|
750
|
+
GEN_AI_CLIENT_OPERATION_DURATION
|
751
|
+
):
|
752
|
+
duration = max((default_timer() - self._operation_start), 0)
|
753
|
+
operation_duration_histogram.record(
|
754
|
+
duration,
|
755
|
+
attributes=metrics_attributes,
|
756
|
+
)
|