pre-metrics

This commit is contained in:
2026-05-15 19:35:17 +02:00
parent 132057217d
commit 1a2ec26f27
+198
View File
@@ -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<Self> {
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<HttpMetrics>, 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;
}
});
}