diff --git a/AGENTS.md b/AGENTS.md index 01743261..fb14d462 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,17 +13,17 @@ ## Runtime & Data Flow - Applications call `set_reporter` (e.g., `ConsoleReporter`, Datadog/Jaeger/OTel) to spin up a background global collector thread (`collector/global_collector.rs`). It polls per-thread SPSC queues (`util/spsc.rs`) at `Config.report_interval`. -- `Span::root` allocates a collect_id (if sampled) and stores a `CollectToken` describing parent linkage and sampling. Dropping a span records end time and submits a `SpanSet` to the global collector; root spans also emit `CommitCollect`. +- `Span::root` allocates a collect_id (if sampled) and stores a `CollectToken` describing parent linkage and sampling. Dropping a span records end time and submits a `SpanSet` to the global collector; root spans also emit `DropCollect`. - `LocalParentGuard` from `Span::set_local_parent` installs a thread-local `LocalSpanStack`; dropping the guard drains collected `LocalSpan`s and submits them as `SpanSet::LocalSpansInner`. - The collector converts `RawSpan`s into `SpanRecord`s using a single `Anchor` for timestamp to unix conversion. Events and property-only raw entries are re-attached to their parent in `mount_danglings`. -- Tail sampling: `Config.tail_sampled(true)` holds spans until root commit; `Span::cancel()` on root triggers `DropCollect` to discard the trace. +- Tail sampling: spans are held until root commit; `Span::cancel()` on the root triggers `CancelCollect` to discard the trace. ## Key Types & Behaviors - `Span` (`fastrace/src/span.rs`): thread-safe span, can be cross-thread; supports `enter_with_parent(s)`, `enter_with_local_parent`, `add_event`, `add_properties`, `push_child_spans`, `elapsed`, `cancel`. - `LocalSpan` (`local/local_span.rs`): single-thread fast path; requires an active local parent (`Span::set_local_parent` or nested `LocalSpan`). Stack discipline enforced; dropping out of order panics in tests. - `LocalCollector` (`local/local_collector.rs`): start/collect local spans when parent may be set later; returns `LocalSpans` attachable via `Span::push_child_spans` or convertible to `SpanRecord`s without a reporter. - ID types (`collector/id.rs`): `TraceId(u128)`, `SpanId(u64)`; thread-local generator (`SpanId::next_id`) and W3C traceparent encode/decode helpers. `SpanContext` carries trace, parent span id, and `sampled` flag. -- `Config` (`collector/mod.rs`): `report_interval`, `tail_sampled`; note `max_spans_per_trace` deprecated no-op. +- `Config` (`collector/mod.rs`): `report_interval`; note `max_spans_per_trace` deprecated no-op and `tail_sampled` deprecated no-op. - `Reporter` trait (`collector/global_collector.rs`): synchronous `report(Vec)`. TestReporter collects into a shared Vec; ConsoleReporter prints. - Async adapters: `FutureExt::{in_span, enter_on_poll}` (`src/future.rs`); Stream/Sink adapters in `fastrace-futures`. - Proc macro `#[trace]` (`fastrace-macro/src/lib.rs`): wraps sync fns with `LocalSpan`; async fns with `Span::enter_with_local_parent` plus `in_span` by default or `enter_on_poll=true`. Options: `name`, `short_name`, `enter_on_poll`, `properties={k:"fmt"}`, `crate=path`. @@ -52,7 +52,7 @@ - Always call `set_reporter` early; spans emitted before reporter init are dropped. Call `flush()` before shutdown to drain SPSC queues. - `LocalSpan` does nothing without a local parent; `Span::enter_with_local_parent` falls back to noop if no stack token exists. - Default local stack capacity: 4096 span lines and span queue size 10240; overflows drop spans silently (return None). -- Tail sampling only effective when `Config.tail_sampled(true)` and `Span::cancel()` invoked on the root. +- Tail sampling is the default; `Span::cancel()` on the root discards spans collected up to the root's drop (spans submitted after the root finishes may still be reported). - Keep both code paths healthy: when `enable` is off, public APIs stay callable but must remain no-ops without panicking. ## Where to Start diff --git a/CHANGELOG.md b/CHANGELOG.md index ed2de830..d2d08e3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Deprecate `Config::tail_sampled()`; spans are held until the root span finishes by default, and `Span::cancel()` discards spans collected up to the root's drop. + ## v0.7.15 - `#[trace]` macro now supports trait-object futures and preserves input tokens more faithfully to avoid compilation errors. diff --git a/fastrace/src/collector/command.rs b/fastrace/src/collector/command.rs index 0cfdc1bd..ab6dcab1 100644 --- a/fastrace/src/collector/command.rs +++ b/fastrace/src/collector/command.rs @@ -6,8 +6,8 @@ use crate::util::CollectToken; #[derive(Debug)] pub enum CollectCommand { StartCollect(StartCollect), + CancelCollect(CancelCollect), DropCollect(DropCollect), - CommitCollect(CommitCollect), SubmitSpans(SubmitSpans), } @@ -17,12 +17,12 @@ pub struct StartCollect { } #[derive(Debug)] -pub struct DropCollect { +pub struct CancelCollect { pub collect_id: usize, } #[derive(Debug)] -pub struct CommitCollect { +pub struct DropCollect { pub collect_id: usize, } diff --git a/fastrace/src/collector/global_collector.rs b/fastrace/src/collector/global_collector.rs index b8d3f58a..a5e93f4e 100644 --- a/fastrace/src/collector/global_collector.rs +++ b/fastrace/src/collector/global_collector.rs @@ -22,8 +22,8 @@ use crate::collector::SpanId; use crate::collector::SpanRecord; use crate::collector::SpanSet; use crate::collector::TraceId; +use crate::collector::command::CancelCollect; use crate::collector::command::CollectCommand; -use crate::collector::command::CommitCollect; use crate::collector::command::DropCollect; use crate::collector::command::StartCollect; use crate::collector::command::SubmitSpans; @@ -129,8 +129,8 @@ impl GlobalCollect { collect_id } - pub fn commit_collect(&self, collect_id: usize) { - send_command(CollectCommand::CommitCollect(CommitCollect { collect_id })); + pub fn cancel_collect(&self, collect_id: usize) { + send_command(CollectCommand::CancelCollect(CancelCollect { collect_id })); } pub fn drop_collect(&self, collect_id: usize) { @@ -202,6 +202,7 @@ impl SpanCollection { struct ActiveCollector { span_collections: Vec, danglings: HashMap>, + canceled: bool, } pub(crate) struct GlobalCollector { @@ -213,8 +214,8 @@ pub(crate) struct GlobalCollector { // Vectors to be reused by collection loops. They must be empty outside of the // `handle_commands` loop. start_collects: Vec, + cancel_collects: Vec, drop_collects: Vec, - commit_collects: Vec, submit_spans: Vec, stale_spans: Vec, } @@ -237,8 +238,8 @@ impl GlobalCollector { active_collectors: HashMap::new(), start_collects: vec![], + cancel_collects: vec![], drop_collects: vec![], - commit_collects: vec![], submit_spans: vec![], stale_spans: vec![], }); @@ -262,15 +263,15 @@ impl GlobalCollector { fn handle_commands(&mut self) { debug_assert!(self.start_collects.is_empty()); + debug_assert!(self.cancel_collects.is_empty()); debug_assert!(self.drop_collects.is_empty()); - debug_assert!(self.commit_collects.is_empty()); debug_assert!(self.submit_spans.is_empty()); debug_assert!(self.stale_spans.is_empty()); COMMAND_BUS.drain(|cmd| match cmd { CollectCommand::StartCollect(cmd) => self.start_collects.push(cmd), + CollectCommand::CancelCollect(cmd) => self.cancel_collects.push(cmd), CollectCommand::DropCollect(cmd) => self.drop_collects.push(cmd), - CollectCommand::CommitCollect(cmd) => self.commit_collects.push(cmd), CollectCommand::SubmitSpans(cmd) => self.submit_spans.push(cmd), }); @@ -278,8 +279,8 @@ impl GlobalCollector { // all messages. if self.reporter.is_none() { self.start_collects.clear(); + self.cancel_collects.clear(); self.drop_collects.clear(); - self.commit_collects.clear(); self.submit_spans.clear(); return; } @@ -289,8 +290,12 @@ impl GlobalCollector { .insert(collect_id, ActiveCollector::default()); } - for DropCollect { collect_id } in self.drop_collects.drain(..) { - self.active_collectors.remove(&collect_id); + for CancelCollect { collect_id } in self.cancel_collects.drain(..) { + if let Some(active_collector) = self.active_collectors.get_mut(&collect_id) { + active_collector.span_collections.clear(); + active_collector.danglings.clear(); + active_collector.canceled = true; + } } for SubmitSpans { @@ -303,14 +308,16 @@ impl GlobalCollector { if collect_token.len() == 1 { let item = collect_token[0]; if let Some(active_collector) = self.active_collectors.get_mut(&item.collect_id) { - active_collector - .span_collections - .push(SpanCollection::Owned { - spans, - trace_id: item.trace_id, - parent_id: item.parent_id, - }); - } else if !self.config.tail_sampled { + if !active_collector.canceled { + active_collector + .span_collections + .push(SpanCollection::Owned { + spans, + trace_id: item.trace_id, + parent_id: item.parent_id, + }); + } + } else { self.stale_spans.push(SpanCollection::Owned { spans, trace_id: item.trace_id, @@ -322,14 +329,16 @@ impl GlobalCollector { for item in &collect_token { if let Some(active_collector) = self.active_collectors.get_mut(&item.collect_id) { - active_collector - .span_collections - .push(SpanCollection::Shared { - spans: spans.clone(), - trace_id: item.trace_id, - parent_id: item.parent_id, - }); - } else if !self.config.tail_sampled { + if !active_collector.canceled { + active_collector + .span_collections + .push(SpanCollection::Shared { + spans: spans.clone(), + trace_id: item.trace_id, + parent_id: item.parent_id, + }); + } + } else { self.stale_spans.push(SpanCollection::Shared { spans: spans.clone(), trace_id: item.trace_id, @@ -343,26 +352,16 @@ impl GlobalCollector { let anchor = Anchor::new(); let mut committed_records = Vec::new(); - for CommitCollect { collect_id } in self.commit_collects.drain(..) { + for DropCollect { collect_id } in self.drop_collects.drain(..) { if let Some(mut active_collector) = self.active_collectors.remove(&collect_id) { - postprocess_span_collection( - &active_collector.span_collections, - &anchor, - &mut committed_records, - &mut active_collector.danglings, - ); - } - } - - if !self.config.tail_sampled { - for active_collector in self.active_collectors.values_mut() { - postprocess_span_collection( - &active_collector.span_collections, - &anchor, - &mut committed_records, - &mut active_collector.danglings, - ); - active_collector.span_collections.clear(); + if !active_collector.canceled { + postprocess_span_collection( + &active_collector.span_collections, + &anchor, + &mut committed_records, + &mut active_collector.danglings, + ); + } } } diff --git a/fastrace/src/collector/mod.rs b/fastrace/src/collector/mod.rs index 04d5df0f..e98179e7 100644 --- a/fastrace/src/collector/mod.rs +++ b/fastrace/src/collector/mod.rs @@ -77,17 +77,13 @@ pub struct CollectTokenItem { #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct Config { pub(crate) report_interval: Duration, - pub(crate) tail_sampled: bool, } impl Config { - /// Sets the time duration between two reports. The reporter will be invoked when the specified - /// duration elapses, even if no spans have been collected. This allows for batching in the - /// reporter. + /// Sets the maximum interval between two report cycles in the background collector thread. /// - /// In some scenarios, particularly under high load, you may notice spans being lost. This is - /// likely due to the channel being full during the reporting interval. To mitigate this issue, - /// consider reducing the report interval, potentially down to zero, to prevent losing spans. + /// The reporter may be invoked *earlier* than this interval. Do not rely on this value for + /// precise scheduling or batching. /// /// Defaults to 1 second. /// @@ -100,54 +96,26 @@ impl Config { /// fastrace::set_reporter(fastrace::collector::ConsoleReporter, config); /// ``` pub fn report_interval(self, report_interval: Duration) -> Self { - Self { - report_interval, - ..self - } + Self { report_interval } } /// Configures whether to hold spans before the root span finishes. - /// - /// This is useful for tail sampling, where child spans are held and allow the entire trace to - /// be cancelled before the root span finishes. - /// - /// Defaults to `false`. - /// - /// # Examples - /// - /// ``` - /// use fastrace::collector::Config; - /// use fastrace::collector::SpanContext; - /// - /// let config = Config::default().tail_sampled(true); - /// fastrace::set_reporter(fastrace::collector::ConsoleReporter, config); - /// - /// let root = fastrace::Span::root("root", SpanContext::random()); - /// - /// root.cancel(); - /// ``` - pub fn tail_sampled(self, tail_sampled: bool) -> Self { - Self { - tail_sampled, - ..self - } + #[deprecated(since = "0.7.16", note = "This method is now a no-op.")] + pub fn tail_sampled(self, _tail_sampled: bool) -> Self { + self } /// Sets a soft limit for the total number of spans and events in a trace, typically /// used to prevent out-of-memory issues. - /// - /// # Note - /// - /// This is a no-op since it was deprecated. - #[deprecated(since = "0.7.10")] + #[deprecated(since = "0.7.10", note = "This method is now a no-op.")] pub fn max_spans_per_trace(self, _max_spans_per_trace: Option) -> Self { self } /// Configures whether to report spans before the root span finishes. - #[deprecated(since = "0.7.10", note = "Use Config::tail_sampled() instead.")] - pub fn report_before_root_finish(self, report_before_root_finish: bool) -> Self { - self.tail_sampled(!report_before_root_finish) + #[deprecated(since = "0.7.10", note = "This method is now a no-op.")] + pub fn report_before_root_finish(self, _report_before_root_finish: bool) -> Self { + self } } @@ -155,7 +123,6 @@ impl Default for Config { fn default() -> Self { Self { report_interval: Duration::from_secs(1), - tail_sampled: false, } } } diff --git a/fastrace/src/lib.rs b/fastrace/src/lib.rs index 7cdf53b5..3435278d 100644 --- a/fastrace/src/lib.rs +++ b/fastrace/src/lib.rs @@ -303,9 +303,9 @@ //! records to stderr. For more advanced use, crates like `fastrace-jaeger`, `fastrace-datadog`, //! and `fastrace-opentelemetry` are available. //! -//! By default, the reporter is triggered every 500 milliseconds. The reporter can also be -//! triggered manually by calling [`flush()`]. See [`Config`] for customizing the reporting -//! behavior. +//! The reporter runs in a background collector thread. [`Config::report_interval()`] controls the +//! *maximum* interval between report cycles, but the reporter may be invoked earlier. Do not rely +//! on it for precise scheduling or batching. //! //! ``` //! use std::time::Duration; @@ -359,6 +359,7 @@ //! [`Reporter`]: crate::collector::Reporter //! [`ConsoleReporter`]: crate::collector::ConsoleReporter //! [`Config`]: crate::collector::Config +//! [`Config::report_interval()`]: crate::collector::Config::report_interval //! [`Future`]: std::future::Future // Suppress a false-positive lint from clippy diff --git a/fastrace/src/local/local_span.rs b/fastrace/src/local/local_span.rs index cdd9fec6..f56ee04f 100644 --- a/fastrace/src/local/local_span.rs +++ b/fastrace/src/local/local_span.rs @@ -2,6 +2,7 @@ use std::borrow::Cow; use std::cell::RefCell; +use std::fmt; use std::rc::Rc; use crate::Event; @@ -24,6 +25,12 @@ struct LocalSpanInner { span_handle: LocalSpanHandle, } +impl fmt::Debug for LocalSpan { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "LocalSpan") + } +} + impl LocalSpan { /// Create a new child span associated with the current local span in the current thread, and /// then it will become the new local parent. diff --git a/fastrace/src/span.rs b/fastrace/src/span.rs index 2f2e357f..e0736416 100644 --- a/fastrace/src/span.rs +++ b/fastrace/src/span.rs @@ -2,6 +2,7 @@ use std::borrow::Cow; use std::cell::RefCell; +use std::fmt; use std::rc::Rc; use std::sync::Arc; use std::time::Duration; @@ -41,6 +42,12 @@ pub(crate) struct SpanInner { collect: GlobalCollect, } +impl fmt::Debug for Span { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Span") + } +} + impl Span { /// Create a place-holder span that never starts recording. /// @@ -426,9 +433,11 @@ impl Span { /// /// # Note /// - /// To enable this feature, the - /// [`Config::tail_sampled()`](crate::collector::Config::tail_sampled) must be set to - /// `true`. + /// Trace cancellation is best-effort. Spans are held until the root span finishes, so calling + /// `cancel()` on the root span will discard spans already collected (and those collected until + /// the root span is dropped). Spans submitted after the root span finishes may still be + /// reported because there is no longer an active collector to associate them with the + /// cancelled trace. /// /// This method only dismisses the entire trace when called on the root span. /// If called on a non-root span, it will do nothing. @@ -447,7 +456,7 @@ impl Span { #[cfg(feature = "enable")] if let Some(inner) = &self.inner { if let Some(collect_id) = inner.collect_id { - inner.collect.drop_collect(collect_id); + inner.collect.cancel_collect(collect_id); } } } @@ -566,7 +575,7 @@ impl Drop for Span { inner.submit_spans(); if let Some(collect_id) = collect_id { - collect.commit_collect(collect_id); + collect.drop_collect(collect_id); } } } @@ -697,12 +706,11 @@ mod tests { ), ) .return_const(()); - mock.expect_commit_collect() + mock.expect_drop_collect() .times(1) .in_sequence(&mut seq) .with(predicate::eq(42_usize)) .return_const(()); - mock.expect_drop_collect().times(0); let mock = Arc::new(mock); set_mock_collect(mock); @@ -728,7 +736,7 @@ mod tests { .times(1) .in_sequence(&mut seq) .return_const(42_usize); - mock.expect_drop_collect() + mock.expect_cancel_collect() .times(1) .in_sequence(&mut seq) .with(predicate::eq(42_usize)) @@ -741,7 +749,7 @@ mod tests { let span_sets = span_sets.clone(); move |span_set, token| span_sets.lock().unwrap().push((span_set, token)) }); - mock.expect_commit_collect() + mock.expect_drop_collect() .times(1) .in_sequence(&mut seq) .with(predicate::eq(42_usize)) @@ -802,12 +810,11 @@ root [] let span_sets = span_sets.clone(); move |span_set, token| span_sets.lock().unwrap().push((span_set, token)) }); - mock.expect_commit_collect() + mock.expect_drop_collect() .times(1) .in_sequence(&mut seq) .with(predicate::eq(42_usize)) .return_const(()); - mock.expect_drop_collect().times(0); let mock = Arc::new(mock); set_mock_collect(mock); @@ -883,11 +890,10 @@ root [] let span_sets = span_sets.clone(); move |span_set, token| span_sets.lock().unwrap().push((span_set, token)) }); - mock.expect_commit_collect() + mock.expect_drop_collect() .times(5) .with(predicate::in_iter([1_usize, 2, 3, 4, 5])) .return_const(()); - mock.expect_drop_collect().times(0); let mock = Arc::new(mock); set_mock_collect(mock); @@ -976,11 +982,10 @@ parent5 [] let span_sets = span_sets.clone(); move |span_set, token| span_sets.lock().unwrap().push((span_set, token)) }); - mock.expect_commit_collect() + mock.expect_drop_collect() .times(5) .with(predicate::in_iter([1_usize, 2, 3, 4, 5])) .return_const(()); - mock.expect_drop_collect().times(0); let mock = Arc::new(mock); set_mock_collect(mock); @@ -1051,12 +1056,11 @@ parent5 [] let span_sets = span_sets.clone(); move |span_set, token| span_sets.lock().unwrap().push((span_set, token)) }); - mock.expect_commit_collect() + mock.expect_drop_collect() .times(1) .in_sequence(&mut seq) .with(predicate::eq(42_usize)) .return_const(()); - mock.expect_drop_collect().times(0); let mock = Arc::new(mock); set_mock_collect(mock); diff --git a/fastrace/src/util/command_bus.rs b/fastrace/src/util/command_bus.rs index b90e9306..90ffe98c 100644 --- a/fastrace/src/util/command_bus.rs +++ b/fastrace/src/util/command_bus.rs @@ -1,4 +1,16 @@ -// Copyright 2020 TiKV Project Authors. Licensed under Apache-2.0. +// Copyright 2025 FastLabs Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. use std::time::Duration; @@ -110,12 +122,6 @@ struct NotifySender { impl NotifySender { fn notify(&self) { #[cfg(not(target_family = "wasm"))] - { - self.notify_tx.try_send(()).ok(); - } - #[cfg(target_family = "wasm")] - { - Ok(()) - } + self.notify_tx.try_send(()).ok(); } } diff --git a/fastrace/src/util/spsc.rs b/fastrace/src/util/spsc.rs index 7ff57d90..abf668ef 100644 --- a/fastrace/src/util/spsc.rs +++ b/fastrace/src/util/spsc.rs @@ -12,7 +12,7 @@ pub fn bounded(capacity: usize) -> (Sender, Receiver) { ( Sender { tx, - pending_messages: VecDeque::new(), + pending: VecDeque::new(), }, Receiver { rx }, ) @@ -20,7 +20,7 @@ pub fn bounded(capacity: usize) -> (Sender, Receiver) { pub struct Sender { tx: Producer, - pending_messages: VecDeque, + pending: VecDeque, } pub struct Receiver { @@ -38,22 +38,23 @@ impl Sender { } pub fn send(&mut self, value: T) { - while let Some(value) = self.pending_messages.pop_front() { - if let Err(PushError::Full(value)) = self.tx.push(value) { - self.pending_messages.push_front(value); - break; + while let Some(pending_value) = self.pending.pop_front() { + if let Err(PushError::Full(pending_value)) = self.tx.push(pending_value) { + self.pending.push_front(pending_value); + self.pending.push_back(value); + return; } } if let Err(PushError::Full(value)) = self.tx.push(value) { - self.pending_messages.push_back(value); + self.pending.push_back(value); } } } impl Drop for Sender { fn drop(&mut self) { - for command in self.pending_messages.drain(..) { + for command in self.pending.drain(..) { drop(self.tx.push(command)); } } diff --git a/fastrace/tests/lib.rs b/fastrace/tests/lib.rs index 7b0ae06a..393ab589 100644 --- a/fastrace/tests/lib.rs +++ b/fastrace/tests/lib.rs @@ -318,7 +318,7 @@ fn multiple_threads_multiple_spans() { #[serial] fn multiple_spans_without_local_spans() { let (reporter, collected_spans) = TestReporter::new(); - fastrace::set_reporter(reporter, Config::default().tail_sampled(true)); + fastrace::set_reporter(reporter, Config::default()); { let root1 = Span::root("root1", SpanContext::new(TraceId(12), SpanId::default())); diff --git a/tests/statically-disable/src/main.rs b/tests/statically-disable/src/main.rs index 2c005f90..0f925136 100644 --- a/tests/statically-disable/src/main.rs +++ b/tests/statically-disable/src/main.rs @@ -36,9 +36,7 @@ fn main() { fastrace::set_reporter( ConsoleReporter, - Config::default() - .report_interval(Duration::from_millis(10)) - .tail_sampled(false), + Config::default().report_interval(Duration::from_millis(10)), ); let root = Span::root("root", SpanContext::new(TraceId(0), SpanId(0)))