Files
stack/tests/perf/test_export.py
kert 7696c1c6da feat: add perf module — OTel pipeline telemetry with Grafana dashboards
Add `perf` as the 12th skinny package with full OpenTelemetry
instrumentation for pipeline traces, Prometheus metrics, and
Loki-correlated logs.

- src/perf/: TracerProvider, MeterProvider, LoggerProvider bridge,
  PipelineCollector, psutil system metrics, FileExporter fallback,
  FastAPI middleware, auto-file Gitea issues on step/test failure
- infra/otel/: OTel Collector fan-out (traces→Jaeger, metrics→Prometheus,
  logs→Loki), all services migrated from jaeger:4317 to collector
- infra/grafana/dashboards/: 8-panel pipeline performance dashboard
- Pipeline runner auto-instruments all 189 steps with zero-cost
  MockTracer/MockSpan no-ops when telemetry is disabled
- 82 new tests, 12,055 total passing

Closes #203, closes #204, closes #205, closes #206, closes #207,
closes #208, closes #209, closes #210, closes #211, closes #212,
closes #213
2026-03-24 21:37:41 -04:00

100 lines
3.1 KiB
Python

"""Tests for FileSpanExporter fallback and CLI show command."""
from __future__ import annotations
import json
from unittest.mock import MagicMock
from perf.export import FileSpanExporter
def _mock_span(name="test", trace_id=0xABCD, span_id=0x1234, attrs=None):
ctx = MagicMock()
ctx.trace_id = trace_id
ctx.span_id = span_id
status = MagicMock()
status.status_code.name = "OK"
span = MagicMock()
span.get_span_context.return_value = ctx
span.name = name
span.start_time = 1_000_000_000
span.end_time = 2_000_000_000
span.status = status
span.attributes = attrs or {}
return span
class TestFileExporter:
def test_creates_directory(self, tmp_path):
out = tmp_path / "deep" / "dir" / "spans.jsonl"
exporter = FileSpanExporter(path=out)
exporter.export([_mock_span()])
assert out.exists()
def test_appends_multiple_exports(self, tmp_path):
out = tmp_path / "spans.jsonl"
exporter = FileSpanExporter(path=out)
exporter.export([_mock_span("a")])
exporter.export([_mock_span("b"), _mock_span("c")])
lines = out.read_text().strip().split("\n")
assert len(lines) == 3
names = [json.loads(l)["name"] for l in lines]
assert names == ["a", "b", "c"]
def test_attributes_preserved(self, tmp_path):
out = tmp_path / "spans.jsonl"
exporter = FileSpanExporter(path=out)
exporter.export([_mock_span(attrs={"rows.out": 42, "cache.hit": True})])
record = json.loads(out.read_text().strip())
assert record["attributes"]["rows.out"] == 42
assert record["attributes"]["cache.hit"] is True
def test_shutdown_noop(self, tmp_path):
exporter = FileSpanExporter(path=tmp_path / "spans.jsonl")
exporter.shutdown() # should not raise
def test_force_flush(self, tmp_path):
exporter = FileSpanExporter(path=tmp_path / "spans.jsonl")
assert exporter.force_flush() is True
class TestCLIShow:
def test_show_with_data(self, tmp_path):
from typer.testing import CliRunner
from cli.perf import app
runner = CliRunner()
trace_file = tmp_path / "spans.jsonl"
exporter = FileSpanExporter(path=trace_file)
exporter.export(
[
_mock_span("pipeline.run", attrs={"rows.out": 100}),
_mock_span(
"step.core.encounter",
attrs={
"rows.out": 50,
"cache.hit": False,
"memory.delta_mb": 2.5,
},
),
]
)
result = runner.invoke(app, ["--path", str(trace_file)])
assert result.exit_code == 0
assert "pipeline.run" in result.output
assert "step.core.encounter" in result.output
assert "rows=50" in result.output
def test_show_missing_file(self, tmp_path):
from typer.testing import CliRunner
from cli.perf import app
runner = CliRunner()
result = runner.invoke(app, ["--path", str(tmp_path / "nope.jsonl")])
assert result.exit_code == 1