use std::future::Future;
use opentelemetry::trace::TraceContextExt;
use tracing_opentelemetry::OpenTelemetrySpanExt;
use wyrd_spec::trace::TraceContext;
tokio::task_local! {
static CALLER: TraceContext;
static PRODUCER: Box<dyn Fn(&str) -> Option<TraceContext>>;
}
pub(crate) const TRACEPARENT: &str = "traceparent";
pub(crate) const TRACESTATE: &str = "tracestate";
pub async fn with_caller_trace<F: Future>(context: Option<TraceContext>, future: F) -> F::Output {
match context {
Some(context) => CALLER.scope(context, future).await,
None => future.await,
}
}
pub fn with_producer_span<R>(
open: impl Fn(&str) -> Option<TraceContext> + 'static,
call: impl FnOnce() -> R,
) -> R {
PRODUCER.sync_scope(Box::new(open), call)
}
pub(crate) fn outbound() -> Option<TraceContext> {
CALLER
.try_with(Clone::clone)
.ok()
.or_else(current_span_context)
}
pub(crate) fn producer(table: &str) -> Option<TraceContext> {
if let Ok(opened) = PRODUCER.try_with(|open| open(table)) {
return opened;
}
CALLER.try_with(Clone::clone).ok().or_else(|| {
tracing::info_span!(
"bifrost.enqueue",
otel.kind = "producer",
messaging.system = "bifrost",
messaging.operation.type = "create",
messaging.destination.name = table,
)
.in_scope(current_span_context)
})
}
pub(crate) fn headers() -> Vec<(&'static str, String)> {
outbound().map_or_else(Vec::new, |context| {
let mut headers = vec![(TRACEPARENT, context.to_traceparent())];
if !context.trace_state.is_empty() {
headers.push((TRACESTATE, context.trace_state));
}
headers
})
}
fn current_span_context() -> Option<TraceContext> {
let context = tracing::Span::current().context();
let span = context.span();
let span_context = span.span_context();
span_context.is_valid().then(|| TraceContext {
trace_id: span_context.trace_id().to_string(),
span_id: span_context.span_id().to_string(),
trace_flags: span_context.trace_flags().to_u8(),
trace_state: span_context.trace_state().header(),
})
}
#[cfg(test)]
mod tests {
use wyrd_spec::trace::TraceContext;
use super::{outbound, producer, with_caller_trace, with_producer_span};
const PARENT: &str = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01";
#[test]
fn no_active_context_sends_none() {
assert_eq!(outbound(), None);
}
#[tokio::test]
async fn scoped_caller_context_is_outbound() {
let context = TraceContext::parse_traceparent(PARENT, "wyrd=journey".to_owned())
.expect("valid traceparent");
let sent = with_caller_trace(Some(context.clone()), async { outbound() }).await;
assert_eq!(sent, Some(context));
assert_eq!(with_caller_trace(None, async { outbound() }).await, None);
}
#[tokio::test]
async fn scoped_caller_context_is_the_producer() {
let context =
TraceContext::parse_traceparent(PARENT, String::new()).expect("valid traceparent");
let produced = with_caller_trace(Some(context.clone()), async { producer("t") }).await;
assert_eq!(produced, Some(context));
}
#[tokio::test]
async fn scoped_span_opener_is_the_producer() {
let caller =
TraceContext::parse_traceparent(PARENT, String::new()).expect("valid traceparent");
let opened = TraceContext {
span_id: "00f067aa0ba902b7".to_owned(),
..caller.clone()
};
let expected = opened.clone();
let produced = with_caller_trace(Some(caller), async move {
with_producer_span(
move |table| (table == "t").then(|| opened.clone()),
|| producer("t"),
)
})
.await;
assert_eq!(produced, Some(expected));
}
#[test]
fn producer_span_continues_the_current_trace() {
use opentelemetry::trace::TracerProvider as _;
use tracing_subscriber::layer::SubscriberExt as _;
let traced = tracing_subscriber::registry().with(
tracing_opentelemetry::layer().with_tracer(
opentelemetry_sdk::trace::SdkTracerProvider::builder()
.build()
.tracer("producer"),
),
);
tracing::subscriber::with_default(traced, || {
tracing::info_span!("caller").in_scope(|| {
let caller = outbound().expect("the caller span has a context");
let produced = producer("t").expect("the PRODUCER span has a context");
assert_eq!(produced.trace_id, caller.trace_id);
assert_ne!(produced.span_id, caller.span_id);
});
});
}
}