Architecture
perf-sentinel est un détecteur polyglotte d'anti-patterns de performance, construit sous forme de workspace Rust avec deux crates :
- sentinel-core : bibliothèque contenant toute la logique du pipeline
- sentinel-cli : binaire fournissant le point d'entrée CLI (
perf-sentinel)
Glossaire
Quelques termes récurrents dans la doc et le code, dont les distinctions comptent :
- Event : un SpanEvent normalisé. L'unité pré-détection (entrée).
- Finding : un anti-pattern détecté (sortie de
detect). Le concept produit, exposé dans la sortie CLI, JSON, SARIF, HTML, l'API daemon et les acks. - Pattern : le template SQL, HTTP ou messaging normalisé qui regroupe les events d'un finding (ex.
SELECT * FROM users WHERE id = ?). - Detection : l'action, ou la famille de détecteurs (
n_plus_one,chatty, ...). N'est pas synonyme definding.
Modes opérationnels :
- Mode batch (
perf-sentinel analyze) : traite un jeu complet de traces et produit un seul Report. Utilisé pour les tests d'intégration CI, l'analyse ponctuelle, etperf-sentinel report(HTML). - Mode CI (
perf-sentinel analyze --ci) : identique au batch, mais une violation du quality gate fait sortir le process en code 1. Le champConfidencede chaque finding est taguéci_batch(confiance la plus faible : variété de trafic limitée). - Mode daemon / streaming / watch (
perf-sentinel watch) : process long-running qui ingère les events OTLP/JSON en temps réel, avec éviction TTL et détection per-trace. Les findings sont taguésdaemon_stagingoudaemon_productionselon[daemon] daemon_environment(booste la sévérité chez les consommateurs downstream comme perf-lint).
L'axe Confidence (ci_batch < daemon_staging < daemon_production) est tagué automatiquement par le runtime et exposé dans JSON, SARIF, HTML et l'API de requête du daemon.
Vue d'ensemble du pipeline
Modes de fonctionnement
Mode batch (perf-sentinel analyze)
Traite un ensemble complet d'événements et produit un rapport unique avec évaluation du quality gate.
Vec<SpanEvent>
-> normalize::normalize_all() -> Vec<NormalizedEvent>
-> correlate::correlate() -> Vec<Trace>
-> detect::detect() -> Vec<Finding>
-> score::score_green() -> (Vec<Finding>, GreenSummary, Vec<PerEndpointIoOps>)
-> quality_gate::evaluate() -> QualityGate
-> Report { analysis, findings, green_summary, quality_gate }En mode CI (--ci), le processus se termine avec le code 1 si le quality gate échoue.
Mode streaming (perf-sentinel watch)
Fonctionne comme un daemon, recevant les événements en temps réel et émettant les findings au fur et à mesure de leur détection.
OTLP gRPC (port 4317) \
OTLP HTTP (port 4318) +---> canal mpsc ---> TraceWindow (LRU + TTL)
Socket unix JSON / |
+--------+--------+
| |
Éviction LRU Éviction TTL
| |
+--------+--------+
|
normalize -> detect -> score
|
NDJSON findings (stdout)
Prometheus /metrics- Les événements sont normalisés en dehors du verrou TraceWindow pour minimiser le temps de détention du verrou.
- Les routes, liens parent et destinations de consumer OTLP valides et échantillonnés de services nommés sont conservés avec les événements, afin que la fenêtre puisse résoudre une route d'entrée externe du même service, ou un span consumer, arrivé dans un export ultérieur. Les contextes d'endpoint, destinations de consumer et entrées d'ascendance sont plafonnés séparément par
max_events_per_trace, partagent le LRU et le TTL de la trace et ne comptent jamais comme événements I/O. - Les traces sont évincées lorsque le cache LRU est plein (
max_active_traces) ou lorsque le TTL expire (trace_ttl_ms). - À l'éviction, la trace est analysée via les étapes detect et score.
- Les findings sont émis en JSON délimité par des sauts de ligne sur stdout.
- Les métriques Prometheus sont exposées sur le même port HTTP (4318) à
/metrics.
API de requête du daemon
En mode watch, le daemon expose son état interne via des endpoints HTTP sur le port 4318 aux côtés de /v1/traces et /metrics :
GET /api/findings(filtrable, plafonné à 1000)GET /api/findings/{trace_id}GET /api/explain/{trace_id}(arbre de trace avec findings en ligne, depuis la mémoire du daemon)GET /api/correlations(corrélations cross-trace, plafonné à 1000)GET /api/status(uptime, traces actives, nombre de findings stockés)
Un FindingsStore (ring buffer) retient les findings récents pour l'API (dimensionné par [daemon] max_retained_findings, défaut 10k). La sous-commande compagnon perf-sentinel query rend ces endpoints en sortie coloré terminal. Gated par [daemon] api_enabled (défaut true). Voir Limites pour le threat model sans authentification.
Responsabilités des modules
| Module | Chemin | Responsabilité |
|---|---|---|
| event | event.rs | Type central SpanEvent (variantes SQL, HTTP et messaging) avec timestamp, IDs trace/span, service, opération, cible, durée |
| ingest | ingest/ | Sources d'entrée : parseur JSON avec auto-détection du format (json.rs), import Jaeger JSON (jaeger.rs), import Zipkin JSON v2 (zipkin.rs), récepteur OTLP gRPC+HTTP (otlp.rs), dont la conversion recoud les requêtes SQL qu'une instrumentation en couches a scindées entre spans porteurs du statement et spans porteurs de la durée (PHP Doctrine + PDO). Implémente le trait IngestSource. Parseur PostgreSQL pg_stat_statements (pg_stat.rs) et parseur des digests MySQL Performance Schema (mysql_stat.rs) pour l'analyse de hotspots. Récupérateurs de traces distants pour Grafana Tempo (tempo.rs) et l'API de requête Jaeger (jaeger_query.rs), avec helpers partagés pour l'en-tête d'authentification (auth_header.rs), les fenêtres de recherche en arrière (lookback.rs) et l'encodage d'URL (url_enc.rs) |
| normalize | normalize/ | Produit des NormalizedEvent avec template + paramètres extraits. Tokenizer SQL (sql.rs) : remplace les littéraux, UUIDs, listes IN. Normaliseur HTTP (http.rs) : remplace les segments numériques/UUID, supprime les paramètres de requête |
| correlate | correlate/ | Regroupe les événements par trace_id. Mode batch (mod.rs) : agrégation par HashMap. Mode streaming (window.rs) : cache LRU avec buffer circulaire par trace et éviction TTL |
| detect | detect/ | Détection de patterns sur les traces corrélées. N+1 (n_plus_one.rs) : même template, paramètres différents, dans une fenêtre. Redondant (redundant.rs) : même template et paramètres. Lent (slow.rs) : durée au-dessus du seuil avec template récurrent. Fanout (fanout.rs) : span parent avec trop de spans enfants. Service bavard (chatty.rs) : trop d'appels HTTP sortants par trace. Saturation du pool (pool_saturation.rs) : spans SQL concurrents dépassant le seuil via algorithme de balayage. Appels sérialisés (serialized.rs) : appels siblings séquentiels indépendants potentiellement parallélisables. Corrélateur inter-traces pour le mode daemon (correlate_cross.rs). Pistes de remédiation attachées aux findings (suggestions.rs). Classification sensible aux sanitizers (sanitizer_aware.rs) : reconnaît le SQL déjà paramétré pour que la détection N+1 écarte les cas sûrs (SQL uniquement aujourd'hui) |
| score | score/ | Scoring GreenOps (mod.rs) : IIS par endpoint, ratio de gaspillage, top offenders, green_impact par finding. Pipeline carbone réparti entre carbon.rs (table d'intensité grid embarquée, constantes SCI), carbon_compute.rs (boucle d'accumulation par span), carbon_profiles.rs (profils horaires de grid) et region_breakdown.rs (fold par région, sélection du model-tag, finalisation du CarbonReport). Six backends de puissance et d'énergie opt-in, chacun un sous-module structuré : alumet/, scaphandre/ et kepler/ (RAPL), redfish/ (télémétrie BMC), cloud_energy/ (CPU% + interpolation SPECpower embarquée), electricity_maps/ (intensité de grid en direct, uniquement si configuré). Plomberie partagée des scrapers : energy_state.rs (cache de coefficients par service), ops_snapshot_diff.rs (deltas d'opérations par service et par fenêtre de scrape) et prom_parser.rs (parseur d'exposition Prometheus partagé par les scrapers Kepler et Alumet). Calcul de l'évitable à seuil canonique fixe contre la manipulation des divulgations (canonical.rs). Multi-région SCI v1.0 via attribut OTel cloud.region, intervalles de confiance, profils horaires de grid pour 22 régions (mensuel x horaire complet pour FR/DE/GB/US-East, journalier représentatif pour 18 de plus) |
| report | report/ | Formatage de sortie. Rapport JSON (json.rs), export SARIF v2.1.0 (sarif.rs), sortie CLI colorée (mod.rs), métriques Prometheus avec exemplars OpenMetrics (metrics.rs), dashboard HTML autonome en un fichier (html/ + html_template.html embarqué) avec onglets Findings, Explain, pg_stat, mysql_stat, Diff, Correlations et Green. Interprétations en langage clair des findings (interpret.rs), avertissements de rapport (warnings.rs), et la pile de divulgation publique périodique (periodic/ : schéma, agrégateur, validateur, configuration d'organisation, hacheur de contenu, attestation) |
| quality_gate | quality_gate.rs | Évalue des règles de seuils configurables par rapport aux findings et au résumé green |
| pipeline | pipeline.rs | Connecte toutes les étapes pour le mode batch : normalize -> correlate -> detect -> score -> quality_gate -> Report |
| daemon | daemon/ | Mode streaming : mod.rs héberge DaemonError et l'orchestrateur run(). Responsabilités réparties entre event_loop.rs (boucle tokio::select, éviction TraceWindow, worker d'analyse unique exécutant detect/score hors de la boucle), listeners.rs (spawn OTLP gRPC/HTTP, scrapers énergie optionnels), tls.rs (chargement certs, MaybeTlsStream, boucle HTTPS avec cap de concurrence sur les handshakes), json_socket.rs (ingestion NDJSON Unix, cfg(unix)), sampling.rs (échantillonnage par hash de trace-id), findings_store.rs + query_api.rs (findings retenus et endpoints HTTP de requête), ack.rs (store d'acquittements et endpoints HTTP d'ack), health.rs (liveness et readiness), archive.rs (archive NDJSON Report par fenêtre consommée par disclose) |
| config | config/ | Parse .perf-sentinel.toml avec le format sectionné ([thresholds], [detection], [green], [daemon]). Les 8 clés top-level legacy acceptées en 0.5.x ont été retirées en 0.6.0 (voir Configuration pour la table de migration) |
| time | time.rs | Helpers de conversion timestamp partagés (nanos_to_iso8601, micros_to_iso8601). Utilisé par l'ingestion OTLP, Jaeger et Zipkin |
| explain | explain.rs | Visualiseur d'arbre de trace : construit l'arbre des spans à partir de parent_span_id, annote les findings inline (suggestion, fix par framework, code location). Sortie texte et JSON |
| diff | diff.rs | Delta de régression entre deux jeux de traces. Sous-tend la sous-commande diff utilisée en CI de PR |
| calibrate | calibrate.rs | Ajuste les coefficients I/O vers énergie à partir de traces de référence et d'un CSV de puissance mesurée. Sous-tend la sous-commande calibrate |
| acknowledgments | acknowledgments.rs | Filtrage .perf-sentinel-acknowledgments.toml pour la CI, avec une signature canonique sha2 par finding |
| http_client | http_client.rs | Le client HTTP sortant unique compatible TLS (limites de taille de corps, caviardage des endpoints) partagé par les scrapers d'énergie, l'ingestion Tempo et Jaeger, et query |
| shutdown | shutdown.rs | Future de signal d'arrêt (SIGINT partout, SIGTERM sous Unix) partagée par la boucle d'événements du daemon et la boucle de récupération Tempo |
| text_safety | text_safety.rs | Helpers purs partagés par tous les renderers texte qui impriment des chaînes contrôlées par l'attaquant vers un terminal : sanitize_for_terminal retire les caractères de contrôle ASCII pour neutraliser les injections ANSI / OSC 8 / curseur ; safe_url filtre suggested_fix.reference_url aux seules URL HTTPS sans caractères de contrôle |
Types principaux
| Type | Module | Description |
|---|---|---|
SpanEvent | event | Événement I/O brut (requête SQL ou appel HTTP) avec métadonnées et parent_span_id optionnel |
NormalizedEvent | normalize | SpanEvent enrichi d'un template normalisé et de paramètres extraits |
Trace | correlate | Collection de NormalizedEvents partageant le même trace_id |
Finding | detect | Anti-pattern détecté avec type, sévérité, détails du pattern, timestamps, green_impact et confidence (CI batch / daemon staging / daemon production) |
GreenSummary | score | Statistiques I/O agrégées : total ops, ops évitables, ratio de gaspillage, top offenders, CO2 optionnel |
QualityGate | quality_gate | Résultat pass/fail avec évaluations individuelles des règles |
Report | report | Sortie d'analyse complète : métadonnées d'analyse, findings, résumé green, quality gate |
Config | config | Configuration parsée avec toutes les sections et champs validés |
TraceWindow | correlate/window | Cache LRU/TTL de traces actives avec contexte OTLP d'endpoint et de parent borné pour l'attribution entre exports |
Frontières des crates
sentinel-cli (binaire)
|
+-- CLI clap : sous-commandes analyze / explain / diff / report / watch / query / ack / inspect / tempo / jaeger-query / pg-stat / mysql-stat / calibrate / disclose / verify-hash / hash-bake / demo / bench / man / completions
|
+-- dépend de sentinel-core (bibliothèque)
|
+-- Toute la logique du pipeline
+-- Traits uniquement aux frontières : IngestSource, ReportSink
+-- Fonctions pures entre les étapesLe crate CLI est intentionnellement léger : il parse les arguments, charge la configuration et délègue aux fonctions de sentinel-core. Toute la logique métier réside dans sentinel-core.
Features Cargo
Des sous-commandes entières sont conditionnées à la compilation par des features Cargo :
| Feature | Définie dans | Défaut | Conditionne |
|---|---|---|---|
daemon | core et cli | activée dans cli, désactivée dans core | watch, query, ack, la boucle d'événements du daemon et les scrapers d'énergie |
tui | cli uniquement | activée | inspect et les prévisualisations interactives --tui |
tempo | core et cli | activée dans cli, désactivée dans core | la sous-commande tempo (ingestion via l'API Grafana Tempo) |
jaeger-query | core et cli | activée dans cli, désactivée dans core | la sous-commande jaeger-query (ingestion via l'API de requête Jaeger) |
Le binaire publié active les quatre. Les consommateurs de la bibliothèque sentinel-core partent de tout désactivé et activent au besoin. À noter que tonic, axum, tower, prometheus et opentelemetry-proto restent des dépendances inconditionnelles de sentinel-core car report/metrics.rs et ingest/otlp/ utilisent leurs types même quand aucun écouteur ne tourne.