| name | rust-tracing |
| description | | Use when this capability is needed. |
Rust tracing — Structured Diagnostics
Deep Knowledge: Use mcp__documentation__fetch_docs with technology: tracing.
Why tracing Over log
| Aspect | log crate | tracing |
|---|
| Granularity | Single events | Events + spans (nested context) |
| Async-aware | ❌ Loses context across .await | ✅ Span context follows tasks |
| Structured fields | ❌ String-only | ✅ Typed key-value |
| Multiple subscribers | One global | Composable layers |
| Sampling/filtering | Global level | Per-target, per-field, dynamic |
| OpenTelemetry | Manual bridge | First-class via tracing-opentelemetry |
| Tokio integration | None | Tokio 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"] }
tracing-appender = "0.2"
tracing-opentelemetry = "0.27"
opentelemetry = "0.26"
opentelemetry_sdk = "0.26"
opentelemetry-otlp = "0.26"
console-subscriber = "0.4"
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
RUST_LOG=debug cargo run
RUST_LOG=bhodl=debug,tower=info cargo run
RUST_LOG="bhodl::wallet=trace" cargo run
Events (Like Log Lines)
use tracing::{trace, debug, info, warn, error};
info!("server started");
warn!("retrying after error");
error!("send failed");
info!(user_id = 42, action = "login", "user authenticated");
warn!(retry_count = 3, ?error, "retry exhausted");
error!(%error, "send failed");
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);
}
#[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> {
Err(MyError::Boom)
}
#[instrument(ret)]
async fn computes() -> u64 {
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")))
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();
}
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"
#[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"
#[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;
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");
}
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 {
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)))
.build();
For wallet apps: typically not needed (low traffic per user).
Anti-Patterns
| Anti-pattern | Why it's bad | Correct approach |
|---|
println! for production logs | No filter, no levels, no context | Use tracing macros |
| Logging raw secrets (seeds, balances, addresses) | Leaks via logs | Redact via Display impl or skip fields |
format! inside log macros for filtered levels | Wasteful when level disabled | Use ? or % and let macro handle |
| One huge subscriber with all levels enabled | High overhead | Use EnvFilter per-target |
Forgetting .in_current_span() for spawned tasks | Lost context | Tokio's tracing feature handles auto, otherwise wrap |
| Synchronous file writer | Blocks async runtime | Use tracing_appender::non_blocking |
Initializing tracing-subscriber twice | Panic | Use Once for FFI init |
error! for expected errors (404, validation) | Alarms ops | warn! or info! for expected, error! for unexpected |
| Logging in tight loops without level check | Volume explosion | Check level or sample |
OpenTelemetry without tokio runtime | Async export fails | Pair OTel exporter with Tokio runtime |
| Hardcoded log file path on mobile | No write access in app sandbox | Use platform-specific writable dir |
Troubleshooting
| Symptom | Cause | Fix |
|---|
init() called twice panic | Subscriber set globally | Use try_init() or Once::call_once |
| No output | EnvFilter rejects | Check RUST_LOG env or default filter |
Span context lost across .await | Async without tracing feature on Tokio | Use #[tokio::main] or Builder with enable_all |
oslog symbols missing on iOS | Missing framework link | Link os framework in build script |
| Logs not appearing in Logcat | Wrong tag or low level | Increase Logcat verbosity, check tag |
| OTel exporter never sends | Tokio runtime mismatch | Build TracerProvider on same runtime |
| JSON output mangled | Pretty printer interfering | Use .json().flatten_event(true) |
| Stack traces missing in errors | error!(%err) only formats Display | error!(?err) for Debug or use tracing-error crate |
When NOT to Use This Skill
| Scenario | Use Instead |
|---|
| Java/Kotlin logging (slf4j, Logback) | Java/Kotlin specific |
| Mobile-specific platform log integration | mobile/android-native / mobile/ios-native |
| Generic Rust language | languages/rust |
| Distributed tracing protocol details | OpenTelemetry-specific docs |
| Spring Boot logging | Spring Boot specific |
Source: claude-dev-suite/claude-dev-suite — distributed by TomeVault.