Tracing
Comprehensive observability for your workflows.
Cano provides comprehensive observability through the optional tracing feature using the
tracing library.
Setup
To enable tracing, add the feature to your Cargo.toml and initialize a subscriber in your code.
[dependencies]
cano = { version = "0.7", features = ["tracing"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
Initialization
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
// Initialize tracing with environment filter
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info".into()))
.with(tracing_subscriber::fmt::layer())
.init();
What Gets Traced
Workflow Level
Orchestration start/completion, state transitions, final states.
Task/Node Level
Execution attempts, retry logic, delays, success/failure outcomes.
Scheduler Level
Workflow scheduling, concurrent execution, run counts, durations.
Custom Spans
You can attach custom tracing spans to your workflows to include business-specific context.
// Create workflow with custom tracing span
let workflow_span = info_span!(
"user_data_processing",
user_id = "12345",
batch_id = "batch_001"
);
let mut workflow = Workflow::new(MyState::Start)
.with_tracing_span(workflow_span);
Example Output
Running with RUST_LOG=info produces structured logs:
INFO user_data_processing{user_id="12345"}: Starting workflow orchestration
INFO user_data_processing{user_id="12345"}:task_execution: Starting task execution
INFO user_data_processing{user_id="12345"}:task_attempt{attempt=1}: Node execution completed success=true
INFO user_data_processing{user_id="12345"}:task_attempt{attempt=1}: processor_id=basic_processor input_records=3: Data processing completed
INFO user_data_processing{user_id="12345"}: Workflow completed successfully
Full Example
use cano::prelude::*;
use async_trait::async_trait;
use tracing::{info, info_span};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum State {
Start,
Complete,
}
#[derive(Clone)]
struct ProcessOrderNode;
#[async_trait]
impl Task for ProcessOrderNode {
async fn run(&self, _store: &MemoryStore) -> Result, CanoError> {
info!("Processing order...");
Ok(TaskResult::Single(State::Complete))
}
}
#[tokio::main]
async fn main() -> Result<(), CanoError> {
// 1. Setup Subscriber
tracing_subscriber::fmt::init();
let store = MemoryStore::new();
// 2. Create Workflow with Custom Span
// This span will wrap all logs generated by this workflow
let workflow_span = info_span!(
"order_processing",
order_id = "ORD-2025-001",
customer = "Acme Corp"
);
let workflow = Workflow::new(store.clone())
.register(State::Start, ProcessOrderNode)
.add_exit_state(State::Complete)
.with_tracing_span(workflow_span); // Attach span
// 3. Run
info!("Submitting order...");
workflow.orchestrate(State::Start).await?;
Ok(())
}