Skip to main content

CommonLibrary/Telemetry/
Traceparent.rs

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