pre-metrics
This commit is contained in:
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user