rs_zero/observability/
trace.rs1use http::{HeaderMap, HeaderValue, header::InvalidHeaderValue};
2
3pub const REQUEST_ID_HEADER: &str = "x-request-id";
5pub const TRACEPARENT_HEADER: &str = "traceparent";
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct CurrentRequestId(pub String);
11
12pub fn request_id_from_headers(headers: &HeaderMap) -> Option<String> {
14 headers
15 .get(REQUEST_ID_HEADER)
16 .and_then(|value| value.to_str().ok())
17 .map(str::trim)
18 .filter(|value| !value.is_empty())
19 .map(ToOwned::to_owned)
20}
21
22pub fn traceparent_from_headers(headers: &HeaderMap) -> Option<String> {
24 headers
25 .get(TRACEPARENT_HEADER)
26 .and_then(|value| value.to_str().ok())
27 .map(str::trim)
28 .filter(|value| is_valid_traceparent(value))
29 .map(ToOwned::to_owned)
30}
31
32pub fn insert_traceparent_header(
34 headers: &mut HeaderMap,
35 traceparent: &str,
36) -> Result<(), InvalidHeaderValue> {
37 headers.insert(TRACEPARENT_HEADER, HeaderValue::from_str(traceparent)?);
38 Ok(())
39}
40
41#[cfg(feature = "rpc")]
43pub fn request_id_from_metadata(metadata: &tonic::metadata::MetadataMap) -> Option<String> {
44 metadata
45 .get(REQUEST_ID_HEADER)
46 .and_then(|value| value.to_str().ok())
47 .map(str::trim)
48 .filter(|value| !value.is_empty())
49 .map(ToOwned::to_owned)
50}
51
52#[cfg(feature = "rpc")]
54pub fn traceparent_from_metadata(metadata: &tonic::metadata::MetadataMap) -> Option<String> {
55 metadata
56 .get(TRACEPARENT_HEADER)
57 .and_then(|value| value.to_str().ok())
58 .map(str::trim)
59 .filter(|value| is_valid_traceparent(value))
60 .map(ToOwned::to_owned)
61}
62
63#[cfg(feature = "rpc")]
65pub fn insert_traceparent_metadata(
66 metadata: &mut tonic::metadata::MetadataMap,
67 traceparent: &str,
68) -> Result<(), tonic::metadata::errors::InvalidMetadataValue> {
69 metadata.insert(TRACEPARENT_HEADER, traceparent.parse()?);
70 Ok(())
71}
72
73pub fn current_trace_id() -> Option<String> {
75 #[cfg(feature = "otlp")]
76 {
77 use opentelemetry::trace::TraceContextExt;
78 use tracing_opentelemetry::OpenTelemetrySpanExt;
79
80 let context = tracing::Span::current().context();
81 let span = context.span();
82 let span_context = span.span_context();
83 if span_context.is_valid() {
84 return Some(span_context.trace_id().to_string());
85 }
86 }
87
88 None
89}
90
91pub fn current_span_id() -> Option<String> {
93 #[cfg(feature = "otlp")]
94 {
95 use opentelemetry::trace::TraceContextExt;
96 use tracing_opentelemetry::OpenTelemetrySpanExt;
97
98 let context = tracing::Span::current().context();
99 let span = context.span();
100 let span_context = span.span_context();
101 if span_context.is_valid() {
102 return Some(span_context.span_id().to_string());
103 }
104 }
105
106 None
107}
108
109pub fn current_traceparent() -> Option<String> {
111 let trace_id = current_trace_id()?;
112 let span_id = current_span_id()?;
113 Some(format!("00-{trace_id}-{span_id}-01"))
114}
115
116#[cfg(feature = "otlp")]
118pub fn opentelemetry_context_from_headers(headers: &HeaderMap) -> Option<opentelemetry::Context> {
119 use opentelemetry::{global, trace::TraceContextExt};
120
121 traceparent_from_headers(headers)?;
122 let extractor = HeaderMapExtractor { headers };
123 let context = global::get_text_map_propagator(|propagator| propagator.extract(&extractor));
124 context.span().span_context().is_valid().then_some(context)
125}
126
127#[cfg(feature = "otlp")]
129pub fn opentelemetry_context_from_traceparent(traceparent: &str) -> Option<opentelemetry::Context> {
130 use opentelemetry::{global, trace::TraceContextExt};
131
132 if !is_valid_traceparent(traceparent) {
133 return None;
134 }
135
136 let extractor = TraceParentExtractor { traceparent };
137 let context = global::get_text_map_propagator(|propagator| propagator.extract(&extractor));
138 context.span().span_context().is_valid().then_some(context)
139}
140
141#[cfg(feature = "otlp")]
143pub fn set_span_parent_from_headers(span: &tracing::Span, headers: &HeaderMap) -> bool {
144 use tracing_opentelemetry::OpenTelemetrySpanExt;
145
146 let Some(context) = opentelemetry_context_from_headers(headers) else {
147 return false;
148 };
149
150 span.set_parent(context).is_ok()
151}
152
153#[cfg(all(feature = "otlp", feature = "rpc"))]
155pub fn opentelemetry_context_from_metadata(
156 metadata: &tonic::metadata::MetadataMap,
157) -> Option<opentelemetry::Context> {
158 use opentelemetry::{global, trace::TraceContextExt};
159
160 traceparent_from_metadata(metadata)?;
161 let extractor = MetadataMapExtractor { metadata };
162 let context = global::get_text_map_propagator(|propagator| propagator.extract(&extractor));
163 context.span().span_context().is_valid().then_some(context)
164}
165
166#[cfg(all(feature = "otlp", feature = "rpc"))]
168pub fn set_span_parent_from_metadata(
169 span: &tracing::Span,
170 metadata: &tonic::metadata::MetadataMap,
171) -> bool {
172 use tracing_opentelemetry::OpenTelemetrySpanExt;
173
174 let Some(context) = opentelemetry_context_from_metadata(metadata) else {
175 return false;
176 };
177
178 span.set_parent(context).is_ok()
179}
180
181#[cfg(all(feature = "otlp", feature = "rpc"))]
183pub fn inject_current_context_metadata(
184 metadata: &mut tonic::metadata::MetadataMap,
185) -> Result<bool, tonic::metadata::errors::InvalidMetadataValue> {
186 use opentelemetry::{global, trace::TraceContextExt};
187 use tracing_opentelemetry::OpenTelemetrySpanExt;
188
189 let context = tracing::Span::current().context();
190 if !context.span().span_context().is_valid() {
191 return Ok(false);
192 }
193
194 let mut injector = MetadataMapInjector {
195 metadata,
196 invalid_value: None,
197 };
198 global::get_text_map_propagator(|propagator| {
199 propagator.inject_context(&context, &mut injector);
200 });
201 if let Some(error) = injector.invalid_value {
202 return Err(error);
203 }
204 Ok(injector.metadata.contains_key(TRACEPARENT_HEADER))
205}
206
207pub fn trace_id_from_traceparent(traceparent: &str) -> Option<&str> {
209 if !is_valid_traceparent(traceparent) {
210 return None;
211 }
212 traceparent.split('-').nth(1)
213}
214
215pub fn span_id_from_traceparent(traceparent: &str) -> Option<&str> {
217 if !is_valid_traceparent(traceparent) {
218 return None;
219 }
220 traceparent.split('-').nth(2)
221}
222
223fn is_valid_traceparent(value: &str) -> bool {
224 let mut parts = value.split('-');
225 let Some(version) = parts.next() else {
226 return false;
227 };
228 let Some(trace_id) = parts.next() else {
229 return false;
230 };
231 let Some(span_id) = parts.next() else {
232 return false;
233 };
234 let Some(flags) = parts.next() else {
235 return false;
236 };
237 parts.next().is_none()
238 && version.len() == 2
239 && trace_id.len() == 32
240 && span_id.len() == 16
241 && flags.len() == 2
242 && trace_id != "00000000000000000000000000000000"
243 && span_id != "0000000000000000"
244 && version.chars().all(|value| value.is_ascii_hexdigit())
245 && trace_id.chars().all(|value| value.is_ascii_hexdigit())
246 && span_id.chars().all(|value| value.is_ascii_hexdigit())
247 && flags.chars().all(|value| value.is_ascii_hexdigit())
248}
249
250#[cfg(feature = "otlp")]
251struct HeaderMapExtractor<'a> {
252 headers: &'a HeaderMap,
253}
254
255#[cfg(feature = "otlp")]
256impl opentelemetry::propagation::Extractor for HeaderMapExtractor<'_> {
257 fn get(&self, key: &str) -> Option<&str> {
258 self.headers.get(key).and_then(|value| value.to_str().ok())
259 }
260
261 fn keys(&self) -> Vec<&str> {
262 self.headers.keys().map(|key| key.as_str()).collect()
263 }
264}
265
266#[cfg(feature = "otlp")]
267struct TraceParentExtractor<'a> {
268 traceparent: &'a str,
269}
270
271#[cfg(feature = "otlp")]
272impl opentelemetry::propagation::Extractor for TraceParentExtractor<'_> {
273 fn get(&self, key: &str) -> Option<&str> {
274 key.eq_ignore_ascii_case(TRACEPARENT_HEADER)
275 .then_some(self.traceparent)
276 }
277
278 fn keys(&self) -> Vec<&str> {
279 vec![TRACEPARENT_HEADER]
280 }
281}
282
283#[cfg(all(feature = "otlp", feature = "rpc"))]
284struct MetadataMapExtractor<'a> {
285 metadata: &'a tonic::metadata::MetadataMap,
286}
287
288#[cfg(all(feature = "otlp", feature = "rpc"))]
289impl opentelemetry::propagation::Extractor for MetadataMapExtractor<'_> {
290 fn get(&self, key: &str) -> Option<&str> {
291 self.metadata.get(key).and_then(|value| value.to_str().ok())
292 }
293
294 fn keys(&self) -> Vec<&str> {
295 self.metadata
296 .keys()
297 .filter_map(|key| match key {
298 tonic::metadata::KeyRef::Ascii(key) => Some(key.as_str()),
299 tonic::metadata::KeyRef::Binary(_) => None,
300 })
301 .collect()
302 }
303}
304
305#[cfg(all(feature = "otlp", feature = "rpc"))]
306struct MetadataMapInjector<'a> {
307 metadata: &'a mut tonic::metadata::MetadataMap,
308 invalid_value: Option<tonic::metadata::errors::InvalidMetadataValue>,
309}
310
311#[cfg(all(feature = "otlp", feature = "rpc"))]
312impl opentelemetry::propagation::Injector for MetadataMapInjector<'_> {
313 fn set(&mut self, key: &str, value: String) {
314 let Ok(key) = key.parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>() else {
315 return;
316 };
317 match tonic::metadata::MetadataValue::try_from(value.as_str()) {
318 Ok(value) => {
319 self.metadata.insert(key, value);
320 }
321 Err(error) => {
322 self.invalid_value = Some(error);
323 }
324 }
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use http::HeaderMap;
331
332 use super::{
333 REQUEST_ID_HEADER, TRACEPARENT_HEADER, current_trace_id, request_id_from_headers,
334 trace_id_from_traceparent, traceparent_from_headers,
335 };
336
337 #[test]
338 fn extracts_request_id_from_headers() {
339 let mut headers = HeaderMap::new();
340 headers.insert(REQUEST_ID_HEADER, "req-1".parse().expect("header"));
341
342 assert_eq!(request_id_from_headers(&headers).as_deref(), Some("req-1"));
343 }
344
345 #[test]
346 fn trace_id_is_not_forged_without_active_context() {
347 assert!(current_trace_id().is_none());
348 }
349
350 #[test]
351 fn extracts_valid_traceparent_from_headers() {
352 let mut headers = HeaderMap::new();
353 headers.insert(
354 TRACEPARENT_HEADER,
355 "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
356 .parse()
357 .expect("traceparent"),
358 );
359
360 let value = traceparent_from_headers(&headers).expect("traceparent");
361 assert_eq!(
362 trace_id_from_traceparent(&value),
363 Some("4bf92f3577b34da6a3ce929d0e0e4736")
364 );
365 }
366}