quorum_rs/
api_error_middleware.rs1use std::sync::Arc;
12
13use axum::{body::Body, extract::MatchedPath, http::Request, middleware::Next, response::Response};
14
15use crate::telemetry::{ApiError, TelemetryContext, TelemetryEmitterMux, TelemetryEvent};
16
17#[derive(Clone)]
22pub struct ApiErrorTelemetry {
23 pub emitter: TelemetryEmitterMux,
24 pub ctx: TelemetryContext,
25}
26
27impl ApiErrorTelemetry {
28 pub fn new(emitter: TelemetryEmitterMux, agent_id: &str) -> Self {
31 let ctx = TelemetryContext::new(agent_id, None, None, None);
32 Self { emitter, ctx }
33 }
34}
35
36pub async fn api_error_telemetry_middleware(
41 axum::extract::State(state): axum::extract::State<Option<Arc<ApiErrorTelemetry>>>,
42 req: Request<Body>,
43 next: Next,
44) -> Response {
45 let start = std::time::Instant::now();
46 let method = req.method().as_str().to_string();
47 let endpoint = req
48 .extensions()
49 .get::<MatchedPath>()
50 .map(|m| m.as_str().to_string())
51 .unwrap_or_else(|| req.uri().path().to_string());
52
53 let resp = next.run(req).await;
54 let status = resp.status().as_u16();
55
56 if status >= 400
57 && let Some(t) = state.as_ref()
58 {
59 t.emitter.emit(&TelemetryEvent::ApiError(ApiError {
60 common: t.ctx.common(),
61 http_status: status,
62 error_code: None,
63 endpoint,
64 method,
65 duration_ms: start.elapsed().as_millis() as u64,
66 }));
67 }
68
69 resp
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75 use axum::http::Method;
76
77 #[test]
78 fn endpoint_label_falls_back_to_raw_path() {
79 let req = Request::builder()
80 .method(Method::GET)
81 .uri("/some/path?q=1")
82 .body(Body::empty())
83 .unwrap();
84 let label = req
85 .extensions()
86 .get::<MatchedPath>()
87 .map(|m| m.as_str().to_string())
88 .unwrap_or_else(|| req.uri().path().to_string());
89 assert_eq!(label, "/some/path");
90 }
91}