From 52ff0780b24810de3dd3aa95b901834e0cdd0ac9 Mon Sep 17 00:00:00 2001 From: Lucas Gomide Date: Tue, 23 Jun 2026 12:17:49 -0300 Subject: [PATCH] test: stop telemetry fixtures from leaking the global TracerProvider `test_otel.py`'s `span_exporter` fixture installed an SDK `TracerProvider` once via module-level globals and never restored the default `ProxyTracerProvider`, so `test_otel_noop.py`'s unconfigured- default-state assertions failed whenever the two files ran on the same worker. Install the SDK provider fresh per test and reset the global slot back to `ProxyTracerProvider` in `finally`; `_tracer()` re-resolves on every span so swapping providers between tests is safe. --- lib/crewai/tests/telemetry/test_otel.py | 60 +++++++++++-------------- 1 file changed, 27 insertions(+), 33 deletions(-) diff --git a/lib/crewai/tests/telemetry/test_otel.py b/lib/crewai/tests/telemetry/test_otel.py index b76f8111b..16acfe2b7 100644 --- a/lib/crewai/tests/telemetry/test_otel.py +++ b/lib/crewai/tests/telemetry/test_otel.py @@ -48,10 +48,6 @@ from opentelemetry.trace import ( # --------------------------------------------------------------------------- -_SHARED_EXPORTER: InMemorySpanExporter | None = None -_SHARED_PROVIDER: TracerProvider | None = None - - def _reset_global_tracer_provider() -> None: """Reset OTel's process-global tracer provider slot. @@ -78,43 +74,41 @@ def _reset_global_tracer_provider() -> None: @pytest.fixture def span_exporter(monkeypatch: pytest.MonkeyPatch) -> Iterator[InMemorySpanExporter]: - """Install (once) an SDK TracerProvider and yield the in-memory exporter. + """Install an SDK TracerProvider for one test and tear it back down. - The OTel global tracer provider is process-wide AND ``ProxyTracer`` - instances cache the first resolved real tracer. That means we cannot - safely swap providers between tests without poisoning every ``operation`` - call site that resolved its tracer earlier. We instead install one SDK - provider for the whole session and clear the exporter between tests so - each test sees only its own spans. + The OTel global tracer provider is process-wide, so leaving an SDK + provider installed after the test ends bleeds into anything that + asserts on the default unconfigured state (notably + ``test_otel_noop.py``). We install a fresh provider on setup and + restore the default ``ProxyTracerProvider`` on teardown so each test + sees a clean slate and the suite's overall state is preserved. + + Re-resolving providers between tests is safe here because + ``crewai.telemetry.otel._tracer()`` calls ``trace.get_tracer()`` on + every span — nothing caches a ``ProxyTracer`` across the swap. ``.env.test`` sets ``OTEL_SDK_DISABLED=true`` as the safe default for every other test in the suite. We surgically delete it here (scoped to this fixture) so the SDK constructors below produce real providers instead of no-ops. ``OTEL_SDK_DISABLED`` is only read at provider - construction time, so restoring the env after teardown does not affect - the now-built ``_SHARED_PROVIDER``. - - The "default behavior" tests verify the NoOp path in a separate test - file (``test_otel_noop.py``) that runs in its own xdist worker thanks - to ``--dist=loadfile``; we never tear the provider back down here. + construction time, so restoring the env after teardown does not + affect the already-built provider. """ - global _SHARED_EXPORTER, _SHARED_PROVIDER - - if _SHARED_EXPORTER is None: - monkeypatch.delenv("OTEL_SDK_DISABLED", raising=False) - _SHARED_EXPORTER = InMemorySpanExporter() - _SHARED_PROVIDER = TracerProvider() - _SHARED_PROVIDER.add_span_processor(SimpleSpanProcessor(_SHARED_EXPORTER)) + monkeypatch.delenv("OTEL_SDK_DISABLED", raising=False) + exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + _reset_global_tracer_provider() + trace.set_tracer_provider(provider) + actual = trace.get_tracer_provider() + assert actual is provider, ( + f"failed to install SDK TracerProvider; got {type(actual).__name__}" + ) + try: + yield exporter + finally: + provider.shutdown() _reset_global_tracer_provider() - trace.set_tracer_provider(_SHARED_PROVIDER) - actual = trace.get_tracer_provider() - assert actual is _SHARED_PROVIDER, ( - f"failed to install SDK TracerProvider; got {type(actual).__name__}" - ) - - _SHARED_EXPORTER.clear() - yield _SHARED_EXPORTER - _SHARED_EXPORTER.clear() @pytest.fixture