From f72f038b7fc25949299698dfca4bd30e41d833bc Mon Sep 17 00:00:00 2001 From: Mar Witek Date: Wed, 17 Jun 2026 13:05:58 +0200 Subject: [PATCH 1/2] Introduce routing metadata as a span property Introduces RoutingMetadata as a span property describing where a span's data should be routed. It's set once via StartSpanOptions at span creation, inherited by child spans, and carried through into the emitted FinishedSpan so exporters can act on it. --- src/lib.rs | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++- src/span.rs | 47 ++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 99d82ba..d0122a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,7 +71,7 @@ pub type Result = std::result::Result; mod tests { use super::*; use crate::sampler::AllSampler; - use crate::span::{FinishedSpan, Span}; + use crate::span::{FinishedSpan, RoutingMetadata, Span}; use crate::tag::{StdTag, Tag, TagValue}; use std::sync::atomic::{AtomicI64, Ordering}; use std::thread; @@ -174,4 +174,80 @@ mod tests { fn trait_check() {} trait_check::>(); } + + #[test] + fn routing_inherited_by_children() { + let routing = RoutingMetadata { + zone_id: 7, + account_id: 99, + account_tag: "ws".to_string(), + destinations: vec!["d1".to_string()], + persist: true, + }; + + let (tracer, mut span_rx) = Tracer::new(AllSampler); + { + // Routing is set once, at construction, on the root only. + let parent = tracer + .span("parent") + .routing(routing.clone()) + .start_with_state(()); + let child = parent.child("child", |s| s.start_with_state(())); + let grandchild = child.child("grandchild", |s| s.start_with_state(())); + + drop(grandchild); + drop(child); + drop(parent); + } + + // Every span in the tree carries the same routing, without it being + // passed to each child. + for expected in ["grandchild", "child", "parent"] { + let span = span_rx.try_recv().unwrap(); + assert_eq!(span.operation_name(), expected); + + let r = span + .routing() + .unwrap_or_else(|| panic!("{expected} should inherit routing")); + assert_eq!(r.zone_id, 7); + assert_eq!(r.account_id, 99); + assert_eq!(r.account_tag, "ws"); + assert_eq!(r.destinations, vec!["d1".to_string()]); + assert!(r.persist); + } + } + + #[test] + fn routing_last_set_wins() { + let (tracer, mut span_rx) = Tracer::new(AllSampler); + { + // If the builder is called more than once, the last value wins. + let span = tracer + .span("span") + .routing(RoutingMetadata { + zone_id: 1, + account_id: 1, + account_tag: "first".to_string(), + destinations: vec![], + persist: false, + }) + .routing(RoutingMetadata { + zone_id: 2, + account_id: 2, + account_tag: "second".to_string(), + destinations: vec!["d".to_string()], + persist: true, + }) + .start_with_state(()); + drop(span); + } + + let span = span_rx.try_recv().unwrap(); + let r = span.routing().expect("routing should be set"); + assert_eq!(r.zone_id, 2); + assert_eq!(r.account_id, 2); + assert_eq!(r.account_tag, "second"); + assert_eq!(r.destinations, vec!["d".to_string()]); + assert!(r.persist); + } } diff --git a/src/span.rs b/src/span.rs index b269856..c7b36aa 100644 --- a/src/span.rs +++ b/src/span.rs @@ -106,6 +106,28 @@ impl) + Send + Sync + 'static> From for FinishSpanCallb } } +/// Routing metadata attached to a span. +/// +/// Set once at span construction via [`StartSpanOptions::routing`] and inherited +/// by child spans, then carried into the resulting [`FinishedSpan`], where an +/// exporter can read it to route the span to the correct destination. +/// +/// There is no live setter: routing cannot be changed or cleared after a span +/// has started, so it cannot be accidentally dropped by later span mutations. +#[derive(Clone, Debug)] +pub struct RoutingMetadata { + /// The zone the traced request belongs to. + pub zone_id: u64, + /// The account that owns the zone. + pub account_id: u64, + /// The account tag the traced request belongs to. + pub account_tag: String, + /// The destinations the trace should be forwarded to. + pub destinations: Vec, + /// Whether the trace is persisted to the managed (ClickHouse) store. + pub persist: bool, +} + /// Span. /// /// When this span is dropped, it will be converted to `FinishedSpan` and @@ -293,6 +315,9 @@ impl Span { if let Some(finish_cb) = self.0.as_ref().and_then(|s| s.finish_cb.clone()) { opts = opts.finish_callback(finish_cb); } + if let Some(routing) = self.0.as_ref().and_then(|s| s.routing.clone()) { + opts = opts.routing(routing); + } f(opts) }) } @@ -319,6 +344,7 @@ impl Span { context, finish_cb: opts.finish_cb, span_tx: opts.span_tx.clone(), + routing: opts.routing, }; Span(Some(inner)) } @@ -338,6 +364,7 @@ impl Drop for Span { tags: inner.tags, logs: inner.logs, context: inner.context, + routing: inner.routing, }; inner.span_tx.0.consume_span(finished); } @@ -360,6 +387,7 @@ struct SpanInner { context: SpanContext, finish_cb: Option>, span_tx: SharedSpanConsumer, + routing: Option, } /// Finished span. @@ -372,6 +400,7 @@ pub struct FinishedSpan { tags: Vec, logs: Vec, context: SpanContext, + routing: Option, } impl FinishedSpan { /// Returns the operation name of this span. @@ -408,6 +437,11 @@ impl FinishedSpan { pub fn context(&self) -> &SpanContext { &self.context } + + /// Returns the routing metadata of this span, if any was set. + pub fn routing(&self) -> Option<&RoutingMetadata> { + self.routing.as_ref() + } } /// Span context. @@ -601,6 +635,7 @@ pub struct StartSpanOptions<'a, S: 'a, T: 'a> { references: Vec>, baggage_items: Vec, finish_cb: Option>, + routing: Option, span_tx: &'a SharedSpanConsumer, sampler: &'a S, } @@ -629,6 +664,17 @@ where self } + /// Attaches routing metadata to the span being built. + /// + /// The metadata is carried into the resulting [`FinishedSpan`] for the + /// exporter to read, and is inherited by child spans (see [`Span::child`]). + /// This is the only way to set routing — there is no live setter, so routing + /// cannot be changed or cleared after the span has started. + pub fn routing(mut self, routing: RoutingMetadata) -> Self { + self.routing = Some(routing); + self + } + /// Adds the `ChildOf` reference to this span. pub fn child_of(mut self, context: &C) -> Self where @@ -696,6 +742,7 @@ where references: Vec::new(), baggage_items: Vec::new(), finish_cb: None, + routing: None, span_tx, sampler, } From e32cfd6625b86f95c68f83d615def2dc6f6734a0 Mon Sep 17 00:00:00 2001 From: Mar Witek Date: Mon, 6 Jul 2026 15:23:17 -0500 Subject: [PATCH 2/2] Generalize RoutingMetadata into an object-safe trait --- src/lib.rs | 62 ++++++++++++++++++++++++++++++----------------------- src/span.rs | 41 +++++++++++++---------------------- 2 files changed, 50 insertions(+), 53 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d0122a8..1b07de8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,9 +74,26 @@ mod tests { use crate::span::{FinishedSpan, RoutingMetadata, Span}; use crate::tag::{StdTag, Tag, TagValue}; use std::sync::atomic::{AtomicI64, Ordering}; + use std::sync::Arc; use std::thread; use std::time::Duration; + #[derive(Debug)] + struct TestRouting { + zone_id: u64, + account_id: u64, + } + + impl RoutingMetadata for TestRouting { + fn group_key(&self) -> String { + format!("{}|{}", self.zone_id, self.account_id) + } + + fn encode(&self) -> String { + format!("zone={};account={}", self.zone_id, self.account_id) + } + } + #[tokio::test] async fn it_works() { let (tracer, mut span_rx) = Tracer::new(AllSampler); @@ -177,13 +194,12 @@ mod tests { #[test] fn routing_inherited_by_children() { - let routing = RoutingMetadata { + let routing = Arc::new(TestRouting { zone_id: 7, account_id: 99, - account_tag: "ws".to_string(), - destinations: vec!["d1".to_string()], - persist: true, - }; + }); + let expected_group_key = routing.group_key(); + let expected_encoded = routing.encode(); let (tracer, mut span_rx) = Tracer::new(AllSampler); { @@ -209,45 +225,37 @@ mod tests { let r = span .routing() .unwrap_or_else(|| panic!("{expected} should inherit routing")); - assert_eq!(r.zone_id, 7); - assert_eq!(r.account_id, 99); - assert_eq!(r.account_tag, "ws"); - assert_eq!(r.destinations, vec!["d1".to_string()]); - assert!(r.persist); + assert_eq!(r.group_key(), expected_group_key); + assert_eq!(r.encode(), expected_encoded); } } #[test] fn routing_last_set_wins() { + let second = Arc::new(TestRouting { + zone_id: 2, + account_id: 2, + }); + let expected_group_key = second.group_key(); + let expected_encoded = second.encode(); + let (tracer, mut span_rx) = Tracer::new(AllSampler); { // If the builder is called more than once, the last value wins. let span = tracer .span("span") - .routing(RoutingMetadata { + .routing(Arc::new(TestRouting { zone_id: 1, account_id: 1, - account_tag: "first".to_string(), - destinations: vec![], - persist: false, - }) - .routing(RoutingMetadata { - zone_id: 2, - account_id: 2, - account_tag: "second".to_string(), - destinations: vec!["d".to_string()], - persist: true, - }) + })) + .routing(second.clone()) .start_with_state(()); drop(span); } let span = span_rx.try_recv().unwrap(); let r = span.routing().expect("routing should be set"); - assert_eq!(r.zone_id, 2); - assert_eq!(r.account_id, 2); - assert_eq!(r.account_tag, "second"); - assert_eq!(r.destinations, vec!["d".to_string()]); - assert!(r.persist); + assert_eq!(r.group_key(), expected_group_key); + assert_eq!(r.encode(), expected_encoded); } } diff --git a/src/span.rs b/src/span.rs index c7b36aa..eabe7c7 100644 --- a/src/span.rs +++ b/src/span.rs @@ -106,26 +106,15 @@ impl) + Send + Sync + 'static> From for FinishSpanCallb } } -/// Routing metadata attached to a span. -/// -/// Set once at span construction via [`StartSpanOptions::routing`] and inherited -/// by child spans, then carried into the resulting [`FinishedSpan`], where an -/// exporter can read it to route the span to the correct destination. -/// -/// There is no live setter: routing cannot be changed or cleared after a span -/// has started, so it cannot be accidentally dropped by later span mutations. -#[derive(Clone, Debug)] -pub struct RoutingMetadata { - /// The zone the traced request belongs to. - pub zone_id: u64, - /// The account that owns the zone. - pub account_id: u64, - /// The account tag the traced request belongs to. - pub account_tag: String, - /// The destinations the trace should be forwarded to. - pub destinations: Vec, - /// Whether the trace is persisted to the managed (ClickHouse) store. - pub persist: bool, +/// Caller-defined routing attached to a span, inherited by children, and read by +/// the exporter to route the resulting [`FinishedSpan`]. +pub trait RoutingMetadata: fmt::Debug + Send + Sync { + /// Batching key: spans sharing it are exported together. Must differ + /// whenever [`encode`](RoutingMetadata::encode) would. + fn group_key(&self) -> String; + + /// The value the exporter transmits for this routing. + fn encode(&self) -> String; } /// Span. @@ -387,7 +376,7 @@ struct SpanInner { context: SpanContext, finish_cb: Option>, span_tx: SharedSpanConsumer, - routing: Option, + routing: Option>, } /// Finished span. @@ -400,7 +389,7 @@ pub struct FinishedSpan { tags: Vec, logs: Vec, context: SpanContext, - routing: Option, + routing: Option>, } impl FinishedSpan { /// Returns the operation name of this span. @@ -439,8 +428,8 @@ impl FinishedSpan { } /// Returns the routing metadata of this span, if any was set. - pub fn routing(&self) -> Option<&RoutingMetadata> { - self.routing.as_ref() + pub fn routing(&self) -> Option<&dyn RoutingMetadata> { + self.routing.as_deref() } } @@ -635,7 +624,7 @@ pub struct StartSpanOptions<'a, S: 'a, T: 'a> { references: Vec>, baggage_items: Vec, finish_cb: Option>, - routing: Option, + routing: Option>, span_tx: &'a SharedSpanConsumer, sampler: &'a S, } @@ -670,7 +659,7 @@ where /// exporter to read, and is inherited by child spans (see [`Span::child`]). /// This is the only way to set routing — there is no live setter, so routing /// cannot be changed or cleared after the span has started. - pub fn routing(mut self, routing: RoutingMetadata) -> Self { + pub fn routing(mut self, routing: Arc) -> Self { self.routing = Some(routing); self }