From c2fa1acb029dc9c95f9136cab1542ad405b6b545 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 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spans gain an optional routing property, set at construction and inherited by children. `RoutingMetadata` is a trait — `group_key` (groups spans for batching) and `encode` (produces the wire value) — so consumers define their own routing type; spans store it as `Option>`. --- src/lib.rs | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++- src/span.rs | 36 ++++++++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 99d82ba..1b07de8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,12 +71,29 @@ 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::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); @@ -174,4 +191,71 @@ mod tests { fn trait_check() {} trait_check::>(); } + + #[test] + fn routing_inherited_by_children() { + let routing = Arc::new(TestRouting { + zone_id: 7, + account_id: 99, + }); + let expected_group_key = routing.group_key(); + let expected_encoded = routing.encode(); + + 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.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(Arc::new(TestRouting { + zone_id: 1, + account_id: 1, + })) + .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.group_key(), expected_group_key); + assert_eq!(r.encode(), expected_encoded); + } } diff --git a/src/span.rs b/src/span.rs index b269856..eabe7c7 100644 --- a/src/span.rs +++ b/src/span.rs @@ -106,6 +106,17 @@ impl) + Send + Sync + 'static> From for FinishSpanCallb } } +/// 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. /// /// When this span is dropped, it will be converted to `FinishedSpan` and @@ -293,6 +304,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 +333,7 @@ impl Span { context, finish_cb: opts.finish_cb, span_tx: opts.span_tx.clone(), + routing: opts.routing, }; Span(Some(inner)) } @@ -338,6 +353,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 +376,7 @@ struct SpanInner { context: SpanContext, finish_cb: Option>, span_tx: SharedSpanConsumer, + routing: Option>, } /// Finished span. @@ -372,6 +389,7 @@ pub struct FinishedSpan { tags: Vec, logs: Vec, context: SpanContext, + routing: Option>, } impl FinishedSpan { /// Returns the operation name of this span. @@ -408,6 +426,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<&dyn RoutingMetadata> { + self.routing.as_deref() + } } /// Span context. @@ -601,6 +624,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 +653,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: Arc) -> 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 +731,7 @@ where references: Vec::new(), baggage_items: Vec::new(), finish_cb: None, + routing: None, span_tx, sampler, } From dc14e3183b95b6abce9055493d5726f9100dc587 Mon Sep 17 00:00:00 2001 From: Mar Witek Date: Sun, 12 Jul 2026 23:53:11 -0500 Subject: [PATCH 2/2] Document routing metadata inheritance in Span::child --- src/span.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/span.rs b/src/span.rs index eabe7c7..7ebff24 100644 --- a/src/span.rs +++ b/src/span.rs @@ -292,8 +292,9 @@ impl Span { /// Starts a `ChildOf` span if this span is sampled. /// - /// The child will inherit this span's finish callback, if it has one. To avoid - /// this kind of inheritance, you can use `span.handle().child(...)` instead. + /// The child will inherit this span's finish callback and routing metadata, + /// if set. To avoid this kind of inheritance, you can use + /// `span.handle().child(...)` instead. pub fn child(&self, operation_name: N, f: F) -> Span where N: Into>,