guts_node/observability/
middleware.rs

1//! Observability middleware for request tracking and metrics.
2//!
3//! Provides:
4//! - Request ID generation and propagation
5//! - HTTP metrics collection
6//! - Request/response logging
7
8use axum::{
9    body::Body,
10    extract::Request,
11    http::{header::HeaderName, HeaderValue},
12    middleware::Next,
13    response::Response,
14};
15use std::future::Future;
16use std::pin::Pin;
17use std::time::Instant;
18use uuid::Uuid;
19
20use super::metrics::METRICS;
21
22/// Header name for request ID.
23pub const REQUEST_ID_HEADER: &str = "x-request-id";
24
25/// Type alias for the middleware future.
26type MiddlewareFuture = Pin<Box<dyn Future<Output = Response> + Send>>;
27
28/// Type alias for middleware function pointer.
29type MiddlewareFn = fn(Request, Next) -> MiddlewareFuture;
30
31/// Type alias for the middleware layer.
32pub type MiddlewareLayer = axum::middleware::FromFnLayer<MiddlewareFn, (), Request>;
33
34/// Create a request ID layer function.
35pub fn request_id_layer() -> MiddlewareLayer {
36    axum::middleware::from_fn(request_id_middleware_fn)
37}
38
39fn request_id_middleware_fn(request: Request, next: Next) -> MiddlewareFuture {
40    Box::pin(async move {
41        let request_id = request
42            .headers()
43            .get(REQUEST_ID_HEADER)
44            .and_then(|v| v.to_str().ok())
45            .map(String::from)
46            .unwrap_or_else(|| Uuid::new_v4().to_string());
47
48        // Create span with request ID
49        let span = tracing::info_span!(
50            "request",
51            request_id = %request_id,
52            method = %request.method(),
53            uri = %request.uri(),
54        );
55
56        let _guard = span.enter();
57
58        let mut response = next.run(request).await;
59
60        // Add request ID to response headers
61        if let Ok(header_value) = HeaderValue::from_str(&request_id) {
62            response
63                .headers_mut()
64                .insert(HeaderName::from_static("x-request-id"), header_value);
65        }
66
67        response
68    })
69}
70
71/// Request ID middleware - adds request ID to all requests.
72pub async fn request_id_middleware(mut request: Request, next: Next) -> Response {
73    // Get or generate request ID
74    let request_id = request
75        .headers()
76        .get(REQUEST_ID_HEADER)
77        .and_then(|v| v.to_str().ok())
78        .map(String::from)
79        .unwrap_or_else(|| Uuid::new_v4().to_string());
80
81    // Insert request ID into extensions for handlers to access
82    request
83        .extensions_mut()
84        .insert(RequestId(request_id.clone()));
85
86    // Create span with request ID
87    let span = tracing::info_span!(
88        "request",
89        request_id = %request_id,
90        method = %request.method(),
91        uri = %request.uri(),
92    );
93
94    let _guard = span.enter();
95
96    let mut response = next.run(request).await;
97
98    // Add request ID to response headers
99    if let Ok(header_value) = HeaderValue::from_str(&request_id) {
100        response
101            .headers_mut()
102            .insert(HeaderName::from_static("x-request-id"), header_value);
103    }
104
105    response
106}
107
108/// Request ID extension type.
109#[derive(Clone, Debug)]
110pub struct RequestId(pub String);
111
112/// Metrics middleware - records HTTP request metrics.
113pub async fn metrics_middleware(request: Request, next: Next) -> Response {
114    let start = Instant::now();
115    let method = request.method().to_string();
116    let path = request.uri().path().to_string();
117
118    // Increment active connections
119    METRICS.http_active_connections.inc();
120
121    let response = next.run(request).await;
122
123    // Decrement active connections
124    METRICS.http_active_connections.dec();
125
126    // Record metrics
127    let duration = start.elapsed().as_secs_f64();
128    let status = response.status().as_u16();
129
130    METRICS.record_http_request(&method, &path, status, duration);
131
132    tracing::debug!(
133        method = %method,
134        path = %path,
135        status = %status,
136        duration_ms = %format!("{:.2}", duration * 1000.0),
137        "Request completed"
138    );
139
140    response
141}
142
143/// Create a metrics layer.
144pub fn metrics_layer() -> MiddlewareLayer {
145    axum::middleware::from_fn(metrics_middleware_fn)
146}
147
148fn metrics_middleware_fn(request: Request, next: Next) -> MiddlewareFuture {
149    Box::pin(async move {
150        let start = Instant::now();
151        let method = request.method().to_string();
152        let path = request.uri().path().to_string();
153
154        METRICS.http_active_connections.inc();
155
156        let response = next.run(request).await;
157
158        METRICS.http_active_connections.dec();
159
160        let duration = start.elapsed().as_secs_f64();
161        let status = response.status().as_u16();
162
163        METRICS.record_http_request(&method, &path, status, duration);
164
165        tracing::debug!(
166            method = %method,
167            path = %path,
168            status = %status,
169            duration_ms = %format!("{:.2}", duration * 1000.0),
170            "Request completed"
171        );
172
173        response
174    })
175}
176
177/// Alias for request_id_layer
178pub type RequestIdLayer = axum::middleware::FromFnLayer<
179    fn(Request, Next) -> std::pin::Pin<Box<dyn std::future::Future<Output = Response> + Send>>,
180    (),
181    Request,
182>;
183
184/// Alias for metrics_layer
185pub type MetricsLayer = axum::middleware::FromFnLayer<
186    fn(Request, Next) -> std::pin::Pin<Box<dyn std::future::Future<Output = Response> + Send>>,
187    (),
188    Request,
189>;
190
191/// Get metrics endpoint handler.
192pub async fn metrics_handler() -> Response<Body> {
193    let metrics_output = METRICS.encode();
194
195    Response::builder()
196        .status(200)
197        .header("content-type", "text/plain; version=0.0.4; charset=utf-8")
198        .body(Body::from(metrics_output))
199        .unwrap_or_else(|_| {
200            Response::builder()
201                .status(500)
202                .body(Body::from("Failed to encode metrics"))
203                .expect("Failed to build error response")
204        })
205}