//! Métrologie du serveur HTTP. //! //! Ce module expose : //! - [`HttpMetrics`] : compteurs atomiques lock-free, sans contention dans les //! handlers. //! - [`HttpMetricsSnapshot`] : lecture cohérente de tous les compteurs à un //! instant T, utilisable pour calculer des deltas. //! - [`HttpRates`] : taux moyens par seconde calculés entre deux snapshots. //! - [`spawn_reporter`] : tâche tokio de reporting périodique via `tracing`. //! //! # Métriques collectées //! //! | Compteur | Description | //! |--------------------|----------------------------------------------------| //! | `requests_total` | Requêtes HTTP reçues | //! | `responses_2xx` | Réponses 2xx (succès) | //! | `responses_4xx` | Réponses 4xx (erreurs client) | //! | `responses_5xx` | Réponses 5xx (erreurs serveur) | //! | `panics_caught` | Panics interceptés par `CatchPanicLayer` | //! | `latency_ms_total` | Latence cumulée en ms (pour moyenne glissante) | use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use std::time::{Duration, Instant}; use crate::metrics::{Metrics, MetricsSnapshot}; // ── Compteurs ──────────────────────────────────────────────────────────────── /// Compteurs atomiques du serveur HTTP. /// /// Partagé via [`Arc`] entre les handlers, les layers Tower et le reporter /// périodique. Tous les accès utilisent [`Ordering::Relaxed`] : on accepte /// des lectures légèrement décalées entre compteurs (suffisant pour de la /// métrologie), ce qui évite tout overhead de synchronisation. #[derive(Debug, Default)] pub struct HttpMetrics { /// Nombre total de requêtes HTTP reçues. pub requests_total: AtomicU64, /// Nombre de réponses avec un statut 2xx. pub responses_2xx: AtomicU64, /// Nombre de réponses avec un statut 4xx. pub responses_4xx: AtomicU64, /// Nombre de réponses avec un statut 5xx. pub responses_5xx: AtomicU64, /// Nombre de panics interceptés par `CatchPanicLayer`. pub panics_caught: AtomicU64, /// Latence cumulée en millisecondes sur toutes les requêtes terminées. pub latency_ms_total: AtomicU64, } impl HttpMetrics { /// Crée un jeu de métriques vide enroulé dans un [`Arc`]. pub fn new() -> Arc { Arc::new(Self::default()) } /// Enregistre une requête reçue. #[inline] pub fn inc_request(&self) { self.requests_total.fetch_add(1, Ordering::Relaxed); } /// Enregistre la fin d'une requête avec son statut et sa latence. #[inline] pub fn record_response(&self, status: u16, latency: Duration) { let ms = latency.as_millis() as u64; self.latency_ms_total.fetch_add(ms, Ordering::Relaxed); match status { 200..=299 => { self.responses_2xx.fetch_add(1, Ordering::Relaxed); } 400..=499 => { self.responses_4xx.fetch_add(1, Ordering::Relaxed); } 500..=599 => { self.responses_5xx.fetch_add(1, Ordering::Relaxed); } _ => {} } } /// Enregistre un panic intercepté. #[inline] pub fn inc_panic(&self) { self.panics_caught.fetch_add(1, Ordering::Relaxed); } /// Prend un snapshot cohérent de tous les compteurs. pub fn snapshot(&self) -> HttpMetricsSnapshot { HttpMetricsSnapshot { taken_at: Instant::now(), requests_total: self.requests_total.load(Ordering::Relaxed), responses_2xx: self.responses_2xx.load(Ordering::Relaxed), responses_4xx: self.responses_4xx.load(Ordering::Relaxed), responses_5xx: self.responses_5xx.load(Ordering::Relaxed), panics_caught: self.panics_caught.load(Ordering::Relaxed), latency_ms_total: self.latency_ms_total.load(Ordering::Relaxed), } } } impl Metrics for HttpMetrics { type Snapshot = HttpMetricsSnapshot; fn snapshot(&self) -> HttpMetricsSnapshot { self.snapshot() } } // ── Snapshot ───────────────────────────────────────────────────────────────── /// Lecture cohérente des compteurs à un instant T. #[derive(Debug, Clone)] pub struct HttpMetricsSnapshot { pub taken_at: Instant, pub requests_total: u64, pub responses_2xx: u64, pub responses_4xx: u64, pub responses_5xx: u64, pub panics_caught: u64, pub latency_ms_total: u64, } impl HttpMetricsSnapshot { /// Calcule les taux moyens par seconde par rapport à un snapshot antérieur. pub fn rates_since(&self, previous: &HttpMetricsSnapshot) -> HttpRates { let elapsed = self .taken_at .duration_since(previous.taken_at) .as_secs_f64(); if elapsed == 0.0 { return HttpRates::default(); } let delta_req = self.requests_total.saturating_sub(previous.requests_total); let avg_latency = if delta_req > 0 { let delta_lat = self .latency_ms_total .saturating_sub(previous.latency_ms_total); delta_lat as f64 / delta_req as f64 } else { 0.0 }; HttpRates { requests_per_sec: delta_req as f64 / elapsed, responses_2xx_per_sec: self.responses_2xx.saturating_sub(previous.responses_2xx) as f64 / elapsed, responses_4xx_per_sec: self.responses_4xx.saturating_sub(previous.responses_4xx) as f64 / elapsed, responses_5xx_per_sec: self.responses_5xx.saturating_sub(previous.responses_5xx) as f64 / elapsed, avg_latency_ms: avg_latency, } } } impl MetricsSnapshot for HttpMetricsSnapshot { fn taken_at(&self) -> Instant { self.taken_at } } // ── Taux ───────────────────────────────────────────────────────────────────── /// Taux moyens par seconde calculés entre deux snapshots. #[derive(Debug, Default, Clone)] pub struct HttpRates { pub requests_per_sec: f64, pub responses_2xx_per_sec: f64, pub responses_4xx_per_sec: f64, pub responses_5xx_per_sec: f64, /// Latence moyenne en millisecondes sur la période. pub avg_latency_ms: f64, } // ── Reporter ───────────────────────────────────────────────────────────────── /// Démarre une tâche tokio qui loggue les métriques HTTP à intervalle régulier. /// /// La tâche s'arrête automatiquement à la fin de la session tokio. pub fn spawn_reporter(metrics: Arc, interval: Duration) { tokio::spawn(async move { let mut previous = metrics.snapshot(); let mut ticker = tokio::time::interval(interval); ticker.tick().await; // première tick immédiate, on la consomme loop { ticker.tick().await; let current = metrics.snapshot(); let rates = current.rates_since(&previous); tracing::info!( requests_total = current.requests_total, responses_2xx = current.responses_2xx, responses_4xx = current.responses_4xx, responses_5xx = current.responses_5xx, panics_caught = current.panics_caught, req_per_sec = format_args!("{:.2}", rates.requests_per_sec), p2xx_per_sec = format_args!("{:.2}", rates.responses_2xx_per_sec), p4xx_per_sec = format_args!("{:.2}", rates.responses_4xx_per_sec), p5xx_per_sec = format_args!("{:.2}", rates.responses_5xx_per_sec), avg_latency_ms = format_args!("{:.1}", rates.avg_latency_ms), "HTTP metrics" ); previous = current; } }); }