Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<SpanRecord>)`. 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`.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions fastrace/src/collector/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ use crate::util::CollectToken;
#[derive(Debug)]
pub enum CollectCommand {
StartCollect(StartCollect),
CancelCollect(CancelCollect),
DropCollect(DropCollect),
CommitCollect(CommitCollect),
SubmitSpans(SubmitSpans),
}

Expand All @@ -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,
}

Expand Down
89 changes: 44 additions & 45 deletions fastrace/src/collector/global_collector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -202,6 +202,7 @@ impl SpanCollection {
struct ActiveCollector {
span_collections: Vec<SpanCollection>,
danglings: HashMap<SpanId, Vec<DanglingItem>>,
canceled: bool,
}

pub(crate) struct GlobalCollector {
Expand All @@ -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<StartCollect>,
cancel_collects: Vec<CancelCollect>,
drop_collects: Vec<DropCollect>,
commit_collects: Vec<CommitCollect>,
submit_spans: Vec<SubmitSpans>,
stale_spans: Vec<SpanCollection>,
}
Expand All @@ -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![],
});
Expand All @@ -262,24 +263,24 @@ 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),
});

// If the reporter is not set, global collectior only clears the channel and then dismiss
// 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;
}
Expand All @@ -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 {
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
);
}
}
}

Expand Down
55 changes: 11 additions & 44 deletions fastrace/src/collector/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -100,62 +96,33 @@ 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<usize>) -> 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
}
}

impl Default for Config {
fn default() -> Self {
Self {
report_interval: Duration::from_secs(1),
tail_sampled: false,
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions fastrace/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions fastrace/src/local/local_span.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use std::borrow::Cow;
use std::cell::RefCell;
use std::fmt;
use std::rc::Rc;

use crate::Event;
Expand All @@ -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.
Expand Down
Loading