Tracing & OpenTelemetry Diagnostics

Distributed tracing, span propagation, and structured diagnostics with tracing (150M+ downloads) and OpenTelemetry.


🎯 Architecture: Distributed Tracing Spans

flowchart TD
    Span[HTTP / gRPC Request Span: trace_id = 4bf9...] --> Step[Interpreter Microstep Span]
    Step --> Guard[Guard: checkInventory]
    Step --> Action[Action: deductCredits]
    Step --> Plugin[TracingPlugin: Formatted Structured Fields]
    Plugin --> OTel[OpenTelemetry Exporter -> Jaeger / Datadog / Honeycomb]

xstate-statemachine uses the tracing facade throughout its codebase under target xstate. When an event is processed, TracingPlugin emits structured fields without performing any heap allocations on the fast path.


📦 Dependencies

[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
tracing-opentelemetry = "0.28"
xstate-statemachine = "0.1"

🚀 Setup: Formatted Structured Logging

use std::sync::Arc;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
use xstate_statemachine::prelude::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. Initialize structured logging layer
    tracing_subscriber::registry()
        .with(EnvFilter::new("info,xstate=debug"))
        .with(tracing_subscriber::fmt::layer().pretty())
        .init();

    // 2. Attach TracingPlugin to your statechart
    let machine = Machine::from_json(ORDER_JSON)?;
    let mut interp = SyncInterpreter::new(machine, MachineLogic::dynamic());
    interp.plugin(Arc::new(TracingPlugin));

    interp.start()?;
    interp.send("SUBMIT")?;
    interp.send("APPROVE")?;

    Ok(())
}

JSON Output for Production (CloudWatch / Datadog)

To emit machine-readable JSON logs in Kubernetes or AWS:

#![allow(unused)]
fn main() {
tracing_subscriber::registry()
    .with(EnvFilter::from_default_env())
    .with(tracing_subscriber::fmt::layer().json())
    .init();
}

Emitted JSON record:

{
  "timestamp": "2026-11-10T02:50:12.348210Z",
  "level": "INFO",
  "target": "xstate",
  "fields": {
    "machine": "order",
    "event": "APPROVE",
    "changed": true,
    "unhandled": false,
    "actions": ["sendInvoice", "notifyCustomer"],
    "states": ["order.approved"],
    "status": "running",
    "message": "event processed"
  }
}

🔒 Automated Credential Redaction

TracingPlugin and TransitionLogObserver automatically mask sensitive fields in context and event payloads:

#![allow(unused)]
fn main() {
use xstate_statemachine::prelude::*;

// DEFAULT_REDACT_KEYS matches: password, secret, token, api_key, authorization, credential, card, cvv
let payload = serde_json::json!({
    "user_id": 42,
    "api_token": "sk_live_992384729384",
    "credit_card": "4111-2222-3333-4444"
});

let clean = redact(&payload, &DEFAULT_REDACT_KEYS);
assert_eq!(clean["api_token"], "***");
assert_eq!(clean["credit_card"], "***");
assert_eq!(clean["user_id"], 42);
}

🌐 OpenTelemetry & Jaeger Export

#![allow(unused)]
fn main() {
use opentelemetry::global;
use tracing_opentelemetry::OpenTelemetryLayer;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

pub fn init_telemetry() {
    let tracer = opentelemetry_otlp::new_pipeline()
        .tracing()
        .with_exporter(opentelemetry_otlp::new_exporter().tonic())
        .install_batch(opentelemetry_sdk::runtime::Tokio)
        .expect("Failed to initialize OTLP tracer");

    tracing_subscriber::registry()
        .with(OpenTelemetryLayer::new(tracer))
        .with(tracing_subscriber::fmt::layer())
        .init();
}
}