From ebaf02ffac7b1edb1884c884d83d9ef0b7395e1a Mon Sep 17 00:00:00 2001 From: lorenzejay Date: Fri, 14 Aug 2026 10:31:47 -0700 Subject: [PATCH] feat(telemetry): add execution UUID to telemetry spans Enhance the `operation` function to include the execution UUID in span attributes, allowing for better tracking of execution contexts. If an execution UUID is present, it is added to the span attributes unless explicitly provided. Additionally, introduce tests to verify that the execution UUID is correctly stamped from the context and that explicit UUIDs are preserved. This improves traceability in telemetry data. --- lib/crewai/src/crewai/telemetry/otel.py | 11 ++++++++- lib/crewai/tests/telemetry/test_otel.py | 33 ++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/lib/crewai/src/crewai/telemetry/otel.py b/lib/crewai/src/crewai/telemetry/otel.py index 7ff086f26..d0476bf18 100644 --- a/lib/crewai/src/crewai/telemetry/otel.py +++ b/lib/crewai/src/crewai/telemetry/otel.py @@ -21,6 +21,8 @@ from opentelemetry.trace import ( TraceFlags, ) +from crewai.execution import get_execution_uuid + _TRACER_NAME = "crewai" @@ -70,9 +72,14 @@ def operation( The active :class:`Span`. Callers may attach additional attributes or events to it as the operation progresses. """ + attrs: dict[str, Any] = dict(attributes or {}) + execution_uuid = get_execution_uuid() + if execution_uuid and "crewai.execution_uuid" not in attrs: + attrs["crewai.execution_uuid"] = execution_uuid + with _tracer().start_as_current_span( name, - attributes=attributes or {}, + attributes=attrs, links=links or [], record_exception=False, set_status_on_exception=False, @@ -85,6 +92,8 @@ def operation( span.record_exception(exc, escaped=True) span.set_status(Status(StatusCode.ERROR, f"{type(exc).__name__}: {exc}")) raise + else: + span.set_status(Status(StatusCode.OK)) def follows_from( diff --git a/lib/crewai/tests/telemetry/test_otel.py b/lib/crewai/tests/telemetry/test_otel.py index 16acfe2b7..9bf0aee6f 100644 --- a/lib/crewai/tests/telemetry/test_otel.py +++ b/lib/crewai/tests/telemetry/test_otel.py @@ -210,7 +210,37 @@ class TestOperation: finished = span_exporter.get_finished_spans() assert [s.name for s in finished] == ["sample op"] assert finished[0].attributes["crewai.test.key"] == "value" - assert finished[0].status.status_code == StatusCode.UNSET + assert finished[0].status.status_code == StatusCode.OK + + def test_stamps_execution_uuid_from_contextvar( + self, span_exporter: InMemorySpanExporter + ) -> None: + from crewai.execution import clear_execution_uuid, set_execution_uuid + + token = set_execution_uuid("kickoff-uuid-1") + try: + with operation("sample op"): + pass + finally: + clear_execution_uuid(token) + + finished = span_exporter.get_finished_spans() + assert finished[0].attributes["crewai.execution_uuid"] == "kickoff-uuid-1" + + def test_leaves_explicit_execution_uuid_in_place( + self, span_exporter: InMemorySpanExporter + ) -> None: + from crewai.execution import clear_execution_uuid, set_execution_uuid + + token = set_execution_uuid("contextvar-id") + try: + with operation("sample op", {"crewai.execution_uuid": "explicit-id"}): + pass + finally: + clear_execution_uuid(token) + + finished = span_exporter.get_finished_spans() + assert finished[0].attributes["crewai.execution_uuid"] == "explicit-id" def test_exception_marks_span_error( self, span_exporter: InMemorySpanExporter @@ -310,6 +340,7 @@ class TestHotPathSpans: ] assert len(crew_spans) == 1 assert crew_spans[0].attributes["crewai.crew.id"] == str(crew.id) + assert crew_spans[0].attributes.get("crewai.execution_uuid") def test_nested_spans_share_trace_id( self, span_exporter: InMemorySpanExporter