rust-tracing

v2026.09.24

Rust `tracing` crate — structured, async-aware diagnostics. Spans, events, fields, instrument macro, subscribers (fmt, JSON, OpenTelemetry, Tokio Console), filtering (env-filter, RUST_LOG), targets, contextual fields with span data, integration with `tracing-subscriber` layers, OpenTelemetry export, and patterns for embedded/mobile (FFI to Android Logcat / iOS os.Logger), wallet apps with privacy-respecting log redaction. USE WHEN: user mentions "tracing", "tracing-subscriber", "instrument", "Span", "info!/error!/debug!", "tracing-opentelemetry", "tokio-console", "env-filter", "RUST_LOG", "JSON logs Rust", "structured logging Rust" DO NOT USE FOR: Mobile-specific log integration only - use `mobile/android-native` or `mobile/ios-native` DO NOT USE FOR: Java/Kotlin logging (slf4j) - use Java/Kotlin specific DO NOT USE FOR: Generic Rust language - use `languages/rust`

GitHub
Install command
npx skhub add claude-dev-suite/rust-tracing
Markdown
SKILL.md

Rust tracing — Structured Diagnostics

Deep Knowledge: Use mcp__documentation__fetch_docs with technology: tracing.

Why tracing Over log

Aspectlog cratetracing
GranularitySingle eventsEvents + spans (nested context)
Async-aware❌ Loses context across .await✅ Span context follows tasks
Structured fields❌ String-only✅ Typed key-value
Multiple subscribersOne globalComposable layers
Sampling/filteringGlobal levelPer-target, per-field, dynamic
OpenTelemetryManual bridgeFirst-class via tracing-opentelemetry
Tokio integrationNoneTokio Console for live debugging

For modern Rust async services and wallet apps: tracing.

Setup

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

# Optional
tracing-appender = "0.2"                       # rolling file output
tracing-opentelemetry = "0.27"                 # OTel export
opentelemetry = "0.26"
opentelemetry_sdk = "0.26"
opentelemetry-otlp = "0.26"
console-subscriber = "0.4"                     # tokio-console

Initialize Subscriber

use tracing_subscriber::{fmt, EnvFilter, prelude::*};

fn init_tracing() {
    tracing_subscriber::registry()
        .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")))
        .with(fmt::layer().with_target(true).with_line_number(true))
        .init();
}

fn main() {
    init_tracing();
    tracing::info!("started");
}

Filter via env:

RUST_LOG=info cargo run                       # default level info
RUST_LOG=debug cargo run                      # debug everything
RUST_LOG=bhodl=debug,tower=info cargo run    # per-crate
RUST_LOG="bhodl::wallet=trace" cargo run     # per-module

Events (Like Log Lines)

use tracing::{trace, debug, info, warn, error};

info!("server started");
warn!("retrying after error");
error!("send failed");

// With fields (structured)
info!(user_id = 42, action = "login", "user authenticated");
warn!(retry_count = 3, ?error, "retry exhausted");      // ?error = Debug format
error!(%error, "send failed");                           // %error = Display format

// Conditional formatting
debug!(target: "payments", amount_sats = 1000, "payment initiated");

Output (default fmt layer):

2026-05-04T10:00:00.123456Z  INFO bhodl::auth: user authenticated user_id=42 action=login
2026-05-04T10:00:01.123456Z  WARN bhodl::sync: retry exhausted retry_count=3 error=ConnectionError

Spans (Nested Context)

use tracing::{info_span, instrument};

fn process_request(req: Request) {
    let span = info_span!("process_request", request_id = %req.id, method = req.method);
    let _enter = span.enter();

    info!("validating");
    validate(&req);
    info!("storing");
    store(&req);
}                                              // span exits here

// All `info!` inside the span are tagged with request_id and method

#[instrument] Attribute (Idiomatic)

use tracing::instrument;

#[instrument(skip(repo), fields(wallet_id = %wallet.id))]
async fn sync_wallet(wallet: &Wallet, repo: &Repo) -> Result<()> {
    info!("syncing");
    let txs = repo.fetch_transactions(&wallet.id).await?;
    info!(count = txs.len(), "fetched transactions");
    repo.save_transactions(txs).await?;
    Ok(())
}

skip(repo) excludes repo from instrumented fields (often big or non-Display). fields(wallet_id = %wallet.id) adds custom fields not from arguments.

Async-aware: span context survives .await boundaries automatically.

Span on Result

#[instrument(err)]
async fn risky() -> Result<(), MyError> {
    // returns Err — auto-logged at error level
    Err(MyError::Boom)
}

