use opentelemetry::baggage::BaggageExt;
use opentelemetry::trace::Span as _;
use opentelemetry::{Context, KeyValue};
use opentelemetry_sdk::error::OTelSdkResult;
use opentelemetry_sdk::trace::{Span, SpanData, SpanProcessor};
#[derive(Debug, Clone, Default)]
pub struct BaggageSpanProcessor;
impl BaggageSpanProcessor {
#[must_use]
pub fn new() -> Self {
Self
}
}
impl SpanProcessor for BaggageSpanProcessor {
fn on_start(&self, span: &mut Span, cx: &Context) {
if !span.is_recording() {
return;
}
for (key, (value, _meta)) in cx.baggage().iter() {
span.set_attribute(KeyValue::new(
key.as_str().to_string(),
value.as_str().to_string(),
));
}
}
fn on_end(&self, _span: SpanData) {}
fn force_flush(&self) -> OTelSdkResult {
Ok(())
}
fn shutdown_with_timeout(&self, _timeout: std::time::Duration) -> OTelSdkResult {
Ok(())
}
fn shutdown(&self) -> OTelSdkResult {
self.shutdown_with_timeout(std::time::Duration::from_secs(5))
}
}
#[cfg(test)]
mod tests {
use super::*;
use opentelemetry::baggage::BaggageExt;
use opentelemetry::trace::{Tracer, TracerProvider};
use opentelemetry::{Context, KeyValue};
use opentelemetry_sdk::trace::{InMemorySpanExporter, SdkTracerProvider, SimpleSpanProcessor};
fn build_test_provider(processor: BaggageSpanProcessor) -> (impl Tracer, InMemorySpanExporter) {
let exporter = InMemorySpanExporter::default();
let provider = SdkTracerProvider::builder()
.with_span_processor(processor)
.with_span_processor(SimpleSpanProcessor::new(exporter.clone()))
.build();
let tracer = provider.tracer("test");
(tracer, exporter)
}
fn first_span_attr(exporter: &InMemorySpanExporter, key: &str) -> Option<String> {
let spans = exporter.get_finished_spans().expect("exporter ok");
spans.first().and_then(|s| {
s.attributes
.iter()
.find(|kv| kv.key.as_str() == key)
.map(|kv| kv.value.as_str().to_string())
})
}
#[test]
fn copies_every_baggage_entry_to_attributes() {
let (tracer, exporter) = build_test_provider(BaggageSpanProcessor);
let cx = Context::new().with_baggage(vec![
KeyValue::new("iii.session.id", "S-1"),
KeyValue::new("iii.session.name", "refactor auth"),
KeyValue::new("iii.message.id", "M-1"),
KeyValue::new("iii.function.id", "auth::set_token"),
KeyValue::new("iii.tag.message", "fix the login bug"),
KeyValue::new("tenant.id", "t-42"),
]);
let span = tracer
.span_builder("inner")
.start_with_context(&tracer, &cx);
drop(span);
for (key, expected) in [
("iii.session.id", "S-1"),
("iii.session.name", "refactor auth"),
("iii.message.id", "M-1"),
("iii.function.id", "auth::set_token"),
("iii.tag.message", "fix the login bug"),
("tenant.id", "t-42"),
] {
assert_eq!(
first_span_attr(&exporter, key).as_deref(),
Some(expected),
"baggage key {key} must be copied",
);
}
}
#[test]
fn missing_baggage_entry_means_attribute_not_set() {
let (tracer, exporter) = build_test_provider(BaggageSpanProcessor);
let cx = Context::new().with_baggage(vec![KeyValue::new("iii.message.id", "M-only")]);
let span = tracer
.span_builder("inner")
.start_with_context(&tracer, &cx);
drop(span);
assert_eq!(
first_span_attr(&exporter, "iii.message.id").as_deref(),
Some("M-only"),
);
assert!(first_span_attr(&exporter, "iii.session.id").is_none());
}
#[test]
fn empty_parent_context_produces_no_attributes() {
let (tracer, exporter) = build_test_provider(BaggageSpanProcessor);
let span = tracer.start("inner");
drop(span);
assert!(first_span_attr(&exporter, "iii.session.id").is_none());
assert!(first_span_attr(&exporter, "iii.message.id").is_none());
}
#[test]
fn noop_guard_skips_processing_when_sampled_out() {
let exporter = InMemorySpanExporter::default();
let provider = SdkTracerProvider::builder()
.with_sampler(opentelemetry_sdk::trace::Sampler::AlwaysOff)
.with_span_processor(BaggageSpanProcessor)
.with_span_processor(SimpleSpanProcessor::new(exporter.clone()))
.build();
let tracer = provider.tracer("test");
let cx = Context::new().with_baggage(vec![
KeyValue::new("iii.session.id", "S-1"),
KeyValue::new("iii.message.id", "M-1"),
]);
let span = tracer
.span_builder("inner")
.start_with_context(&tracer, &cx);
drop(span);
let spans = exporter.get_finished_spans().expect("exporter ok");
assert!(
spans.is_empty(),
"AlwaysOff sampler should drop the span; no export expected"
);
}
}