Tracing

tracing-crate span instrumentation for your workflows.

Behind the tracing feature gate (features = ["tracing"]).

See also

This page covers Cano's built-in tracing spans. For the synchronous callback API (WorkflowObserver) and the ready-made TracingObserver that re-emits those callbacks as tracing events, see Observers. For the sibling metrics feature — which re-emits the same observer hooks as metrics-crate counters and histograms — see Metrics.

Cano provides comprehensive observability through the optional tracing feature using the tracing library. All tracing instrumentation is behind conditional compilation, so it adds zero overhead when disabled.

For a callback-style API — get notified on workflow lifecycle and failure events without depending on the tracing ecosystem — see Observers. The tracing feature also ships a ready-made TracingObserver that bridges the two: attach it with .with_observer(Arc::new(TracingObserver::new())) to re-emit those observer events as tracing events under the cano::observer target.


Setup

Enable the tracing feature flag in your Cargo.toml. You can also use features = ["all"] to enable everything (scheduler + tracing + recovery + metrics) at once.

[dependencies]
cano = { version = "0.13", features = ["tracing"] }

tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

# Or enable everything (scheduler + tracing + recovery + metrics):
# cano = { version = "0.13", features = ["all"] }


Basic Initialization

For quick setup during development, use the default formatter.

use tracing_subscriber;

// Simple setup for development
tracing_subscriber::fmt::init();

Production Setup with Environment Filter

For production use, configure an environment filter so you can control log levels at runtime via the RUST_LOG environment variable.

use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

// Production setup with env 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 Level

Execution attempts, retry logic, delays, success/failure outcomes.

Scheduler Level

Workflow scheduling, concurrent execution, run counts, durations.


Error Tracing

When the tracing feature is enabled, Cano automatically instruments error paths at appropriate severity levels so you can diagnose failures without adding custom logging.

Failed Task Attempts

Each failed attempt is logged as a warning with the error details and current attempt number. This lets you see transient failures that are recovered by retry logic.

WARN task_attempt{attempt=2 max_attempts=4}: Task execution failed, will retry error="connection timeout"

Retry Exhaustion

When all retry attempts are exhausted, the final failure is logged as an error with the total attempt count. This indicates a permanent failure that bubbles up as CanoError.

ERROR task_attempt{attempt=4 max_attempts=4}: Task execution failed after all retry attempts error="connection timeout"

Workflow-Level Errors

Workflow orchestration traces include the current state context, so errors are always associated with the state that produced them.

INFO workflow_orchestrate: Starting workflow execution initial_state=FetchData
ERROR workflow_orchestrate: Task failed in state FetchData after exhausting retries

Filtering Trace Output

Use the RUST_LOG environment variable to control which modules emit trace output. This is especially useful in production to reduce noise.

# Show only Cano debug logs
RUST_LOG=cano=debug cargo run

# Show Cano info + your app's debug logs
RUST_LOG=cano=info,my_app=debug cargo run

# Show retry-related details only
RUST_LOG=cano::task=debug cargo run

# Silence everything except errors
RUST_LOG=error cargo run


Scheduler Tracing

When both the scheduler and tracing features are enabled, the scheduler produces trace output for workflow lifecycle events. You can enable both with features = ["all"].

Workflow Execution

The scheduler traces when each managed workflow starts and completes, including the workflow identifier and scheduling trigger type.

Retry Attempts

Retry delay durations are logged at debug level, so you can see the backoff progression for failing tasks within scheduled workflows.

Split Task Execution

When a scheduled workflow uses split/join, each parallel task is traced with its task ID within a split_task span, making it straightforward to correlate concurrent execution in log output.

# Enable everything: scheduler + tracing + recovery + metrics
[dependencies]
cano = { version = "0.13", features = ["all"] }



Custom Spans

Attach custom tracing spans to your workflows to include business-specific context. The span wraps all trace output generated during that workflow's execution.

use tracing::info_span;
use cano::prelude::*;

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum MyState { Start, Complete }

#[derive(Clone)]
struct MyProcessor;

