mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-24 00:05:08 +00:00
Open spans directly on the user's thread so that stdlib log records emitted during hot paths like `Crew.kickoff`, `BaseTool.run`, and `LLM.call` carry the active trace context and correlate with the spans they belong to — a gap the previous metrics-only telemetry could not close. Introduces a `crewai.telemetry.otel` module exposing `operation` and `follows_from`, instruments the execution hot paths, and propagates the active context across every parallel-dispatch site. Depends only on `opentelemetry-api` so provider and exporter choice stays with the host application per the standard OTel library pattern; without an installed SDK the `ProxyTracer` keeps everything as a NoOp. Co-authored-by: Cursor <cursoragent@cursor.com>
72 lines
2.2 KiB
Python
72 lines
2.2 KiB
Python
"""Default-behaviour tests for OpenTelemetry instrumentation.
|
|
|
|
These tests assert that, when no SDK ``TracerProvider`` is installed,
|
|
``operation()`` and every hot-path wrapper degrade to NoOp spans and
|
|
``Crew.kickoff`` runs without exception. They MUST live in their own file
|
|
because ``ProxyTracer`` instances cache the first resolved real tracer
|
|
process-wide; once another test (in any other file under the same xdist
|
|
worker) installs an SDK provider, the proxy is no longer observable.
|
|
|
|
``pytest --dist=loadfile`` (configured in ``pyproject.toml``) is what
|
|
guarantees this file gets its own worker.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from crewai import Agent, Crew, Task
|
|
from crewai.llms.base_llm import BaseLLM
|
|
from crewai.telemetry.otel import operation
|
|
from opentelemetry import trace
|
|
from opentelemetry.trace import NonRecordingSpan, ProxyTracerProvider
|
|
|
|
|
|
class _FakeLLM(BaseLLM):
|
|
def __init__(self) -> None:
|
|
super().__init__(model="test-model")
|
|
|
|
def call( # type: ignore[override]
|
|
self,
|
|
messages: Any,
|
|
tools: Any = None,
|
|
callbacks: Any = None,
|
|
available_functions: Any = None,
|
|
from_task: Any = None,
|
|
from_agent: Any = None,
|
|
response_model: Any = None,
|
|
) -> str:
|
|
return "ok"
|
|
|
|
def supports_function_calling(self) -> bool:
|
|
return False
|
|
|
|
|
|
def test_default_provider_is_proxy() -> None:
|
|
assert isinstance(trace.get_tracer_provider(), ProxyTracerProvider)
|
|
|
|
|
|
def test_operation_yields_non_recording_span_when_no_provider() -> None:
|
|
with operation("standalone") as span:
|
|
assert isinstance(span, NonRecordingSpan)
|
|
|
|
|
|
def test_kickoff_runs_cleanly_without_provider() -> None:
|
|
agent = Agent(
|
|
role="tester",
|
|
goal="goal",
|
|
backstory="backstory",
|
|
llm=_FakeLLM(),
|
|
allow_delegation=False,
|
|
)
|
|
task = Task(description="do a thing", expected_output="anything", agent=agent)
|
|
crew = Crew(agents=[agent], tasks=[task])
|
|
|
|
result = crew.kickoff()
|
|
|
|
assert result is not None
|
|
assert str(result)
|
|
# Provider must still be the proxy; operation() should not have flipped a
|
|
# real SDK provider into place.
|
|
assert isinstance(trace.get_tracer_provider(), ProxyTracerProvider)
|