Skip to main content

CommonLibrary/Telemetry/
Traceparent.rs

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