Monitoring Rust Applications: Metrics and Alerts from A to Z
\n\nWhen your Rust application goes into production, the code ceases to be just code. It becomes a service that users and business depend on. Without monitoring, you are flying blind: failures are discovered through complaints, and performance is estimated "by eye." In this article, we will break down how to build a full-fledged monitoring system for Rust — from collecting basic metrics to setting up alerts in Prometheus and visualization in Grafana.
\n\n1. Why Monitoring is Important for Rust Projects
\nRust is famous for its performance and memory safety, but this does not negate the need to monitor the application. Typical problems in production:
\n- \n
- Memory leaks (for example, via
Arcor cyclic references). \n - Blocking in async code (deadlocks or starvation in tokio). \n
- Unexpected panics (even with
catch_unwind). \n - Performance degradation due to suboptimal algorithms. \n
Monitoring provides answers to the questions: "What is happening now?", "Why did it happen?" and "How to prevent it?".
\n\n2. Collecting Metrics: The metrics Library and Exporting to Prometheus
\nThe standard stack for Rust is the metrics crate (or prometheus). We use metrics because it abstracts the export and easily changes the backend.
2.1. Installation and Basic Setup
\nAdd dependencies to Cargo.toml:
[dependencies]\nmetrics = "0.21"\nmetrics-exporter-prometheus = "0.12"\ntokio = { version = "1", features = ["full"] }\n\nCreate a simple HTTP server for the /metrics endpoint:
use metrics_exporter_prometheus::PrometheusBuilder;\nuse std::net::SocketAddr;\nuse tokio::net::TcpListener;\n\n#[tokio::main]\nasync fn main() {\n // Initialize the Prometheus exporter\n let (recorder, exporter) = PrometheusBuilder::new()\n .build()\n .expect("Failed to create Prometheus exporter");\n\n metrics::set_boxed_recorder(Box::new(recorder))\n .expect("Failed to set global recorder");\n\n // Start the metrics server\n let addr: SocketAddr = "0.0.0.0:9090".parse().unwrap();\n let listener = TcpListener::bind(addr).await.unwrap();\n println!("Metrics available at http://{}", addr);\n\n loop {\n let (mut stream, _) = listener.accept().await.unwrap();\n let body = exporter.encode().unwrap();\n let response = format!(\n "HTTP/1.1 200 OK\\r\\nContent-Type: text/plain\\r\\nContent-Length: {}\\r\\n\\r\\n{}",\n body.len(),\n body\n );\n tokio::io::AsyncWriteExt::write_all(&mut stream, response.as_bytes())\n .await\n .unwrap();\n }\n}\n\n2.2. Registering Custom Metrics
\nLet's add a request counter and a response time histogram:
\nuse metrics::{counter, histogram, increment_counter};\nuse std::time::Instant;\n\nasync fn handle_request() {\n let start = Instant::now();\n\n // Your business logic\n tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;\n\n // Record metrics\n increment_counter!("http_requests_total", "method" => "GET");\n histogram!("http_request_duration_seconds", start.elapsed().as_secs_f64(), "method" => "GET");\n}\nNow at /metrics you will see:
# HELP http_requests_total Total HTTP requests\n# TYPE http_requests_total counter\nhttp_requests_total{method="GET"} 1\n# HELP http_request_duration_seconds HTTP request duration\n# TYPE http_request_duration_seconds histogram\nhttp_request_duration_seconds_bucket{method="GET",le="0.005"} 0\nhttp_request_duration_seconds_bucket{method="GET",le="0.01"} 0\nhttp_request_duration_seconds_bucket{method="GET",le="0.025"} 0\n