use tonic::{metadata, service::Interceptor, Request, Status};
use tracing_opentelemetry::OpenTelemetrySpanExt;
struct MetadataMap<'a>(&'a mut metadata::MetadataMap);
impl opentelemetry::propagation::Extractor for MetadataMap<'_> {
fn get(&self, key: &str) -> Option<&str> {
self.0.get(key).and_then(|metadata| metadata.to_str().ok())
}
fn keys(&self) -> Vec<&str> {
self.0
.keys()
.map(|key| match key {
tonic::metadata::KeyRef::Ascii(v) => v.as_str(),
tonic::metadata::KeyRef::Binary(v) => v.as_str(),
})
.collect::<Vec<_>>()
}
}
impl opentelemetry::propagation::Injector for MetadataMap<'_> {
fn set(&mut self, key: &str, value: String) {
if let Ok(key) = metadata::MetadataKey::from_bytes(key.as_bytes()) {
if let Ok(val) = metadata::MetadataValue::try_from(&value) {
self.0.insert(key, val);
}
}
}
}
#[derive(Clone)]
pub struct InjectTracingInterceptor;
impl Interceptor for InjectTracingInterceptor {
fn call(&mut self, mut request: Request<()>) -> std::result::Result<Request<()>, Status> {
let context = tracing::Span::current().context();
opentelemetry::global::get_text_map_propagator(|prop| {
prop.inject_context(&context, &mut MetadataMap(request.metadata_mut()));
});
Ok(request)
}
}
#[derive(Clone)]
pub struct ExtractTracingInterceptor;
impl Interceptor for ExtractTracingInterceptor {
fn call(&mut self, mut request: Request<()>) -> std::result::Result<Request<()>, Status> {
let parent_cx = opentelemetry::global::get_text_map_propagator(|prop| {
prop.extract(&MetadataMap(request.metadata_mut()))
});
request.extensions_mut().insert(parent_cx);
Ok(request)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::type_complexity)]
use super::*;
use opentelemetry::propagation::{Extractor, Injector};
use opentelemetry::trace::{SpanContext, SpanId, TraceContextExt, TraceId};
use opentelemetry_sdk::propagation::TraceContextPropagator;
const TRACEPARENT: &str = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01";
#[test]
fn metadata_map_set_drops_entries_tonic_rejects() {
let test_cases: Vec<(&str, &str, fn(&MetadataMap))> = vec![
("traceparent", TRACEPARENT, |map| {
assert_eq!(map.get("traceparent"), Some(TRACEPARENT));
}),
("Trace-Parent", TRACEPARENT, |map| {
assert_eq!(map.keys(), vec!["trace-parent"]);
}),
("bad key", TRACEPARENT, |map| assert!(map.keys().is_empty())),
("traceparent-bin", TRACEPARENT, |map| {
assert!(map.keys().is_empty());
}),
("traceparent", "bad\nvalue", |map| {
assert!(map.keys().is_empty());
}),
];
for (key, value, expect) in test_cases {
let mut metadata = metadata::MetadataMap::new();
let mut map = MetadataMap(&mut metadata);
map.set(key, value.to_string());
expect(&map);
}
}
#[test]
fn metadata_map_extractor_lists_ascii_and_binary_keys() {
let mut metadata = metadata::MetadataMap::new();
metadata.insert("traceparent", TRACEPARENT.parse().unwrap());
metadata.insert_bin("baggage-bin", metadata::MetadataValue::from_bytes(b"value"));
let map = MetadataMap(&mut metadata);
assert_eq!(map.get("traceparent"), Some(TRACEPARENT));
assert_eq!(map.get("baggage-bin"), None);
assert_eq!(map.keys(), vec!["traceparent", "baggage-bin"]);
}
#[test]
fn extract_tracing_interceptor_inserts_the_propagated_context() {
opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new());
let test_cases: Vec<(Option<&str>, fn(&SpanContext))> = vec![
(Some(TRACEPARENT), |span_context| {
assert!(span_context.is_remote());
assert!(span_context.is_sampled());
assert_eq!(
span_context.trace_id(),
TraceId::from_hex("0af7651916cd43dd8448eb211c80319c").unwrap()
);
assert_eq!(
span_context.span_id(),
SpanId::from_hex("b7ad6b7169203331").unwrap()
);
}),
(None, |span_context| assert!(!span_context.is_valid())),
];
for (traceparent, expect) in test_cases {
let mut request = Request::new(());
if let Some(traceparent) = traceparent {
request
.metadata_mut()
.insert("traceparent", traceparent.parse().unwrap());
}
let request = ExtractTracingInterceptor.call(request).unwrap();
let context = request
.extensions()
.get::<opentelemetry::Context>()
.unwrap();
expect(context.span().span_context());
}
}
}