use opentelemetry::Context;
use opentelemetry::global;
use opentelemetry::propagation::{Extractor, Injector};
use tracing_opentelemetry::OpenTelemetrySpanExt as _;
pub struct HttpHeaderInjector<'a>(pub &'a mut http::HeaderMap);
impl Injector for HttpHeaderInjector<'_> {
fn set(&mut self, key: &str, value: String) {
if let (Ok(name), Ok(val)) = (
http::header::HeaderName::try_from(key),
http::header::HeaderValue::try_from(value),
) {
self.0.insert(name, val);
}
}
}
pub struct HttpHeaderExtractor<'a>(pub &'a http::HeaderMap);
impl Extractor for HttpHeaderExtractor<'_> {
fn get(&self, key: &str) -> Option<&str> {
self.0.get(key).and_then(|v| v.to_str().ok())
}
fn keys(&self) -> Vec<&str> {
self.0
.keys()
.map(http::header::HeaderName::as_str)
.collect()
}
}
pub fn inject_current_span_into(headers: &mut http::HeaderMap) {
let cx = tracing::Span::current().context();
inject_context_into(&cx, headers);
}
pub fn inject_context_into(cx: &Context, headers: &mut http::HeaderMap) {
global::get_text_map_propagator(|propagator| {
propagator.inject_context(cx, &mut HttpHeaderInjector(headers));
});
}
pub fn extract_parent_into_current_span(headers: &http::HeaderMap) {
let parent_cx = extract_context_from(headers);
if let Err(err) = tracing::Span::current().set_parent(parent_cx) {
tracing::debug!(error = %err, "could not set parent trace context");
}
}
#[must_use]
pub fn extract_context_from(headers: &http::HeaderMap) -> Context {
global::get_text_map_propagator(|propagator| propagator.extract(&HttpHeaderExtractor(headers)))
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use super::*;
use opentelemetry::trace::{
SpanContext, SpanId, TraceContextExt, TraceFlags, TraceId, TraceState,
};
use opentelemetry_sdk::propagation::TraceContextPropagator;
fn ctx_with(trace_id: TraceId, span_id: SpanId) -> Context {
let span_ctx = SpanContext::new(
trace_id,
span_id,
TraceFlags::SAMPLED,
true, TraceState::default(),
);
Context::new().with_remote_span_context(span_ctx)
}
fn install_w3c() {
global::set_text_map_propagator(TraceContextPropagator::new());
}
#[test]
fn injector_extractor_round_trip_preserves_trace_id() {
install_w3c();
let trace_id = TraceId::from_hex("0af7651916cd43dd8448eb211c80319c").unwrap();
let span_id = SpanId::from_hex("b7ad6b7169203331").unwrap();
let cx = ctx_with(trace_id, span_id);
let mut headers = http::HeaderMap::new();
inject_context_into(&cx, &mut headers);
assert!(
headers.get("traceparent").is_some(),
"traceparent should be injected"
);
let extracted = extract_context_from(&headers);
let extracted_span = extracted.span();
let extracted_ctx = extracted_span.span_context();
assert_eq!(
extracted_ctx.trace_id(),
trace_id,
"trace id must round-trip"
);
assert_eq!(extracted_ctx.span_id(), span_id, "span id must round-trip");
}
#[test]
fn extract_from_empty_headers_yields_invalid_context() {
install_w3c();
let headers = http::HeaderMap::new();
let extracted = extract_context_from(&headers);
assert!(
!extracted.span().span_context().is_valid(),
"empty headers should yield an invalid (root) context"
);
}
#[test]
fn injector_silently_drops_invalid_header_names() {
let mut map = http::HeaderMap::new();
let mut injector = HttpHeaderInjector(&mut map);
injector.set("invalid name with spaces", "v".to_owned());
injector.set("x-good", "ok".to_owned());
assert!(map.get("invalid name with spaces").is_none());
assert_eq!(map.get("x-good").unwrap(), "ok");
}
#[test]
fn extractor_keys_returns_header_names() {
let mut map = http::HeaderMap::new();
map.insert("traceparent", http::HeaderValue::from_static("x"));
map.insert("x-custom", http::HeaderValue::from_static("y"));
let extractor = HttpHeaderExtractor(&map);
let mut keys: Vec<&str> = extractor.keys();
keys.sort_unstable();
assert_eq!(keys, vec!["traceparent", "x-custom"]);
assert_eq!(extractor.get("traceparent"), Some("x"));
assert_eq!(extractor.get("missing"), None);
}
}