#[instrument(ret)]
async fn computes() -> u64 {
    // returns Ok(42) — auto-logged
    42
}

#[instrument(level = "debug", skip_all, fields(user_id = %user.id))]
async fn detailed(user: &User) { /* ... */ }

JSON Output (Structured for Log Aggregators)

use tracing_subscriber::fmt::format::Json;

tracing_subscriber::fmt()
    .json()
    .with_current_span(true)
    .with_span_list(false)
    .flatten_event(true)
    .with_max_level(Level::INFO)
    .init();

Output:

{"timestamp":"2026-05-04T10:00:00.123Z","level":"INFO","fields":{"message":"user authenticated","user_id":42},"target":"bhodl::auth","span":{"name":"process_request","request_id":"abc"}}

Pipe to log aggregator (Loki, Elasticsearch, Datadog).

Multiple Layers Composition

use tracing_subscriber::{fmt, EnvFilter, prelude::*, Registry};

let stdout_log = fmt::layer().with_target(true);
let json_log = fmt::layer().json().with_writer(std::io::stderr);

let file_appender = tracing_appender::rolling::daily("logs", "bhodl.log");
let (file_writer, _guard) = tracing_appender::non_blocking(file_appender);
let file_log = fmt::layer().json().with_writer(file_writer);

Registry::default()
    .with(EnvFilter::from_default_env())
    .with(stdout_log)
    .with(json_log)
    .with(file_log)
    .init();

Each layer can have its own filter:

.with(stdout_log.with_filter(EnvFilter::new("info")))
.with(file_log.with_filter(EnvFilter::new("trace")))    // file gets everything

OpenTelemetry Export

use opentelemetry::trace::TracerProvider as _;
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::trace::TracerProvider;
use tracing_opentelemetry::OpenTelemetryLayer;

fn init_otel() {
    let exporter = opentelemetry_otlp::SpanExporter::builder()
        .with_tonic()
        .with_endpoint("http://localhost:4317")
        .build()
        .unwrap();

    let provider = TracerProvider::builder()
        .with_batch_exporter(exporter, opentelemetry_sdk::runtime::Tokio)
        .build();

    let tracer = provider.tracer("bhodl");

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

Sends spans to OTel collector → Jaeger, Tempo, Honeycomb, etc.

Tokio Console (Live Async Debugging)

[dependencies]
console-subscriber = "0.4"
tokio = { version = "1", features = ["full", "tracing"] }
fn main() {
    console_subscriber::init();
    // ... your app
}
# In another terminal
cargo install --locked tokio-console
tokio-console

Live view of all tasks, polls, locks, blocking I/O. Identifies stuck tasks, contention, slow polls.

Mobile / FFI Integration

For wallet apps shipping Rust core via UniFFI to Android/iOS, route Rust logs to native log systems.

Android Logcat

[dependencies]
android_logger = "0.14"
tracing-android = "0.2"                        # bridges tracing to Logcat
#[cfg(target_os = "android")]
fn init_for_android() {
    use tracing_subscriber::prelude::*;
    let android_layer = tracing_android::layer("BhodlCore").unwrap();

    tracing_subscriber::registry()
        .with(EnvFilter::new("info,bhodl=debug"))
        .with(android_layer)
        .init();
}

Logs appear in adb logcat -s BhodlCore.

iOS os_log

[dependencies]
oslog = "0.2"
tracing-oslog = "0.2"                          # community crate
#[cfg(target_os = "ios")]
fn init_for_ios() {
    use tracing_subscriber::prelude::*;
    let oslog_layer = tracing_oslog::OsLogger::new("com.bhodl.core", "default");

    tracing_subscriber::registry()
        .with(EnvFilter::new("info,bhodl=debug"))
        .with(oslog_layer)
        .init();
}

Logs appear in Console.app filtered by subsystem com.bhodl.core.

Cross-Platform Init (UniFFI)

use std::sync::Once;

#[uniffi::export]
pub fn init_logging() {
    static INIT: Once = Once::new();
    INIT.call_once(|| {
        #[cfg(target_os = "android")]
        init_for_android();
        #[cfg(target_os = "ios")]
        init_for_ios();
        #[cfg(not(any(target_os = "android", target_os = "ios")))]
        init_for_default();
    });
}

Call from Kotlin/Swift on app start.

Privacy-Respecting Logging (Wallet Apps)

For BHODL: never log raw addresses, balances, seeds in production logs.

use tracing::Span;

// Custom Display that redacts
struct RedactedAddr<'a>(&'a str);

