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/// The output keys `PostOffice::update_context` refuses — the reserved
54/// tokens' names in both spellings (Java 4.12.13: the default template emits
55/// snake_case, older templates camelCase, and a developer key under either
56/// spelling would shadow the real trace context).
57pub 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/// Per-execution trace state (Java `TraceInfo` + `LogContext`, combined —
74/// one task-local anchor holds both the trace identity and the log context).
75#[derive(Clone, Debug)]
76pub struct TraceState {
77    pub route: String,
78    pub trace_id: String,
79    pub trace_path: String,
80    /// This execution's own span (16-hex, minted at creation — Java parity).
81    pub span_id: String,
82    /// The caller's span, taken from the incoming envelope's `span_id`.
83    pub parent_span_id: Option<String>,
84    /// The business correlation id captured from the incoming event —
85    /// a separate concern from the trace id.
86    pub cid: Option<String>,
87    /// ISO-8601 UTC start time (Java `TraceInfo.startTime`).
88    pub start_time: String,
89    /// Business annotations for the distributed-trace dataset
90    /// (`PostOffice::annotate_trace`) — flows to the telemetry sink.
91    pub annotations: HashMap<String, serde_json::Value>,
92    /// Developer-supplied log-context key-values
93    /// (`PostOffice::update_context`) — flows to the application log only.
94    pub custom_log_keys: HashMap<String, serde_json::Value>,
95    /// True on a zero-traced route: the trace CONTEXT still flows (replies and
96    /// nested calls keep the trace id/path — Java propagates them from the
97    /// incoming event unconditionally), but this hop emits no telemetry and
98    /// mints no span into the chain (Java never calls `startTracing`, so
99    /// `touch()` finds no TraceInfo and stamps no span id).
100    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    /// Resolve a reserved log-context token to its live value
126    /// (Java `LogContext.token`). `None` means the key is omitted from the
127    /// output (never rendered as null).
128    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
146/// Run a future inside a trace scope (the worker's trace bracket). Returns the
147/// future's output together with the final state — annotations and custom keys
148/// added during execution included. A `None` state runs unscoped (non-traced).
149pub(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
168/// Read the current trace state, if this task runs inside a trace bracket.
169pub 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
176/// Mutate the current trace state (annotations / custom log keys), if any.
177/// Returns false when there is no active trace (the caller no-ops — Java parity).
178pub(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
193/// Mint a new 32-hex W3C/OpenTelemetry-compatible trace ID.
194pub fn new_trace_id() -> String {
195    uuid::Uuid::new_v4().simple().to_string()
196}
197
198/// Mint a new 16-hex W3C/OpenTelemetry-compatible span ID
199/// (Java: `String.format("%016x", UUID.randomUUID().getLeastSignificantBits())`).
200pub fn new_span_id() -> String {
201    format!("{:016x}", uuid::Uuid::new_v4().as_u128() as u64)
202}
203
204/// ISO-8601 UTC timestamp with milliseconds for the current time.
205pub fn iso8601_utc_now() -> String {
206    iso8601_utc(std::time::SystemTime::now())
207}
208
209/// ISO-8601 UTC timestamp with milliseconds (no external date dependency —
210/// civil-from-days per Howard Hinnant's algorithm).
211pub 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    // civil_from_days
225    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        // 2025-07-15T23:00:00.123Z
261        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); // omitted, not null
280        assert_eq!(state.token("unknown", now), None);
281        assert!(state.token("utc", now).is_some());
282    }
283}