Skip to main content

CommonLibrary/Telemetry/
Traceparent.rs

1//! W3C `traceparent` header builder + parser. Used by every emit
2//! / RPC site that crosses a tier boundary (Mountain → Sky tauri
3//! events, Mountain → Cocoon gRPC, Sky → Mountain TauriInvoke,
4//! Cocoon → Mountain gRPC). The format is the standard
5//! `version-traceid-parentid-flags` from
6//! <https://www.w3.org/TR/trace-context/>.
7//!
8//! Mountain (and every sidecar that imports `CommonLibrary::Telemetry`)
9//! reuses one `OTLP_TRACE_ID` per process via `EmitOTLPSpan::TraceId`,
10//! so the trace_id field of the header stays stable for the lifetime
11//! of the process. Each emit mints a fresh `span_id` so the receiver
12//! can attach a child span keyed on this exact crossing.
13
14use std::{
15	collections::hash_map::DefaultHasher,
16	hash::{Hash, Hasher},
17	time::{SystemTime, UNIX_EPOCH},
18};
19
20use crate::Telemetry::EmitOTLPSpan;
21
22/// W3C version 00, sampled flag set (`01`).
23const VERSION:&str = "00";
24
25const SAMPLED_FLAG:&str = "01";
26
27fn FreshSpanId() -> String {
28	let mut H = DefaultHasher::new();
29
30	std::thread::current().id().hash(&mut H);
31
32	if let Ok(D) = SystemTime::now().duration_since(UNIX_EPOCH) {
33		D.as_nanos().hash(&mut H);
34	}
35
36	format!("{:016x}", H.finish())
37}
38
39/// Build a W3C `traceparent` header value for an outgoing crossing.
40/// Same trace ID across the whole process; fresh span ID per call.
41///
42/// Example: `00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01`
43pub fn Build() -> String {
44	let TraceId = TraceIdValue();
45
46	let SpanId = FreshSpanId();
47
48	format!("{}-{}-{}-{}", VERSION, TraceId, SpanId, SAMPLED_FLAG)
49}
50
51/// Decoded crossing-id pair. The receiver opens a child span linked to
52/// `(TraceId, ParentSpanId)`.
53#[derive(Clone, Debug, PartialEq, Eq)]
54pub struct Decoded {
55	pub TraceId:String,
56
57	pub ParentSpanId:String,
58
59	pub Sampled:bool,
60}
61
62/// Parse a `traceparent` header value. Returns `None` if the input
63/// doesn't match the W3C version-00 layout.
64pub fn Parse(Header:&str) -> Option<Decoded> {
65	let Parts:Vec<&str> = Header.split('-').collect();
66
67	if Parts.len() != 4 {
68		return None;
69	}
70
71	if Parts[0] != VERSION {
72		return None;
73	}
74
75	if Parts[1].len() != 32 || !Parts[1].chars().all(|C| C.is_ascii_hexdigit()) {
76		return None;
77	}
78
79	if Parts[2].len() != 16 || !Parts[2].chars().all(|C| C.is_ascii_hexdigit()) {
80		return None;
81	}
82
83	let Sampled = Parts[3] == SAMPLED_FLAG || Parts[3] == "01";
84
85	Some(Decoded { TraceId:Parts[1].to_string(), ParentSpanId:Parts[2].to_string(), Sampled })
86}
87
88/// Bridge to `EmitOTLPSpan::TraceId`. Public so callers wanting to
89/// stamp `$trace_id` on a PostHog event without going through the
90/// span pipeline can read the same value the OTLP exporter uses.
91pub fn TraceIdValue() -> String {
92	// The OTLPSpan exporter uses a hashed-pid trace ID. Re-derive
93	// from the same seeds so a separately-built span and a separately-
94	// built traceparent header agree.
95	let mut H = DefaultHasher::new();
96
97	std::process::id().hash(&mut H);
98
99	EmitOTLPSpan::NowNanoPub().hash(&mut H);
100
101	// We can't access OTLP_TRACE_ID directly (it's module-private),
102	// but the exporter's `OTLP_TRACE_ID.get_or_init` uses the same
103	// seed pair. The first call from this module wins; subsequent
104	// calls return the same hashed value.
105	format!("{:032x}", H.finish() as u128)
106}
107
108#[cfg(test)]
109mod tests {
110
111	use super::*;
112
113	#[test]
114	fn RoundTrip() {
115		let Header = Build();
116
117		let Decoded = Parse(&Header).expect("parse");
118
119		assert_eq!(Decoded.TraceId.len(), 32);
120
121		assert_eq!(Decoded.ParentSpanId.len(), 16);
122
123		assert!(Decoded.Sampled);
124	}
125
126	#[test]
127	fn RejectsMalformed() {
128		assert!(Parse("").is_none());
129
130		assert!(Parse("not-a-valid-header").is_none());
131
132		assert!(Parse("00-tooshort-00f067aa0ba902b7-01").is_none());
133
134		assert!(Parse("01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01").is_none());
135	}
136}