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
39fn is_baggage_token(key: &str) -> bool {
43 !key.is_empty()
44 && key
45 .bytes()
46 .all(|b| b.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&b))
47}
48
49fn strip_control_chars(value: &str) -> String {
59 value
60 .chars()
61 .filter(|c| !matches!(*c, '\x00'..='\x08' | '\x0a'..='\x1f' | '\x7f'))
62 .collect()
63}
64
65pub fn parse_baggage(raw: &str) -> BTreeMap<String, String> {
72 let mut result = BTreeMap::new();
73 for member in raw.split(',') {
74 let kv = member.split(';').next().unwrap_or("");
75 if let Some(eq_idx) = kv.find('=') {
76 let key = kv[..eq_idx].trim();
77 if is_baggage_token(key) {
78 let value = kv[eq_idx + 1..].trim();
79 result.insert(key.to_string(), strip_control_chars(value));
80 }
81 }
82 }
83 result
84}
85
86fn parse_traceparent(value: Option<&str>) -> (Option<String>, Option<String>, Option<String>) {
87 let Some(raw) = value else {
88 return (None, None, None);
89 };
90 let parts = raw.split('-').collect::<Vec<_>>();
91 if parts.len() != 4 {
92 return (None, None, None);
93 }
94 let version = parts[0];
95 let trace_id = parts[1];
96 let span_id = parts[2];
97 let flags = parts[3];
98 let valid = version.len() == 2
99 && trace_id.len() == 32
100 && span_id.len() == 16
101 && flags.len() == 2
102 && !version.eq_ignore_ascii_case("ff")
103 && trace_id != "00000000000000000000000000000000"
104 && span_id != "0000000000000000"
105 && [version, trace_id, span_id, flags]
106 .iter()
107 .all(|part| part.chars().all(|ch| ch.is_ascii_hexdigit()));
108
109 if !valid {
110 return (None, None, None);
111 }
112
113 (
114 Some(raw.to_string()),
115 Some(trace_id.to_ascii_lowercase()),
116 Some(span_id.to_ascii_lowercase()),
117 )
118}
119
120fn is_forwardable_tracestate(value: &str) -> bool {
131 value.split(',').all(is_tracestate_member)
132}
133
134fn is_tracestate_member(member: &str) -> bool {
135 let trimmed = member.trim_matches([' ', '\t']);
136 let Some((key, val)) = trimmed.split_once('=') else {
137 return false;
138 };
139 if key.is_empty() || key.len() > 256 {
140 return false;
141 }
142 let mut chars = key.chars();
143 let first = chars.next().expect("key checked non-empty");
144 if !(first.is_ascii_lowercase() || first.is_ascii_digit()) {
145 return false;
146 }
147 if !chars.all(|c| {
148 c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '_' | '-' | '*' | '/' | '@')
149 }) {
150 return false;
151 }
152 val.chars()
153 .all(|c| matches!(c, '\x20'..='\x2b' | '\x2d'..='\x3c' | '\x3e'..='\x7e'))
154}
155
156pub fn extract_w3c_context(
157 traceparent: Option<&str>,
158 tracestate: Option<&str>,
159 baggage: Option<&str>,
160) -> PropagationContext {
161 let traceparent = traceparent.filter(|value| value.len() <= MAX_HEADER_LENGTH);
162 let tracestate = tracestate.and_then(|value| {
163 if value.len() > MAX_HEADER_LENGTH
164 || value.split(',').count() > MAX_TRACESTATE_PAIRS
165 || !is_forwardable_tracestate(value)
166 {
167 None
168 } else {
169 Some(value.to_string())
170 }
171 });
172 let baggage = baggage.and_then(|value| {
173 if value.len() > MAX_BAGGAGE_LENGTH {
174 None
175 } else {
176 Some(value.to_string())
177 }
178 });
179 let (traceparent, trace_id, span_id) = parse_traceparent(traceparent);
180
181 PropagationContext {
182 traceparent,
183 tracestate,
184 baggage,
185 trace_id,
186 span_id,
187 }
188}
189
190pub fn bind_propagation_context(context: PropagationContext) -> PropagationGuard {
191 let mut fields = Vec::new();
192 if let Some(traceparent) = context.traceparent.clone() {
193 fields.push(("traceparent".to_string(), Value::String(traceparent)));
194 }
195 if let Some(tracestate) = context.tracestate.clone() {
196 fields.push(("tracestate".to_string(), Value::String(tracestate)));
197 }
198 if let Some(ref baggage) = context.baggage {
199 fields.push(("baggage".to_string(), Value::String(baggage.clone())));
200 for (k, v) in parse_baggage(baggage) {
201 fields.push((format!("baggage.{k}"), Value::String(v)));
202 }
203 }
204
205 let context_guard = if fields.is_empty() {
206 None
207 } else {
208 Some(bind_context(fields))
209 };
210 let trace_guard = if context.trace_id.is_some() || context.span_id.is_some() {
211 Some(set_trace_context(context.trace_id, context.span_id))
212 } else {
213 None
214 };
215
216 PropagationGuard {
217 trace_guard,
218 context_guard,
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 use serde_json::json;
227
228 use crate::context::get_context;
229 use crate::testing::acquire_test_state_lock;
230 use crate::tracer::get_trace_context;
231
232 #[test]
233 fn propagation_test_a_parse_baggage_keeps_pairs_and_strips_parameters() {
234 let baggage = parse_baggage("user=alice;prop=x,env=prod;ttl=100,invalid,=skip");
235
236 assert_eq!(baggage.get("user").map(String::as_str), Some("alice"));
237 assert_eq!(baggage.get("env").map(String::as_str), Some("prod"));
238 assert_eq!(baggage.len(), 2);
239 }
240
241 #[test]
242 fn propagation_test_a_bind_propagation_context_roundtrip_restores_state() {
243 let _guard = acquire_test_state_lock();
244 let context = PropagationContext {
245 traceparent: Some(
246 "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01".to_string(),
247 ),
248 tracestate: Some("k=v".to_string()),
249 baggage: Some("user=alice,env=prod".to_string()),
250 trace_id: Some("4bf92f3577b34da6a3ce929d0e0e4736".to_string()),
251 span_id: Some("00f067aa0ba902b7".to_string()),
252 };
253
254 {
255 let _propagation = bind_propagation_context(context);
256 let trace = get_trace_context();
257 let fields = get_context();
258 assert_eq!(
259 trace.get("trace_id").and_then(std::clone::Clone::clone),
260 Some("4bf92f3577b34da6a3ce929d0e0e4736".to_string())
261 );
262 assert_eq!(
263 trace.get("span_id").and_then(std::clone::Clone::clone),
264 Some("00f067aa0ba902b7".to_string())
265 );
266 assert_eq!(
267 fields.get("traceparent"),
268 Some(&json!(
269 "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
270 ))
271 );
272 assert_eq!(fields.get("tracestate"), Some(&json!("k=v")));
273 assert_eq!(fields.get("baggage"), Some(&json!("user=alice,env=prod")));
274 assert_eq!(fields.get("baggage.user"), Some(&json!("alice")));
275 assert_eq!(fields.get("baggage.env"), Some(&json!("prod")));
276 }
277
278 assert!(get_context().is_empty());
279 let trace = get_trace_context();
280 assert_eq!(trace.get("trace_id"), Some(&None));
281 assert_eq!(trace.get("span_id"), Some(&None));
282 }
283}