#[task(state = MyState)]
impl MyProcessor {
    async fn run_bare(&self) -> Result<TaskResult<MyState>, CanoError> {
        Ok(TaskResult::Single(MyState::Complete))
    }
}

fn build_workflow() -> Workflow<MyState> {
    // Create workflow with custom tracing span
    let workflow_span = info_span!(
        "user_data_processing",
        user_id = "12345",
        batch_id = "batch_001"
    );

    let store = MemoryStore::new();
    Workflow::new(Resources::new().insert("store", store))
        .with_tracing_span(workflow_span)
        .register(MyState::Start, MyProcessor)
        .add_exit_state(MyState::Complete)
}

Custom Instrumentation

You can add your own tracing to Task implementations using the tracing crate's macros directly. This is useful for adding domain-specific context that Cano's built-in instrumentation does not cover.

Instrumenting a Task

use cano::prelude::*;
use tracing::{info, warn};

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum AppState { Process, Confirm }

async fn process_payment(_order_id: &str) -> Result<String, CanoError> {
    Ok("receipt-1234".to_string())
}

#[derive(Clone)]
struct PaymentTask;

#[task(state = AppState)]
impl PaymentTask {
    async fn run(&self, res: &Resources) -> Result<TaskResult<AppState>, CanoError> {
        let store = res.get::<MemoryStore, _>("store")?;
        let order_id: String = store.get("order_id")?;
        info!(order_id = %order_id, "Processing payment");

        match process_payment(&order_id).await {
            Ok(receipt) => {
                info!(order_id = %order_id, receipt = %receipt, "Payment succeeded");
                store.put("receipt", receipt)?;
                Ok(TaskResult::Single(AppState::Confirm))
            }
            Err(e) => {
                warn!(order_id = %order_id, error = %e, "Payment failed");
                Err(CanoError::task_execution(format!("payment failed: {e}")))
            }
        }
    }
}


Correlating with Metrics

When the metrics feature is also enabled, tracing spans and metrics interoperate through the spans themselves. With the metrics-tracing-context crate wired in — a TracingContextLayer around your metrics recorder plus a MetricsLayer on your tracing-subscriber registry — every cano_* metric emitted while a span is entered inherits that span's fields as labels.

So a custom span you attach with with_tracing_span (or simply open around orchestrate()) flows its fields — a request_id, a tenant, … — onto the metrics recorded during that run. Cano's own default workflow_orchestrate and workflow_resume spans carry a workflow_id field whenever one is set via with_workflow_id, so it shows up both in the trace output and as a metric label.

See also

Metrics → Correlating with Traces has the full layer wiring, the cardinality caveat for TracingContextLayer::all(), and the runnable metrics_tracing_context example (cargo run --example metrics_tracing_context --features "metrics tracing").


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}: Task 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 tracing::{info, info_span};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum State {
    Start,
    Complete,
}

#[derive(Clone)]
struct ProcessOrder;

#[task(state = State)]
impl ProcessOrder {
    async fn run(&self, _res: &Resources) -> Result<TaskResult<State>, CanoError> {
        info!("Processing order...");
        Ok(TaskResult::Single(State::Complete))
    }
}

#[tokio::main]
async fn main() -> Result<(), CanoError> {
    // 1. Setup Subscriber with env filter
    tracing_subscriber::registry()
        .with(tracing_subscriber::EnvFilter::try_from_default_env()
            .unwrap_or_else(|_| "info".into()))
        .with(tracing_subscriber::fmt::layer())
        .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(Resources::new().insert("store", store))
        .register(State::Start, ProcessOrder)
        .add_exit_state(State::Complete)
        .with_tracing_span(workflow_span); // Attach span

    // 3. Run
    info!("Submitting order...");
    workflow.orchestrate(State::Start).await?;

    Ok(())
}

Runnable example: cargo run --example tracing_demo --features "tracing scheduler" — sets up a tracing-subscriber, runs a workflow and a scheduled flow, and prints the structured span output. Pair it with TracingObserver (see Observers) for flat lifecycle events alongside the spans.