Your code works. But what's really happening?
You've written a cool service in Rust. The compiler is happy, tests are green, speed is off the charts. You deploy it on a server, and... silence. How do you know what's happening inside? Has it crashed? Is there a memory leak? Is request processing slowing down?
Without monitoring and logging, your code is a black box. In the world of Rust, where every byte counts and performance is critical, the ability to look inside the process becomes a superpower. In this article, we'll break down how to turn your Rust application from a silent executor into a talkative assistant that tells you about its own problems.
Why does Rust require a special approach to observability?
Unlike languages with a runtime (Java, Python, Go), Rust doesn't give you ready-made tools out of the box. There's no built-in profiler or metrics agent. But there are also advantages: you control everything. The type system and ownership model allow you to create incredibly efficient monitoring tools without overhead. The key is to choose the right libraries and architecture.
The modern observability stack in Rust is built on three pillars:
- Logging — structured records of events
- Metrics — numerical indicators (counters, histograms)
- Tracing — call chains in distributed systems
And all of this should work with minimal impact on performance. Let's see how to implement this in practice.
1. Logging: from println! to structured data
Let's start with the basics. Forget about println! in production. Instead, use the log crate — the standard logging facade in the Rust ecosystem. It's not tied to a specific backend, which gives you flexibility.
Adding log and env_logger
// Cargo.toml[dependencies]log = "0.4"env_logger = "0.11"A simple usage example:
use log::{info, warn, error};
fn main() { env_logger::init(); info!("Service started on port {}", 8080); warn!("Memory exceeds 80%"); error!("Database connection error: {}", "timeout");}Control the logging level via the RUST_LOG environment variable:
RUST_LOG=info cargo runRUST_LOG=warn,my_crate=debug cargo run # For a specific crateBut this is just the beginning. For production, you need structured logging (JSON). Use log4rs or slog for advanced formatting.
2. Tracing: logging on steroids
The tracing crate is an evolution of log. It adds the concept of spans — named time intervals that can be nested within each other. This is ideal for tracking call chains.
Installing tracing
[dependencies]tracing = "0.1"tracing-subscriber = "0.3"Example using macros:
use tracing::{info, instrument, span, Level};use tracing_subscriber;
#[instrument] // Automatically creates a span for the functionfn process_request(user_id: u64) { // Nested span let validation_span = span!(Level::INFO, "validation", user_id); let _guard = validation_span.enter(); // Some action std::thread::sleep(std::time::Duration::from_millis(10)); info!("Validation passed"); // Exiting the span happens automatically when _guard is dropped}
fn main() { tracing_subscriber::fmt() .with_target(true) .init(); process_request(42);}The output will contain structured information about the execution time of each span and nesting. This is invaluable for debugging latency issues.
3. Metrics: counting everything
Logs are good, but to see