From 1a2ec26f27c8446ef9c5bc12de93da681c7ca99b Mon Sep 17 00:00:00 2001 From: Nell Date: Fri, 15 May 2026 19:35:17 +0200 Subject: [PATCH] pre-metrics --- src/http/metrics.rs | 198 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 src/http/metrics.rs diff --git a/src/http/metrics.rs b/src/http/metrics.rs new file mode 100644 index 0000000..4611d20 --- /dev/null +++ b/src/http/metrics.rs @@ -0,0 +1,198 @@ +//! 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}; + +// ── 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), + } + } +} + +// ── 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, + } + } +} + +// ── 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; + } + }); +}