Skip to main content

modkit/api/
trace_layer.rs

1//! Trace propagation utilities for Problem responses
2//!
3//! This module provides helper traits and functions to automatically enrich
4//! `Problem` with trace context:
5//! - `trace_id`: extracted from the current tracing span
6//! - `instance`: extracted from the request URI
7//!
8//! This eliminates per-callsite boilerplate and ensures consistent error reporting.
9
10use crate::api::problem::Problem;
11
12/// Extract `trace_id` from the current tracing span
13fn extract_trace_id() -> Option<String> {
14    // Try to extract from the current span's trace_id field
15    // This requires coordination with the tracing subscriber
16    tracing::Span::current().id().map(|id| format!("{id:?}"))
17}
18
19/// Helper trait for enriching Problem with trace context
20pub trait WithTraceContext {
21    /// Enrich this Problem with `trace_id` and instance from the current request context
22    #[must_use]
23    fn with_trace_context(self, instance: impl Into<String>) -> Self;
24}
25
26impl WithTraceContext for Problem {
27    fn with_trace_context(mut self, instance: impl Into<String>) -> Self {
28        self = self.with_instance(instance);
29        if let Some(tid) = extract_trace_id() {
30            self = self.with_trace_id(tid);
31        }
32        self
33    }
34}
35
36/// Middleware-friendly: enrich errors from Axum extractors
37///
38/// Use this in handlers to automatically add trace context:
39///
40/// ```ignore
41/// async fn handler(uri: Uri) -> Result<Json<Data>, Problem> {
42///     let data = fetch_data()
43///         .await
44///         .map_err(Problem::from)
45///         .map_err(|p| p.with_request_context(&uri))?;
46///     Ok(Json(data))
47/// }
48/// ```
49pub trait WithRequestContext {
50    /// Add `trace_id` and instance from the current request
51    #[must_use]
52    fn with_request_context(self, uri: &axum::http::Uri) -> Self;
53}
54
55impl WithRequestContext for Problem {
56    fn with_request_context(self, uri: &axum::http::Uri) -> Self {
57        self.with_trace_context(uri.path())
58    }
59}
60
61#[cfg(test)]
62#[cfg_attr(coverage_nightly, coverage(off))]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn test_with_trace_context() {
68        use http::StatusCode;
69
70        let problem = Problem::new(StatusCode::NOT_FOUND, "Not Found", "Resource not found")
71            .with_trace_context("/tests/v1/users/123");
72
73        assert_eq!(problem.instance, "/tests/v1/users/123");
74        // trace_id may or may not be set depending on tracing context
75    }
76
77    #[test]
78    fn test_with_request_context() {
79        use axum::http::Uri;
80        use http::StatusCode;
81
82        let uri: Uri = "/tests/v1/users/123".parse().unwrap();
83        let problem = Problem::new(StatusCode::NOT_FOUND, "Not Found", "Resource not found")
84            .with_request_context(&uri);
85
86        assert_eq!(problem.instance, "/tests/v1/users/123");
87    }
88}