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
202 lines
5.8 KiB
Python
202 lines
5.8 KiB
Python
"""Tests for the perf package scaffold — public API, mock objects, config."""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
class TestEnabled:
|
|
"""perf._enabled() resolves from env var and stack.toml."""
|
|
|
|
def test_env_true(self, monkeypatch):
|
|
monkeypatch.setenv("STACK_TELEMETRY", "true")
|
|
from perf import _enabled
|
|
|
|
assert _enabled() is True
|
|
|
|
def test_env_false(self, monkeypatch):
|
|
monkeypatch.setenv("STACK_TELEMETRY", "false")
|
|
from perf import _enabled
|
|
|
|
assert _enabled() is False
|
|
|
|
def test_env_1(self, monkeypatch):
|
|
monkeypatch.setenv("STACK_TELEMETRY", "1")
|
|
from perf import _enabled
|
|
|
|
assert _enabled() is True
|
|
|
|
def test_env_0(self, monkeypatch):
|
|
monkeypatch.setenv("STACK_TELEMETRY", "0")
|
|
from perf import _enabled
|
|
|
|
assert _enabled() is False
|
|
|
|
def test_fallback_to_config(self, monkeypatch):
|
|
monkeypatch.delenv("STACK_TELEMETRY", raising=False)
|
|
from perf import _enabled
|
|
|
|
# stack.toml has enabled = false by default
|
|
assert _enabled() is False
|
|
|
|
|
|
class TestMockTracer:
|
|
"""MockTracer and MockSpan are zero-cost no-ops."""
|
|
|
|
def test_mock_span_context_manager(self):
|
|
from perf._tracer import MockSpan
|
|
|
|
span = MockSpan()
|
|
with span as s:
|
|
s.set_attribute("key", "value")
|
|
s.set_status("OK")
|
|
s.record_exception(ValueError("test"))
|
|
s.add_event("test_event", {"k": "v"})
|
|
|
|
def test_mock_tracer_start_as_current_span(self):
|
|
from perf._tracer import MockTracer
|
|
|
|
tracer = MockTracer()
|
|
with tracer.start_as_current_span("test_span") as span:
|
|
span.set_attribute("rows.out", 100)
|
|
|
|
def test_mock_tracer_start_span(self):
|
|
from perf._tracer import MockTracer
|
|
|
|
tracer = MockTracer()
|
|
span = tracer.start_span("test_span")
|
|
assert span is not None
|
|
|
|
|
|
class TestMockMeter:
|
|
"""MockMeter instruments are zero-cost no-ops."""
|
|
|
|
def test_mock_counter(self):
|
|
from perf._meter import MockMeter
|
|
|
|
meter = MockMeter()
|
|
counter = meter.create_counter("test_counter")
|
|
counter.add(1, {"pipeline": "test"})
|
|
|
|
def test_mock_histogram(self):
|
|
from perf._meter import MockMeter
|
|
|
|
meter = MockMeter()
|
|
hist = meter.create_histogram("test_histogram")
|
|
hist.record(0.5, {"step": "test"})
|
|
|
|
def test_mock_up_down_counter(self):
|
|
from perf._meter import MockMeter
|
|
|
|
meter = MockMeter()
|
|
udc = meter.create_up_down_counter("test_udc")
|
|
udc.add(-1)
|
|
|
|
def test_mock_observable_gauge(self):
|
|
from perf._meter import MockMeter
|
|
|
|
meter = MockMeter()
|
|
gauge = meter.create_observable_gauge("test_gauge")
|
|
gauge.set(42.0)
|
|
|
|
|
|
class TestPublicAPI:
|
|
"""perf.tracer() and perf.meter() return mocks when disabled."""
|
|
|
|
def test_tracer_returns_mock_when_disabled(self, monkeypatch):
|
|
monkeypatch.setenv("STACK_TELEMETRY", "false")
|
|
from perf import tracer
|
|
from perf._tracer import MockTracer
|
|
|
|
t = tracer("test.scope")
|
|
assert isinstance(t, MockTracer)
|
|
|
|
def test_meter_returns_mock_when_disabled(self, monkeypatch):
|
|
monkeypatch.setenv("STACK_TELEMETRY", "false")
|
|
from perf import meter
|
|
from perf._meter import MockMeter
|
|
|
|
m = meter("test.scope")
|
|
assert isinstance(m, MockMeter)
|
|
|
|
def test_init_noop_when_disabled(self, monkeypatch):
|
|
monkeypatch.setenv("STACK_TELEMETRY", "false")
|
|
import perf
|
|
|
|
perf._initialised = False # reset state
|
|
perf.init()
|
|
# Should not raise — just a no-op
|
|
perf.shutdown()
|
|
|
|
|
|
class TestFileSpanExporter:
|
|
"""FileSpanExporter writes JSONL to disk."""
|
|
|
|
def test_export_writes_jsonl(self, tmp_path):
|
|
import json
|
|
from unittest.mock import MagicMock
|
|
|
|
from perf.export import FileSpanExporter
|
|
|
|
out = tmp_path / "spans.jsonl"
|
|
exporter = FileSpanExporter(path=out)
|
|
|
|
# Build a mock span
|
|
mock_ctx = MagicMock()
|
|
mock_ctx.trace_id = 0x1234567890ABCDEF1234567890ABCDEF
|
|
mock_ctx.span_id = 0xABCDEF1234567890
|
|
|
|
mock_status = MagicMock()
|
|
mock_status.status_code.name = "OK"
|
|
|
|
mock_span = MagicMock()
|
|
mock_span.get_span_context.return_value = mock_ctx
|
|
mock_span.name = "test.step"
|
|
mock_span.start_time = 1000000000
|
|
mock_span.end_time = 2000000000
|
|
mock_span.status = mock_status
|
|
mock_span.attributes = {"rows.out": 42}
|
|
|
|
result = exporter.export([mock_span])
|
|
assert result == 0
|
|
|
|
lines = out.read_text().strip().split("\n")
|
|
assert len(lines) == 1
|
|
|
|
record = json.loads(lines[0])
|
|
assert record["name"] == "test.step"
|
|
assert record["attributes"]["rows.out"] == 42
|
|
assert len(record["trace_id"]) == 32
|
|
assert len(record["span_id"]) == 16
|
|
|
|
|
|
class TestResource:
|
|
"""_resource.py builds OTel-compatible resource attributes."""
|
|
|
|
def test_service_name_from_env(self, monkeypatch):
|
|
monkeypatch.setenv("OTEL_SERVICE_NAME", "my-service")
|
|
from perf._resource import _service_name
|
|
|
|
assert _service_name() == "my-service"
|
|
|
|
def test_service_name_default(self, monkeypatch):
|
|
monkeypatch.delenv("OTEL_SERVICE_NAME", raising=False)
|
|
from perf._resource import _service_name
|
|
|
|
# Falls back to stack.toml or "stack"
|
|
name = _service_name()
|
|
assert isinstance(name, str)
|
|
assert len(name) > 0
|
|
|
|
def test_git_sha(self):
|
|
from perf._resource import _git_sha
|
|
|
|
sha = _git_sha()
|
|
# We're in a git repo, so this should return a short SHA
|
|
assert sha is not None
|
|
assert len(sha) >= 7
|
|
|
|
def test_version(self):
|
|
from perf._resource import _version
|
|
|
|
v = _version()
|
|
assert isinstance(v, str)
|