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";
26
27const SAMPLED_FLAG:&str = "01";
28
29fn FreshSpanId() -> String {
30	let mut H = DefaultHasher::new();
31
32	std::thread::current().id().hash(&mut H);
33
34	if let Ok(D) = SystemTime::now().duration_since(UNIX_EPOCH) {
35		D.as_nanos().hash(&mut H);
36	}
37
38	format!("{:016x}", H.finish())
39}
40
41/// Build a W3C `traceparent` header value for an outgoing crossing.
42/// Same trace ID across the whole process; fresh span ID per call.
43///
44/// Example: `00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01`
45pub fn Build() -> String {
46	let TraceId = TraceIdValue();
47
48	let SpanId = FreshSpanId();
49
50	format!("{}-{}-{}-{}", VERSION, TraceId, SpanId, SAMPLED_FLAG)
51}
52
53/// Decoded crossing-id pair. The receiver opens a child span linked to
54/// `(TraceId, ParentSpanId)`.
55#[derive(Clone, Debug, PartialEq, Eq)]
56pub struct Decoded {
57	pub TraceId:String,
58
59	pub ParentSpanId:String,
60
61	pub Sampled:bool,
62}
63
64/// Parse a `traceparent` header value. Returns `None` if the input
65/// doesn't match the W3C version-00 layout.
66pub fn Parse(Header:&str) -> Option<Decoded> {
67	let Parts:Vec<&str> = Header.split('-').collect();
68
69	if Parts.len() != 4 {
70		return None;
71	}
72
73	if Parts[0] != VERSION {
74		return None;
75	}
76
77	if Parts[1].len() != 32 || !Parts[1].chars().all(|C| C.is_ascii_hexdigit()) {
78		return None;
79	}
80
81	if Parts[2].len() != 16 || !Parts[2].chars().all(|C| C.is_ascii_hexdigit()) {
82		return None;
83	}
84
85	let Sampled = Parts[3] == SAMPLED_FLAG || Parts[3] == "01";
86
87	Some(Decoded { TraceId:Parts[1].to_string(), ParentSpanId:Parts[2].to_string(), Sampled })
88}
89
90/// Bridge to `EmitOTLPSpan::TraceId`. Public so callers wanting to
91/// stamp `$trace_id` on a PostHog event without going through the
92/// span pipeline can read the same value the OTLP exporter uses.
93pub fn TraceIdValue() -> String {
94	// The OTLPSpan exporter uses a hashed-pid trace ID. Re-derive
95	// from the same seeds so a separately-built span and a separately-
96	// built traceparent header agree.
97	let mut H = DefaultHasher::new();
98
99	std::process::id().hash(&mut H);
100
101	EmitOTLPSpan::NowNanoPub().hash(&mut H);
102
103	// We can't access OTLP_TRACE_ID directly (it's module-private),
104	// but the exporter's `OTLP_TRACE_ID.get_or_init` uses the same
105	// seed pair. The first call from this module wins; subsequent
106	// calls return the same hashed value.
107	format!("{:032x}", H.finish() as u128)
108}
109
110#[cfg(test)]
111mod tests {
112
113	use super::*;
114
115	#[test]
116	fn RoundTrip() {
117		let Header = Build();
118
119		let Decoded = Parse(&Header).expect("parse");
120
121		assert_eq!(Decoded.TraceId.len(), 32);
122
123		assert_eq!(Decoded.ParentSpanId.len(), 16);
124
125		assert!(Decoded.Sampled);
126	}
127
128	#[test]
129	fn RejectsMalformed() {
130		assert!(Parse("").is_none());
131
132		assert!(Parse("not-a-valid-header").is_none());
133
134		assert!(Parse("00-tooshort-00f067aa0ba902b7-01").is_none());
135
136		assert!(Parse("01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01").is_none());
137	}
138}