Files
crewAI/lib/crewai/tests/telemetry/test_otel_noop.py
Lucas Gomide d38130bfad feat: add native OpenTelemetry instrumentation
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>
2026-06-23 11:42:08 -03:00

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)