docspec_http/metrics/
middleware.rs1use std::time::Instant;
8
9use axum::{body::Body, extract::MatchedPath, http::Request, middleware::Next, response::Response};
10
11use crate::metrics::{
12 LABEL_METHOD, LABEL_PATH, LABEL_STATUS, METRIC_HTTP_REQUESTS_TOTAL,
13 METRIC_HTTP_REQUEST_DURATION_SECONDS, PATH_UNKNOWN,
14};
15
16const METRICS_ROUTE_PATH: &str = "/metrics";
20
21#[inline]
29pub async fn record_http_metrics(request: Request<Body>, next: Next) -> Response {
30 let matched_path = request
32 .extensions()
33 .get::<MatchedPath>()
34 .map(|matched: &MatchedPath| matched.as_str().to_owned());
35
36 if matched_path.as_deref() == Some(METRICS_ROUTE_PATH) {
38 return next.run(request).await;
39 }
40
41 let method = request.method().to_string();
42 let path_label = matched_path.unwrap_or_else(|| PATH_UNKNOWN.to_owned());
43
44 let started_at = Instant::now();
45 let response = next.run(request).await;
46 let duration_seconds = started_at.elapsed().as_secs_f64();
47
48 let status_label = response.status().as_u16().to_string();
49
50 metrics::counter!(
51 METRIC_HTTP_REQUESTS_TOTAL,
52 LABEL_METHOD => method.clone(),
53 LABEL_PATH => path_label.clone(),
54 LABEL_STATUS => status_label.clone()
55 )
56 .increment(1);
57
58 metrics::histogram!(
59 METRIC_HTTP_REQUEST_DURATION_SECONDS,
60 LABEL_METHOD => method,
61 LABEL_PATH => path_label,
62 LABEL_STATUS => status_label
63 )
64 .record(duration_seconds);
65
66 response
67}
68
69#[cfg(test)]
70mod tests {
71 #![allow(
72 clippy::tests_outside_test_module,
73 clippy::unwrap_used,
74 clippy::expect_used
75 )]
76
77 use super::*;
78 use axum::{
79 body::Body,
80 http::{Request, StatusCode},
81 middleware,
82 routing::get,
83 Router,
84 };
85 use tower::ServiceExt as _;
86
87 #[test]
88 fn metrics_route_path_is_metrics() {
89 assert_eq!(METRICS_ROUTE_PATH, "/metrics");
90 }
91
92 #[test]
100 fn metrics_path_early_return_skips_recording() {
101 let (recorder, handle) = crate::metrics::build_recorder().expect("test recorder builds");
102
103 let runtime = tokio::runtime::Builder::new_current_thread()
104 .enable_all()
105 .build()
106 .expect("runtime builds");
107
108 let router = Router::new()
110 .route("/metrics", get(|| async { "ok" }))
111 .layer(middleware::from_fn(record_http_metrics));
112
113 let request = Request::builder()
114 .method("GET")
115 .uri("/metrics")
116 .body(Body::empty())
117 .expect("request builds");
118
119 let response = metrics::with_local_recorder(&recorder, || {
120 runtime
121 .block_on(router.oneshot(request))
122 .expect("oneshot succeeds")
123 });
124
125 assert_eq!(response.status(), StatusCode::OK);
126
127 let rendered = handle.render();
128 assert!(
129 !rendered.contains(crate::metrics::METRIC_HTTP_REQUESTS_TOTAL),
130 "Expected no request counter for /metrics path; got:\n{rendered}"
131 );
132 }
133}