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