Skip to main content

modkit/api/
canonical_trace.rs

1//! Migration-time trace/instance helper for the canonical `Problem`.
2//!
3//! Mirrors [`crate::api::trace_layer::WithTraceContext`] /
4//! [`crate::api::trace_layer::WithRequestContext`] but operates on
5//! [`modkit_canonical_errors::Problem`] instead of the legacy
6//! [`crate::api::problem::Problem`].
7//!
8//! **Scheduled for deletion** once the canonical error middleware
9//! (`cpt-cf-errors-component-error-middleware`, see
10//! `docs/arch/errors/DESIGN.md` §3.2) lands and starts injecting `trace_id`
11//! / `instance` from request context. At that point every call site of
12//! [`CanonicalProblemMigrationExt::with_temporary_request_context`]
13//! disappears together with this trait.
14
15use modkit_canonical_errors::Problem;
16
17/// Extension trait that fills `trace_id` and `instance` on a canonical
18/// [`Problem`] using the temporary span-id fallback documented in
19/// `docs/arch/errors/DESIGN.md` §3.7.
20///
21/// Per-module `From<DomainError> for Problem` impls call this at the end of
22/// the conversion so every wire response carries the same shape until the
23/// canonical error middleware takes over.
24pub trait CanonicalProblemMigrationExt: Sized {
25    /// Set `instance` to the supplied path and `trace_id` to a span-id
26    /// fallback derived from `tracing::Span::current()`.
27    ///
28    /// Pass `"/"` when no request URI is plumbed through to the call site
29    /// (the common case for `From<DomainError> for Problem`).
30    #[must_use]
31    fn with_temporary_request_context(self, instance: impl Into<String>) -> Self;
32}
33
34impl CanonicalProblemMigrationExt for Problem {
35    fn with_temporary_request_context(self, instance: impl Into<String>) -> Self {
36        let mut problem = self.with_instance(instance);
37        // TODO(cpt-cf-errors-component-error-middleware): replace with
38        // header-aware extraction (`crate::api::error_layer::extract_trace_id`)
39        // performed in the canonical error middleware.
40        if let Some(id) = tracing::Span::current().id() {
41            problem = problem.with_trace_id(id.into_u64().to_string());
42        }
43        problem
44    }
45}
46
47#[cfg(test)]
48#[cfg_attr(coverage_nightly, coverage(off))]
49mod tests {
50    use super::*;
51    use modkit_canonical_errors::CanonicalError;
52
53    #[test]
54    fn sets_instance() {
55        let problem: Problem = CanonicalError::internal("boom").create().into();
56        let problem = problem.with_temporary_request_context("/api/v1/widgets/42");
57        assert_eq!(problem.instance.as_deref(), Some("/api/v1/widgets/42"));
58    }
59
60    #[test]
61    fn sets_trace_id_when_in_span() {
62        // A registered subscriber is required for `Span::current().id()` to
63        // return Some — without it the span-id fallback is silently a no-op.
64        use tracing_subscriber::fmt;
65        let subscriber = fmt().with_test_writer().finish();
66        tracing::subscriber::with_default(subscriber, || {
67            let span = tracing::info_span!("trace_id_test");
68            let _enter = span.enter();
69            let problem: Problem = CanonicalError::internal("boom").create().into();
70            let problem = problem.with_temporary_request_context("/");
71            assert!(
72                problem.trace_id.is_some(),
73                "expected span-id fallback to populate trace_id"
74            );
75        });
76    }
77
78    #[test]
79    fn no_trace_id_outside_any_span() {
80        let problem: Problem = CanonicalError::internal("boom").create().into();
81        let problem = problem.with_temporary_request_context("/");
82        assert!(problem.trace_id.is_none());
83    }
84}