provide_telemetry/
propagation.rs1use std::collections::BTreeMap;
7
8use serde_json::Value;
9
10use crate::context::{bind_context, ContextGuard};
11use crate::tracer::set_trace_context;
12
13const MAX_HEADER_LENGTH: usize = 512;
14const MAX_TRACESTATE_PAIRS: usize = 32;
15const MAX_BAGGAGE_LENGTH: usize = 8192;
16
17#[derive(Clone, Debug, Default, PartialEq, Eq)]
18pub struct PropagationContext {
19 pub traceparent: Option<String>,
20 pub tracestate: Option<String>,
21 pub baggage: Option<String>,
22 pub trace_id: Option<String>,
23 pub span_id: Option<String>,
24}
25
26pub struct PropagationGuard {
27 trace_guard: Option<ContextGuard>,
28 context_guard: Option<ContextGuard>,
29}
30
31impl Drop for PropagationGuard {
32 #[cfg_attr(test, mutants::skip)] fn drop(&mut self) {
34 drop(self.trace_guard.take());
35 drop(self.context_guard.take());
36 }
37}
38
39pub fn parse_baggage(raw: &str) -> BTreeMap<String, String> {
42 let mut result = BTreeMap::new();
43 for member in raw.split(',') {
44 let kv = member.split(';').next().unwrap_or("");
45 if let Some(eq_idx) = kv.find('=') {
46 let key = kv[..eq_idx].trim();
47 if !key.is_empty() {
48 let value = kv[eq_idx + 1..].trim();
49 result.insert(key.to_string(), value.to_string());
50 }
51 }
52 }
53 result
54}
55
56fn parse_traceparent(value: Option<&str>) -> (Option<String>, Option<String>, Option<String>) {
57 let Some(raw) = value else {
58 return (None, None, None);
59 };
60 let parts = raw.split('-').collect::<Vec<_>>();
61 if parts.len() != 4 {
62 return (None, None, None);
63 }
64 let version = parts[0];
65 let trace_id = parts[1];
66 let span_id = parts[2];
67 let flags = parts[3];
68 let valid = version.len() == 2
69 && trace_id.len() == 32
70 && span_id.len() == 16
71 && flags.len() == 2
72 && !version.eq_ignore_ascii_case("ff")
73 && trace_id != "00000000000000000000000000000000"
74 && span_id != "0000000000000000"
75 && [version, trace_id, span_id, flags]
76 .iter()
77 .all(|part| part.chars().all(|ch| ch.is_ascii_hexdigit()));
78
79 if !valid {
80 return (None, None, None);
81 }
82
83 (
84 Some(raw.to_string()),
85 Some(trace_id.to_ascii_lowercase()),
86 Some(span_id.to_ascii_lowercase()),
87 )
88}
89
90pub fn extract_w3c_context(
91 traceparent: Option<&str>,
92 tracestate: Option<&str>,
93 baggage: Option<&str>,
94) -> PropagationContext {
95 let traceparent = traceparent.and_then(|value| {
96 if value.len() > MAX_HEADER_LENGTH {
97 None
98 } else {
99 Some(value)
100 }
101 });
102 let tracestate = tracestate.and_then(|value| {
103 if value.len() > MAX_HEADER_LENGTH || value.split(',').count() > MAX_TRACESTATE_PAIRS {
104 None
105 } else {
106 Some(value.to_string())
107 }
108 });
109 let baggage = baggage.and_then(|value| {
110 if value.len() > MAX_BAGGAGE_LENGTH {
111 None
112 } else {
113 Some(value.to_string())
114 }
115 });
116 let (traceparent, trace_id, span_id) = parse_traceparent(traceparent);
117
118 PropagationContext {
119 traceparent,
120 tracestate,
121 baggage,
122 trace_id,
123 span_id,
124 }
125}
126
127pub fn bind_propagation_context(context: PropagationContext) -> PropagationGuard {
128 let mut fields = Vec::new();
129 if let Some(traceparent) = context.traceparent.clone() {
130 fields.push(("traceparent".to_string(), Value::String(traceparent)));
131 }
132 if let Some(tracestate) = context.tracestate.clone() {
133 fields.push(("tracestate".to_string(), Value::String(tracestate)));
134 }
135 if let Some(ref baggage) = context.baggage {
136 fields.push(("baggage".to_string(), Value::String(baggage.clone())));
137 for (k, v) in parse_baggage(baggage) {
138 fields.push((format!("baggage.{k}"), Value::String(v)));
139 }
140 }
141
142 let context_guard = if fields.is_empty() {
143 None
144 } else {
145 Some(bind_context(fields))
146 };
147 let trace_guard = if context.trace_id.is_some() || context.span_id.is_some() {
148 Some(set_trace_context(context.trace_id, context.span_id))
149 } else {
150 None
151 };
152
153 PropagationGuard {
154 trace_guard,
155 context_guard,
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 use serde_json::json;
164
165 use crate::context::get_context;
166 use crate::testing::acquire_test_state_lock;
167 use crate::tracer::get_trace_context;
168
169 #[test]
170 fn propagation_test_a_parse_baggage_keeps_pairs_and_strips_parameters() {
171 let baggage = parse_baggage("user=alice;prop=x,env=prod;ttl=100,invalid,=skip");
172
173 assert_eq!(baggage.get("user").map(String::as_str), Some("alice"));
174 assert_eq!(baggage.get("env").map(String::as_str), Some("prod"));
175 assert_eq!(baggage.len(), 2);
176 }
177
178 #[test]
179 fn propagation_test_a_bind_propagation_context_roundtrip_restores_state() {
180 let _guard = acquire_test_state_lock();
181 let context = PropagationContext {
182 traceparent: Some(
183 "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01".to_string(),
184 ),
185 tracestate: Some("k=v".to_string()),
186 baggage: Some("user=alice,env=prod".to_string()),
187 trace_id: Some("4bf92f3577b34da6a3ce929d0e0e4736".to_string()),
188 span_id: Some("00f067aa0ba902b7".to_string()),
189 };
190
191 {
192 let _propagation = bind_propagation_context(context);
193 let trace = get_trace_context();
194 let fields = get_context();
195 assert_eq!(
196 trace.get("trace_id").and_then(std::clone::Clone::clone),
197 Some("4bf92f3577b34da6a3ce929d0e0e4736".to_string())
198 );
199 assert_eq!(
200 trace.get("span_id").and_then(std::clone::Clone::clone),
201 Some("00f067aa0ba902b7".to_string())
202 );
203 assert_eq!(
204 fields.get("traceparent"),
205 Some(&json!(
206 "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
207 ))
208 );
209 assert_eq!(fields.get("tracestate"), Some(&json!("k=v")));
210 assert_eq!(fields.get("baggage"), Some(&json!("user=alice,env=prod")));
211 assert_eq!(fields.get("baggage.user"), Some(&json!("alice")));
212 assert_eq!(fields.get("baggage.env"), Some(&json!("prod")));
213 }
214
215 assert!(get_context().is_empty());
216 let trace = get_trace_context();
217 assert_eq!(trace.get("trace_id"), Some(&None));
218 assert_eq!(trace.get("span_id"), Some(&None));
219 }
220}