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}