Files
crewAI/lib/crewai-core/tests/test_telemetry_shutdown.py
João Moura 6b6a39d503 fix(events): Improve coding agents instructions (#7447)
* feat: scaffold project with assistant instruction files

- Updated project creation to include `CLAUDE.md` and `GEMINI.md` that import `AGENTS.md`, ensuring consistent guidance across coding assistants.
- Implemented utility functions to copy assistant instruction files during project setup.
- Enhanced documentation in `AGENTS.md` to emphasize the importance of keeping telemetry enabled for optimal performance.
- Added tests to verify the correct scaffolding of assistant instruction files and their contents.

* fix(cli): neutral observability guidance in scaffolded AGENTS.md

- State the observability rule as the user's decision, never a fix for
  console warnings, speed, or a "clean" configuration
- Rewrite the AMP section as built-in capabilities: no "free",
  "proactively", "sales pitch", or scripted pitches
- Turn the research mandate into a list of sources to consult when
  version details matter
- Retarget the scaffold tests to the new wording

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(cli): scaffold assistant files for JSON crews and harden telemetry tests

- create_json_crew, the default `crewai create crew` path, now copies
  AGENTS.md, CLAUDE.md and GEMINI.md; AGENTS.md documents the JSON layout
- span helper no longer depends on OTEL_SDK_DISABLED being popped by an
  earlier test; thread-scope test stops its worker before leaving the mock
- single import style in the shutdown test

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 02:07:57 -03:00

115 lines
3.6 KiB
Python

"""Process exit must not wait on telemetry retries.
``BatchSpanProcessor.force_flush`` ignores its timeout, so with the collector
unreachable the exit hook used to block for the exporter's whole retry budget.
"""
from __future__ import annotations
from collections.abc import Callable, Iterator
import logging
import time
from typing import cast
from unittest.mock import MagicMock, patch
from crewai_core.telemetry import SafeOTLPSpanExporter, Telemetry
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
import pytest
import requests
OTLP_LOGGER = OTLPSpanExporter.__module__
ENDPOINT = "http://127.0.0.1:9/v1/traces"
def _otlp_messages(caplog: pytest.LogCaptureFixture) -> list[str]:
return [r.getMessage() for r in caplog.records if r.name == OTLP_LOGGER]
@pytest.fixture
def live_telemetry(monkeypatch: pytest.MonkeyPatch) -> Iterator[Telemetry]:
"""A fresh, enabled Telemetry with its real exporter and no lifecycle hooks."""
monkeypatch.setattr(Telemetry, "_instance", None)
monkeypatch.setattr(Telemetry, "_register_shutdown_handlers", lambda self: None)
for var in (
"CREWAI_DISABLE_TELEMETRY",
"CREWAI_DISABLE_TRACKING",
"OTEL_SDK_DISABLED",
):
monkeypatch.setenv(var, "false")
telemetry = Telemetry()
try:
yield telemetry
finally:
if telemetry.ready:
telemetry.provider.shutdown()
Telemetry._instance = None
def test_exit_hook_stops_waiting_at_the_flush_deadline(
live_telemetry: Telemetry,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
monkeypatch.setattr("crewai_core.telemetry.FINAL_FLUSH_SECONDS", 1)
live_telemetry.provider.get_tracer("test").start_span("probe").end()
with (
patch(
"requests.Session.post",
side_effect=requests.exceptions.ConnectionError("collector down"),
),
caplog.at_level(logging.DEBUG),
):
started = time.monotonic()
live_telemetry._shutdown()
elapsed = time.monotonic() - started
# The exporter's own retry budget is 30s; unbounded, this takes 15s or more.
assert elapsed < 5
assert live_telemetry.ready is False
assert _otlp_messages(caplog) == []
def test_exit_hook_returns_as_soon_as_the_flush_succeeds(
live_telemetry: Telemetry, caplog: pytest.LogCaptureFixture
) -> None:
live_telemetry.provider.get_tracer("test").start_span("probe").end()
with (
patch("requests.Session.post", return_value=MagicMock(ok=True)) as post,
caplog.at_level(logging.DEBUG),
):
started = time.monotonic()
live_telemetry._shutdown()
elapsed = time.monotonic() - started
assert post.call_count == 1
assert elapsed < 5 # not the 10s deadline
assert _otlp_messages(caplog) == []
def test_repeated_exporter_shutdown_is_quiet(caplog: pytest.LogCaptureFixture) -> None:
exporter = SafeOTLPSpanExporter(endpoint=ENDPOINT, timeout=1)
with caplog.at_level(logging.DEBUG):
exporter.shutdown()
exporter.shutdown()
assert _otlp_messages(caplog) == []
def test_repeated_plain_exporter_shutdown_warns(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Control for the test above: the base class does warn on the repeat call."""
exporter = OTLPSpanExporter(endpoint=ENDPOINT, timeout=1)
# OTLPSpanExporter.shutdown is unannotated upstream.
plain_shutdown = cast(Callable[[], None], exporter.shutdown)
with caplog.at_level(logging.DEBUG):
plain_shutdown()
plain_shutdown()
assert "Exporter already shutdown, ignoring call" in _otlp_messages(caplog)