Skip to content

Commit 45f972a

Browse files
committed
Add tests and handle only non streaming responses
1 parent 4c5f988 commit 45f972a

File tree

8 files changed

+467
-164
lines changed

8 files changed

+467
-164
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = (
2+
"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"
3+
)

instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock.py

Lines changed: 28 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,11 @@
1818

1919
from __future__ import annotations
2020

21-
import io
2221
import json
2322
import logging
2423
import math
2524
from typing import Any
2625

27-
from botocore.response import StreamingBody
28-
2926
from opentelemetry.instrumentation.botocore.extensions.types import (
3027
_AttributeMapT,
3128
_AwsSdkExtension,
@@ -58,13 +55,19 @@ class _BedrockRuntimeExtension(_AwsSdkExtension):
5855
"""
5956

6057
def extract_attributes(self, attributes: _AttributeMapT):
61-
attributes[GEN_AI_SYSTEM] = GenAiSystemValues.AWS_BEDROCK
62-
attributes[GEN_AI_OPERATION_NAME] = GenAiOperationNameValues.CHAT
58+
attributes[GEN_AI_SYSTEM] = GenAiSystemValues.AWS_BEDROCK.value
6359

6460
model_id = self._call_context.params.get(_MODEL_ID_KEY)
6561
if model_id:
6662
attributes[GEN_AI_REQUEST_MODEL] = model_id
6763

64+
# FIXME: add other model patterns
65+
text_model_patterns = ["amazon.titan-text"]
66+
if any(pattern in model_id for pattern in text_model_patterns):
67+
attributes[GEN_AI_OPERATION_NAME] = (
68+
GenAiOperationNameValues.CHAT.value
69+
)
70+
6871
# Get the request body if it exists
6972
body = self._call_context.params.get("body")
7073
if body:
@@ -209,176 +212,38 @@ def _set_if_not_none(attributes, key, value):
209212
if value is not None:
210213
attributes[key] = value
211214

215+
def before_service_call(self, span: Span):
216+
if not span.is_recording():
217+
return
218+
219+
operation_name = span.attributes.get(GEN_AI_OPERATION_NAME, "")
220+
request_model = span.attributes.get(GEN_AI_REQUEST_MODEL, "")
221+
# avoid setting to an empty string if are not available
222+
if operation_name and request_model:
223+
span.update_name(f"{operation_name} {request_model}")
224+
212225
# pylint: disable=too-many-branches
213226
def on_success(self, span: Span, result: dict[str, Any]):
214227
model_id = self._call_context.params.get(_MODEL_ID_KEY)
215228

216229
if not model_id:
217230
return
218231

219-
if "body" in result and isinstance(result["body"], StreamingBody):
220-
original_body = None
221-
try:
222-
original_body = result["body"]
223-
body_content = original_body.read()
224-
225-
# Use one stream for telemetry
226-
stream = io.BytesIO(body_content)
227-
telemetry_content = stream.read()
228-
response_body = json.loads(telemetry_content.decode("utf-8"))
229-
if "amazon.titan" in model_id:
230-
self._handle_amazon_titan_response(span, response_body)
231-
if "amazon.nova" in model_id:
232-
self._handle_amazon_nova_response(span, response_body)
233-
elif "anthropic.claude" in model_id:
234-
self._handle_anthropic_claude_response(span, response_body)
235-
elif "meta.llama" in model_id:
236-
self._handle_meta_llama_response(span, response_body)
237-
elif "cohere.command" in model_id:
238-
self._handle_cohere_command_response(span, response_body)
239-
elif "ai21.jamba" in model_id:
240-
self._handle_ai21_jamba_response(span, response_body)
241-
elif "mistral" in model_id:
242-
self._handle_mistral_mistral_response(span, response_body)
243-
# Replenish stream for downstream application use
244-
new_stream = io.BytesIO(body_content)
245-
result["body"] = StreamingBody(new_stream, len(body_content))
246-
247-
except json.JSONDecodeError:
248-
_logger.debug(
249-
"Error: Unable to parse the response body as JSON"
250-
)
251-
except Exception as e: # pylint: disable=broad-exception-caught, invalid-name
252-
_logger.debug("Error processing response: %s", e)
253-
finally:
254-
if original_body is not None:
255-
original_body.close()
256-
257-
# pylint: disable=no-self-use
258-
def _handle_amazon_titan_response(
259-
self, span: Span, response_body: dict[str, Any]
260-
):
261-
if "inputTextTokenCount" in response_body:
262-
span.set_attribute(
263-
GEN_AI_USAGE_INPUT_TOKENS, response_body["inputTextTokenCount"]
264-
)
265-
if "results" in response_body and response_body["results"]:
266-
result = response_body["results"][0]
267-
if "tokenCount" in result:
268-
span.set_attribute(
269-
GEN_AI_USAGE_OUTPUT_TOKENS, result["tokenCount"]
270-
)
271-
if "completionReason" in result:
272-
span.set_attribute(
273-
GEN_AI_RESPONSE_FINISH_REASONS,
274-
[result["completionReason"]],
275-
)
276-
277-
# pylint: disable=no-self-use
278-
def _handle_amazon_nova_response(
279-
self, span: Span, response_body: dict[str, Any]
280-
):
281-
if "usage" in response_body:
282-
usage = response_body["usage"]
283-
if "inputTokens" in usage:
284-
span.set_attribute(
285-
GEN_AI_USAGE_INPUT_TOKENS, usage["inputTokens"]
286-
)
287-
if "outputTokens" in usage:
232+
# FIXME: this is tested only with titan
233+
if usage := result.get("usage"):
234+
if input_tokens := usage.get("inputTokens"):
288235
span.set_attribute(
289-
GEN_AI_USAGE_OUTPUT_TOKENS, usage["outputTokens"]
236+
GEN_AI_USAGE_INPUT_TOKENS,
237+
input_tokens,
290238
)
291-
if "stopReason" in response_body:
292-
span.set_attribute(
293-
GEN_AI_RESPONSE_FINISH_REASONS, [response_body["stopReason"]]
294-
)
295-
296-
# pylint: disable=no-self-use
297-
def _handle_anthropic_claude_response(
298-
self, span: Span, response_body: dict[str, Any]
299-
):
300-
if "usage" in response_body:
301-
usage = response_body["usage"]
302-
if "input_tokens" in usage:
239+
if output_tokens := usage.get("outputTokens"):
303240
span.set_attribute(
304-
GEN_AI_USAGE_INPUT_TOKENS, usage["input_tokens"]
305-
)
306-
if "output_tokens" in usage:
307-
span.set_attribute(
308-
GEN_AI_USAGE_OUTPUT_TOKENS, usage["output_tokens"]
241+
GEN_AI_USAGE_OUTPUT_TOKENS,
242+
output_tokens,
309243
)
310-
if "stop_reason" in response_body:
311-
span.set_attribute(
312-
GEN_AI_RESPONSE_FINISH_REASONS, [response_body["stop_reason"]]
313-
)
314244

315-
# pylint: disable=no-self-use
316-
def _handle_cohere_command_response(
317-
self, span: Span, response_body: dict[str, Any]
318-
):
319-
# Output tokens: Approximate from the response text
320-
if "text" in response_body:
321-
span.set_attribute(
322-
GEN_AI_USAGE_OUTPUT_TOKENS,
323-
math.ceil(len(response_body["text"]) / 6),
324-
)
325-
if "finish_reason" in response_body:
245+
if stop_reason := result.get("stopReason"):
326246
span.set_attribute(
327247
GEN_AI_RESPONSE_FINISH_REASONS,
328-
[response_body["finish_reason"]],
329-
)
330-
331-
# pylint: disable=no-self-use
332-
def _handle_ai21_jamba_response(
333-
self, span: Span, response_body: dict[str, Any]
334-
):
335-
if "usage" in response_body:
336-
usage = response_body["usage"]
337-
if "prompt_tokens" in usage:
338-
span.set_attribute(
339-
GEN_AI_USAGE_INPUT_TOKENS, usage["prompt_tokens"]
340-
)
341-
if "completion_tokens" in usage:
342-
span.set_attribute(
343-
GEN_AI_USAGE_OUTPUT_TOKENS, usage["completion_tokens"]
344-
)
345-
if "choices" in response_body:
346-
choices = response_body["choices"][0]
347-
if "finish_reason" in choices:
348-
span.set_attribute(
349-
GEN_AI_RESPONSE_FINISH_REASONS, [choices["finish_reason"]]
350-
)
351-
352-
# pylint: disable=no-self-use
353-
def _handle_meta_llama_response(
354-
self, span: Span, response_body: dict[str, Any]
355-
):
356-
if "prompt_token_count" in response_body:
357-
span.set_attribute(
358-
GEN_AI_USAGE_INPUT_TOKENS, response_body["prompt_token_count"]
359-
)
360-
if "generation_token_count" in response_body:
361-
span.set_attribute(
362-
GEN_AI_USAGE_OUTPUT_TOKENS,
363-
response_body["generation_token_count"],
364-
)
365-
if "stop_reason" in response_body:
366-
span.set_attribute(
367-
GEN_AI_RESPONSE_FINISH_REASONS, [response_body["stop_reason"]]
368-
)
369-
370-
# pylint: disable=no-self-use
371-
def _handle_mistral_mistral_response(
372-
self, span: Span, response_body: dict[str, Any]
373-
):
374-
if "outputs" in response_body:
375-
outputs = response_body["outputs"][0]
376-
if "text" in outputs:
377-
span.set_attribute(
378-
GEN_AI_USAGE_OUTPUT_TOKENS,
379-
math.ceil(len(outputs["text"]) / 6),
380-
)
381-
if "stop_reason" in outputs:
382-
span.set_attribute(
383-
GEN_AI_RESPONSE_FINISH_REASONS, [outputs["stop_reason"]]
248+
[stop_reason],
384249
)

instrumentation/opentelemetry-instrumentation-botocore/test-requirements.txt renamed to instrumentation/opentelemetry-instrumentation-botocore/test-requirements-0.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ pluggy==1.5.0
1919
py-cpuinfo==9.0.0
2020
pycparser==2.21
2121
pytest==7.4.4
22+
pytest-vcr==1.0.2
2223
python-dateutil==2.8.2
2324
pytz==2024.1
2425
PyYAML==6.0.1
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
asgiref==3.8.1
2+
aws-xray-sdk==2.12.1
3+
boto3==1.35.56
4+
botocore==1.35.56
5+
certifi==2024.7.4
6+
cffi==1.17.0
7+
charset-normalizer==3.3.2
8+
cryptography==43.0.1
9+
Deprecated==1.2.14
10+
docker==7.0.0
11+
idna==3.7
12+
iniconfig==2.0.0
13+
Jinja2==3.1.4
14+
jmespath==1.0.1
15+
MarkupSafe==2.1.5
16+
moto==5.0.9
17+
packaging==24.0
18+
pluggy==1.5.0
19+
py-cpuinfo==9.0.0
20+
pycparser==2.21
21+
pytest==7.4.4
22+
pytest-vcr==1.0.2
23+
python-dateutil==2.8.2
24+
pytz==2024.1
25+
PyYAML==6.0.1
26+
requests==2.32.3
27+
responses==0.25.0
28+
s3transfer==0.10.0
29+
six==1.16.0
30+
tomli==2.0.1
31+
typing_extensions==4.12.2
32+
urllib3==1.26.19
33+
Werkzeug==3.0.6
34+
wrapt==1.16.0
35+
xmltodict==0.13.0
36+
zipp==3.19.2
37+
-e opentelemetry-instrumentation
38+
-e propagator/opentelemetry-propagator-aws-xray
39+
-e instrumentation/opentelemetry-instrumentation-botocore
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
interactions:
2+
- request:
3+
body: |-
4+
{
5+
"messages": [
6+
{
7+
"role": "user",
8+
"content": [
9+
{
10+
"text": "Say this is a test"
11+
}
12+
]
13+
}
14+
],
15+
"inferenceConfig": {
16+
"maxTokens": 100
17+
}
18+
}
19+
headers:
20+
Content-Length:
21+
- '116'
22+
Content-Type:
23+
- !!binary |
24+
YXBwbGljYXRpb24vanNvbg==
25+
User-Agent:
26+
- !!binary |
27+
Qm90bzMvMS4zNS41NiBtZC9Cb3RvY29yZSMxLjM1LjU2IHVhLzIuMCBvcy9saW51eCM2LjEuMC0x
28+
MDM0LW9lbSBtZC9hcmNoI3g4Nl82NCBsYW5nL3B5dGhvbiMzLjEwLjEyIG1kL3B5aW1wbCNDUHl0
29+
aG9uIGNmZy9yZXRyeS1tb2RlI2xlZ2FjeSBCb3RvY29yZS8xLjM1LjU2
30+
X-Amz-Date:
31+
- !!binary |
32+
MjAyNDEyMzBUMTY0NjU1Wg==
33+
X-Amz-Security-Token:
34+
- test_aws_security_token
35+
X-Amzn-Trace-Id:
36+
- !!binary |
37+
Um9vdD0xLWRjYjVjZTc2LWZjMjkyNWZmMTRhZGEzYTgzNDBiNjc3ZTtQYXJlbnQ9ZjU1MTk0MWRm
38+
OGM5MGFjMTtTYW1wbGVkPTE=
39+
amz-sdk-invocation-id:
40+
- !!binary |
41+
YzdiMmUzNGMtYThmYi00YzczLTlhNzMtZDIxNzFkMGY3NzAw
42+
amz-sdk-request:
43+
- !!binary |
44+
YXR0ZW1wdD0x
45+
authorization:
46+
- Bearer test_aws_authorization
47+
method: POST
48+
uri: https://bedrock-runtime.eu-central-1.amazonaws.com/model/amazon.titan-text-lite-v1/converse
49+
response:
50+
body:
51+
string: |-
52+
{
53+
"metrics": {
54+
"latencyMs": 585
55+
},
56+
"output": {
57+
"message": {
58+
"content": [
59+
{
60+
"text": "This is a test."
61+
}
62+
],
63+
"role": "assistant"
64+
}
65+
},
66+
"stopReason": "end_turn",
67+
"usage": {
68+
"inputTokens": 8,
69+
"outputTokens": 9,
70+
"totalTokens": 17
71+
}
72+
}
73+
headers:
74+
Connection:
75+
- keep-alive
76+
Content-Length:
77+
- '194'
78+
Content-Type:
79+
- application/json
80+
Date:
81+
- Mon, 30 Dec 2024 16:46:56 GMT
82+
Set-Cookie: test_set_cookie
83+
x-amzn-RequestId:
84+
- 4d340a17-68b2-4727-b0df-a98dcd3a5af1
85+
status:
86+
code: 200
87+
message: OK
88+
version: 1

0 commit comments

Comments
 (0)