Skip to content

Add ensure_ascii=False to json.dumps() calls in telemetry tracer #37

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions src/strands/telemetry/tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ def start_model_invoke_span(
"gen_ai.system": "strands-agents",
"agent.name": agent_name,
"gen_ai.agent.name": agent_name,
"gen_ai.prompt": json.dumps(messages, cls=JSONEncoder),
"gen_ai.prompt": serialize(messages),
}

if model_id:
Expand All @@ -338,7 +338,7 @@ def end_model_invoke_span(
error: Optional exception if the model call failed.
"""
attributes: Dict[str, AttributeValue] = {
"gen_ai.completion": json.dumps(message["content"], cls=JSONEncoder),
"gen_ai.completion": serialize(message["content"]),
"gen_ai.usage.prompt_tokens": usage["inputTokens"],
"gen_ai.usage.completion_tokens": usage["outputTokens"],
"gen_ai.usage.total_tokens": usage["totalTokens"],
Expand All @@ -360,10 +360,10 @@ def start_tool_call_span(
The created span, or None if tracing is not enabled.
"""
attributes: Dict[str, AttributeValue] = {
"gen_ai.prompt": json.dumps(tool, cls=JSONEncoder),
"gen_ai.prompt": serialize(tool),
"tool.name": tool["name"],
"tool.id": tool["toolUseId"],
"tool.parameters": json.dumps(tool["input"], cls=JSONEncoder),
"tool.parameters": serialize(tool["input"]),
}

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

tool_result_content_json = json.dumps(tool_result.get("content"), cls=JSONEncoder)
tool_result_content_json = serialize(tool_result.get("content"))
attributes.update(
{
"tool.result": tool_result_content_json,
Expand Down Expand Up @@ -420,7 +420,7 @@ def start_event_loop_cycle_span(
parent_span = parent_span if parent_span else event_loop_kwargs.get("event_loop_parent_span")

attributes: Dict[str, AttributeValue] = {
"gen_ai.prompt": json.dumps(messages, cls=JSONEncoder),
"gen_ai.prompt": serialize(messages),
"event_loop.cycle_id": event_loop_cycle_id,
}

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

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

self._end_span(span, attributes, error)

Expand Down Expand Up @@ -490,7 +490,7 @@ def start_agent_span(
attributes["gen_ai.request.model"] = model_id

if tools:
tools_json = json.dumps(tools, cls=JSONEncoder)
tools_json = serialize(tools)
attributes["agent.tools"] = tools_json
attributes["gen_ai.agent.tools"] = tools_json

Expand Down Expand Up @@ -571,3 +571,15 @@ def get_tracer(
)

return _tracer_instance


def serialize(obj: Any) -> str:
"""Serialize an object to JSON with consistent settings.

Args:
obj: The object to serialize

Returns:
JSON string representation of the object
"""
return json.dumps(obj, ensure_ascii=False, cls=JSONEncoder)
49 changes: 48 additions & 1 deletion tests/strands/telemetry/test_tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import pytest
from opentelemetry.trace import StatusCode # type: ignore

from strands.telemetry.tracer import JSONEncoder, Tracer, get_tracer
from strands.telemetry.tracer import JSONEncoder, Tracer, get_tracer, serialize
from strands.types.streaming import Usage


Expand Down Expand Up @@ -635,3 +635,50 @@ def test_json_encoder_value_error():
# Test just the value
result = json.loads(encoder.encode(huge_number))
assert result == "<replaced>"


def test_serialize_non_ascii_characters():
"""Test that non-ASCII characters are preserved in JSON serialization."""
from strands.telemetry.tracer import serialize

# Test with Japanese text
japanese_text = "こんにちは世界"
result = serialize({"text": japanese_text})
assert japanese_text in result
assert "\\u" not in result

# Test with emoji
emoji_text = "Hello 🌍"
result = serialize({"text": emoji_text})
assert emoji_text in result
assert "\\u" not in result

# Test with Chinese characters
chinese_text = "你好,世界"
result = serialize({"text": chinese_text})
assert chinese_text in result
assert "\\u" not in result

# Test with mixed content
mixed_text = {"ja": "こんにちは", "emoji": "😊", "zh": "你好", "en": "hello"}
result = serialize(mixed_text)
assert "こんにちは" in result
assert "😊" in result
assert "你好" in result
assert "\\u" not in result


def test_serialize_vs_json_dumps():
"""Test that serialize behaves differently from default json.dumps for non-ASCII characters."""

# Test with Japanese text
japanese_text = "こんにちは世界"

# Default json.dumps should escape non-ASCII characters
default_result = json.dumps({"text": japanese_text})
assert "\\u" in default_result

# Our serialize function should preserve non-ASCII characters
custom_result = serialize({"text": japanese_text})
assert japanese_text in custom_result
assert "\\u" not in custom_result
Loading