Votre code fonctionne. Mais que se passe-t-il réellement ?
Vous avez écrit un super service en Rust. Le compilateur est content, les tests sont verts, la vitesse est au maximum. Vous le lancez sur le serveur, et... silence. Comment savoir ce qui se passe à l'intérieur ? N'a-t-il pas planté ? N'y a-t-il pas de fuite mémoire ? Le traitement des requêtes ne ralentit-il pas ?
Sans surveillance et journalisation, votre code est une boîte noire. Dans le monde de Rust, où chaque octet compte et où la performance est critique, la capacité à regarder à l'intérieur du processus devient un super-pouvoir. Dans cet article, nous allons voir comment transformer votre application Rust d'un exécutant muet en un assistant bavard qui vous parlera lui-même de ses problèmes.
Pourquoi Rust nécessite-t-il une approche particulière de l'observabilité ?
Contrairement aux langages avec runtime (Java, Python, Go), Rust ne vous fournit pas d'outils prêts à l'emploi "hors de la boîte". Pas de profileur intégré ni d'agent de métriques. Mais il y a aussi des avantages : vous contrôlez tout. Le système de types et de possession permet de créer des outils de surveillance incroyablement efficaces sans surcharge. L'essentiel est de bien choisir les bibliothèques et l'architecture.
La pile d'observabilité moderne en Rust repose sur trois piliers :
- Journalisation (Logging) — enregistrements structurés d'événements
- Métriques (Metrics) — indicateurs numériques (compteurs, histogrammes)
- Traçage (Tracing) — chaînes d'appels dans les systèmes distribués
Et tout cela doit fonctionner avec un impact minimal sur les performances. Voyons comment le mettre en œuvre concrètement.
1. Journalisation : de println! aux données structurées
Commençons par les bases. Oubliez println! en production. Utilisez plutôt la crate log — la façade standard pour la journalisation dans l'écosystème Rust. Elle n'est liée à aucun backend spécifique, ce qui offre de la flexibilité.
Ajout de log et env_logger
// Cargo.toml[dependencies]log = "0.4"env_logger = "0.11"Exemple simple d'utilisation :
use log::{info, warn, error};
fn main() { env_logger::init(); info!("Service démarré sur le port {}", 8080); warn!("Mémoire dépassant 80%"); error!("Erreur de connexion à la BDD : {}", "timeout");}Gérez le niveau de journalisation via la variable d'environnement RUST_LOG :
RUST_LOG=info cargo runRUST_LOG=warn,my_crate=debug cargo run # Pour une crate spécifiqueMais ce n'est que le début. Pour la production, une journalisation structurée (JSON) est nécessaire. Utilisez log4rs ou slog pour un formatage avancé.
2. Tracing : la journalisation sous stéroïdes
La crate tracing est l'évolution de log. Elle ajoute le concept de spans — des intervalles de temps nommés qui peuvent être imbriqués les uns dans les autres. C'est idéal pour suivre les chaînes d'appels.
Installation de tracing
[dependencies]tracing = "0.1"tracing-subscriber = "0.3"Exemple avec utilisation de macros :
use tracing::{info, instrument, span, Level};use tracing_subscriber;
#[instrument] // Crée automatiquement un span pour la fonctionfn process_request(user_id: u64) { // Span imbriqué let validation_span = span!(Level::INFO, "validation", user_id); let _guard = validation_span.enter(); // Une action quelconque std::thread::sleep(std::time::Duration::from_millis(10)); info!("Validation réussie"); // Sortie du span automatique lors de la suppression de _guard}
fn main() { tracing_subscriber::fmt() .with_target(true) .init(); process_request(42);}La sortie contiendra des informations structurées sur le temps d'exécution de chaque span et leur imbrication. C'est inestimable pour déboguer les problèmes de latence.
3. Métriques : comptons tout
Les logs, c'est bien, mais pour voir