Skip to content

Commit 1831bdb

Browse files
authored
Add ensure_ascii=False to json.dumps() calls in telemetry tracer (#37)
1 parent 77f5fa7 commit 1831bdb

File tree

2 files changed

+68
-10
lines changed

2 files changed

+68
-10
lines changed

src/strands/telemetry/tracer.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ def start_model_invoke_span(
315315
"gen_ai.system": "strands-agents",
316316
"agent.name": agent_name,
317317
"gen_ai.agent.name": agent_name,
318-
"gen_ai.prompt": json.dumps(messages, cls=JSONEncoder),
318+
"gen_ai.prompt": serialize(messages),
319319
}
320320

321321
if model_id:
@@ -338,7 +338,7 @@ def end_model_invoke_span(
338338
error: Optional exception if the model call failed.
339339
"""
340340
attributes: Dict[str, AttributeValue] = {
341-
"gen_ai.completion": json.dumps(message["content"], cls=JSONEncoder),
341+
"gen_ai.completion": serialize(message["content"]),
342342
"gen_ai.usage.prompt_tokens": usage["inputTokens"],
343343
"gen_ai.usage.completion_tokens": usage["outputTokens"],
344344
"gen_ai.usage.total_tokens": usage["totalTokens"],
@@ -360,10 +360,10 @@ def start_tool_call_span(
360360
The created span, or None if tracing is not enabled.
361361
"""
362362
attributes: Dict[str, AttributeValue] = {
363-
"gen_ai.prompt": json.dumps(tool, cls=JSONEncoder),
363+
"gen_ai.prompt": serialize(tool),
364364
"tool.name": tool["name"],
365365
"tool.id": tool["toolUseId"],
366-
"tool.parameters": json.dumps(tool["input"], cls=JSONEncoder),
366+
"tool.parameters": serialize(tool["input"]),
367367
}
368368

369369
# Add additional kwargs as attributes
@@ -387,7 +387,7 @@ def end_tool_call_span(
387387
status = tool_result.get("status")
388388
status_str = str(status) if status is not None else ""
389389

390-
tool_result_content_json = json.dumps(tool_result.get("content"), cls=JSONEncoder)
390+
tool_result_content_json = serialize(tool_result.get("content"))
391391
attributes.update(
392392
{
393393
"tool.result": tool_result_content_json,
@@ -420,7 +420,7 @@ def start_event_loop_cycle_span(
420420
parent_span = parent_span if parent_span else event_loop_kwargs.get("event_loop_parent_span")
421421

422422
attributes: Dict[str, AttributeValue] = {
423-
"gen_ai.prompt": json.dumps(messages, cls=JSONEncoder),
423+
"gen_ai.prompt": serialize(messages),
424424
"event_loop.cycle_id": event_loop_cycle_id,
425425
}
426426

@@ -449,11 +449,11 @@ def end_event_loop_cycle_span(
449449
error: Optional exception if the cycle failed.
450450
"""
451451
attributes: Dict[str, AttributeValue] = {
452-
"gen_ai.completion": json.dumps(message["content"], cls=JSONEncoder),
452+
"gen_ai.completion": serialize(message["content"]),
453453
}
454454

455455
if tool_result_message:
456-
attributes["tool.result"] = json.dumps(tool_result_message["content"], cls=JSONEncoder)
456+
attributes["tool.result"] = serialize(tool_result_message["content"])
457457

458458
self._end_span(span, attributes, error)
459459

@@ -490,7 +490,7 @@ def start_agent_span(
490490
attributes["gen_ai.request.model"] = model_id
491491

492492
if tools:
493-
tools_json = json.dumps(tools, cls=JSONEncoder)
493+
tools_json = serialize(tools)
494494
attributes["agent.tools"] = tools_json
495495
attributes["gen_ai.agent.tools"] = tools_json
496496

@@ -571,3 +571,15 @@ def get_tracer(
571571
)
572572

573573
return _tracer_instance
574+
575+
576+
def serialize(obj: Any) -> str:
577+
"""Serialize an object to JSON with consistent settings.
578+
579+
Args:
580+
obj: The object to serialize
581+
582+
Returns:
583+
JSON string representation of the object
584+
"""
585+
return json.dumps(obj, ensure_ascii=False, cls=JSONEncoder)

tests/strands/telemetry/test_tracer.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import pytest
77
from opentelemetry.trace import StatusCode # type: ignore
88

9-
from strands.telemetry.tracer import JSONEncoder, Tracer, get_tracer
9+
from strands.telemetry.tracer import JSONEncoder, Tracer, get_tracer, serialize
1010
from strands.types.streaming import Usage
1111

1212

@@ -635,3 +635,49 @@ def test_json_encoder_value_error():
635635
# Test just the value
636636
result = json.loads(encoder.encode(huge_number))
637637
assert result == "<replaced>"
638+
639+
640+
def test_serialize_non_ascii_characters():
641+
"""Test that non-ASCII characters are preserved in JSON serialization."""
642+
643+
# Test with Japanese text
644+
japanese_text = "こんにちは世界"
645+
result = serialize({"text": japanese_text})
646+
assert japanese_text in result
647+
assert "\\u" not in result
648+
649+
# Test with emoji
650+
emoji_text = "Hello 🌍"
651+
result = serialize({"text": emoji_text})
652+
assert emoji_text in result
653+
assert "\\u" not in result
654+
655+
# Test with Chinese characters
656+
chinese_text = "你好,世界"
657+
result = serialize({"text": chinese_text})
658+
assert chinese_text in result
659+
assert "\\u" not in result
660+
661+
# Test with mixed content
662+
mixed_text = {"ja": "こんにちは", "emoji": "😊", "zh": "你好", "en": "hello"}
663+
result = serialize(mixed_text)
664+
assert "こんにちは" in result
665+
assert "😊" in result
666+
assert "你好" in result
667+
assert "\\u" not in result
668+
669+
670+
def test_serialize_vs_json_dumps():
671+
"""Test that serialize behaves differently from default json.dumps for non-ASCII characters."""
672+
673+
# Test with Japanese text
674+
japanese_text = "こんにちは世界"
675+
676+
# Default json.dumps should escape non-ASCII characters
677+
default_result = json.dumps({"text": japanese_text})
678+
assert "\\u" in default_result
679+
680+
# Our serialize function should preserve non-ASCII characters
681+
custom_result = serialize({"text": japanese_text})
682+
assert japanese_text in custom_result
683+
assert "\\u" not in custom_result

0 commit comments

Comments
 (0)