Rust Observability Patterns
Tracing Setup
[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
pub fn init_tracing() {
let env_filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info"));
tracing_subscriber::registry()
.with(env_filter)
.with(tracing_subscriber::fmt::layer().json())
.init();
}
Structured Logging
use tracing::{info, warn, error, debug, instrument};
fn process_request(user_id: u64, action: &str) {
info!(user_id, action, "processing request");
debug!(user_id, action, details = "extra context", "detailed log");
}
// Span for grouping related log entries
async fn handle_order(order_id: u64) -> anyhow::Result<()> {
let span = tracing::info_span!("handle_order", order_id);
let _enter = span.enter();
info!("started processing");
let items = fetch_items(order_id).await?;
info!(item_count = items.len(), "fetched items");
Ok(())
}
// #[instrument] automatically creates a span
#[instrument(skip(db, password), fields(user_id = ?user.id))]
async fn authenticate_user(db: &DbPool, user: &User, password: &str) -> anyhow::Result<Token> {
debug!("verifying credentials");
let token = verify_and_issue(db, user, password).await?;
info!("authentication successful");
Ok(token)
}
Metrics with prometheus
[dependencies]
prometheus = "0.13"
lazy_static = "1"
use prometheus::{IntCounterVec, HistogramVec, register_int_counter_vec, register_histogram_vec};
use lazy_static::lazy_static;
lazy_static! {
static ref HTTP_REQUESTS: IntCounterVec = register_int_counter_vec!(
"http_requests_total", "Total HTTP requests",
&["method", "path", "status"]
).unwrap();
static ref REQUEST_DURATION: HistogramVec = register_histogram_vec!(
"http_request_duration_seconds", "Request duration",
&["method", "path"],
vec![0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0]
).unwrap();
}
// Metrics middleware
async fn track_metrics(req: axum::extract::Request, next: axum::middleware::Next) -> axum::response::Response {
let method = req.method().to_string();
let path = req.uri().path().to_string();
let timer = REQUEST_DURATION.with_label_values(&[&method, &path]).start_timer();
let response = next.run(req).await;
HTTP_REQUESTS.with_label_values(&[&method, &path, &response.status().as_u16().to_string()]).inc();
timer.observe_duration();
response
}
// Prometheus scrape endpoint
async fn metrics_handler() -> String {
use prometheus::{Encoder, TextEncoder};
let encoder = TextEncoder::new();
let mut buffer = Vec::new();
encoder.encode(&prometheus::gather(), &mut buffer).unwrap();
String::from_utf8(buffer).unwrap()
}
Health Check
use axum::response::Json;
use serde::Serialize;
#[derive(Serialize)]
struct Health {
status: &'static str,
version: &'static str,
}
async fn health_check(State(state): State<AppState>) -> (axum::http::StatusCode, Json<Health>) {
let db_ok = state.db.ping().await.is_ok();
let status = if db_ok { "healthy" } else { "degraded" };
let code = if db_ok { axum::http::StatusCode::OK } else { axum::http::StatusCode::SERVICE_UNAVAILABLE };
(code, Json(Health { status, version: env!("CARGO_PKG_VERSION") }))
}
Log Levels and Filtering
# Set log level via env var
RUST_LOG=info cargo run
RUST_LOG=my_crate=debug,tower_http=warn cargo run
# JSON logs for production
LOG_JSON=1 cargo run
Common Anti-Patterns
println! / eprintln! in production — use tracing::info! / tracing::error!
- Logging entire request bodies — they may contain secrets; log IDs and summaries only
unwrap() on metric registration — register metrics at startup; panics surface immediately
- Missing span context in spawned tasks — use
#[instrument] or pass spans explicitly
- No log level filtering — always configure
EnvFilter; logging everything creates noise and cost