chore: add logging under tracing feature

This commit is contained in:
swaits committed 2025-10-07 21:16:04 -07:00
1 parent ac848c6608
commit ec94ade329
11 files changed
+719 -8

No files matched your search

+11
View File
@@ -8,6 +8,9 @@ use crate::runner::DagRunner;
use crate::task::Task;
use crate::types::{NodeId, TaskHandle};
#[cfg(feature = "tracing")]
use tracing::debug;
/// Marker trait to identify the unit type at compile time.
///
/// This is used to enforce that tasks with non-unit inputs MUST call `.depends_on()`,
@@ -132,6 +135,14 @@ impl<'a, Tk: Task, Deps> TaskBuilder<'a, Tk, Deps> {
// Register dependencies in the DAG
let dep_ids = deps.to_node_ids();
#[cfg(feature = "tracing")]
debug!(
task_id = self.id.0,
dependency_ids = ?dep_ids.iter().map(|id| id.0).collect::<Vec<_>>(),
dependency_count = dep_ids.len(),
"wiring task dependencies"
);
let mut edges = self.dag.edges.lock();
let mut dependents = self.dag.dependents.lock();
+57
View File
@@ -655,6 +655,63 @@
//! Run `cargo bench` to see comprehensive benchmarks including basic operations, scaling
//! characteristics, common patterns (fan-out, diamond), and realistic workloads.
//!
//! # Optional Tracing Support
//!
//! dagx provides optional observability through the `tracing` crate with **zero runtime overhead
//! when disabled**. The tracing instrumentation is conditionally compiled using feature flags.
//!
//! ## Enabling Tracing
//!
//! Add the `tracing` feature to your `Cargo.toml`:
//!
//! ```toml
//! [dependencies]
//! dagx = { version = "0.2", features = ["tracing"] }
//! tracing-subscriber = "0.3"
//! ```
//!
//! Then initialize a tracing subscriber in your application:
//!
//! ```no_run
//! use tracing_subscriber::{fmt, EnvFilter};
//!
//! fmt()
//! .with_env_filter(
//! EnvFilter::try_from_default_env()
//! .unwrap_or_else(|_| EnvFilter::new("dagx=info"))
//! )
//! .init();
//! ```
//!
//! ## Log Levels
//!
//! - **INFO**: DAG execution start/completion
//! - **DEBUG**: Task additions, dependency wiring, layer computation
//! - **TRACE**: Individual task execution (inline vs spawned), detailed execution flow
//! - **ERROR**: Panics, cycles, type errors
//!
//! Control log level with the `RUST_LOG` environment variable:
//!
//! ```bash
//! RUST_LOG=dagx=info cargo run # High-level execution info
//! RUST_LOG=dagx=debug cargo run # Task and layer details
//! RUST_LOG=dagx=trace cargo run # All execution details
//! ```
//!
//! ## Zero-Cost Guarantee
//!
//! When the `tracing` feature is disabled (the default), there is **literally 0ns overhead**:
//! - Logging code is removed at compile time via `#[cfg(feature = "tracing")]`
//! - No branches, no function calls, nothing in the compiled binary
//! - The `tracing` crate isn't even linked
//! - Benchmarks verify identical performance with/without the feature
//!
//! This follows the same zero-cost pattern used by Tokio, Hyper, and other performance-critical
//! Rust async libraries.
//!
//! See [`examples/tracing_example.rs`](https://github.com/swaits/dagx/blob/main/examples/tracing_example.rs)
//! for a complete working example.
//!
#![allow(private_bounds, private_interfaces)]
+81 -8
View File
@@ -17,6 +17,9 @@ use futures::future::BoxFuture;
use futures::{FutureExt, StreamExt};
use parking_lot::Mutex;
#[cfg(feature = "tracing")]
use tracing::{debug, error, info, trace};
use crate::builder::TaskBuilder;
use crate::error::{DagError, DagResult};
use crate::extract::ExtractInput;
@@ -197,6 +200,14 @@ impl DagRunner {
Tk::Output: 'static + Clone,
{
let id = self.alloc_id();
#[cfg(feature = "tracing")]
debug!(
task_id = id.0,
task_type = std::any::type_name::<Tk>(),
"adding task to DAG"
);
let node = TypedNode::new(id, task);
self.nodes.lock().push(Some(Box::new(node)));
self.edges.lock().insert(id, Vec::new());
@@ -266,10 +277,13 @@ impl DagRunner {
/// This eliminates Mutex contention on outputs and enables true streaming execution.
/// Type erasure occurs only at the ExecutableNode trait boundary - channels are created
/// with full type information and type-erased just before passing to execute_with_channels.
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self, spawner)))]
pub async fn run<S>(&self, spawner: S) -> DagResult<()>
where
S: Fn(BoxFuture<'static, ()>),
{
#[cfg(feature = "tracing")]
info!("starting DAG execution");
// Acquire run lock to prevent concurrent executions
// Use atomic compare_exchange to check if already running
if self
@@ -277,6 +291,9 @@ impl DagRunner {
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_err()
{
#[cfg(feature = "tracing")]
error!("DAG is already running - concurrent execution not supported");
return Err(DagError::CycleDetected {
nodes: vec![],
description: "DAG is already running - concurrent execution not supported"
@@ -292,6 +309,13 @@ impl DagRunner {
// Build topological layers
let layers = self.compute_layers()?;
#[cfg(feature = "tracing")]
debug!(
layer_count = layers.len(),
total_tasks = layers.iter().map(|l| l.len()).sum::<usize>(),
"computed topological layers"
);
let edges = self.edges.lock().clone();
// Create all channels upfront
@@ -337,7 +361,19 @@ impl DagRunner {
)>();
// Execute layer by layer
#[cfg(feature = "tracing")]
let mut layer_idx = 0;
for layer in layers {
#[cfg(feature = "tracing")]
{
debug!(
layer = layer_idx,
task_count = layer.len(),
"executing layer"
);
layer_idx += 1;
}
// ============================================================================
// PERFORMANCE OPTIMIZATION: Inline execution for single-task layers
// ============================================================================
@@ -366,6 +402,13 @@ impl DagRunner {
// ============================================================================
if layer.len() == 1 {
let node_id = layer[0];
#[cfg(feature = "tracing")]
trace!(
task_id = node_id.0,
"executing task inline (single-task layer optimization)"
);
let out_tx = output_tx.clone();
// Take ownership of the node
@@ -392,16 +435,25 @@ impl DagRunner {
.await
.unwrap_or_else(|panic_payload| {
// Convert panic to error
let panic_message = if let Some(s) = panic_payload.downcast_ref::<&str>()
{
s.to_string()
} else if let Some(s) = panic_payload.downcast_ref::<String>() {
s.clone()
} else {
"unknown panic".to_string()
};
#[cfg(feature = "tracing")]
error!(
task_id = node_id.0,
panic_message = %panic_message,
"task panicked during inline execution"
);
Err(DagError::TaskPanicked {
task_id: node_id.0,
panic_message: if let Some(s) = panic_payload.downcast_ref::<&str>()
{
s.to_string()
} else if let Some(s) = panic_payload.downcast_ref::<String>() {
s.clone()
} else {
"unknown panic".to_string()
},
panic_message,
})
});
@@ -415,6 +467,8 @@ impl DagRunner {
// Spawn each task in this layer
for &node_id in &layer {
#[cfg(feature = "tracing")]
trace!(task_id = node_id.0, "spawning task");
let mut task_tx = tx.clone();
let out_tx = output_tx.clone();
@@ -480,9 +534,14 @@ impl DagRunner {
// Return first error if any
if let Some(err) = first_error {
#[cfg(feature = "tracing")]
error!(?err, "DAG execution failed");
return Err(err);
}
#[cfg(feature = "tracing")]
info!("DAG execution completed successfully");
Ok(())
}
@@ -551,6 +610,9 @@ impl DagRunner {
}
fn compute_layers(&self) -> DagResult<Vec<Vec<NodeId>>> {
#[cfg(feature = "tracing")]
debug!("computing topological layers");
let mut in_degree: HashMap<NodeId, usize> = HashMap::new();
let mut layers = Vec::new();
@@ -620,12 +682,23 @@ impl DagRunner {
unvisited.len(),
unvisited
);
#[cfg(feature = "tracing")]
error!(
unvisited_count = unvisited.len(),
unvisited_nodes = ?unvisited,
"cycle detected in DAG"
);
return Err(DagError::CycleDetected {
nodes: unvisited,
description,
});
}
#[cfg(feature = "tracing")]
debug!(layer_count = layers.len(), "topological layers computed");
Ok(layers)
}
}