krabka_trace_context/
propagation.rs1use std::{collections::HashMap, str::from_utf8};
16
17use opentelemetry::{
18 Context, global,
19 propagation::{Extractor, Injector},
20};
21use tracing_opentelemetry::OpenTelemetrySpanExt as _;
22
23pub const TRACEPARENT: &str = "traceparent";
27pub const TRACESTATE: &str = "tracestate";
28
29struct VecInjector<'a>(&'a mut Vec<(String, String)>);
34
35impl Injector for VecInjector<'_> {
36 fn set(&mut self, key: &str, value: String) {
37 self.0.push((key.to_owned(), value));
38 }
39}
40
41struct MapExtractor<'a>(&'a HashMap<String, String>);
43
44impl Extractor for MapExtractor<'_> {
45 fn get(&self, key: &str) -> Option<&str> {
46 self.0.get(key).map(String::as_str)
47 }
48
49 fn keys(&self) -> Vec<&str> {
50 self.0.keys().map(String::as_str).collect()
51 }
52}
53
54#[must_use]
68pub fn current_trace_headers() -> Vec<(String, String)> {
69 let cx = tracing::Span::current().context();
70 let mut out = Vec::new();
71 global::get_text_map_propagator(|prop| {
72 prop.inject_context(&cx, &mut VecInjector(&mut out));
73 });
74 out
75}
76
77#[must_use]
83pub fn extract_context<'a, I, V>(headers: I) -> Context
84where
85 I: IntoIterator<Item = (&'a str, V)>,
86 V: AsRef<[u8]>,
87{
88 let map: HashMap<String, String> = headers
89 .into_iter()
90 .filter_map(|(k, v)| {
91 from_utf8(v.as_ref())
92 .ok()
93 .map(|s| (k.to_owned(), s.to_owned()))
94 })
95 .collect();
96 global::get_text_map_propagator(|prop| prop.extract(&MapExtractor(&map)))
97}
98
99pub fn set_remote_parent<'a, I, V>(span: &tracing::Span, headers: I)
105where
106 I: IntoIterator<Item = (&'a str, V)>,
107 V: AsRef<[u8]>,
108{
109 let _ = span.set_parent(extract_context(headers));
111}
112
113#[cfg(test)]
114mod tests {
115
116 use std::iter;
117
118 use opentelemetry::trace::{TraceContextExt as _, TraceId};
119 use opentelemetry_sdk::propagation::TraceContextPropagator;
120 use tracing::subscriber;
121
122 use super::*;
123
124 #[test]
125 fn extract_context_roundtrips_w3c_traceparent() {
126 global::set_text_map_propagator(TraceContextPropagator::new());
130
131 let traceparent = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01";
132 let cx = extract_context([(TRACEPARENT, traceparent.as_bytes())]);
133 let sc = cx.span().span_context().clone();
134
135 assert2::assert!(sc.is_valid());
136 assert2::assert!(
137 sc.trace_id() == TraceId::from_hex("0af7651916cd43dd8448eb211c80319c").unwrap()
138 );
139 assert2::assert!(sc.is_sampled());
140 }
141
142 #[test]
143 fn extract_context_skips_non_utf8_and_missing() {
144 global::set_text_map_propagator(TraceContextPropagator::new());
145 let cx = extract_context([(TRACEPARENT, [0xff, 0xfe].as_slice())]);
147 assert2::assert!(!cx.span().span_context().is_valid());
148 }
149
150 fn with_otel_subscriber(f: impl FnOnce()) {
156 use opentelemetry::trace::TracerProvider as _;
157 use opentelemetry_sdk::trace::{Sampler, SdkTracerProvider};
158 use tracing_subscriber::prelude::*;
159
160 global::set_text_map_propagator(TraceContextPropagator::new());
161 let provider = SdkTracerProvider::builder()
162 .with_sampler(Sampler::AlwaysOn)
163 .build();
164 let tracer = provider.tracer("propagation-test");
165 let subscriber =
166 tracing_subscriber::registry().with(tracing_opentelemetry::layer().with_tracer(tracer));
167 subscriber::with_default(subscriber, f);
168 }
169
170 #[test]
171 fn vec_injector_collects_key_value_pairs() {
172 let mut out = Vec::new();
175 VecInjector(&mut out).set(TRACEPARENT, "abc".to_owned());
176 assert2::assert!(out == vec![(TRACEPARENT.to_owned(), "abc".to_owned())]);
177 }
178
179 #[test]
180 fn map_extractor_reads_values_and_lists_keys() {
181 let map: HashMap<String, String> =
184 iter::once((TRACEPARENT.to_owned(), "v".to_owned())).collect();
185 let ex = MapExtractor(&map);
186 assert2::assert!(ex.get(TRACEPARENT) == Some("v"));
187 assert2::assert!(ex.get("absent").is_none());
188 assert2::assert!(ex.keys() == vec![TRACEPARENT]);
189 }
190
191 #[test]
192 fn current_trace_headers_emits_traceparent_for_active_span() {
193 with_otel_subscriber(|| {
194 let span = tracing::info_span!("producer");
195 let _g = span.enter();
196
197 let headers = current_trace_headers();
198 let tp = headers.iter().find(|(k, _)| k == TRACEPARENT);
199 assert2::assert!(tp.is_some());
200
201 let (_, value) = tp.unwrap();
205 let trace_id = tracing::Span::current()
206 .context()
207 .span()
208 .span_context()
209 .trace_id();
210 assert2::assert!(value.contains(&trace_id.to_string()));
211 assert2::assert!(value.ends_with("-01"));
212 });
213 }
214
215 #[test]
216 fn current_trace_headers_empty_without_active_span() {
217 assert2::assert!(current_trace_headers().is_empty());
219 }
220
221 #[test]
222 fn set_remote_parent_reparents_span_into_header_trace() {
223 with_otel_subscriber(|| {
224 let traceparent = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01";
225 let span = tracing::info_span!("consumer");
226 set_remote_parent(&span, [(TRACEPARENT, traceparent.as_bytes())]);
227
228 let sc = span.context().span().span_context().clone();
230 assert2::assert!(
231 sc.trace_id() == TraceId::from_hex("0af7651916cd43dd8448eb211c80319c").unwrap()
232 );
233 });
234 }
235}