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
use std::borrow::Cow;
use std::time::Instant;
use axum::extract::{MatchedPath, Request};
use axum::http::header;
use axum::middleware::Next;
use axum::response::Response;
use crate::metrics;
/// HTTP metrics middleware that records request count, duration, and emits a
/// structured access log line per request.
pub async fn http_metrics_middleware(
matched_path: Option<MatchedPath>,
req: Request,
next: Next,
) -> Response {
// `Method` is an inline enum for the standard verbs, so cloning it is
// cheaper than the `to_string()` this used to do — and `as_str()` below
// then costs nothing. Only an extension method allocates.
let method = req.method().clone();
// The requested target, for the access-log line. Borrowed from
// `matched_path`, which outlives the response. Only the unmatched fallback
// allocates, and it has to: it reads from `req`, which moves into
// `next.run` below.
let path: Cow<'_, str> = match matched_path.as_ref() {
Some(m) => Cow::Borrowed(m.as_str()),
None => Cow::Owned(req.uri().path().to_string()),
};
// The metric label, distinct from the access-log `path` above. An
// unmatched request (a 404, a scanner probe) has no route template, and
// labelling the metric with the caller-chosen URL would mint a new
// Prometheus series per distinct path — series a recorder never drops, so
// an unauthenticated scanner could grow the registry without bound. Every
// unmatched request shares one `unmatched` series instead.
let metric_path: &str = matched_path.as_ref().map_or("unmatched", |m| m.as_str());
// Request id set by SetRequestIdLayer (inner layer, runs before us).
//
// Owned rather than borrowed: `req` moves into `next.run(req)` below, so a
// borrow of its headers cannot survive to the log line after the response.
let request_id = req
.headers()
.get("x-request-id")
.and_then(|v| v.to_str().ok())
.unwrap_or("-")
.to_string();
let start = Instant::now();
let response = next.run(req).await;
let duration = start.elapsed().as_secs_f64();
let status = response.status().as_u16();
let content_length = response
.headers()
.get(header::CONTENT_LENGTH)
.and_then(|v| v.to_str().ok())
.unwrap_or("-");
tracing::info!(
request_id = %request_id,
http.method = %method.as_str(),
http.route = %path,
http.status_code = status,
http.response_content_length = %content_length,
duration_ms = format_args!("{:.2}", duration * 1000.0),
"HTTP request"
);
metrics::record_http_request(method.as_str(), metric_path, status, duration);
response
}