use std::collections::HashMap;
use opentelemetry::Context;
use opentelemetry::propagation::{Extractor, Injector, TextMapPropagator};
use opentelemetry::trace::TraceContextExt as _;
#[derive(Debug, Default, Clone)]
pub struct TraceStateEnricher;
impl TextMapPropagator for TraceStateEnricher {
fn inject_context(&self, cx: &Context, injector: &mut dyn Injector) {
let Some(session_id) = crate::current_rerun_session_id() else {
return;
};
let span = cx.span();
let span_context = span.span_context();
if !span_context.is_valid() {
return;
}
let trace_state = span_context.trace_state().clone();
let trace_state = trace_state
.insert(crate::RERUN_SESSION_TRACESTATE_KEY.to_owned(), session_id)
.unwrap_or(trace_state);
let header = trace_state.header();
if !header.is_empty() {
injector.set("tracestate", header);
}
}
fn extract_with_context(&self, cx: &Context, _extractor: &dyn Extractor) -> Context {
cx.clone()
}
fn fields(&self) -> opentelemetry::propagation::text_map_propagator::FieldIter<'_> {
static FIELDS: &[String] = &[];
opentelemetry::propagation::text_map_propagator::FieldIter::new(FIELDS)
}
}
#[derive(Debug)]
pub(crate) struct RerunSessionRootSpanProcessor;
impl opentelemetry_sdk::trace::SpanProcessor for RerunSessionRootSpanProcessor {
fn on_start(&self, span: &mut opentelemetry_sdk::trace::Span, cx: &opentelemetry::Context) {
use opentelemetry::trace::{Span as _, TraceContextExt as _};
if cx.span().span_context().is_valid() {
return;
}
if let Some(id) = crate::current_rerun_session_id() {
span.set_attribute(opentelemetry::KeyValue::new(
crate::RERUN_SESSION_TRACESTATE_KEY,
id.to_string(),
));
}
}
fn on_end(&self, _span: opentelemetry_sdk::trace::SpanData) {}
fn force_flush(&self) -> opentelemetry_sdk::error::OTelSdkResult {
Ok(())
}
fn shutdown_with_timeout(
&self,
_timeout: std::time::Duration,
) -> opentelemetry_sdk::error::OTelSdkResult {
Ok(())
}
fn shutdown(&self) -> opentelemetry_sdk::error::OTelSdkResult {
Ok(())
}
}
pub fn parse_pairs(input: &str) -> HashMap<String, String> {
if input.is_empty() {
return HashMap::default();
}
input
.split(',')
.filter_map(|pair| {
let pair = pair.trim();
if pair.is_empty() {
return None;
}
let mut parts = pair.splitn(2, '=');
match (parts.next(), parts.next()) {
(Some(k), Some(v)) if !k.is_empty() && !v.is_empty() => {
Some((k.trim().to_owned(), v.trim().to_owned()))
}
_ => {
tracing::debug!("Ignoring malformed tracestate pair: '{}'", pair);
None
}
}
})
.collect()
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use opentelemetry::trace::{
SpanContext, SpanId, TraceContextExt as _, TraceFlags, TraceId, TraceState,
};
use super::*;
#[derive(Default)]
struct MapInjector {
entries: HashMap<String, String>,
}
impl Injector for MapInjector {
fn set(&mut self, key: &str, value: String) {
self.entries.insert(key.to_owned(), value);
}
}
fn ctx_with_state(state: TraceState) -> Context {
let span_cx = SpanContext::new(
TraceId::from_bytes([
0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab,
0xcd, 0xef,
]),
SpanId::from_bytes([0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
TraceFlags::SAMPLED,
false,
state,
);
Context::new().with_remote_span_context(span_cx)
}
#[test]
fn enricher_no_session_is_noop() {
let cx = ctx_with_state(TraceState::default());
let mut injector = MapInjector::default();
TraceStateEnricher.inject_context(&cx, &mut injector);
assert!(
injector.entries.is_empty(),
"expected no header writes, got {:?}",
injector.entries
);
}
#[test]
fn enricher_invalid_span_context_is_noop() {
let cx = Context::new();
let mut injector = MapInjector::default();
TraceStateEnricher.inject_context(&cx, &mut injector);
assert!(injector.entries.is_empty());
}
fn build_tracer_with_processor() -> (
opentelemetry_sdk::trace::Tracer,
opentelemetry_sdk::trace::InMemorySpanExporter,
opentelemetry_sdk::trace::SdkTracerProvider,
) {
use opentelemetry::trace::TracerProvider as _;
let exporter = opentelemetry_sdk::trace::InMemorySpanExporter::default();
let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
.with_span_processor(RerunSessionRootSpanProcessor)
.with_span_processor(opentelemetry_sdk::trace::SimpleSpanProcessor::new(
exporter.clone(),
))
.build();
let tracer = provider.tracer("test");
(tracer, exporter, provider)
}
fn rerun_session_attr(span: &opentelemetry_sdk::trace::SpanData) -> Option<String> {
span.attributes
.iter()
.find(|kv| kv.key.as_str() == crate::RERUN_SESSION_TRACESTATE_KEY)
.map(|kv| kv.value.to_string())
}
#[test]
fn processor_no_session_root_has_no_attribute() {
use opentelemetry::trace::Tracer as _;
let (tracer, exporter, provider) = build_tracer_with_processor();
let span = tracer.start("root");
drop(span);
provider.force_flush().ok();
let spans = exporter.get_finished_spans().unwrap();
assert_eq!(spans.len(), 1);
assert!(
rerun_session_attr(&spans[0]).is_none(),
"expected no rerun_session_id on root: got {:?}",
spans[0].attributes,
);
}
#[test]
fn processor_no_session_child_has_no_attribute() {
use opentelemetry::trace::Tracer as _;
let (tracer, exporter, provider) = build_tracer_with_processor();
let parent_cx = ctx_with_state(TraceState::default());
let _attach = parent_cx.attach();
let span = tracer.start("child");
drop(span);
drop(_attach);
provider.force_flush().ok();
let spans = exporter.get_finished_spans().unwrap();
assert_eq!(spans.len(), 1);
assert!(
rerun_session_attr(&spans[0]).is_none(),
"expected no rerun_session_id on child: got {:?}",
spans[0].attributes,
);
}
#[test]
fn processor_active_session_root_sets_attribute() {
use opentelemetry::trace::Tracer as _;
let rt = tokio::runtime::Builder::new_current_thread() .enable_all()
.build()
.unwrap();
let sid = crate::tracing_session::RerunTracingSessionId::parse("rs_cafebabe").unwrap();
let expected = sid.to_string();
let (tracer, exporter, provider) = build_tracer_with_processor();
rt.block_on(crate::tracing_session::scope_session_id_for_test(
Some(sid),
async {
let span = tracer.start("root");
drop(span);
},
));
provider.force_flush().ok();
let spans = exporter.get_finished_spans().unwrap();
assert_eq!(spans.len(), 1);
let attr = rerun_session_attr(&spans[0]).unwrap_or_else(|| {
panic!(
"expected rerun_session_id on root span: got {:?}",
spans[0].attributes,
)
});
assert_eq!(attr, expected);
}
#[test]
fn test_parse_pairs_resilient() {
let result = parse_pairs("my_id=id123,env=prod");
assert_eq!(result.len(), 2);
assert_eq!(result.get("my_id"), Some(&"id123".to_owned()));
assert_eq!(result.get("env"), Some(&"prod".to_owned()));
let result = parse_pairs("valid=ok,invalid,key=,=value,also_valid=good");
assert_eq!(result.len(), 2);
assert_eq!(result.get("valid"), Some(&"ok".to_owned()));
assert_eq!(result.get("also_valid"), Some(&"good".to_owned()));
let result = parse_pairs("");
assert!(result.is_empty());
let result = parse_pairs("invalid,key=,=value");
assert!(result.is_empty());
}
}