Files
stack/tests/perf/test_meter.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

104 lines
3.4 KiB
Python

"""Tests for real OTel MeterProvider setup and metric instruments."""
from __future__ import annotations
import pytest
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import InMemoryMetricReader
@pytest.fixture()
def meter_setup():
"""Create a fresh MeterProvider + reader pair (no global mutation)."""
reader = InMemoryMetricReader()
provider = MeterProvider(metric_readers=[reader])
yield provider, reader
provider.shutdown()
class TestRealMetrics:
"""Verify metric instrument creation and recording."""
def test_counter(self, meter_setup):
provider, reader = meter_setup
meter = provider.get_meter("test.pipeline")
counter = meter.create_counter(
"stack_cache_hits_total",
description="Cache hits",
)
counter.add(1, {"pipeline": "readmissions"})
counter.add(3, {"pipeline": "readmissions"})
data = reader.get_metrics_data()
rm = data.resource_metrics
assert len(rm) > 0
sm = rm[0].scope_metrics
assert len(sm) > 0
metric = sm[0].metrics[0]
assert metric.name == "stack_cache_hits_total"
dp = metric.data.data_points[0]
assert dp.value == 4
def test_histogram(self, meter_setup):
provider, reader = meter_setup
meter = provider.get_meter("test.pipeline")
hist = meter.create_histogram(
"stack_step_duration_seconds",
unit="s",
description="Step duration",
)
hist.record(0.5, {"pipeline": "core", "step": "encounter"})
hist.record(1.2, {"pipeline": "core", "step": "encounter"})
data = reader.get_metrics_data()
metric = data.resource_metrics[0].scope_metrics[0].metrics[0]
assert metric.name == "stack_step_duration_seconds"
dp = metric.data.data_points[0]
assert dp.count == 2
assert dp.sum == pytest.approx(1.7)
def test_gauge_via_up_down_counter(self, meter_setup):
provider, reader = meter_setup
meter = provider.get_meter("test.pipeline")
gauge = meter.create_up_down_counter(
"stack_step_rows_out",
description="Rows output by step",
)
gauge.add(100, {"step": "core.encounter"})
data = reader.get_metrics_data()
metric = data.resource_metrics[0].scope_metrics[0].metrics[0]
assert metric.name == "stack_step_rows_out"
def test_multiple_instruments(self, meter_setup):
provider, reader = meter_setup
meter = provider.get_meter("test.pipeline")
counter = meter.create_counter("test_counter_a")
hist = meter.create_histogram("test_hist_a")
counter.add(5)
hist.record(2.0)
data = reader.get_metrics_data()
names = {
m.name
for rm in data.resource_metrics
for sm in rm.scope_metrics
for m in sm.metrics
}
assert "test_counter_a" in names
assert "test_hist_a" in names
class TestCreateMeter:
"""Verify create_meter returns MockMeter or real meter."""
def test_returns_mock_when_disabled(self, monkeypatch):
monkeypatch.setenv("STACK_TELEMETRY", "false")
import perf._meter as mod
mod._provider_ready = False
m = mod.create_meter("test")
from perf._meter import MockMeter
assert isinstance(m, MockMeter)