mockforge_http/
metrics_middleware.rs1use axum::{
11 extract::{MatchedPath, Request},
12 middleware::Next,
13 response::Response,
14};
15use mockforge_observability::get_global_registry;
16use std::time::Instant;
17use tracing::debug;
18
19fn determine_pillar_from_path(path: &str) -> &'static str {
24 let path_lower = path.to_lowercase();
25
26 if path_lower.contains("/reality")
28 || path_lower.contains("/personas")
29 || path_lower.contains("/chaos")
30 || path_lower.contains("/fidelity")
31 || path_lower.contains("/continuum")
32 {
33 return "reality";
34 }
35
36 if path_lower.contains("/contracts")
38 || path_lower.contains("/validation")
39 || path_lower.contains("/drift")
40 || path_lower.contains("/schema")
41 || path_lower.contains("/sync")
42 {
43 return "contracts";
44 }
45
46 if path_lower.contains("/sdk")
48 || path_lower.contains("/playground")
49 || path_lower.contains("/plugins")
50 || path_lower.contains("/cli")
51 || path_lower.contains("/generator")
52 {
53 return "devx";
54 }
55
56 if path_lower.contains("/registry")
58 || path_lower.contains("/workspace")
59 || path_lower.contains("/org")
60 || path_lower.contains("/marketplace")
61 || path_lower.contains("/collab")
62 {
63 return "cloud";
64 }
65
66 if path_lower.contains("/ai")
68 || path_lower.contains("/mockai")
69 || path_lower.contains("/voice")
70 || path_lower.contains("/llm")
71 || path_lower.contains("/studio")
72 {
73 return "ai";
74 }
75
76 "unknown"
78}
79
80pub async fn collect_http_metrics(
89 matched_path: Option<MatchedPath>,
90 req: Request,
91 next: Next,
92) -> Response {
93 let start_time = Instant::now();
94 let method = req.method().to_string();
95 let uri_path = req.uri().path().to_string();
96 let path = matched_path.as_ref().map(|mp| mp.as_str().to_string()).unwrap_or(uri_path);
97
98 let registry = get_global_registry();
100
101 registry.increment_in_flight("http");
103 debug!(
104 method = %method,
105 path = %path,
106 "Starting HTTP request metrics collection"
107 );
108
109 let response = next.run(req).await;
111
112 registry.decrement_in_flight("http");
114
115 let duration = start_time.elapsed();
117 let duration_seconds = duration.as_secs_f64();
118 let status_code = response.status().as_u16();
119
120 let pillar = determine_pillar_from_path(&path);
122
123 registry.record_http_request_with_pillar(&method, status_code, duration_seconds, pillar);
125
126 mockforge_foundation::rate_counters::record_response(status_code);
128
129 if status_code >= 400 {
131 let error_type = if status_code >= 500 {
132 "server_error"
133 } else {
134 "client_error"
135 };
136 registry.record_error_with_pillar("http", error_type, pillar);
137 }
138
139 debug!(
140 method = %method,
141 path = %path,
142 status = status_code,
143 duration_ms = duration.as_millis(),
144 pillar = pillar,
145 "HTTP request metrics recorded with pillar dimension"
146 );
147
148 response
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use axum::{
155 body::Body,
156 http::{Request, StatusCode},
157 middleware,
158 response::IntoResponse,
159 Router,
160 };
161 use tower::ServiceExt;
162
163 async fn test_handler() -> impl IntoResponse {
164 (StatusCode::OK, "test response")
165 }
166
167 #[test]
170 fn test_pillar_reality_path() {
171 assert_eq!(determine_pillar_from_path("/api/reality/test"), "reality");
172 }
173
174 #[test]
175 fn test_pillar_personas_path() {
176 assert_eq!(determine_pillar_from_path("/api/personas/user-1"), "reality");
177 }
178
179 #[test]
180 fn test_pillar_chaos_path() {
181 assert_eq!(determine_pillar_from_path("/chaos/scenarios"), "reality");
182 }
183
184 #[test]
185 fn test_pillar_fidelity_path() {
186 assert_eq!(determine_pillar_from_path("/fidelity/config"), "reality");
187 }
188
189 #[test]
190 fn test_pillar_continuum_path() {
191 assert_eq!(determine_pillar_from_path("/api/continuum/timeline"), "reality");
192 }
193
194 #[test]
197 fn test_pillar_contracts_path() {
198 assert_eq!(determine_pillar_from_path("/api/contracts/v1"), "contracts");
199 }
200
201 #[test]
202 fn test_pillar_validation_path() {
203 assert_eq!(determine_pillar_from_path("/validation/schema"), "contracts");
204 }
205
206 #[test]
207 fn test_pillar_drift_path() {
208 assert_eq!(determine_pillar_from_path("/api/drift/analysis"), "contracts");
209 }
210
211 #[test]
212 fn test_pillar_schema_path() {
213 assert_eq!(determine_pillar_from_path("/schema/openapi"), "contracts");
214 }
215
216 #[test]
217 fn test_pillar_sync_path() {
218 assert_eq!(determine_pillar_from_path("/sync/status"), "contracts");
219 }
220
221 #[test]
224 fn test_pillar_sdk_path() {
225 assert_eq!(determine_pillar_from_path("/sdk/download"), "devx");
226 }
227
228 #[test]
229 fn test_pillar_playground_path() {
230 assert_eq!(determine_pillar_from_path("/playground/execute"), "devx");
231 }
232
233 #[test]
234 fn test_pillar_plugins_path() {
235 assert_eq!(determine_pillar_from_path("/api/plugins/list"), "devx");
236 }
237
238 #[test]
239 fn test_pillar_cli_path() {
240 assert_eq!(determine_pillar_from_path("/cli/config"), "devx");
241 }
242
243 #[test]
244 fn test_pillar_generator_path() {
245 assert_eq!(determine_pillar_from_path("/generator/create"), "devx");
246 }
247
248 #[test]
251 fn test_pillar_registry_path() {
252 assert_eq!(determine_pillar_from_path("/registry/packages"), "cloud");
253 }
254
255 #[test]
256 fn test_pillar_workspace_path() {
257 assert_eq!(determine_pillar_from_path("/api/workspace/list"), "cloud");
258 }
259
260 #[test]
261 fn test_pillar_org_path() {
262 assert_eq!(determine_pillar_from_path("/org/settings"), "cloud");
263 }
264
265 #[test]
266 fn test_pillar_marketplace_path() {
267 assert_eq!(determine_pillar_from_path("/marketplace/browse"), "cloud");
268 }
269
270 #[test]
271 fn test_pillar_collab_path() {
272 assert_eq!(determine_pillar_from_path("/collab/sessions"), "cloud");
273 }
274
275 #[test]
278 fn test_pillar_ai_path() {
279 assert_eq!(determine_pillar_from_path("/api/ai/generate"), "ai");
280 }
281
282 #[test]
283 fn test_pillar_mockai_path() {
284 assert_eq!(determine_pillar_from_path("/mockai/responses"), "ai");
285 }
286
287 #[test]
288 fn test_pillar_voice_path() {
289 assert_eq!(determine_pillar_from_path("/voice/recognize"), "ai");
290 }
291
292 #[test]
293 fn test_pillar_llm_path() {
294 assert_eq!(determine_pillar_from_path("/llm/completion"), "ai");
295 }
296
297 #[test]
298 fn test_pillar_studio_path() {
299 assert_eq!(determine_pillar_from_path("/studio/projects"), "ai");
300 }
301
302 #[test]
305 fn test_pillar_unknown_path() {
306 assert_eq!(determine_pillar_from_path("/api/users/123"), "unknown");
307 }
308
309 #[test]
310 fn test_pillar_root_path() {
311 assert_eq!(determine_pillar_from_path("/"), "unknown");
312 }
313
314 #[test]
315 fn test_pillar_health_path() {
316 assert_eq!(determine_pillar_from_path("/health"), "unknown");
317 }
318
319 #[test]
320 fn test_pillar_empty_path() {
321 assert_eq!(determine_pillar_from_path(""), "unknown");
322 }
323
324 #[test]
327 fn test_pillar_uppercase_reality() {
328 assert_eq!(determine_pillar_from_path("/API/REALITY/test"), "reality");
329 }
330
331 #[test]
332 fn test_pillar_mixed_case_contracts() {
333 assert_eq!(determine_pillar_from_path("/Api/Contracts/V1"), "contracts");
334 }
335
336 #[test]
337 fn test_pillar_mixed_case_ai() {
338 assert_eq!(determine_pillar_from_path("/API/Ai/Generate"), "ai");
339 }
340
341 #[tokio::test]
344 async fn test_metrics_middleware_records_success() {
345 use axum::Router;
346 let app = Router::new()
347 .route("/test", axum::routing::get(test_handler))
348 .layer(middleware::from_fn(collect_http_metrics));
349
350 let request = Request::builder().uri("/test").body(Body::empty()).unwrap();
351
352 let response = app.oneshot(request).await.unwrap();
353 assert_eq!(response.status(), StatusCode::OK);
354 }
355
356 #[tokio::test]
357 async fn test_metrics_middleware_records_errors() {
358 async fn error_handler() -> impl IntoResponse {
359 (StatusCode::INTERNAL_SERVER_ERROR, "error")
360 }
361
362 use axum::Router;
363 let app = Router::new()
364 .route("/error", axum::routing::get(error_handler))
365 .layer(middleware::from_fn(collect_http_metrics));
366
367 let request = Request::builder().uri("/error").body(Body::empty()).unwrap();
368
369 let response = app.oneshot(request).await.unwrap();
370 assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
371 }
372
373 #[tokio::test]
374 async fn test_metrics_middleware_records_client_errors() {
375 async fn not_found_handler() -> impl IntoResponse {
376 (StatusCode::NOT_FOUND, "not found")
377 }
378
379 let app = Router::new()
380 .route("/notfound", axum::routing::get(not_found_handler))
381 .layer(middleware::from_fn(collect_http_metrics));
382
383 let request = Request::builder().uri("/notfound").body(Body::empty()).unwrap();
384
385 let response = app.oneshot(request).await.unwrap();
386 assert_eq!(response.status(), StatusCode::NOT_FOUND);
387 }
388
389 #[tokio::test]
390 async fn test_metrics_middleware_records_bad_request() {
391 async fn bad_request_handler() -> impl IntoResponse {
392 (StatusCode::BAD_REQUEST, "bad request")
393 }
394
395 let app = Router::new()
396 .route("/bad", axum::routing::get(bad_request_handler))
397 .layer(middleware::from_fn(collect_http_metrics));
398
399 let request = Request::builder().uri("/bad").body(Body::empty()).unwrap();
400
401 let response = app.oneshot(request).await.unwrap();
402 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
403 }
404
405 #[tokio::test]
406 async fn test_metrics_middleware_with_reality_pillar() {
407 let app = Router::new()
408 .route("/api/reality/test", axum::routing::get(test_handler))
409 .layer(middleware::from_fn(collect_http_metrics));
410
411 let request = Request::builder().uri("/api/reality/test").body(Body::empty()).unwrap();
412
413 let response = app.oneshot(request).await.unwrap();
414 assert_eq!(response.status(), StatusCode::OK);
415 }
416
417 #[tokio::test]
418 async fn test_metrics_middleware_with_contracts_pillar() {
419 let app = Router::new()
420 .route("/api/contracts/validate", axum::routing::get(test_handler))
421 .layer(middleware::from_fn(collect_http_metrics));
422
423 let request =
424 Request::builder().uri("/api/contracts/validate").body(Body::empty()).unwrap();
425
426 let response = app.oneshot(request).await.unwrap();
427 assert_eq!(response.status(), StatusCode::OK);
428 }
429
430 #[tokio::test]
431 async fn test_metrics_middleware_post_request() {
432 async fn post_handler() -> impl IntoResponse {
433 (StatusCode::CREATED, "created")
434 }
435
436 let app = Router::new()
437 .route("/api/create", axum::routing::post(post_handler))
438 .layer(middleware::from_fn(collect_http_metrics));
439
440 let request = Request::builder()
441 .method("POST")
442 .uri("/api/create")
443 .body(Body::empty())
444 .unwrap();
445
446 let response = app.oneshot(request).await.unwrap();
447 assert_eq!(response.status(), StatusCode::CREATED);
448 }
449
450 #[tokio::test]
451 async fn test_metrics_middleware_delete_request() {
452 async fn delete_handler() -> impl IntoResponse {
453 (StatusCode::NO_CONTENT, "")
454 }
455
456 let app = Router::new()
457 .route("/api/delete", axum::routing::delete(delete_handler))
458 .layer(middleware::from_fn(collect_http_metrics));
459
460 let request = Request::builder()
461 .method("DELETE")
462 .uri("/api/delete")
463 .body(Body::empty())
464 .unwrap();
465
466 let response = app.oneshot(request).await.unwrap();
467 assert_eq!(response.status(), StatusCode::NO_CONTENT);
468 }
469}