Files
stack/tests/perf/test_tracer.py
kert cb530c25d9 chore: snapshot WIP + fix cross-test conf pollution
Bundled commit across unrelated threads: perf collector/meter/logs/export
updates, SSO bootstrap and traefik/nginx/gitea infra, dev backend scripts
(gitea/github/woodpecker), diag test updates, and misc config.

Also fix test pollution: five tests were doing raw sys.modules.pop("conf")
to simulate ImportError on `import conf`, but raw dict mutation isn't
tracked by monkeypatch and leaked the eviction into subsequent tests.
Downstream tests that did `from conf import secret` at module level held
references to the pre-eviction conf, while re-imports inside tests got a
fresh conf, so patches to conf.cfg._data didn't land on the secret()
closure's cfg — causing tests/conf/test_conf.py::TestSecret and
tests/conf/test_connect.py::TestDuckdb::test_custom_db_name to fail.

Fix: use monkeypatch.delitem(sys.modules, "conf", raising=False) so the
eviction is reverted on teardown. Applied to test_diag_ci.py, test_init.py,
test_tracer.py, and test_resource.py (two sites).
2026-04-10 13:09:14 -04:00

279 lines
9.1 KiB
Python

"""Tests for real OTel TracerProvider setup and span lifecycle."""
from __future__ import annotations
import pytest
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import (
SimpleSpanProcessor,
SpanExporter,
SpanExportResult,
)
class _MemoryExporter(SpanExporter):
"""Lightweight in-memory span collector for tests."""
def __init__(self):
self.spans = []
def export(self, spans):
self.spans.extend(spans)
return SpanExportResult.SUCCESS
def shutdown(self):
pass
def get_finished_spans(self):
return list(self.spans)
@pytest.fixture()
def memory_exporter():
"""Set up an in-memory exporter and restore the global provider after."""
original = trace.get_tracer_provider()
exporter = _MemoryExporter()
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(exporter))
trace.set_tracer_provider(provider)
yield exporter
provider.shutdown()
# Reset to original (or no-op proxy)
trace._TRACER_PROVIDER = original
trace._TRACER_PROVIDER_SET_ONCE._done = False
class TestRealTracerSpans:
"""Verify span creation and attributes with a real provider."""
def test_span_created(self, memory_exporter):
tracer = trace.get_tracer("test.pipeline")
with tracer.start_as_current_span("test.step") as span:
span.set_attribute("rows.out", 42)
span.set_attribute("pipeline", "readmissions")
spans = memory_exporter.get_finished_spans()
assert len(spans) == 1
assert spans[0].name == "test.step"
assert spans[0].attributes["rows.out"] == 42
assert spans[0].attributes["pipeline"] == "readmissions"
def test_nested_spans(self, memory_exporter):
tracer = trace.get_tracer("test.pipeline")
with tracer.start_as_current_span("pipeline.run") as root:
root.set_attribute("pipeline", "core")
with tracer.start_as_current_span("step.encounter") as step:
step.set_attribute("step", "core.encounter")
with tracer.start_as_current_span("load.eligibility") as load:
load.set_attribute("table", "input_layer.eligibility")
spans = memory_exporter.get_finished_spans()
assert len(spans) == 3
# Verify parent-child relationships
names = [s.name for s in spans]
assert "load.eligibility" in names
assert "step.encounter" in names
assert "pipeline.run" in names
load_span = next(s for s in spans if s.name == "load.eligibility")
step_span = next(s for s in spans if s.name == "step.encounter")
root_span = next(s for s in spans if s.name == "pipeline.run")
assert load_span.parent.span_id == step_span.context.span_id
assert step_span.parent.span_id == root_span.context.span_id
assert root_span.parent is None
def test_span_records_exception(self, memory_exporter):
tracer = trace.get_tracer("test.pipeline")
try:
with tracer.start_as_current_span("failing.step"):
raise ValueError("schema mismatch")
except ValueError:
pass
spans = memory_exporter.get_finished_spans()
assert len(spans) == 1
events = spans[0].events
assert len(events) == 1
assert events[0].name == "exception"
def test_span_status(self, memory_exporter):
from opentelemetry.trace import StatusCode
tracer = trace.get_tracer("test.pipeline")
with tracer.start_as_current_span("ok.step") as span:
span.set_status(StatusCode.OK)
spans = memory_exporter.get_finished_spans()
assert spans[0].status.status_code == StatusCode.OK
def test_span_timing(self, memory_exporter):
tracer = trace.get_tracer("test.pipeline")
with tracer.start_as_current_span("timed.step"):
pass
spans = memory_exporter.get_finished_spans()
s = spans[0]
assert s.end_time > s.start_time
class TestCreateTracer:
"""Verify create_tracer returns MockTracer or real tracer."""
def test_returns_mock_when_disabled(self, monkeypatch):
monkeypatch.setenv("STACK_TELEMETRY", "false")
import perf._tracer as mod
mod._provider_ready = False
t = mod.create_tracer("test")
from perf._tracer import MockTracer
assert isinstance(t, MockTracer)
def test_returns_real_when_provider_ready(self, memory_exporter):
import perf._tracer as mod
mod._provider_ready = True
t = mod.create_tracer("test.scope")
# Should be a real tracer (not MockTracer)
from perf._tracer import MockTracer
assert not isinstance(t, MockTracer)
mod._provider_ready = False
def test_returns_mock_when_enabled_but_provider_missing(self, monkeypatch):
# Provider not yet ready, but telemetry IS enabled — covers the
# second MockTracer return inside create_tracer().
monkeypatch.setenv("STACK_TELEMETRY", "true")
import perf._tracer as mod
from perf._tracer import MockTracer
mod._provider_ready = False
assert isinstance(mod.create_tracer("test"), MockTracer)
class TestEndpointResolution:
"""Verify OTLP endpoint reads from env and config."""
def test_env_override(self, monkeypatch):
monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://custom:4317")
from perf._tracer import _get_endpoint
assert _get_endpoint() == "http://custom:4317"
def test_default_fallback(self, monkeypatch):
monkeypatch.delenv("OTEL_EXPORTER_OTLP_ENDPOINT", raising=False)
from perf._tracer import _get_endpoint
endpoint = _get_endpoint()
assert "4317" in endpoint
def test_default_when_conf_unavailable(self, monkeypatch):
monkeypatch.delenv("OTEL_EXPORTER_OTLP_ENDPOINT", raising=False)
import sys
from perf._tracer import _get_endpoint
real_import = (
__builtins__["__import__"]
if isinstance(__builtins__, dict)
else __builtins__.__import__
)
def fake_import(name, *args, **kwargs):
if name == "conf":
raise ImportError("conf unavailable")
return real_import(name, *args, **kwargs)
monkeypatch.setattr("builtins.__import__", fake_import)
monkeypatch.delitem(sys.modules, "conf", raising=False)
assert _get_endpoint() == "http://otel-collector:4317"
class TestSetupTracerProvider:
"""setup_tracer_provider() configures the global TracerProvider."""
def test_sets_provider_ready(self):
import perf._tracer as mod
original_ready = mod._provider_ready
try:
mod._provider_ready = False
mod.setup_tracer_provider()
assert mod._provider_ready is True
finally:
mod._provider_ready = original_ready
def test_falls_back_to_file_exporter_when_otlp_fails(self, monkeypatch):
# Force OTLPSpanExporter import to raise so the fallback path runs.
import perf._tracer as mod
real_import = (
__builtins__["__import__"]
if isinstance(__builtins__, dict)
else __builtins__.__import__
)
def fake_import(name, *args, **kwargs):
if "otlp" in name:
raise ImportError("otlp grpc not available")
return real_import(name, *args, **kwargs)
monkeypatch.setattr("builtins.__import__", fake_import)
original_ready = mod._provider_ready
try:
mod._provider_ready = False
mod.setup_tracer_provider()
assert mod._provider_ready is True
finally:
mod._provider_ready = original_ready
class TestShutdownTracerProvider:
"""shutdown_tracer_provider() flushes and resets state."""
def test_noop_when_not_ready(self):
import perf._tracer as mod
mod._provider_ready = False
mod.shutdown_tracer_provider()
assert mod._provider_ready is False
def test_resets_provider_ready(self):
import perf._tracer as mod
mod._provider_ready = True
mod.shutdown_tracer_provider()
assert mod._provider_ready is False
class TestMockObjects:
"""MockSpan and MockTracer methods are zero-cost no-ops."""
def test_mock_span_methods(self):
from perf._tracer import MockSpan
span = MockSpan()
span.set_attribute("k", "v")
span.set_status("OK", description="ok")
span.record_exception(RuntimeError("boom"))
span.add_event("ev", {"a": 1})
def test_mock_tracer_start_span(self):
from perf._tracer import MockSpan, MockTracer
tracer = MockTracer()
span = tracer.start_span("test")
assert isinstance(span, MockSpan)
with tracer.start_as_current_span("ctx") as s:
assert isinstance(s, MockSpan)
def test_mock_span_as_context_manager(self):
from perf._tracer import MockSpan
span = MockSpan()
with span as s:
assert s is span