api_gateway/middleware/
http_metrics.rs1use std::sync::Arc;
13
14use axum::{
15 extract::{MatchedPath, State},
16 middleware::Next,
17 response::Response,
18};
19use opentelemetry::{
20 KeyValue,
21 metrics::{Histogram, UpDownCounter},
22};
23
24pub struct HttpMetrics {
26 duration: Histogram<f64>,
27 active_requests: UpDownCounter<i64>,
28}
29
30impl HttpMetrics {
31 #[must_use]
37 pub fn new(module_name: &str, prefix: &str) -> Self {
38 let prefix = prefix.trim().trim_end_matches('.'); let scope = opentelemetry::InstrumentationScope::builder(module_name.to_owned()).build();
41 let meter = opentelemetry::global::meter_with_scope(scope);
42
43 let (duration_name, active_name) = if prefix.is_empty() {
44 (
45 "http.server.request.duration".to_owned(),
46 "http.server.active_requests".to_owned(),
47 )
48 } else {
49 (
50 format!("{prefix}.http.server.request.duration"),
51 format!("{prefix}.http.server.active_requests"),
52 )
53 };
54
55 let duration = meter
56 .f64_histogram(duration_name)
57 .with_description("Duration of HTTP server requests")
58 .with_unit("s")
59 .build();
60
61 let active_requests = meter
62 .i64_up_down_counter(active_name)
63 .with_description("Number of active HTTP server requests")
64 .build();
65
66 Self {
67 duration,
68 active_requests,
69 }
70 }
71}
72
73struct ActiveRequestGuard {
76 counter: UpDownCounter<i64>,
77 attrs: [KeyValue; 1],
78}
79
80impl Drop for ActiveRequestGuard {
81 fn drop(&mut self) {
82 self.counter.add(-1, &self.attrs);
83 }
84}
85
86pub async fn propagate_matched_path(
89 matched_path: Option<MatchedPath>,
90 req: axum::extract::Request,
91 next: Next,
92) -> Response {
93 let mut response = next.run(req).await;
94 if let Some(path) = matched_path {
95 response.extensions_mut().insert(path);
96 }
97 response
98}
99
100fn normalize_method(method: &axum::http::Method) -> &'static str {
107 match *method {
108 axum::http::Method::GET => "GET",
109 axum::http::Method::POST => "POST",
110 axum::http::Method::PUT => "PUT",
111 axum::http::Method::DELETE => "DELETE",
112 axum::http::Method::PATCH => "PATCH",
113 axum::http::Method::HEAD => "HEAD",
114 axum::http::Method::OPTIONS => "OPTIONS",
115 axum::http::Method::CONNECT => "CONNECT",
116 axum::http::Method::TRACE => "TRACE",
117 _ => "_OTHER",
118 }
119}
120
121pub async fn http_metrics_middleware(
128 State(metrics): State<Arc<HttpMetrics>>,
129 req: axum::extract::Request,
130 next: Next,
131) -> Response {
132 let method_kv = KeyValue::new("http.request.method", normalize_method(req.method()));
133
134 metrics
135 .active_requests
136 .add(1, std::slice::from_ref(&method_kv));
137 let _guard = ActiveRequestGuard {
138 counter: metrics.active_requests.clone(),
139 attrs: [method_kv.clone()],
140 };
141
142 let start = std::time::Instant::now();
143 let response = next.run(req).await;
144 let elapsed = start.elapsed().as_secs_f64();
145
146 let route = response
147 .extensions()
148 .get::<MatchedPath>()
149 .map_or("unmatched", MatchedPath::as_str)
150 .to_owned();
151 let route_kv = KeyValue::new("http.route", route);
152 let status = i64::from(response.status().as_u16());
153
154 metrics.duration.record(
155 elapsed,
156 &[
157 method_kv,
158 route_kv,
159 KeyValue::new("http.response.status_code", status),
160 ],
161 );
162
163 response
164}