use std::collections::HashMap;
use std::future::Future;
use opentelemetry::baggage::BaggageExt;
use opentelemetry::context::{FutureExt, WithContext};
use opentelemetry::propagation::TextMapPropagator;
use opentelemetry::{Context, KeyValue};
use opentelemetry_sdk::propagation::TraceContextPropagator;
use std::collections::BTreeMap;
use kaish_types::{ExecResult, ExecuteOptions};
pub(crate) fn extract_parent(opts: &ExecuteOptions) -> Option<Context> {
if opts.traceparent.is_none() && opts.baggage.is_empty() {
return None;
}
let mut cx = Context::new();
if let Some(traceparent) = &opts.traceparent {
let mut carrier: HashMap<String, String> = HashMap::new();
carrier.insert("traceparent".to_string(), traceparent.clone());
if let Some(tracestate) = &opts.tracestate {
carrier.insert("tracestate".to_string(), tracestate.clone());
}
cx = TraceContextPropagator::new().extract_with_context(&cx, &carrier);
}
if !opts.baggage.is_empty() {
cx = cx.with_baggage(
opts.baggage
.iter()
.map(|(k, v)| KeyValue::new(k.clone(), v.clone())),
);
}
Some(cx)
}
pub(crate) fn merge_egress_baggage(result: &mut ExecResult, embedder: BTreeMap<String, String>) {
for (key, value) in embedder {
result.baggage.entry(key).or_insert(value);
}
}
pub(crate) fn bind_current_context<F: Future>(fut: F) -> WithContext<F> {
fut.with_context(fork_parent_context())
}
fn fork_parent_context() -> Context {
use opentelemetry::trace::TraceContextExt;
use tracing_opentelemetry::OpenTelemetrySpanExt;
let span_cx = tracing::Span::current().context();
if span_cx.span().span_context().is_valid() {
span_cx
} else {
Context::current()
}
}
#[cfg(test)]
mod tests {
use super::*;
use opentelemetry::trace::TraceContextExt;
const TRACEPARENT: &str = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
const TRACE_ID: &str = "4bf92f3577b34da6a3ce929d0e0e4736";
const SPAN_ID: &str = "00f067aa0ba902b7";
#[test]
fn no_trace_context_yields_none() {
assert!(extract_parent(&ExecuteOptions::new()).is_none());
}
#[test]
fn traceparent_becomes_remote_parent() {
let opts = ExecuteOptions::new().with_traceparent(TRACEPARENT);
let cx = extract_parent(&opts).expect("traceparent should yield a context");
let sc = cx.span().span_context().clone();
assert_eq!(sc.trace_id().to_string(), TRACE_ID);
assert_eq!(sc.span_id().to_string(), SPAN_ID);
assert!(sc.is_sampled(), "01 flag should mark the parent sampled");
assert!(sc.is_remote(), "an extracted parent must be marked remote");
}
#[test]
fn tracestate_rides_with_traceparent() {
let opts = ExecuteOptions::new()
.with_traceparent(TRACEPARENT)
.with_tracestate("vendor=opaque");
let cx = extract_parent(&opts).expect("context");
let header = cx.span().span_context().trace_state().header();
assert_eq!(header, "vendor=opaque");
}
#[test]
fn tracestate_without_traceparent_is_dropped() {
let opts = ExecuteOptions::new().with_tracestate("vendor=opaque");
assert!(extract_parent(&opts).is_none());
}
#[test]
fn baggage_is_carried() {
let opts = ExecuteOptions::new()
.with_baggage_entry("owner", "atobey")
.with_baggage_entry("conn", "embedded-1");
let cx = extract_parent(&opts).expect("baggage should yield a context");
assert_eq!(cx.baggage().get("owner").map(|v| v.as_str()), Some("atobey"));
assert_eq!(
cx.baggage().get("conn").map(|v| v.as_str()),
Some("embedded-1")
);
}
#[test]
fn baggage_without_trace_still_produces_context() {
let opts = ExecuteOptions::new().with_baggage_entry("tenant", "acme");
let cx = extract_parent(&opts).expect("context");
assert!(!cx.span().span_context().is_valid());
assert_eq!(cx.baggage().get("tenant").map(|v| v.as_str()), Some("acme"));
}
#[test]
fn traceparent_and_baggage_coexist() {
let opts = ExecuteOptions::new()
.with_traceparent(TRACEPARENT)
.with_baggage_entry("owner", "atobey");
let cx = extract_parent(&opts).expect("context");
assert_eq!(cx.span().span_context().trace_id().to_string(), TRACE_ID);
assert_eq!(cx.baggage().get("owner").map(|v| v.as_str()), Some("atobey"));
}
fn embedder_baggage(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn egress_echoes_embedder_baggage_when_no_tool_set_it() {
let mut result = ExecResult::success("ok");
merge_egress_baggage(&mut result, embedder_baggage(&[("owner", "atobey")]));
assert_eq!(result.baggage.get("owner").map(String::as_str), Some("atobey"));
}
#[test]
fn egress_tool_emitted_baggage_wins_on_collision() {
let mut result = ExecResult::success("ok");
result.baggage.insert("owner".to_string(), "tool-value".to_string());
merge_egress_baggage(
&mut result,
embedder_baggage(&[("owner", "embedder-value"), ("tenant", "acme")]),
);
assert_eq!(
result.baggage.get("owner").map(String::as_str),
Some("tool-value"),
"tool-emitted baggage must win on key collision",
);
assert_eq!(
result.baggage.get("tenant").map(String::as_str),
Some("acme"),
"non-colliding embedder keys are still echoed back",
);
}
#[test]
fn egress_empty_embedder_baggage_is_a_noop() {
let mut result = ExecResult::success("ok");
result.baggage.insert("owner".to_string(), "tool-value".to_string());
merge_egress_baggage(&mut result, BTreeMap::new());
assert_eq!(result.baggage.len(), 1);
assert_eq!(result.baggage.get("owner").map(String::as_str), Some("tool-value"));
}
}