1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Craton Software Company
//! HTTP request metrics middleware.
//!
//! Emits the three series referenced by [`docs/SLO.md`](../../../docs/SLO.md)
//! and the reference Grafana dashboard
//! ([`docs/dashboards/README.md`](../../../docs/dashboards/README.md)):
//!
//! * `tensor_wasm_http_requests_total{route, method, status}` — counter
//! * `tensor_wasm_http_request_duration_seconds_bucket{route, method, status, le=...}` — histogram
//! * `tensor_wasm_http_requests_in_flight{route, method}` — gauge
//!
//! All three families live on the shared
//! [`tensor_wasm_core::metrics::TensorWasmMetrics`] registry so a single
//! `GET /metrics` scrape sees the HTTP series next to the kernel-latency and
//! tenant gauges. The middleware itself is a thin
//! [`axum::middleware::from_fn`] body — the production stack composes it via
//! [`http_metrics_middleware`] in [`crate::server::build_router`], placed
//! *outside* `bearer_auth` so the counter increments for `401 Unauthorized`
//! responses as well (per the SLO doc's `availability_http` SLI, which counts
//! every HTTP response).
//!
//! ## Route-template extraction
//!
//! Axum exposes the matched route template via
//! [`axum::extract::MatchedPath`], which the routing layer inserts into the
//! request extensions before invoking any per-route layer. Our middleware is
//! installed via `Router::layer`, which wraps each route's terminal endpoint
//! — at the point our middleware runs, `MatchedPath` is therefore already
//! present in the extensions whenever the request matched a route. For
//! requests that did not match a route (axum's built-in 404 path), the
//! extension is absent and the middleware labels the metric as `"unknown"`
//! to avoid leaking arbitrary path strings into the label space.
//!
//! ## Cardinality control
//!
//! Label cardinality is bounded by a *runtime allow-list* of route templates
//! initialised at startup ([`RouteAllowList`]). Templates not in the
//! allow-list collapse to `"unknown"`. This choice over a compile-time list
//! is deliberate: the router is built dynamically (`build_router` /
//! `build_router_with_audit` / `build_router_with_full_config`), so a
//! compile-time list would either duplicate the route registry (drift risk)
//! or require a const-fn router builder (not what axum offers in 0.7). The
//! allow-list is constructed from the same string literals the router uses
//! — see [`DEFAULT_ROUTE_ALLOWLIST`] — and any new route added to
//! `build_router_with_audit` MUST also be added to that constant. Failing
//! to do so is not a security or correctness issue (the metric still
//! emits, under `"unknown"`), but it does lose per-route resolution in
//! dashboards. CI does not enforce parity today; the orchestrator should
//! flag any router change that is not accompanied by an
//! [`DEFAULT_ROUTE_ALLOWLIST`] update.
//!
//! ## Performance budget
//!
//! Target overhead is < 1 µs per request. The middleware:
//!
//! * reads the `MatchedPath` extension (one `HashMap::get` lookup),
//! * one [`HashSet::contains`] probe against the allow-list,
//! * two `Family::get_or_create` calls (one read-locked
//! `HashMap::get` on the steady-state hot path; one write-locked insert
//! on the cold path when a new label combination first appears),
//! * one atomic `fetch_add` for the counter,
//! * one atomic `fetch_add` + `fetch_sub` for the in-flight gauge,
//! * one `Histogram::observe` which is a single bucket-array sweep.
//!
//! ## In-flight gauge balancing
//!
//! The in-flight gauge is managed by an RAII `InFlightGuard`: construction
//! increments the gauge and the guard's `Drop` decrements it. Because the
//! guard lives on the middleware future's stack across `next.run(req).await`,
//! the gauge is decremented on *every* exit path — normal completion, an
//! early return, a panic, and crucially **future cancellation** (a client
//! disconnect or an outer `tower::timeout` layer dropping the metrics future
//! mid-`await`). The timeout/load-shed layers in `build_router` sit *outside*
//! this middleware, so cancellation is the realistic leak path; the guard
//! closes it. This replaces the previous manual `inc()`/`dec()` pair, which
//! leaked `tensor_wasm_http_requests_in_flight{route,method}` permanently
//! whenever the future was dropped between the two calls.
//!
//! No heap allocations on the steady-state path beyond what
//! `prometheus-client` itself does on a cold-cache `get_or_create`. The
//! `String` label clones are unavoidable today — `prometheus-client 0.24`
//! does not yet support borrowed label values in the
//! `prometheus_client::metrics::family::Family` API; this is a known
//! cost we accept in exchange for the type-safe label set.
use HashSet;
use Arc;
use Instant;
use ;
use Next;
use Response;
use ;
/// Label substituted when a request did not match any route template (axum's
/// built-in 404 fallback) or when the matched template is not on the
/// configured allow-list.
///
/// Kept distinct from any real route template so dashboards can flag the
/// `route="unknown"` series as a sign that either a real route is missing
/// from [`DEFAULT_ROUTE_ALLOWLIST`] or that scanners are probing the
/// gateway.
pub const UNKNOWN_ROUTE: &str = "unknown";
/// Default allow-list of axum route templates emitted by
/// [`crate::server::build_router_with_audit`]. Kept in sync by hand with the
/// `.route(...)` calls in that function; CI does not enforce parity (see
/// module docs for the cardinality-control rationale).
pub const DEFAULT_ROUTE_ALLOWLIST: & = &;
/// Set of route templates that the metrics middleware is willing to emit as
/// the `route` label value. Anything not in the set collapses to
/// [`UNKNOWN_ROUTE`].
///
/// Cloned into request extensions at server start so the
/// [`http_metrics_middleware`] body does not need to capture the allow-list
/// via a type-erased closure. The set is wrapped in [`Arc`] so the per-request
/// clone is a single refcount bump.
/// Bundle passed via request extensions to [`http_metrics_middleware`].
///
/// Composed of the shared metrics registry plus the route allow-list. Cloned
/// per-request — both fields are cheap [`Arc`]-backed handles.
/// Resolve the route-template label for a request.
///
/// Reads the [`MatchedPath`] extension that axum's router inserts before
/// invoking per-route layers; falls back to [`UNKNOWN_ROUTE`] if the
/// extension is absent (no matching route) or if the matched template is
/// not on the allow-list (cardinality guard).
/// RAII guard that owns one balanced `inc()`/`dec()` of the in-flight gauge.
///
/// Mirrors the [`JobsActiveGuard`](crate::routes) idiom: construction
/// increments the gauge for `(route, method)` and [`Drop`] decrements it, so
/// the gauge is balanced on every exit path. Critically, because the guard is
/// held on the middleware future's stack across `next.run(req).await`, the
/// `Drop` also fires when that future is *cancelled* — i.e. a client
/// disconnect, or an outer `tower::timeout` layer dropping the future
/// mid-`await`. That cancellation path is exactly what the previous manual
/// `inc()`/`dec()` pair leaked (`dec()` was never reached when the future was
/// dropped between the two calls).
///
/// The guard owns a `dec` closure rather than the gauge directly. The
/// closure captures the owned gauge handle returned by
/// `Family::get_or_create_owned` (which is cheap — `prometheus-client`'s
/// gauge wraps `Arc<AtomicI64>`, a single refcount bump — and crucially does
/// *not* hold the `Family`'s internal `RwLock`, which must never be held
/// across an `.await`). Capturing via a closure keeps this module free of a
/// direct `prometheus-client` dependency: the concrete `Gauge<i64,
/// AtomicI64>` type is inferred at the call site and never named here.
/// Tower middleware that emits the three HTTP-metric families per request.
///
/// Operationally:
///
/// 1. Resolve the route template via [`route_label`] (one extension lookup).
/// 2. Snapshot [`Instant::now`] as the request-start time.
/// 3. Construct an `InFlightGuard` for `(route, method)` (increments the
/// in-flight gauge; its `Drop` decrements it on every exit path,
/// including cancellation/timeout).
/// 4. Run the inner service.
/// 5. Drop the guard, decrementing the in-flight gauge.
/// 6. Compute elapsed seconds via [`Instant::elapsed`] (saturating).
/// 7. Increment the request counter and observe the duration histogram for
/// `(route, method, status)`.
///
/// If the [`HttpMetricsLayerConfig`] extension is absent (e.g. a test
/// driver that bypassed the production router), the middleware degrades to
/// a no-op pass-through — no panics, no metric emission. This mirrors the
/// audit-log middleware's behaviour.
pub async