impl std::fmt::Display for RedactedAddr<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        let len = self.0.len();
        if len > 8 {
            write!(f, "{}...{}", &self.0[..4], &self.0[len-4..])
        } else {
            write!(f, "***")
        }
    }
}

#[instrument(skip(amount, address), fields(addr = %RedactedAddr(&address)))]
fn send_payment(amount: u64, address: String) {
    info!("sending payment");                   // amount NOT logged
    // process
}

Or use feature flag:

[features]
default = ["redact-secrets"]
redact-secrets = []
fn log_amount(amount: u64) -> impl std::fmt::Display {
    #[cfg(feature = "redact-secrets")]
    return "***";
    #[cfg(not(feature = "redact-secrets"))]
    return amount.to_string();
}

Custom Filter / Layer

use tracing::{Subscriber, Event, Metadata};
use tracing_subscriber::{Layer, layer::Context, registry::LookupSpan};

struct MetricsLayer;

impl<S> Layer<S> for MetricsLayer
where S: Subscriber + for<'a> LookupSpan<'a>,
{
    fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
        if event.metadata().level() == &Level::ERROR {
            // increment Prometheus counter
            prometheus::ERRORS.inc();
        }
    }
}

Add to subscriber:

tracing_subscriber::registry()
    .with(EnvFilter::from_default_env())
    .with(fmt::layer())
    .with(MetricsLayer)
    .init();

Performance

tracing is designed for production: when a level is filtered out, the macro expands to ~zero cost (no allocation, no formatting). Use trace! and debug! liberally.

For very hot paths, consider:

if tracing::enabled!(target: "hot_loop", Level::DEBUG) {
    debug!(value = expensive_computation());
}

enabled! is cheap; gates the expensive expensive_computation().

Tokio Tasks Naming

use tokio::task::Builder;

Builder::new()
    .name("wallet-sync-worker")
    .spawn(async { /* ... */ })
    .unwrap();

Names show in tokio-console and in span context.

Trace Sampling (Production at Scale)

For high-throughput apps, sample spans to reduce export volume:

use opentelemetry_sdk::trace::Sampler;

let provider = TracerProvider::builder()
    .with_config(opentelemetry_sdk::trace::Config::default()
        .with_sampler(Sampler::TraceIdRatioBased(0.1)))   // 10% sample
    .build();

For wallet apps: typically not needed (low traffic per user).

Anti-Patterns

Anti-patternWhy it's badCorrect approach
println! for production logsNo filter, no levels, no contextUse tracing macros
Logging raw secrets (seeds, balances, addresses)Leaks via logsRedact via Display impl or skip fields
format! inside log macros for filtered levelsWasteful when level disabledUse ? or % and let macro handle
One huge subscriber with all levels enabledHigh overheadUse EnvFilter per-target
Forgetting .in_current_span() for spawned tasksLost contextTokio's tracing feature handles auto, otherwise wrap
Synchronous file writerBlocks async runtimeUse tracing_appender::non_blocking
Initializing tracing-subscriber twicePanicUse Once for FFI init
error! for expected errors (404, validation)Alarms opswarn! or info! for expected, error! for unexpected
Logging in tight loops without level checkVolume explosionCheck level or sample
OpenTelemetry without tokio runtimeAsync export failsPair OTel exporter with Tokio runtime
Hardcoded log file path on mobileNo write access in app sandboxUse platform-specific writable dir

Troubleshooting

SymptomCauseFix
init() called twice panicSubscriber set globallyUse try_init() or Once::call_once
No outputEnvFilter rejectsCheck RUST_LOG env or default filter
Span context lost across .awaitAsync without tracing feature on TokioUse #[tokio::main] or Builder with enable_all
oslog symbols missing on iOSMissing framework linkLink os framework in build script
Logs not appearing in LogcatWrong tag or low levelIncrease Logcat verbosity, check tag
OTel exporter never sendsTokio runtime mismatchBuild TracerProvider on same runtime
JSON output mangledPretty printer interferingUse .json().flatten_event(true)
Stack traces missing in errorserror!(%err) only formats Displayerror!(?err) for Debug or use tracing-error crate

When NOT to Use This Skill

ScenarioUse Instead
Java/Kotlin logging (slf4j, Logback)Java/Kotlin specific
Mobile-specific platform log integrationmobile/android-native / mobile/ios-native
Generic Rust languagelanguages/rust
Distributed tracing protocol detailsOpenTelemetry-specific docs
Spring Boot loggingSpring Boot specific
Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

MIT

Source path

skills/observability/rust-tracing

Default branch

main

Latest commit

9496306

Tree SHA

fe4e2f1