Repository: deepset-ai/haystack

Issue:
LoggingTracer should use `coerce_tag_value` from `tracing/utils.py`
The LoggingTracer doesn't call `coerce_tag_value` which is a function that "Coerces span tag values to compatible types for the tracing backend."

It specifically tries to call `to_dict` on the `tag_value` if present so we can make sure if we are serializing Haystack objects that we utilize that method. 

Currently LoggingTracer doesn't do this so we end up with non-ideal serializations of Haystack objects like Document, ChatMessage, Tool, etc. 

We should update the LoggingTracer to use this utility function. 


Discussion:

Patch:
diff --git a/haystack/tracing/logging_tracer.py b/haystack/tracing/logging_tracer.py
index 166c484a90..b9807ecd49 100644
--- a/haystack/tracing/logging_tracer.py
+++ b/haystack/tracing/logging_tracer.py
@@ -8,6 +8,7 @@
 
 from haystack import logging
 from haystack.tracing import Span, Tracer
+from haystack.tracing.utils import coerce_tag_value
 
 logger = logging.getLogger(__name__)
 
@@ -75,8 +76,9 @@ def trace(
             logger.debug("Operation: {operation_name}", operation_name=operation_name)
             for tag_name, tag_value in tags.items():
                 color_string = self.tags_color_strings.get(tag_name, "")
+                coerced_value = coerce_tag_value(tag_value)
                 logger.debug(
-                    color_string + "{tag_name}={tag_value}" + RESET_COLOR, tag_name=tag_name, tag_value=tag_value
+                    color_string + "{tag_name}={tag_value}" + RESET_COLOR, tag_name=tag_name, tag_value=coerced_value
                 )
 
     def current_span(self) -> Optional[Span]:
diff --git a/releasenotes/notes/coerce-tag-value-serialization-1192e532889be4a3.yaml b/releasenotes/notes/coerce-tag-value-serialization-1192e532889be4a3.yaml
new file mode 100644
index 0000000000..c56088168c
--- /dev/null
+++ b/releasenotes/notes/coerce-tag-value-serialization-1192e532889be4a3.yaml
@@ -0,0 +1,4 @@
+---
+fixes:
+  - |
+    use coerce_tag_value in LoggingTracer to serialize tag values


Test patch:
diff --git a/test/components/agents/test_agent.py b/test/components/agents/test_agent.py
index 7484c1831c..abdb62d2e1 100644
--- a/test/components/agents/test_agent.py
+++ b/test/components/agents/test_agent.py
@@ -817,59 +817,18 @@ def test_agent_tracing_span_run(self, caplog, monkeypatch, weather_tool):
         expected_tag_values = [
             "chat_generator",
             "MockChatGeneratorWithoutRunAsync",
-            {"messages": "list", "tools": "list"},
-            {},
-            {},
-            {
-                "messages": [ChatMessage.from_user(text="What's the weather in Paris?")],
-                "tools": [
-                    Tool(
-                        name="weather_tool",
-                        description="Provides weather information for a given location.",
-                        parameters={
-                            "type": "object",
-                            "properties": {"location": {"type": "string"}},
-                            "required": ["location"],
-                        },
-                        function=weather_function,
-                        outputs_to_string=None,
-                        inputs_from_state=None,
-                        outputs_to_state=None,
-                    )
-                ],
-            },
+            '{"messages": "list", "tools": "list"}',
+            "{}",
+            "{}",
+            '{"messages": [{"role": "user", "meta": {}, "name": null, "content": [{"text": "What\'s the weather in Paris?"}]}], "tools": [{"type": "haystack.tools.tool.Tool", "data": {"name": "weather_tool", "description": "Provides weather information for a given location.", "parameters": {"type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"]}, "function": "test_agent.weather_function", "outputs_to_string": null, "inputs_from_state": null, "outputs_to_state": null}}]}',
             1,
-            {"replies": [ChatMessage.from_assistant(text="Hello")]},
+            '{"replies": [{"role": "assistant", "meta": {}, "name": null, "content": [{"text": "Hello"}]}]}',
             100,
-            [
-                Tool(
-                    name="weather_tool",
-                    description="Provides weather information for a given location.",
-                    parameters={
-                        "type": "object",
-                        "properties": {"location": {"type": "string"}},
-                        "required": ["location"],
-                    },
-                    function=weather_function,
-                    outputs_to_string=None,
-                    inputs_from_state=None,
-                    outputs_to_state=None,
-                )
-            ],
-            ["text"],
-            {
-                "messages": {
-                    "type": "typing.List[haystack.dataclasses.chat_message.ChatMessage]",
-                    "handler": "haystack.dataclasses.state_utils.merge_lists",
-                }
-            },
-            {"messages": [ChatMessage.from_user(text="What's the weather in Paris?")], "streaming_callback": None},
-            {
-                "messages": [
-                    ChatMessage.from_user(text="What's the weather in Paris?"),
-                    ChatMessage.from_assistant(text="Hello"),
-                ]
-            },
+            '[{"type": "haystack.tools.tool.Tool", "data": {"name": "weather_tool", "description": "Provides weather information for a given location.", "parameters": {"type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"]}, "function": "test_agent.weather_function", "outputs_to_string": null, "inputs_from_state": null, "outputs_to_state": null}}]',
+            '["text"]',
+            '{"messages": {"type": "typing.List[haystack.dataclasses.chat_message.ChatMessage]", "handler": "haystack.dataclasses.state_utils.merge_lists"}}',
+            '{"messages": [{"role": "user", "meta": {}, "name": null, "content": [{"text": "What\'s the weather in Paris?"}]}], "streaming_callback": null}',
+            '{"messages": [{"role": "user", "meta": {}, "name": null, "content": [{"text": "What\'s the weather in Paris?"}]}, {"role": "assistant", "meta": {}, "name": null, "content": [{"text": "Hello"}]}]}',
             1,
         ]
         for idx, record in enumerate(tags_records):
@@ -918,59 +877,18 @@ async def test_agent_tracing_span_async_run(self, caplog, monkeypatch, weather_t
         expected_tag_values = [
             "chat_generator",
             "MockChatGeneratorWithRunAsync",
-            {"messages": "list", "tools": "list"},
-            {},
-            {},
-            {
-                "messages": [ChatMessage.from_user(text="What's the weather in Paris?")],
-                "tools": [
-                    Tool(
-                        name="weather_tool",
-                        description="Provides weather information for a given location.",
-                        parameters={
-                            "type": "object",
-                            "properties": {"location": {"type": "string"}},
-                            "required": ["location"],
-                        },
-                        function=weather_function,
-                        outputs_to_string=None,
-                        inputs_from_state=None,
-                        outputs_to_state=None,
-                    )
-                ],
-            },
+            '{"messages": "list", "tools": "list"}',
+            "{}",
+            "{}",
+            '{"messages": [{"role": "user", "meta": {}, "name": null, "content": [{"text": "What\'s the weather in Paris?"}]}], "tools": [{"type": "haystack.tools.tool.Tool", "data": {"name": "weather_tool", "description": "Provides weather information for a given location.", "parameters": {"type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"]}, "function": "test_agent.weather_function", "outputs_to_string": null, "inputs_from_state": null, "outputs_to_state": null}}]}',
             1,
-            {"replies": [ChatMessage.from_assistant(text="Hello from run_async")]},
+            '{"replies": [{"role": "assistant", "meta": {}, "name": null, "content": [{"text": "Hello from run_async"}]}]}',
             100,
-            [
-                Tool(
-                    name="weather_tool",
-                    description="Provides weather information for a given location.",
-                    parameters={
-                        "type": "object",
-                        "properties": {"location": {"type": "string"}},
-                        "required": ["location"],
-                    },
-                    function=weather_function,
-                    outputs_to_string=None,
-                    inputs_from_state=None,
-                    outputs_to_state=None,
-                )
-            ],
-            ["text"],
-            {
-                "messages": {
-                    "type": "typing.List[haystack.dataclasses.chat_message.ChatMessage]",
-                    "handler": "haystack.dataclasses.state_utils.merge_lists",
-                }
-            },
-            {"messages": [ChatMessage.from_user(text="What's the weather in Paris?")], "streaming_callback": None},
-            {
-                "messages": [
-                    ChatMessage.from_user(text="What's the weather in Paris?"),
-                    ChatMessage.from_assistant(text="Hello from run_async"),
-                ]
-            },
+            '[{"type": "haystack.tools.tool.Tool", "data": {"name": "weather_tool", "description": "Provides weather information for a given location.", "parameters": {"type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"]}, "function": "test_agent.weather_function", "outputs_to_string": null, "inputs_from_state": null, "outputs_to_state": null}}]',
+            '["text"]',
+            '{"messages": {"type": "typing.List[haystack.dataclasses.chat_message.ChatMessage]", "handler": "haystack.dataclasses.state_utils.merge_lists"}}',
+            '{"messages": [{"role": "user", "meta": {}, "name": null, "content": [{"text": "What\'s the weather in Paris?"}]}], "streaming_callback": null}',
+            '{"messages": [{"role": "user", "meta": {}, "name": null, "content": [{"text": "What\'s the weather in Paris?"}]}, {"role": "assistant", "meta": {}, "name": null, "content": [{"text": "Hello from run_async"}]}]}',
             1,
         ]
         for idx, record in enumerate(tags_records):
diff --git a/test/tracing/test_logging_tracer.py b/test/tracing/test_logging_tracer.py
index 0f1daeeba0..959c761489 100644
--- a/test/tracing/test_logging_tracer.py
+++ b/test/tracing/test_logging_tracer.py
@@ -58,13 +58,13 @@ def test_tracing_complex_values(self, caplog) -> None:
             span.set_tag("key", {"a": 1, "b": [2, 3, 4]})
 
         assert "Operation: test" in caplog.text
-        assert "key={'a': 1, 'b': [2, 3, 4]}" in caplog.text
+        assert 'key={"a": 1, "b": [2, 3, 4]}' in caplog.text
         assert len(caplog.records) == 2
 
         # structured logging
         assert caplog.records[0].operation_name == "test"
         assert caplog.records[1].tag_name == "key"
-        assert caplog.records[1].tag_value == {"a": 1, "b": [2, 3, 4]}
+        assert caplog.records[1].tag_value == '{"a": 1, "b": [2, 3, 4]}'
 
     def test_apply_color_strings(self, caplog) -> None:
         tracer = LoggingTracer(tags_color_strings={"key": "color_string"})
@@ -119,12 +119,12 @@ def test_logging_pipeline_with_content_tracing(self, caplog) -> None:
         input_tag_value = [
             record.tag_value for record in tags_records if record.tag_name == "haystack.component.input"
         ][0]
-        assert input_tag_value == {"word": "world"}
+        assert input_tag_value == '{"word": "world"}'
 
         output_tag_value = [
             record.tag_value for record in tags_records if record.tag_name == "haystack.component.output"
         ][0]
-        assert output_tag_value == {"output": "Hello, world!"}
+        assert output_tag_value == '{"output": "Hello, world!"}'
 
         tracing.tracer.is_content_tracing_enabled = False
         tracing.disable_tracing()