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
53pub const RESERVED_OUTPUT_KEYS: [&str; 13] = [
58 "cid",
59 "traceId",
60 "trace_id",
61 "tracePath",
62 "trace_path",
63 "spanId",
64 "span_id",
65 "parentSpanId",
66 "parent_span_id",
67 "service",
68 "utc",
69 "timestamp",
70 "time",
71];
72
73#[derive(Clone, Debug)]
76pub struct TraceState {
77 pub route: String,
78 pub trace_id: String,
79 pub trace_path: String,
80 pub span_id: String,
82 pub parent_span_id: Option<String>,
84 pub cid: Option<String>,
87 pub start_time: String,
89 pub annotations: HashMap<String, serde_json::Value>,
92 pub custom_log_keys: HashMap<String, serde_json::Value>,
95 pub zero_traced: bool,
101}
102
103impl TraceState {
104 pub fn new(
105 route: &str,
106 trace_id: &str,
107 trace_path: &str,
108 parent_span_id: Option<&str>,
109 cid: Option<&str>,
110 ) -> Self {
111 TraceState {
112 route: route.to_string(),
113 trace_id: trace_id.to_string(),
114 trace_path: trace_path.to_string(),
115 span_id: new_span_id(),
116 parent_span_id: parent_span_id.map(str::to_string),
117 cid: cid.map(str::to_string),
118 start_time: iso8601_utc_now(),
119 annotations: HashMap::new(),
120 custom_log_keys: HashMap::new(),
121 zero_traced: false,
122 }
123 }
124
125 pub fn token(&self, token: &str, log_time: std::time::SystemTime) -> Option<serde_json::Value> {
129 match token {
130 "cid" => self.cid.clone().map(serde_json::Value::String),
131 "traceId" => Some(serde_json::Value::String(self.trace_id.clone())),
132 "tracePath" => Some(serde_json::Value::String(self.trace_path.clone())),
133 "spanId" => Some(serde_json::Value::String(self.span_id.clone())),
134 "parentSpanId" => self.parent_span_id.clone().map(serde_json::Value::String),
135 "service" => Some(serde_json::Value::String(self.route.clone())),
136 "utc" => Some(serde_json::Value::String(iso8601_utc(log_time))),
137 _ => None,
138 }
139 }
140}
141
142tokio::task_local! {
143 static TRACE_STATE: RefCell<Option<TraceState>>;
144}
145
146pub(crate) async fn run_scoped<F>(
150 state: Option<TraceState>,
151 future: F,
152) -> (F::Output, Option<TraceState>)
153where
154 F: Future,
155{
156 if state.is_none() {
157 return (future.await, None);
158 }
159 TRACE_STATE
160 .scope(RefCell::new(state), async {
161 let output = future.await;
162 let state = TRACE_STATE.with(|cell| cell.borrow_mut().take());
163 (output, state)
164 })
165 .await
166}
167
168pub fn with_current<T>(reader: impl FnOnce(&TraceState) -> T) -> Option<T> {
170 TRACE_STATE
171 .try_with(|cell| cell.borrow().as_ref().map(reader))
172 .ok()
173 .flatten()
174}
175
176pub(crate) fn with_current_mut(mutator: impl FnOnce(&mut TraceState)) -> bool {
179 TRACE_STATE
180 .try_with(|cell| {
181 let mut guard = cell.borrow_mut();
182 match guard.as_mut() {
183 Some(state) => {
184 mutator(state);
185 true
186 }
187 None => false,
188 }
189 })
190 .unwrap_or(false)
191}
192
193pub fn new_trace_id() -> String {
195 uuid::Uuid::new_v4().simple().to_string()
196}
197
198pub fn new_span_id() -> String {
201 format!("{:016x}", uuid::Uuid::new_v4().as_u128() as u64)
202}
203
204pub fn iso8601_utc_now() -> String {
206 iso8601_utc(std::time::SystemTime::now())
207}
208
209pub fn iso8601_utc(time: std::time::SystemTime) -> String {
212 let duration = time
213 .duration_since(std::time::UNIX_EPOCH)
214 .unwrap_or_default();
215 let secs = duration.as_secs() as i64;
216 let millis = duration.subsec_millis();
217 let days = secs.div_euclid(86_400);
218 let secs_of_day = secs.rem_euclid(86_400);
219 let (hh, mm, ss) = (
220 secs_of_day / 3600,
221 (secs_of_day % 3600) / 60,
222 secs_of_day % 60,
223 );
224 let z = days + 719_468;
226 let era = z.div_euclid(146_097);
227 let doe = z.rem_euclid(146_097);
228 let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
229 let year = yoe + era * 400;
230 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
231 let mp = (5 * doy + 2) / 153;
232 let day = doy - (153 * mp + 2) / 5 + 1;
233 let month = if mp < 10 { mp + 3 } else { mp - 9 };
234 let year = if month <= 2 { year + 1 } else { year };
235 format!("{year:04}-{month:02}-{day:02}T{hh:02}:{mm:02}:{ss:02}.{millis:03}Z")
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn trace_and_span_ids_are_w3c_shaped() {
244 let trace = new_trace_id();
245 let span = new_span_id();
246 assert_eq!(trace.len(), 32);
247 assert!(trace
248 .bytes()
249 .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase()));
250 assert_eq!(span.len(), 16);
251 assert!(span
252 .bytes()
253 .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase()));
254 assert_ne!(new_trace_id(), trace);
255 }
256
257 #[test]
258 fn iso8601_formats_known_instant() {
259 let t = std::time::UNIX_EPOCH + std::time::Duration::from_millis(1_752_620_400_123);
260 assert_eq!(iso8601_utc(t), "2025-07-15T23:00:00.123Z");
262 let epoch = std::time::UNIX_EPOCH;
263 assert_eq!(iso8601_utc(epoch), "1970-01-01T00:00:00.000Z");
264 }
265
266 #[test]
267 fn tokens_resolve_and_absent_keys_are_none() {
268 let mut state = TraceState::new("v1.demo", "t".repeat(32).as_str(), "GET /x", None, None);
269 state.cid = Some("cid-1".into());
270 let now = std::time::SystemTime::now();
271 assert_eq!(
272 state.token("service", now),
273 Some(serde_json::Value::String("v1.demo".into()))
274 );
275 assert_eq!(
276 state.token("cid", now),
277 Some(serde_json::Value::String("cid-1".into()))
278 );
279 assert_eq!(state.token("parentSpanId", now), None); assert_eq!(state.token("unknown", now), None);
281 assert!(state.token("utc", now).is_some());
282 }
283}