Files
sencho/backend
Anso 7ec6fe05bb feat(stacks): in-process per-(nodeId, action) metrics + admin endpoint (#1196)
Sencho exports no telemetry by design (privacy-first posture). That left
operators with no answer for "why is this remote node slow today?"
except scrolling logs. The audit log records mutations but has no
latency information.

Adds StackOpMetricsService - a tiny singleton holding per-(nodeId, action)
counters and a 1000-sample ring buffer of latencies for p50/p95.
Exposed through GET /api/stack-metrics (admin-only) so operators can
pull the snapshot when debugging without touching disk or scrolling
journalctl. No external export; the data never leaves the process.

Wiring: deploy, down, restart, stop, start, update routes each capture
t0 at entry, set ok=true after the success path, and record() in a
finally block so failures count too. The record() call is cheap (one
Map lookup, one push, occasional shift on the bounded ring buffer)
and bounded in memory regardless of throughput.

Resolves M-4 from the stack-management audit.

API:
  GET /api/stack-metrics  (admin-only)
  Response: { entries: [{ nodeId, action, count, successCount,
              errorCount, avgMs, p50Ms, p95Ms }, ...] }
  Ordering: nodeId ascending, then action ascending.

Note on route mounting: /api/stack-metrics rather than the audit doc's
suggested /api/meta/stack-metrics because metaRouter is intentionally
mounted before authGate (public /api/health and /api/meta endpoints);
adding an admin-only route to that group would either bypass auth or
need a special inline gate that fights the existing structure. A
dedicated /api/stack-metrics router after authGate is cleaner.

Tests:
  - 9 unit tests in stack-op-metrics-service.test.ts: singleton,
    keyed-by-nodeId-action, p50/p95 math, ring-buffer cap at 1000,
    NaN/negative/Infinity rejection, ordering, reset.
  - 3 integration tests in stack-metrics-route.test.ts: 401 without
    auth, empty on fresh process, shape after recording.
2026-05-24 16:07:54 -04:00
..