metrics.ts50 lines · main
1import type { MiddlewareHandler } from 'hono';
2
3import { incCounter, observeHistogram } from '../lib/metrics.js';
4import { recordAuthRoute5xx } from '../services/auth-reliability.js';
5
6/**
7 * Records `http_requests_total{method,status,route}` and
8 * `http_request_duration_ms{method,route}` for every request that reaches
9 * a handler. Uses Hono's matched-route pattern (`c.req.routePath`) rather
10 * than the raw path so dynamic segments don't blow up label cardinality —
11 * `/v1/projects/p_abc.../functions/foo` becomes the same label as
12 * `/v1/projects/p_xyz.../functions/bar`.
13 *
14 * Skips its own `/metrics` endpoint to keep prometheus scrapes from
15 * polluting the metric they're scraping.
16 */
17export const metricsMiddleware = (): MiddlewareHandler => async (c, next) => {
18 // Hono v4 exposes the matched pattern via `routePath`. Falls back to the
19 // raw path if for some reason it's missing (older middleware order, etc).
20 const start = performance.now();
21 await next();
22 const durationMs = performance.now() - start;
23
24 const route =
25 typeof (c.req as { routePath?: unknown }).routePath === 'string'
26 ? ((c.req as { routePath: string }).routePath || c.req.path)
27 : c.req.path;
28
29 if (route === '/metrics') return;
30
31 const labels = {
32 method: c.req.method,
33 route,
34 };
35 incCounter('http_requests_total', { ...labels, status: String(c.res.status) });
36 observeHistogram('http_request_duration_ms', durationMs, labels);
37
38 // S6.3: auth-path 5xx for operator snapshot + Prometheus.
39 const status = c.res.status;
40 if (status >= 500) {
41 const path = c.req.path;
42 if (
43 path.includes('/auth') ||
44 path.includes('/auth-tenant') ||
45 path.includes('/v1/auth')
46 ) {
47 recordAuthRoute5xx();
48 }
49 }
50};