mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-01 07:13:00 +00:00
fix: serialize Memory objects for telemetry span attributes (#4703)
When crew.memory is a custom Memory instance (not a bool), OpenTelemetry cannot serialize it as a span attribute. Convert non-bool memory values to the class name string before passing to _add_attribute. Fixes #4703 Co-Authored-By: João <joao@crewai.com>
This commit is contained in:
@@ -279,7 +279,11 @@ class Telemetry:
|
|||||||
self._add_attribute(span, "python_version", platform.python_version())
|
self._add_attribute(span, "python_version", platform.python_version())
|
||||||
add_crew_attributes(span, crew, self._add_attribute)
|
add_crew_attributes(span, crew, self._add_attribute)
|
||||||
self._add_attribute(span, "crew_process", crew.process)
|
self._add_attribute(span, "crew_process", crew.process)
|
||||||
self._add_attribute(span, "crew_memory", crew.memory)
|
self._add_attribute(
|
||||||
|
span,
|
||||||
|
"crew_memory",
|
||||||
|
crew.memory if isinstance(crew.memory, bool) else type(crew.memory).__name__,
|
||||||
|
)
|
||||||
self._add_attribute(span, "crew_number_of_tasks", len(crew.tasks))
|
self._add_attribute(span, "crew_number_of_tasks", len(crew.tasks))
|
||||||
self._add_attribute(span, "crew_number_of_agents", len(crew.agents))
|
self._add_attribute(span, "crew_number_of_agents", len(crew.agents))
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
from unittest.mock import patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from crewai import Agent, Crew, Task
|
from crewai import Agent, Crew, Task
|
||||||
@@ -159,3 +159,188 @@ def test_no_signal_handler_traceback_in_non_main_thread():
|
|||||||
mock_holder["logger"].debug.assert_any_call(
|
mock_holder["logger"].debug.assert_any_call(
|
||||||
"Skipping signal handler registration: not running in main thread"
|
"Skipping signal handler registration: not running in main thread"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCrewCreationTelemetryMemorySerialization:
|
||||||
|
"""Tests for issue #4703: telemetry fails with custom Memory instances.
|
||||||
|
|
||||||
|
When crew.memory is a Memory object (not a bool), OpenTelemetry cannot
|
||||||
|
serialize it as a span attribute. The fix converts non-primitive memory
|
||||||
|
values to the class name string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_crew_creation_telemetry_with_memory_true(self):
|
||||||
|
"""crew_memory attribute should be True (bool) when memory=True."""
|
||||||
|
telemetry = Telemetry()
|
||||||
|
telemetry.ready = True
|
||||||
|
|
||||||
|
captured_attrs: dict[str, object] = {}
|
||||||
|
original_add_attribute = telemetry._add_attribute
|
||||||
|
|
||||||
|
def capture_add_attribute(span, key, value):
|
||||||
|
captured_attrs[key] = value
|
||||||
|
original_add_attribute(span, key, value)
|
||||||
|
|
||||||
|
agent = Agent(
|
||||||
|
role="researcher",
|
||||||
|
goal="research",
|
||||||
|
backstory="a researcher",
|
||||||
|
llm="gpt-4o-mini",
|
||||||
|
)
|
||||||
|
task = Task(
|
||||||
|
description="do research",
|
||||||
|
expected_output="results",
|
||||||
|
agent=agent,
|
||||||
|
)
|
||||||
|
crew = Crew(
|
||||||
|
agents=[agent],
|
||||||
|
tasks=[task],
|
||||||
|
memory=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(telemetry, "_add_attribute", side_effect=capture_add_attribute):
|
||||||
|
with patch.object(telemetry, "_safe_telemetry_operation") as mock_safe_op:
|
||||||
|
# Call the inner _operation directly to test attribute logic
|
||||||
|
mock_safe_op.side_effect = lambda op: op()
|
||||||
|
with patch("crewai.telemetry.telemetry.trace") as mock_trace:
|
||||||
|
mock_span = MagicMock()
|
||||||
|
mock_trace.get_tracer.return_value.start_span.return_value = mock_span
|
||||||
|
telemetry.crew_creation(crew, None)
|
||||||
|
|
||||||
|
assert captured_attrs.get("crew_memory") is True
|
||||||
|
|
||||||
|
def test_crew_creation_telemetry_with_memory_false(self):
|
||||||
|
"""crew_memory attribute should be False (bool) when memory=False."""
|
||||||
|
telemetry = Telemetry()
|
||||||
|
telemetry.ready = True
|
||||||
|
|
||||||
|
captured_attrs: dict[str, object] = {}
|
||||||
|
original_add_attribute = telemetry._add_attribute
|
||||||
|
|
||||||
|
def capture_add_attribute(span, key, value):
|
||||||
|
captured_attrs[key] = value
|
||||||
|
original_add_attribute(span, key, value)
|
||||||
|
|
||||||
|
agent = Agent(
|
||||||
|
role="researcher",
|
||||||
|
goal="research",
|
||||||
|
backstory="a researcher",
|
||||||
|
llm="gpt-4o-mini",
|
||||||
|
)
|
||||||
|
task = Task(
|
||||||
|
description="do research",
|
||||||
|
expected_output="results",
|
||||||
|
agent=agent,
|
||||||
|
)
|
||||||
|
crew = Crew(
|
||||||
|
agents=[agent],
|
||||||
|
tasks=[task],
|
||||||
|
memory=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(telemetry, "_add_attribute", side_effect=capture_add_attribute):
|
||||||
|
with patch.object(telemetry, "_safe_telemetry_operation") as mock_safe_op:
|
||||||
|
mock_safe_op.side_effect = lambda op: op()
|
||||||
|
with patch("crewai.telemetry.telemetry.trace") as mock_trace:
|
||||||
|
mock_span = MagicMock()
|
||||||
|
mock_trace.get_tracer.return_value.start_span.return_value = mock_span
|
||||||
|
telemetry.crew_creation(crew, None)
|
||||||
|
|
||||||
|
assert captured_attrs.get("crew_memory") is False
|
||||||
|
|
||||||
|
def test_crew_creation_telemetry_with_custom_memory_instance(self):
|
||||||
|
"""crew_memory attribute should be the class name when a Memory instance is passed.
|
||||||
|
|
||||||
|
Regression test for https://github.com/crewAIInc/crewAI/issues/4703
|
||||||
|
"""
|
||||||
|
telemetry = Telemetry()
|
||||||
|
telemetry.ready = True
|
||||||
|
|
||||||
|
captured_attrs: dict[str, object] = {}
|
||||||
|
original_add_attribute = telemetry._add_attribute
|
||||||
|
|
||||||
|
def capture_add_attribute(span, key, value):
|
||||||
|
captured_attrs[key] = value
|
||||||
|
original_add_attribute(span, key, value)
|
||||||
|
|
||||||
|
# Create a mock Memory object to avoid needing real storage/LLM
|
||||||
|
mock_memory = MagicMock()
|
||||||
|
mock_memory.__class__.__name__ = "Memory"
|
||||||
|
|
||||||
|
agent = Agent(
|
||||||
|
role="researcher",
|
||||||
|
goal="research",
|
||||||
|
backstory="a researcher",
|
||||||
|
llm="gpt-4o-mini",
|
||||||
|
)
|
||||||
|
task = Task(
|
||||||
|
description="do research",
|
||||||
|
expected_output="results",
|
||||||
|
agent=agent,
|
||||||
|
)
|
||||||
|
crew = Crew(
|
||||||
|
agents=[agent],
|
||||||
|
tasks=[task],
|
||||||
|
memory=mock_memory,
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(telemetry, "_add_attribute", side_effect=capture_add_attribute):
|
||||||
|
with patch.object(telemetry, "_safe_telemetry_operation") as mock_safe_op:
|
||||||
|
mock_safe_op.side_effect = lambda op: op()
|
||||||
|
with patch("crewai.telemetry.telemetry.trace") as mock_trace:
|
||||||
|
mock_span = MagicMock()
|
||||||
|
mock_trace.get_tracer.return_value.start_span.return_value = mock_span
|
||||||
|
telemetry.crew_creation(crew, None)
|
||||||
|
|
||||||
|
# Should be the class name string, not the Memory object itself
|
||||||
|
assert captured_attrs.get("crew_memory") == "Memory"
|
||||||
|
assert isinstance(captured_attrs["crew_memory"], str)
|
||||||
|
|
||||||
|
def test_crew_creation_telemetry_memory_value_is_otel_serializable(self):
|
||||||
|
"""The crew_memory value must always be a type OpenTelemetry can serialize.
|
||||||
|
|
||||||
|
OpenTelemetry accepts: bool, str, bytes, int, float, or sequences thereof.
|
||||||
|
"""
|
||||||
|
telemetry = Telemetry()
|
||||||
|
telemetry.ready = True
|
||||||
|
|
||||||
|
captured_attrs: dict[str, object] = {}
|
||||||
|
original_add_attribute = telemetry._add_attribute
|
||||||
|
|
||||||
|
def capture_add_attribute(span, key, value):
|
||||||
|
captured_attrs[key] = value
|
||||||
|
original_add_attribute(span, key, value)
|
||||||
|
|
||||||
|
mock_memory = MagicMock()
|
||||||
|
mock_memory.__class__.__name__ = "Memory"
|
||||||
|
|
||||||
|
agent = Agent(
|
||||||
|
role="researcher",
|
||||||
|
goal="research",
|
||||||
|
backstory="a researcher",
|
||||||
|
llm="gpt-4o-mini",
|
||||||
|
)
|
||||||
|
task = Task(
|
||||||
|
description="do research",
|
||||||
|
expected_output="results",
|
||||||
|
agent=agent,
|
||||||
|
)
|
||||||
|
crew = Crew(
|
||||||
|
agents=[agent],
|
||||||
|
tasks=[task],
|
||||||
|
memory=mock_memory,
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(telemetry, "_add_attribute", side_effect=capture_add_attribute):
|
||||||
|
with patch.object(telemetry, "_safe_telemetry_operation") as mock_safe_op:
|
||||||
|
mock_safe_op.side_effect = lambda op: op()
|
||||||
|
with patch("crewai.telemetry.telemetry.trace") as mock_trace:
|
||||||
|
mock_span = MagicMock()
|
||||||
|
mock_trace.get_tracer.return_value.start_span.return_value = mock_span
|
||||||
|
telemetry.crew_creation(crew, None)
|
||||||
|
|
||||||
|
memory_val = captured_attrs.get("crew_memory")
|
||||||
|
assert isinstance(memory_val, (bool, str, bytes, int, float)), (
|
||||||
|
f"crew_memory value {memory_val!r} (type {type(memory_val).__name__}) "
|
||||||
|
"is not serializable by OpenTelemetry"
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user