1use std::cell::RefCell;
37use std::collections::HashMap;
38use std::future::Future;
39
40pub const RESERVED_KEYS: [&str; 7] = [
44 "cid",
45 "traceId",
46 "tracePath",
47 "spanId",
48 "parentSpanId",
49 "service",
50 "utc",
51];
52
53#[derive(Clone, Debug)]
56pub struct TraceState {
57 pub route: String,
58 pub trace_id: String,
59 pub trace_path: String,
60 pub span_id: String,
62 pub parent_span_id: Option<String>,
64 pub cid: Option<String>,
67 pub start_time: String,
69 pub annotations: HashMap<String, serde_json::Value>,
72 pub custom_log_keys: HashMap<String, serde_json::Value>,
75 pub zero_traced: bool,
81}
82
83impl TraceState {
84 pub fn new(
85 route: &str,
86 trace_id: &str,
87 trace_path: &str,
88 parent_span_id: Option<&str>,
89 cid: Option<&str>,
90 ) -> Self {
91 TraceState {
92 route: route.to_string(),
93 trace_id: trace_id.to_string(),
94 trace_path: trace_path.to_string(),
95 span_id: new_span_id(),
96 parent_span_id: parent_span_id.map(str::to_string),
97 cid: cid.map(str::to_string),
98 start_time: iso8601_utc_now(),
99 annotations: HashMap::new(),
100 custom_log_keys: HashMap::new(),
101 zero_traced: false,
102 }
103 }
104
105 pub fn token(&self, token: &str, log_time: std::time::SystemTime) -> Option<serde_json::Value> {
109 match token {
110 "cid" => self.cid.clone().map(serde_json::Value::String),
111 "traceId" => Some(serde_json::Value::String(self.trace_id.clone())),
112 "tracePath" => Some(serde_json::Value::String(self.trace_path.clone())),
113 "spanId" => Some(serde_json::Value::String(self.span_id.clone())),
114 "parentSpanId" => self.parent_span_id.clone().map(serde_json::Value::String),
115 "service" => Some(serde_json::Value::String(self.route.clone())),
116 "utc" => Some(serde_json::Value::String(iso8601_utc(log_time))),
117 _ => None,
118 }
119 }
120}
121
122tokio::task_local! {
123 static TRACE_STATE: RefCell<Option<TraceState>>;
124}
125
126pub(crate) async fn run_scoped<F>(
130 state: Option<TraceState>,
131 future: F,
132) -> (F::Output, Option<TraceState>)
133where
134 F: Future,
135{
136 if state.is_none() {
137 return (future.await, None);
138 }
139 TRACE_STATE
140 .scope(RefCell::new(state), async {
141 let output = future.await;
142 let state = TRACE_STATE.with(|cell| cell.borrow_mut().take());
143 (output, state)
144 })
145 .await
146}
147
148pub fn with_current<T>(reader: impl FnOnce(&TraceState) -> T) -> Option<T> {
150 TRACE_STATE
151 .try_with(|cell| cell.borrow().as_ref().map(reader))
152 .ok()
153 .flatten()
154}
155
156pub(crate) fn with_current_mut(mutator: impl FnOnce(&mut TraceState)) -> bool {
159 TRACE_STATE
160 .try_with(|cell| {
161 let mut guard = cell.borrow_mut();
162 match guard.as_mut() {
163 Some(state) => {
164 mutator(state);
165 true
166 }
167 None => false,
168 }
169 })
170 .unwrap_or(false)
171}
172
173pub fn new_trace_id() -> String {
175 uuid::Uuid::new_v4().simple().to_string()
176}
177
178pub fn new_span_id() -> String {
181 format!("{:016x}", uuid::Uuid::new_v4().as_u128() as u64)
182}
183
184pub fn iso8601_utc_now() -> String {
186 iso8601_utc(std::time::SystemTime::now())
187}
188
189pub fn iso8601_utc(time: std::time::SystemTime) -> String {
192 let duration = time
193 .duration_since(std::time::UNIX_EPOCH)
194 .unwrap_or_default();
195 let secs = duration.as_secs() as i64;
196 let millis = duration.subsec_millis();
197 let days = secs.div_euclid(86_400);
198 let secs_of_day = secs.rem_euclid(86_400);
199 let (hh, mm, ss) = (
200 secs_of_day / 3600,
201 (secs_of_day % 3600) / 60,
202 secs_of_day % 60,
203 );
204 let z = days + 719_468;
206 let era = z.div_euclid(146_097);
207 let doe = z.rem_euclid(146_097);
208 let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
209 let year = yoe + era * 400;
210 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
211 let mp = (5 * doy + 2) / 153;
212 let day = doy - (153 * mp + 2) / 5 + 1;
213 let month = if mp < 10 { mp + 3 } else { mp - 9 };
214 let year = if month <= 2 { year + 1 } else { year };
215 format!("{year:04}-{month:02}-{day:02}T{hh:02}:{mm:02}:{ss:02}.{millis:03}Z")
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn trace_and_span_ids_are_w3c_shaped() {
224 let trace = new_trace_id();
225 let span = new_span_id();
226 assert_eq!(trace.len(), 32);
227 assert!(trace
228 .bytes()
229 .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase()));
230 assert_eq!(span.len(), 16);
231 assert!(span
232 .bytes()
233 .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase()));
234 assert_ne!(new_trace_id(), trace);
235 }
236
237 #[test]
238 fn iso8601_formats_known_instant() {
239 let t = std::time::UNIX_EPOCH + std::time::Duration::from_millis(1_752_620_400_123);
240 assert_eq!(iso8601_utc(t), "2025-07-15T23:00:00.123Z");
242 let epoch = std::time::UNIX_EPOCH;
243 assert_eq!(iso8601_utc(epoch), "1970-01-01T00:00:00.000Z");
244 }
245
246 #[test]
247 fn tokens_resolve_and_absent_keys_are_none() {
248 let mut state = TraceState::new("v1.demo", "t".repeat(32).as_str(), "GET /x", None, None);
249 state.cid = Some("cid-1".into());
250 let now = std::time::SystemTime::now();
251 assert_eq!(
252 state.token("service", now),
253 Some(serde_json::Value::String("v1.demo".into()))
254 );
255 assert_eq!(
256 state.token("cid", now),
257 Some(serde_json::Value::String("cid-1".into()))
258 );
259 assert_eq!(state.token("parentSpanId", now), None); assert_eq!(state.token("unknown", now), None);
261 assert!(state.token("utc", now).is_some());
262 }
263}