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(())
}