# `api-tools` PrometheusLayer — corrections appliquées
> Ce document a été remplacé par le rapport des modifications réellement
> apportées à la crate. L'analyse d'origine (avant fix) reste accessible dans
> l'historique git de ce fichier.
---
## TL;DR
- **Bug critique corrigé** : suppression du `tokio::time::sleep(200 ms)` qui
bloquait le hot-path HTTP. Latence par requête : **~210 ms → < 1 ms** (×200+).
- **Bug de naming corrigé** : `system_used_disks_usage` → `system_used_disks_space`.
- **Nouvelle API** : `spawn_system_metrics_collector(...)` pour collecter les
métriques host en arrière-plan.
- **Optimisations bonus** : caching des labels (`Arc<str>` pour `service_name`,
`&'static str` pour méthodes/codes HTTP standards) et buckets d'histogramme
paramétrables.
- **Bump de version** : `0.7.0 → 0.8.0` (breaking : retrait de
`disk_mount_points` du `PrometheusLayer` + renommage de la metric ci-dessus).
---
## Cause racine (rappel)
Dans `SystemMetrics::new()` (appelée à chaque requête HTTP) :
```rust
sys.refresh_cpu_usage();
let mut cpu_usage = sys.global_cpu_usage();
tokio::time::sleep(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL).await; // ← 200 ms
sys.refresh_cpu_usage();
cpu_usage += sys.global_cpu_usage();
cpu_usage /= 2.0;
```
`MINIMUM_CPU_UPDATE_INTERVAL` vaut **200 ms** sur Linux/macOS/Windows
(`sysinfo` calcule un delta entre deux snapshots de `/proc/stat`). Le `sleep`
était dans le critical path de chaque requête HTTP, plafonnant le throughput
d'une connexion à ~5 req/s. À chaque tick s'ajoutait :
- `System::new_all()` qui scanne tous les processus (plusieurs ms)
- `Disks::new_with_refreshed_list()` (énumère les block devices)
- 7 lookups + sets sur le registre `metrics` global
---
## Architecture corrigée
Le layer est maintenant scindé en **deux composants distincts** :
### 1. `PrometheusLayer` — middleware HTTP allégé
Ne fait plus que les métriques par-requête (`http_requests_total`,
`http_requests_duration_seconds`). Coût : ~µs par requête. Aucune syscall
système, aucun `sleep`.
### 2. `spawn_system_metrics_collector(...)` — tâche background
Fonction publique qui spawn une tâche Tokio persistante :
- Maintient une instance `System` réutilisée entre ticks
(`new_with_specifics` : skip le scan des processus).
- Sur chaque tick (`tokio::time::interval` configurable, ex. 10 s) :
refresh CPU + mémoire + disques + push des gauges.
- Pas de `sleep` artificiel : `refresh_cpu_usage()` retourne une vraie valeur
dès le 2ᵉ tick puisque `sysinfo` calcule un delta entre snapshots
persistés.
- Retourne un `JoinHandle<()>` que l'utilisateur peut `abort()` au shutdown.
---
## Migration côté utilisateur (0.7 → 0.8)
### 1. `Cargo.toml`
```toml
# Avant
api-tools = { version = "0.7", features = ["full"] }
# Après
api-tools = { version = "0.8", features = ["full"] }
```
### 2. Construction du `PrometheusLayer` — breaking
Le champ `disk_mount_points` a disparu du layer. Le compilateur signalera
chaque site d'usage manqué (erreur de build, pas un warning silencieux).
```rust
// Avant (0.7)
use api_tools::server::axum::layers::prometheus::PrometheusLayer;
let prometheus_layer = PrometheusLayer {
service_name: "myapp".into(),
disk_mount_points: vec![PathBuf::from("/")],
};
router.layer(prometheus_layer);
```
```rust
// Après (0.8)
use api_tools::server::axum::layers::prometheus::{
PrometheusLayer, spawn_system_metrics_collector,
};
use std::path::PathBuf;
use std::time::Duration;
let prometheus_layer = PrometheusLayer {
service_name: "myapp".into(),
};
router.layer(prometheus_layer);
// Au boot de l'app, une seule fois (la tâche est indépendante du serveur HTTP) :
let _collector = spawn_system_metrics_collector(
"myapp".into(),
vec![PathBuf::from("/")],
Duration::from_secs(10), // intervalle de refresh
);
```
Si tu veux pouvoir l'arrêter proprement au shutdown, garde le
`JoinHandle<()>` retourné et appelle `.abort()`.
### 3. Dashboards Grafana / requêtes PromQL — breaking
Une seule metric a été renommée (correction de typo) :
```diff
- system_used_disks_usage{service="myapp"}
+ system_used_disks_space{service="myapp"}
```
Toutes les autres metrics gardent leur nom (`system_cpu_usage`,
`system_total_memory`, `system_used_memory`, `system_total_swap`,
`system_used_swap`, `system_total_disks_space`, `http_requests_total`,
`http_requests_duration_seconds`).
### 4. Buckets d'histogramme — optionnel, nouveau
Si tu veux des buckets adaptés à un service très rapide (par défaut les
buckets vont de 5 ms à 10 s) :
```rust
use api_tools::server::axum::handlers::prometheus::{
PrometheusHandler, DEFAULT_DURATION_BUCKETS,
};
// Default (équivalent à get_handle()) :
let handle = PrometheusHandler::get_handle()?;
// Buckets custom adaptés à un service très rapide :
let handle = PrometheusHandler::get_handle_with_buckets(
&[0.0005, 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0],
)?;
```
L'appel `get_handle()` reste valide. La constante publique
`DEFAULT_DURATION_BUCKETS` est exposée si tu veux partir des defaults et
n'en modifier que quelques-uns.
### 5. Variable `PROMETHEUS_ENABLE` côté `.env`
Si tu l'avais mis à `0` pour contourner la latence, tu peux le repasser à
`1` — il n'y a plus aucune raison fonctionnelle de désactiver Prometheus
pour la performance.
### Checklist de migration
- [ ] Bump `api-tools` à `0.8.0` dans `Cargo.toml`.
- [ ] Retirer `disk_mount_points: ...` de la construction de `PrometheusLayer`.
- [ ] Ajouter un appel à `spawn_system_metrics_collector(...)` dans la
fonction de boot du serveur.
- [ ] Chercher `system_used_disks_usage` dans les dashboards Grafana, les
alertes, et les requêtes PromQL — remplacer par
`system_used_disks_space`.
- [ ] Réactiver `PROMETHEUS_ENABLE=1` (ou le retirer du `.env`) si désactivé.
- [ ] `cargo build` pour valider.
Aucun changement nécessaire sur les extracteurs, les autres layers, ou
`PrometheusHandler::get_handle()`.
---
## Détail des changements
### Breaking changes
| `PrometheusLayer { service_name, disk_mount_points }` | `PrometheusLayer { service_name }` |
| metric `system_used_disks_usage` | metric `system_used_disks_space` |
### Nouvelles API publiques
- `spawn_system_metrics_collector(service_name, disk_mount_points, interval) -> JoinHandle<()>`
dans `api_tools::server::axum::layers::prometheus`
- `PrometheusHandler::get_handle_with_buckets(buckets: &[f64])`
- `pub const DEFAULT_DURATION_BUCKETS: &[f64]` (ex-constante privée
`SECONDS_DURATION_BUCKETS`)
### Optimisations internes (transparentes pour l'utilisateur)
- `service_name` est converti **une seule fois** en `Arc<str>` lors du
`Layer::layer()` ; chaque requête fait un `Arc::clone` (refcount atomique
uniquement, pas d'alloc).
- Les méthodes HTTP standards (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS,
CONNECT, TRACE) sont mappées vers `&'static str` via
`fn method_label(&Method) -> Cow<'static, str>` — pas d'alloc pour 99 % des
requêtes.
- Les codes HTTP usuels (200, 201, 204, 301, 302, 304, 400, 401, 403, 404,
409, 422, 500, 502, 503, 504) sont mappés vers `&'static str` via
`fn status_label(u16) -> Cow<'static, str>` — pas d'alloc pour les status
courants.
- Les labels passent par `metrics::SharedString` (un `Cow<'static, str>`
avec variante `Shared(Arc<str>)`) qui consomme `Arc<str>` sans réallouer.
### Tests ajoutés
Dans `src/server/axum/layers/prometheus.rs` :
1. **`middleware_does_not_block_on_system_metrics`** — sentinelle de
régression : assert que la latence du middleware est < 50 ms. En 0.7 ce
test aurait mesuré ~200 ms.
2. **`collector_ticks_without_panicking`** — smoke test : vérifie que la
tâche démarre, tick au moins une fois, et n'a pas paniqué.
Run : `cargo test --all-features` → **44 tests OK + 8 doctests OK**.
---
## Vérification de performance
Mesure côté ApiRoad attendue après upgrade vers `api-tools 0.8.0` :
- `GET /health` avec `PROMETHEUS_ENABLE=1` :
- **Avant** : ~210 ms (Docker ou natif)
- **Après** : devrait s'aligner sur la latence avec Prometheus désactivé
(~2-3 ms), soit **×80 à ×100**.
Il n'y aura **plus aucune raison fonctionnelle** de désactiver Prometheus
pour la performance. La variable `PROMETHEUS_ENABLE=0` peut être supprimée
de `server/.env.docker`.
---
## Fichiers modifiés dans la crate `api-tools`
- `src/server/axum/layers/prometheus.rs` — réécriture complète : layer
allégé, suppression de `SystemMetrics` inline, ajout de
`spawn_system_metrics_collector`, optimisations labels, 2 tests ajoutés.
- `src/server/axum/handlers/prometheus.rs` — ajout de
`get_handle_with_buckets()` et de la constante publique
`DEFAULT_DURATION_BUCKETS`.
- `Cargo.toml` — bump `0.7.0 → 0.8.0`.
## Hors scope (non fait)
- **Cache du handle `gauge!()` entre ticks** dans le collecteur background.
ROI très faible (intervalle de 10 s, coût négligeable des lookups).
- **Cache du handle `counter!()` / `histogram!()` par route** dans le
middleware. Possible via une `DashMap<RouteKey, (Counter, Histogram)>`,
mais ajoute de la complexité sans résoudre de problème mesurable.