use std::cell::RefCell;
use std::collections::HashMap;
use std::future::Future;
pub const RESERVED_KEYS: [&str; 7] = [
"cid",
"traceId",
"tracePath",
"spanId",
"parentSpanId",
"service",
"utc",
];
pub const RESERVED_OUTPUT_KEYS: [&str; 13] = [
"cid",
"traceId",
"trace_id",
"tracePath",
"trace_path",
"spanId",
"span_id",
"parentSpanId",
"parent_span_id",
"service",
"utc",
"timestamp",
"time",
];
#[derive(Clone, Debug)]
pub struct TraceState {
pub route: String,
pub trace_id: String,
pub trace_path: String,
pub span_id: String,
pub parent_span_id: Option<String>,
pub cid: Option<String>,
pub start_time: String,
pub annotations: HashMap<String, serde_json::Value>,
pub custom_log_keys: HashMap<String, serde_json::Value>,
pub zero_traced: bool,
}
impl TraceState {
pub fn new(
route: &str,
trace_id: &str,
trace_path: &str,
parent_span_id: Option<&str>,
cid: Option<&str>,
) -> Self {
TraceState {
route: route.to_string(),
trace_id: trace_id.to_string(),
trace_path: trace_path.to_string(),
span_id: new_span_id(),
parent_span_id: parent_span_id.map(str::to_string),
cid: cid.map(str::to_string),
start_time: iso8601_utc_now(),
annotations: HashMap::new(),
custom_log_keys: HashMap::new(),
zero_traced: false,
}
}
pub fn token(&self, token: &str, log_time: std::time::SystemTime) -> Option<serde_json::Value> {
match token {
"cid" => self.cid.clone().map(serde_json::Value::String),
"traceId" => Some(serde_json::Value::String(self.trace_id.clone())),
"tracePath" => Some(serde_json::Value::String(self.trace_path.clone())),
"spanId" => Some(serde_json::Value::String(self.span_id.clone())),
"parentSpanId" => self.parent_span_id.clone().map(serde_json::Value::String),
"service" => Some(serde_json::Value::String(self.route.clone())),
"utc" => Some(serde_json::Value::String(iso8601_utc(log_time))),
_ => None,
}
}
}
tokio::task_local! {
static TRACE_STATE: RefCell<Option<TraceState>>;
}
pub(crate) async fn run_scoped<F>(
state: Option<TraceState>,
future: F,
) -> (F::Output, Option<TraceState>)
where
F: Future,
{
if state.is_none() {
return (future.await, None);
}
TRACE_STATE
.scope(RefCell::new(state), async {
let output = future.await;
let state = TRACE_STATE.with(|cell| cell.borrow_mut().take());
(output, state)
})
.await
}
pub fn with_current<T>(reader: impl FnOnce(&TraceState) -> T) -> Option<T> {
TRACE_STATE
.try_with(|cell| cell.borrow().as_ref().map(reader))
.ok()
.flatten()
}
pub(crate) fn with_current_mut(mutator: impl FnOnce(&mut TraceState)) -> bool {
TRACE_STATE
.try_with(|cell| {
let mut guard = cell.borrow_mut();
match guard.as_mut() {
Some(state) => {
mutator(state);
true
}
None => false,
}
})
.unwrap_or(false)
}
pub fn new_trace_id() -> String {
uuid::Uuid::new_v4().simple().to_string()
}
pub fn new_span_id() -> String {
format!("{:016x}", uuid::Uuid::new_v4().as_u128() as u64)
}
pub fn iso8601_utc_now() -> String {
iso8601_utc(std::time::SystemTime::now())
}
pub fn iso8601_utc(time: std::time::SystemTime) -> String {
let duration = time
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let secs = duration.as_secs() as i64;
let millis = duration.subsec_millis();
let days = secs.div_euclid(86_400);
let secs_of_day = secs.rem_euclid(86_400);
let (hh, mm, ss) = (
secs_of_day / 3600,
(secs_of_day % 3600) / 60,
secs_of_day % 60,
);
let z = days + 719_468;
let era = z.div_euclid(146_097);
let doe = z.rem_euclid(146_097);
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let year = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let day = doy - (153 * mp + 2) / 5 + 1;
let month = if mp < 10 { mp + 3 } else { mp - 9 };
let year = if month <= 2 { year + 1 } else { year };
format!("{year:04}-{month:02}-{day:02}T{hh:02}:{mm:02}:{ss:02}.{millis:03}Z")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn trace_and_span_ids_are_w3c_shaped() {
let trace = new_trace_id();
let span = new_span_id();
assert_eq!(trace.len(), 32);
assert!(trace
.bytes()
.all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase()));
assert_eq!(span.len(), 16);
assert!(span
.bytes()
.all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase()));
assert_ne!(new_trace_id(), trace);
}
#[test]
fn iso8601_formats_known_instant() {
let t = std::time::UNIX_EPOCH + std::time::Duration::from_millis(1_752_620_400_123);
assert_eq!(iso8601_utc(t), "2025-07-15T23:00:00.123Z");
let epoch = std::time::UNIX_EPOCH;
assert_eq!(iso8601_utc(epoch), "1970-01-01T00:00:00.000Z");
}
#[test]
fn tokens_resolve_and_absent_keys_are_none() {
let mut state = TraceState::new("v1.demo", "t".repeat(32).as_str(), "GET /x", None, None);
state.cid = Some("cid-1".into());
let now = std::time::SystemTime::now();
assert_eq!(
state.token("service", now),
Some(serde_json::Value::String("v1.demo".into()))
);
assert_eq!(
state.token("cid", now),
Some(serde_json::Value::String("cid-1".into()))
);
assert_eq!(state.token("parentSpanId", now), None); assert_eq!(state.token("unknown", now), None);
assert!(state.token("utc", now).is_some());
}
}