Skip to main content

platform_core/
trace.rs

1//
2// Copyright 2018-2026 Accenture Technology
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15//
16
17//! Distributed-trace context — Rust port of the Java `TraceInfo` +
18//! `LogContext`/`LogContextManager` design (`org.platformlambda.core`).
19//!
20//! Trace and span IDs follow the **W3C Trace Context / OpenTelemetry** format:
21//! a 32-hex trace ID and a 16-hex span ID. Each traced function execution gets
22//! its **own span**; the caller's span travels on the envelope's `span_id`
23//! field and becomes the callee's `parent_span_id` — producing a causal span
24//! tree without any coupling between functions.
25//!
26//! Java threads the context through a per-worker registry keyed by thread id —
27//! deliberately avoiding the ThreadLocal / MDC pattern (an anti-pattern on a
28//! virtual-thread runtime). The Rust analog of that per-task anchor is a
29//! **tokio `task_local!`**: the route worker scopes the state around the
30//! function invocation, so `PostOffice` (propagation, annotations, log
31//! context) and the JSON logger (context block) read it from within the same
32//! task, and it is torn down when the function returns. Work `tokio::spawn`ed
33//! *from inside* a function does not inherit the context — the same boundary
34//! Java has for `Mono`/`Flux` completions after the worker returns.
35
36use std::cell::RefCell;
37use std::collections::HashMap;
38use std::future::Future;
39
40/// Reserved log-context token names (Java `LogContext.RESERVED_KEYS`): these
41/// resolve live per log line and cannot be overridden via
42/// `PostOffice::update_context`.
43pub const RESERVED_KEYS: [&str; 7] = [
44    "cid",
45    "traceId",
46    "tracePath",
47    "spanId",
48    "parentSpanId",
49    "service",
50    "utc",
51];
52
53/// Per-execution trace state (Java `TraceInfo` + `LogContext`, combined —
54/// one task-local anchor holds both the trace identity and the log context).
55#[derive(Clone, Debug)]
56pub struct TraceState {
57    pub route: String,
58    pub trace_id: String,
59    pub trace_path: String,
60    /// This execution's own span (16-hex, minted at creation — Java parity).
61    pub span_id: String,
62    /// The caller's span, taken from the incoming envelope's `span_id`.
63    pub parent_span_id: Option<String>,
64    /// The business correlation id captured from the incoming event —
65    /// a separate concern from the trace id.
66    pub cid: Option<String>,
67    /// ISO-8601 UTC start time (Java `TraceInfo.startTime`).
68    pub start_time: String,
69    /// Business annotations for the distributed-trace dataset
70    /// (`PostOffice::annotate_trace`) — flows to the telemetry sink.
71    pub annotations: HashMap<String, serde_json::Value>,
72    /// Developer-supplied log-context key-values
73    /// (`PostOffice::update_context`) — flows to the application log only.
74    pub custom_log_keys: HashMap<String, serde_json::Value>,
75    /// True on a zero-traced route: the trace CONTEXT still flows (replies and
76    /// nested calls keep the trace id/path — Java propagates them from the
77    /// incoming event unconditionally), but this hop emits no telemetry and
78    /// mints no span into the chain (Java never calls `startTracing`, so
79    /// `touch()` finds no TraceInfo and stamps no span id).
80    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    /// Resolve a reserved log-context token to its live value
106    /// (Java `LogContext.token`). `None` means the key is omitted from the
107    /// output (never rendered as null).
108    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
126/// Run a future inside a trace scope (the worker's trace bracket). Returns the
127/// future's output together with the final state — annotations and custom keys
128/// added during execution included. A `None` state runs unscoped (non-traced).
129pub(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
148/// Read the current trace state, if this task runs inside a trace bracket.
149pub 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
156/// Mutate the current trace state (annotations / custom log keys), if any.
157/// Returns false when there is no active trace (the caller no-ops — Java parity).
158pub(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
173/// Mint a new 32-hex W3C/OpenTelemetry-compatible trace ID.
174pub fn new_trace_id() -> String {
175    uuid::Uuid::new_v4().simple().to_string()
176}
177
178/// Mint a new 16-hex W3C/OpenTelemetry-compatible span ID
179/// (Java: `String.format("%016x", UUID.randomUUID().getLeastSignificantBits())`).
180pub fn new_span_id() -> String {
181    format!("{:016x}", uuid::Uuid::new_v4().as_u128() as u64)
182}
183
184/// ISO-8601 UTC timestamp with milliseconds for the current time.
185pub fn iso8601_utc_now() -> String {
186    iso8601_utc(std::time::SystemTime::now())
187}
188
189/// ISO-8601 UTC timestamp with milliseconds (no external date dependency —
190/// civil-from-days per Howard Hinnant's algorithm).
191pub 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    // civil_from_days
205    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        // 2025-07-15T23:00:00.123Z
241        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); // omitted, not null
260        assert_eq!(state.token("unknown", now), None);
261        assert!(state.token("utc", now).is_some());
262    }
263}