Loading pageā¦
Rust walkthroughs
Loading pageā¦
tracing::Subscriber::event_enabled and enabled for early filtering?tracing::Subscriber::event_enabled and enabled are two levels of the filtering API that allow subscribers to control which traces are recorded at different granularities. The enabled method filters at the span level, deciding whether a span should be constructed based on its Metadata, while event_enabled filters at the event level, deciding whether an individual event within a span should be recorded. The trade-off lies in precision versus performance: enabled is coarser and called earlier (potentially before entering a span), while event_enabled is finer-grained but called more frequently during span execution. A subscriber implementing only enabled can make early decisions to skip entire spans, avoiding the overhead of span construction, but cannot differentiate between events within a span. A subscriber implementing event_enabled can selectively enable specific events within a span, but only after the span has been entered, meaning the span construction overhead has already been incurred. The default implementation of event_enabled delegates to enabled, so subscribers that don't need per-event filtering can implement only enabled.
use tracing::{Level, Subscriber, Metadata};
use tracing_subscriber::layer::Context;
use std::sync::atomic::{AtomicUsize, Ordering};
struct FilteredSubscriber {
events_received: AtomicUsize,
spans_entered: AtomicUsize,
}
impl Subscriber for FilteredSubscriber {
// Called when a span is created - decide if we care about this span
fn enabled(&self, metadata: &Metadata<'_>) -> bool {
// Only enable spans at INFO level or higher
metadata.level() <= &Level::INFO
}
// Called when an event is recorded - decide if we want this event
fn event_enabled(&self, metadata: &Metadata<'_>) -> bool {
// Default implementation delegates to enabled()
// Override to filter events differently from spans
metadata.level() <= &Level::DEBUG
}
fn new_span(&self, _attrs: &tracing::span::Attributes<'_>) -> tracing::Id {
self.spans_entered.fetch_add(1, Ordering::Relaxed);
tracing::Id::from_u64(1)
}
fn event(&self, _event: &tracing::Event<'_>) {
self.events_received.fetch_add(1, Ordering::Relaxed);
}
fn record(&self, _span: &tracing::Id, _values: &tracing::span::Values<'_>) {}
fn record_follows_from(&self, _span: &tracing::Id, _follows: &tracing::Id) {}
fn enter(&self, _span: &tracing::Id) {}
fn exit(&self, _span: &tracing::Id) {}
}enabled filters spans; event_enabled filters events within spans.
use tracing::{info, debug, trace, span, Level};
fn main() {
let subscriber = FilteredSubscriber {
events_received: AtomicUsize::new(0),
spans_entered: AtomicUsize::new(0),
};
tracing::subscriber::set_global_default(subscriber).unwrap();
// enabled() is called first for the span
let span = span!(Level::DEBUG, "debug_span");
// If enabled() returns false, the span isn't constructed
// and event_enabled() is never called for events inside
let _enter = span.enter();
// event_enabled() is called for each event
debug!("debug event");
trace!("trace event"); // Filtered by event_enabled
// If the span was filtered by enabled(), neither event
// would reach event_enabled()
}enabled gates span creation; event_enabled gates individual events.
use tracing::{span, debug, trace, Level};
fn expensive_operation() -> String {
// Simulate expensive computation
std::thread::sleep(std::time::Duration::from_millis(10));
"result".to_string()
}
fn main() {
// Scenario 1: filtered by enabled()
// If enabled() returns false for TRACE spans:
// - The span is never constructed
// - No overhead for span creation
// - No overhead for events inside the span
{
let _span = span!(Level::TRACE, "expensive_span");
let _enter = _span.enter();
// None of this runs if enabled() returned false
let result = expensive_operation();
trace!("result: {}", result);
}
// Scenario 2: enabled() returns true, event_enabled() filters
// - The span IS constructed
// - Span fields are recorded
// - But events can still be filtered
{
let _span = span!(Level::DEBUG, "span_with_trace_events");
let _enter = _span.enter();
debug!("this event passes event_enabled");
trace!("this event is filtered by event_enabled");
}
}enabled prevents overhead earlier; event_enabled allows more precision.
use tracing::Subscriber;
// The default implementation in tracing
trait EventEnabledDefault: Subscriber {
fn event_enabled(&self, metadata: &tracing::Metadata<'_>) -> bool {
// By default, event_enabled delegates to enabled
// This means if you only implement enabled, events use the same filter
self.enabled(metadata)
}
}
// Implementing only enabled is sufficient for simple filtering
struct SimpleFilter;
impl Subscriber for SimpleFilter {
fn enabled(&self, metadata: &tracing::Metadata<'_>) -> bool {
metadata.level() <= &Level::INFO
}
// event_enabled not needed - defaults to calling enabled()
fn new_span(&self, _attrs: &tracing::span::Attributes<'_>) -> tracing::Id {
tracing::Id::from_u64(1)
}
fn event(&self, _event: &tracing::Event<'_>) {}
fn record(&self, _span: &tracing::Id, _values: &tracing::span::Values<'_>) {}
fn record_follows_from(&self, _span: &tracing::Id, _follows: &tracing::Id) {}
fn enter(&self, _span: &tracing::Id) {}
fn exit(&self, _span: &tracing::Id) {}
}For simple cases, implementing only enabled is sufficient.
use tracing::{Subscriber, Metadata, Level, Event};
use tracing::span::Attributes;
struct SelectiveSubscriber {
min_level: Level,
// Filter certain event targets
blocked_targets: Vec<&'static str>,
}
impl Subscriber for SelectiveSubscriber {
fn enabled(&self, metadata: &Metadata<'_>) -> bool {
// Accept spans at INFO or higher
metadata.level() <= &self.min_level
}
fn event_enabled(&self, metadata: &Metadata<'_>) -> bool {
// Additional filtering for events
if metadata.level() > &Level::DEBUG {
return false;
}
// Block specific targets
let target = metadata.target();
if self.blocked_targets.iter().any(|t| target.starts_with(t)) {
return false;
}
true
}
fn new_span(&self, _attrs: &Attributes<'_>) -> tracing::Id {
tracing::Id::from_u64(1)
}
fn event(&self, _event: &Event<'_>) {
// Process the event
}
fn record(&self, _span: &tracing::Id, _values: &tracing::span::Values<'_>) {}
fn record_follows_from(&self, _span: &tracing::Id, _follows: &tracing::Id) {}
fn enter(&self, _span: &tracing::Id) {}
fn exit(&self, _span: &tracing::Id) {}
}event_enabled allows filtering based on event-specific criteria.
use tracing::{span, info, debug, trace, Level};
fn process_data(data: &[u8]) {
// If enabled() returns false for this span level:
// - The span allocation is skipped entirely
// - Any span fields aren't evaluated
// - The code inside the span still runs but isn't traced
let _span = span!(Level::DEBUG, "process_data", len = data.len());
let _enter = _span.enter();
// This event goes through event_enabled
info!("Processing {} bytes", data.len());
// If event_enabled returns false for TRACE:
// - The event is dropped
// - But the string formatting is NOT evaluated (tracing is lazy)
debug!("Data checksum: {:?}", calculate_checksum(data));
trace!("Per-byte breakdown: {:?}", expensive_analysis(data));
}
fn calculate_checksum(data: &[u8]) -> u32 {
data.iter().fold(0, |acc, &b| acc.wrapping_add(b as u32))
}
fn expensive_analysis(data: &[u8]) -> Vec<u8> {
// Expensive operation
data.to_vec()
}
fn main() {
// If enabled filters out DEBUG spans, nothing in process_data is traced
// If enabled allows DEBUG but event_enabled filters TRACE:
// - The span is constructed
// - info! and debug! events are recorded
// - trace! event is filtered (expensive_analysis never runs)
}Both enabled and event_enabled enable lazy evaluation optimizations.
use tracing_subscriber::{layer::Layer, Registry};
use tracing::{Level, Metadata};
struct LevelFilterLayer {
max_level: Level,
}
impl<S: tracing::Subscriber> Layer<S> for LevelFilterLayer {
fn enabled(&self, metadata: &Metadata<'_>, _ctx: tracing_subscriber::layer::Context<'_, S>) -> bool {
metadata.level() <= &self.max_level
}
// Layers can also implement event_enabled
fn event_enabled(&self, metadata: &Metadata<'_>, _ctx: tracing_subscriber::layer::Context<'_, S>) -> bool {
// Additional event-level filtering
metadata.level() <= &Level::DEBUG
}
}
fn main() {
let subscriber = tracing_subscriber::registry()
.with(LevelFilterLayer { max_level: Level::INFO })
.with(tracing_subscriber::fmt::layer());
tracing::subscriber::set_global_default(subscriber).unwrap();
}tracing-subscriber layers can implement both enabled and event_enabled.
use tracing::{Subscriber, Metadata, Level};
struct TargetFilterSubscriber {
allowed_targets: Vec<&'static str>,
}
impl Subscriber for TargetFilterSubscriber {
fn enabled(&self, metadata: &Metadata<'_>) -> bool {
// Check if this span's target is allowed
let target = metadata.target();
self.allowed_targets.iter().any(|t| target.starts_with(t))
}
fn event_enabled(&self, metadata: &Metadata<'_>) -> bool {
// Events from different targets can have different rules
let target = metadata.target();
// Allow all events from "myapp" at any level
if target.starts_with("myapp") {
return true;
}
// Only allow WARN and above from dependencies
if target.starts_with("myapp::deps") {
return metadata.level() <= &Level::WARN;
}
false
}
fn new_span(&self, _attrs: &tracing::span::Attributes<'_>) -> tracing::Id {
tracing::Id::from_u64(1)
}
fn event(&self, _event: &tracing::Event<'_>) {}
fn record(&self, _span: &tracing::Id, _values: &tracing::span::Values<'_>) {}
fn record_follows_from(&self, _span: &tracing::Id, _follows: &tracing::Id) {}
fn enter(&self, _span: &tracing::Id) {}
fn exit(&self, _span: &tracing::Id) {}
}event_enabled can apply different rules to different event targets.
use tracing::{Subscriber, Metadata, Level};
use std::sync::atomic::{AtomicI32, Ordering};
struct DynamicLevelSubscriber {
current_level: AtomicI32, // 0=ERROR, 1=WARN, 2=INFO, 3=DEBUG, 4=TRACE
}
impl DynamicLevelSubscriber {
fn level_from_int(level: i32) -> Level {
match level {
0 => Level::ERROR,
1 => Level::WARN,
2 => Level::INFO,
3 => Level::DEBUG,
_ => Level::TRACE,
}
}
fn int_from_level(level: &Level) -> i32 {
match *level {
Level::ERROR => 0,
Level::WARN => 1,
Level::INFO => 2,
Level::DEBUG => 3,
Level::TRACE => 4,
}
}
}
impl Subscriber for DynamicLevelSubscriber {
fn enabled(&self, metadata: &Metadata<'_>) -> bool {
let current = DynamicLevelSubscriber::level_from_int(
self.current_level.load(Ordering::Relaxed)
);
metadata.level() <= ¤t
}
fn event_enabled(&self, metadata: &Metadata<'_>) -> bool {
// Events can use a different dynamic threshold
self.enabled(metadata)
}
fn new_span(&self, _attrs: &tracing::span::Attributes<'_>) -> tracing::Id {
tracing::Id::from_u64(1)
}
fn event(&self, _event: &tracing::Event<'_>) {}
fn record(&self, _span: &tracing::Id, _values: &tracing::span::Values<'_>) {}
fn record_follows_from(&self, _span: &tracing::Id, _follows: &tracing::Id) {}
fn enter(&self, _span: &tracing::Id) {}
fn exit(&self, _span: &tracing::Id) {}
}Both methods can use dynamic configuration to filter at runtime.
use tracing::{Metadata, Level};
fn analyze_metadata(metadata: &Metadata<'_>) {
// Available for filtering decisions:
println!("Name: {}", metadata.name()); // "process_data"
println!("Target: {}", metadata.target()); // "myapp::module"
println!("Level: {:?}", metadata.level()); // INFO, DEBUG, etc.
println!("Module: {:?}", metadata.module_path()); // "myapp::module"
println!("File: {:?}", metadata.file()); // "src/main.rs"
println!("Line: {:?}", metadata.line()); // 42
println!("Fields: {:?}", metadata.fields()); // Available field names
// enabled() and event_enabled() see the same metadata
// but at different points in the tracing lifecycle
}
fn main() {
let span = tracing::span!(Level::INFO, "test_span", foo = 1);
let metadata = span.metadata();
analyze_metadata(metadata);
}Both methods have access to full span/event metadata for decisions.
use tracing::{span, info, debug, trace, Level};
fn main() {
// Timeline of filtering:
// 1. enabled() is called when creating a span
// - If false: span allocation skipped, children not created
// - If true: span created, can proceed to events
let span = span!(Level::DEBUG, "outer");
// 2. enabled() is called for nested spans
let nested = span!(Level::TRACE, "nested");
// 3. When entering a span and emitting events:
let _enter1 = span.enter();
// 4. event_enabled() is called for each event
info!("info event"); // Checked by event_enabled
debug!("debug event"); // Checked by event_enabled
trace!("trace event"); // Checked by event_enabled
let _enter2 = nested.enter();
// 5. Events in nested span also checked by event_enabled
trace!("nested trace"); // Checked by event_enabled
}enabled gates span hierarchy; event_enabled gates individual events.
use tracing::{span, info, debug, Level};
fn expensive_computation() -> String {
// Imagine this is costly
std::thread::sleep(std::time::Duration::from_millis(100));
"computed".to_string()
}
fn main() {
// If enabled() returns false for DEBUG:
// - The entire span including expensive_computation() is skipped
let _span = span!(
Level::DEBUG,
"operation",
result = expensive_computation() // Evaluated only if enabled
);
let _enter = _span.enter();
// If event_enabled() returns false for DEBUG:
// - The span was created (expensive_computation was evaluated)
// - But individual events can still be filtered
debug!("debug message"); // Checked by event_enabled
info!("info message"); // Checked by event_enabled
}enabled prevents expensive span field computation; event_enabled doesn't.
| Aspect | enabled | event_enabled | |--------|---------|---------------| | Called for | Spans (creation) | Events (recording) | | Default impl | Required | Delegates to enabled | | Granularity | Per-span | Per-event | | Prevents | Span construction overhead | Event recording overhead | | Can filter | Entire span trees | Individual log lines | | Called earlier | Yes | No (after span exists) | | Use case | Level-based filtering | Target-based, fine-grained |
Decision flow:
span!(Level::DEBUG, "my_span")
|
v
enabled(metadata) --> false --> Skip everything, no overhead
|
true
v
Create span, evaluate fields
|
v
Enter span
|
v
event!(Level::DEBUG, "message")
|
v
event_enabled(metadata) --> false --> Skip event only
|
true
v
Record event
Trade-off analysis:
| Situation | Recommendation |
|-----------|----------------|
| Simple level filtering | Implement only enabled |
| Span-level filtering sufficient | Implement only enabled |
| Per-event target filtering | Implement both |
| Reduce span construction cost | Aggressive enabled filter |
| Fine-grained event control | Implement event_enabled |
| Dynamic log levels | Implement both with shared config |
Key insight: The two-level filtering system in tracing reflects a fundamental tension in observability systems: the desire for fine-grained control versus the need to minimize overhead. enabled operates at the span level, making coarse-grained decisions that can eliminate entire subtrees of potential tracing dataāthis is the highest-leverage filter because preventing span construction avoids all downstream costs. event_enabled operates at the event level, allowing finer control but only after the containing span has been constructed. The default delegation from event_enabled to enabled means you only pay for what you use: simple subscribers implement one method, sophisticated subscribers implement both. The practical impact is that if you have expensive computations in span fields (like result = expensive()), filtering at the enabled level prevents those computations from running; filtering at event_enabled does not. This architecture pushes expensive filtering logic to the earliest possible point while still allowing granular control when neededāthe enabled filter is the "gate" for span creation, and event_enabled is the "filter" for events within enabled spans.