perf sentineldocs
ENGitHub
Documentation / API de query

API de requêtage du daemon

Le daemon perf-sentinel expose une API HTTP de requêtage qui permet à des systèmes externes de récupérer les findings, les explications de traces, les corrélations cross-trace et la liveness du daemon. Utilisez-la pour alimenter des alertes Prometheus, des dashboards Grafana, des runbooks on-call ou des scripts de gate CI personnalisés sans parser les logs NDJSON.

L'API a été livrée en v0.4.0. Cette page la documente comme surface produit de premier plan, avec un contrat de stabilité.

Sommaire

Vue d'ensemble des endpoints

MéthodeCheminRôle
GET/api/statusLiveness du daemon, version, uptime, compteurs en cours
GET/api/configConfiguration [daemon] effective, lecture seule, secrets résumés (depuis 0.8.8)
GET/api/energySanté live des backends énergie/intensité (depuis 0.8.8)
GET/api/findingsFindings récents depuis le ring buffer, avec filtres service, type et severity
GET/api/findings/{trace_id}Tous les findings d'une trace
GET/api/explain/{trace_id}Arbre de spans d'une trace encore en mémoire daemon, findings annotés en ligne
GET/api/correlationsCorrélations temporelles cross-trace actives
GET/api/export/reportSnapshot de l'état live en JSON Report, pipe-compatible avec report --input -
POST/api/findings/{signature}/ackAcquitter un finding au runtime (depuis 0.5.20)
DELETE/api/findings/{signature}/ackRévoquer un ack runtime
GET/api/acksLister les acks runtime actifs

Tous les endpoints retournent du application/json. Pas d'authentification intégrée. Le daemon écoute sur 127.0.0.1 par défaut (voir [daemon] listen_address dans Configuration), donc l'API n'est joignable que depuis l'hôte qui exécute le daemon, sauf si vous élargissez explicitement l'adresse de bind. Pour laisser les devs lire les findings tout en réservant les écritures (acks) et l'export du rapport officiel aux architectes ou au DevOps, voir Restreindre les écritures en production.

Notes de déploiement

  • L'API de requêtage partage le même port HTTP que l'ingestion OTLP HTTP ([daemon] listen_port_http, défaut 4318), l'endpoint Prometheus /metrics et la sonde de liveness GET /health. Un seul port, quatre surfaces.
  • L'API de requêtage peut être désactivée au démarrage avec [daemon] api_enabled = false. Utile quand le daemon tourne dans un hôte multi-tenant hostile et que vous ne voulez que l'ingestion OTLP. Dans ce mode, /metrics et /health restent exposés : ce sont des surfaces d'infrastructure, pas partie de l'API de requêtage.
  • Pour les sondes Kubernetes ou load-balancer, préférer GET /health à GET /api/status : /health est toujours actif, ne prend aucun lock et reste réactif sous toute charge d'ingestion.
  • La taille du ring buffer (un buffer circulaire à taille fixe qui évince les plus anciennes entrées une fois plein) des findings est bornée par [daemon] max_retained_findings (défaut 10000). Les findings plus anciens sont évincés en FIFO.

Restreindre les écritures en production (reverse proxy)

Un besoin de production fréquent : laisser n'importe quel dev lire les findings tout en réservant les chemins d'écriture (acquitter et révoquer) ainsi que l'export du rapport officiel aux architectes ou au DevOps. Cela empêche qu'un finding soit acquitté sans l'aval des personnes responsables de la posture de production.

Le daemon ne porte ni fournisseur d'identité ni modèle de rôles. La clé optionnelle [daemon.ack] api_key (voir POST /api/findings/{signature}/ack) est un secret unique partagé : elle garde les écritures grossièrement, mais ne distingue pas un utilisateur d'un autre et ne sait pas exprimer "ce groupe peut, cet autre non". Pour une autorisation par identité, placez un reverse proxy devant le daemon. Le proxy authentifie chaque appelant contre votre SSO, puis autorise selon la méthode HTTP et le chemin. Le daemon reste un pur moteur d'analyse, ce qui colle à son design (pas de surface réseau implicite, pas d'IAM embarqué).

La règle appliquée par le proxy :

CheminGETPOST / DELETE
/api/findings, /api/explain/..., /api/correlations, /api/status, /api/config, /api/energy, /api/ackstout utilisateur authentifiésans objet
/api/findings/{signature}/acksans objetgroupe privilégié uniquement
/api/export/reportgroupe privilégié uniquementsans objet

/api/export/report est dans la colonne privilégiée parce qu'il matérialise le snapshot Report complet qui alimente le dashboard HTML officiel. Produire un rapport officiel est en soi une action privilégiée, voir Divulgation pour le pendant côté CI (qui peut lancer disclose --intent official).

oauth2-proxy + nginx

oauth2-proxy gère l'authentification OIDC et expose l'identité authentifiée sous forme de headers de réponse. Son endpoint /oauth2/auth impose aussi l'appartenance à un groupe par requête via le paramètre de requête allowed_groups, donc la décision d'autorisation est prise par oauth2-proxy, pas par une logique if nginx fragile. nginx route les chemins privilégiés vers une sous-requête d'auth contrôlée par groupe et tout le reste vers une sous-requête simple.

oauth2-proxy.cfg (mode auth-only, c'est nginx qui proxifie) :

ini
provider          = "oidc"
oidc_issuer_url   = "https://sso.example.com/realms/prod"
client_id         = "perf-sentinel"
client_secret     = "${OAUTH2_PROXY_CLIENT_SECRET}"   # depuis votre gestionnaire de secrets, jamais commité
cookie_secret     = "${OAUTH2_PROXY_COOKIE_SECRET}"   # base64 sur 32 octets
email_domains     = ["example.com"]
upstreams         = ["static://202"]   # auth-only : retourne 202 au succès, nginx proxifie le daemon
reverse_proxy     = true
set_xauthrequest  = true               # émet X-Auth-Request-User / -Email / -Groups
oidc_groups_claim = "groups"           # pour que le claim de groupe arrive jusqu'à nginx
scope             = "openid email groups"

nginx.conf (bloc server pertinent) :

nginx
upstream perf_sentinel { server 127.0.0.1:4318; }   # daemon, loopback-only
upstream oauth2_proxy  { server 127.0.0.1:4180; }

server {
    listen 443 ssl;
    server_name perf-sentinel.internal;
    # ssl_certificate / ssl_certificate_key ...

    # Routes de sign-in et de callback d'oauth2-proxy.
    location /oauth2/ {
        proxy_pass        http://oauth2_proxy;
        proxy_set_header  Host                     $host;
        proxy_set_header  X-Real-IP                $remote_addr;
        proxy_set_header  X-Forwarded-Proto        $scheme;
        proxy_set_header  X-Auth-Request-Redirect  $request_uri;
    }

    # Authentification simple : toute session SSO valide.
    location = /oauth2/auth {
        internal;
        proxy_pass               http://oauth2_proxy;
        proxy_pass_request_body  off;
        proxy_set_header         Content-Length "";
        proxy_set_header         X-Original-URI $request_uri;
    }

    # Authentification contrôlée par groupe : oauth2-proxy retourne 403
    # quand l'appelant n'est pas dans le groupe, ce qu'auth_request
    # propage en 403.
    location = /oauth2/auth-admin {
        internal;
        proxy_pass               http://oauth2_proxy/oauth2/auth?allowed_groups=perf-sentinel-admins;
        proxy_pass_request_body  off;
        proxy_set_header         Content-Length "";
        proxy_set_header         X-Original-URI $request_uri;
    }

    # Routes privilégiées : créer/révoquer un ack et l'export du rapport
    # officiel. Une location regex l'emporte sur le préfixe /api/, donc
    # elles ne retombent jamais sur la règle ouverte ci-dessous.
    location ~ ^/api/(findings/[^/]+/ack|export/report)$ {
        auth_request /oauth2/auth-admin;
        error_page 401 = /oauth2/sign_in;
        auth_request_set $auth_user $upstream_http_x_auth_request_user;
        proxy_set_header X-User-Id $auth_user;   # écrase toute valeur fournie par le client
        proxy_pass       http://perf_sentinel;
        proxy_set_header Host $host;
    }

    # Tout le reste sous /api/ : lecture pour tout utilisateur authentifié.
    location /api/ {
        auth_request /oauth2/auth;
        error_page 401 = /oauth2/sign_in;
        auth_request_set $auth_user $upstream_http_x_auth_request_user;
        proxy_set_header X-User-Id $auth_user;
        proxy_pass       http://perf_sentinel;
        proxy_set_header Host $host;
    }
}

Pourquoi c'est sûr

  • Bindez le daemon en loopback ([daemon] listen_address = "127.0.0.1") ou sur une interface interne que seul le proxy atteint. Le proxy est la seule porte d'entrée.
  • Gardez [daemon.ack] api_key défini comme second facteur. Si quelqu'un atteint le port du daemon en direct, en contournant le proxy, il ne peut toujours pas écrire sans la clé.
  • Le daemon fait confiance à X-User-Id pour le champ d'audit by. Le bloc nginx le pose depuis la sous-requête authentifiée ($auth_user) et écrase donc toute valeur fournie par le client, ce qui ferme la faille de spoofing. L'identité authentifiée atterrit alors dans le store JSONL d'acks, vous donnant une piste d'audit de qui a acquitté quoi.
  • perf-sentinel-admins est illustratif. Utilisez le groupe que votre IdP expose dans le claim groups.

Endpoints

GET /api/status

Retourne un objet de liveness compact. Utilisez-le comme readiness probe ou comme moyen le moins coûteux de vérifier que le daemon est up.

Paramètres de requête : aucun.

Forme de la réponse :

ChampTypeDescription
versionstringVersion du binaire daemon (version du package Cargo)
uptime_secondsnumberSecondes depuis le démarrage du processus daemon
active_tracesnumberTraces actuellement présentes dans la fenêtre de corrélation
max_active_tracesnumberPlafond configuré de la fenêtre de corrélation (depuis 0.8.8)
analysis_queue_depthnumberBatches en attente dans la file du worker d'analyse (depuis 0.8.8)
analysis_queue_capacitynumberPlafond configuré de cette file (depuis 0.8.8)
stored_findingsnumberFindings actuellement retenus dans le ring buffer
max_retained_findingsnumberPlafond configuré de ce ring buffer (depuis 0.8.8)

Les trois paires gauge/plafond alimentent le graphe Headroom de l'onglet Trends de perf-sentinel query monitor : chaque paire se lit comme "à quel point cette gauge runtime approche de son plafond configuré". Le conseiller de réglages commence à émettre des hints à 90 % de max_active_traces. Les champs sont additifs, les clients écrits contre des daemons plus anciens continuent de parser.

Exemple :

bash
curl -sS http://127.0.0.1:4318/api/status
json
{
  "version": "0.8.8",
  "uptime_seconds": 48,
  "active_traces": 12,
  "max_active_traces": 10000,
  "analysis_queue_depth": 0,
  "analysis_queue_capacity": 1024,
  "stored_findings": 5,
  "max_retained_findings": 10000
}

GET /api/config

La configuration [daemon] effective du daemon, en lecture seule (depuis 0.8.8). Alimente l'onglet Config de perf-sentinel query monitor. Construite en liste blanche explicite, jamais une sérialisation brute de la config interne, donc aucun secret n'est exposé : les chemins de cert/clé TLS et la clé d'API ack sont résumés en booléens (tls_configured, ack_api_key_set) et jamais renvoyés. Les valeurs sont figées au démarrage du daemon.

Paramètres de requête : aucun.

Forme de réponse : un objet avec les scalaires [daemon] (listen_addr, listen_port, listen_port_grpc, json_socket, max_active_traces, trace_ttl_ms, sampling_rate, max_events_per_trace, max_payload_size, environment, max_retained_findings, ingest_queue_capacity, analysis_queue_capacity, api_enabled), les sous-systèmes résumés (tls_configured, ack_enabled, ack_api_key_set, cors_allowed_origins, archive_configured) et le bloc de corrélation (correlation_enabled, correlation_window_ms, correlation_lag_threshold_ms, correlation_min_co_occurrences, correlation_min_confidence, correlation_max_tracked_pairs).

Exemple :

bash
curl -sS http://127.0.0.1:4318/api/config
json
{
  "listen_addr": "127.0.0.1",
  "listen_port": 4318,
  "max_active_traces": 10000,
  "trace_ttl_ms": 30000,
  "sampling_rate": 1.0,
  "environment": "staging",
  "api_enabled": true,
  "tls_configured": false,
  "ack_enabled": true,
  "ack_api_key_set": false,
  "cors_allowed_origins": [],
  "archive_configured": false,
  "correlation_enabled": false,
  "correlation_max_tracked_pairs": 10000
}

(Champs abrégés ci-dessus, la réponse live porte l'ensemble complet listé sous Forme de réponse.)

GET /api/energy

Santé live des cinq backends énergie/intensité (depuis 0.8.8) : les quatre sources d'énergie mesurée scrappées (Scaphandre, Kepler, Redfish, SPECpower cloud) et l'API d'intensité temps réel Electricity Maps. Alimente l'onglet Scrapers de perf-sentinel query monitor. Le mix effectif lui-même (quelle source a gagné la chaîne de précédence par service, intensité de grille par région) vit sur /api/export/report sous green_summary, cet endpoint répond seulement "chaque backend est-il configuré, frais, et en succès".

Paramètres de requête : aucun.

Forme de réponse : un objet avec un tableau backends de cinq entrées dans un ordre fixe (scaphandre, kepler, redfish, cloud_energy, electricity_maps), chacune :

ChampTypeDescription
backendstringNom stable du backend
configuredbooleanSi le backend est configuré, d'après la config [green] figée au démarrage du daemon
last_scrape_age_secondsnumberSecondes depuis le dernier scrape réussi, à la date du dernier tick du scraper (mêmes sémantiques que la gauge /metrics). Omis si non configuré ou si le backend n'a pas de gauge de fraîcheur
scrapes_oknumberScrapes réussis depuis le démarrage. Omis si non configuré ou non scrappé (cloud_energy, electricity_maps)
scrapes_failednumberScrapes échoués depuis le démarrage. Mêmes règles d'omission que scrapes_ok

Les champs optionnels sont omis plutôt que mis à zéro pour les backends non configurés : les gauges Prometheus sous-jacentes sont pré-enregistrées à 0, et un 0 littéral se lirait comme un scrape frais. electricity_maps n'a pas de gauge de fraîcheur par construction, sa vivacité se lit dans les entrées intensity_source = "real_time" du breakdown par région du report.

Deux précautions de lecture sur l'âge. Un backend configuré lit encore last_scrape_age_seconds = 0.0 pendant son premier intervalle de scrape après le démarrage du daemon, avant le moindre scrape effectif : le lire avec scrapes_ok = 0 pour distinguer "pas encore scrappé" de "frais". Et pour cloud_energy, l'âge trace la joignabilité de l'endpoint Prometheus configuré, pas la couverture par service : un tick compte comme réussi dès qu'un service produit une lecture.

Exemple :

bash
curl -sS http://127.0.0.1:4318/api/energy
json
{
  "backends": [
    {
      "backend": "scaphandre",
      "configured": true,
      "last_scrape_age_seconds": 3.0,
      "scrapes_ok": 120,
      "scrapes_failed": 2
    },
    { "backend": "kepler", "configured": false },
    { "backend": "redfish", "configured": false },
    { "backend": "cloud_energy", "configured": false },
    { "backend": "electricity_maps", "configured": true }
  ]
}

GET /api/findings

Retourne un tableau JSON des findings récents, du plus récent au plus ancien. Chaque élément encapsule le finding lui-même plus un timestamp d'ingestion côté daemon.

Paramètres de requête :

NomTypeDéfautDescription
servicestringaucunMatch exact sur le champ finding.service
typestringaucunMatch exact sur finding.type en snake_case (ex. n_plus_one_sql, redundant_sql)
severitystringaucunMatch exact sur finding.severity en snake_case (critical, warning, info)
limitinteger100Nombre maximum d'entrées retournées, capé côté serveur à 1000 (les valeurs supérieures sont silencieusement ramenées)

Les paramètres inconnus sont ignorés. Les valeurs malformées (ex. limit=abc) retournent un HTTP 400 avec un corps d'erreur généré par axum.

Forme de la réponse : tableau de StoredFinding. Chaque StoredFinding contient :

  • finding : le finding détecté. Voir le schéma Finding ci-dessous.
  • stored_at_ms : timestamp Unix entier en millisecondes, enregistré au moment où le daemon a inséré ce finding dans le ring buffer.

Exemple :

bash
curl -sS "http://127.0.0.1:4318/api/findings?severity=warning&limit=2"
json
[
  {
    "finding": {
      "type": "n_plus_one_sql",
      "severity": "warning",
      "trace_id": "trace-n1-sql",
      "service": "order-svc",
      "source_endpoint": "POST /api/orders/42/submit",
      "pattern": {
        "template": "SELECT * FROM order_item WHERE order_id = ?",
        "occurrences": 6,
        "window_ms": 250,
        "distinct_params": 6
      },
      "suggestion": "Use WHERE ... IN (?) to batch 6 queries into one",
      "first_timestamp": "2025-07-10T14:32:01.000Z",
      "last_timestamp": "2025-07-10T14:32:01.250Z",
      "green_impact": {
        "estimated_extra_io_ops": 5,
        "io_intensity_score": 6.0,
        "io_intensity_band": "high"
      },
      "confidence": "daemon_staging"
    },
    "stored_at_ms": 1776350162450
  },
  {
    "finding": {
      "type": "n_plus_one_http",
      "severity": "warning",
      "trace_id": "trace-n1-http",
      "service": "order-svc",
      "source_endpoint": "POST /api/orders/42/submit",
      "pattern": {
        "template": "GET /api/users/{id}",
        "occurrences": 6,
        "window_ms": 200,
        "distinct_params": 6
      },
      "suggestion": "Use batch endpoint with ?ids=... to batch 6 calls into one",
      "first_timestamp": "2025-07-10T14:32:01.000Z",
      "last_timestamp": "2025-07-10T14:32:01.200Z",
      "green_impact": {
        "estimated_extra_io_ops": 5,
        "io_intensity_score": 6.0,
        "io_intensity_band": "high"
      },
      "confidence": "daemon_staging"
    },
    "stored_at_ms": 1776350162450
  }
]

Schéma Finding

L'objet finding exposé par /api/findings et /api/findings/{trace_id} est identique au JSON émis par perf-sentinel analyze --format json. Champs stables à partir de v0.4.1 :

ChampTypeDescription
typestring (enum)n_plus_one_sql, n_plus_one_http, redundant_sql, redundant_http, slow_sql, slow_http, excessive_fanout, chatty_service, pool_saturation, serialized_calls
severitystring (enum)critical, warning, info
trace_idstringTrace ID où le pattern a été détecté
servicestringService qui a émis l'anti-pattern
source_endpointstringEndpoint entrant normalisé qui héberge le pattern
patternobject{ template, occurrences, window_ms, distinct_params }
suggestionstringIndication de remédiation lisible
first_timestampstring (ISO 8601)Premier span du groupe détecté
last_timestampstring (ISO 8601)Dernier span du groupe détecté
confidencestring (enum)ci_batch, daemon_staging, daemon_production
green_impactobject (optionnel){ estimated_extra_io_ops, io_intensity_score, io_intensity_band } quand le scoring green est activé
code_locationobject (optionnel){ function?, filepath?, lineno?, namespace? } quand les attributs OTel code.* sont présents
suggested_fixobject (optionnel){ pattern, framework, recommendation, reference_url? } quand le framework peut être inféré (Java/JPA en v1)

GET /api/findings/{trace_id}

Retourne tous les findings dont le trace_id matche le segment de chemin, sous forme de tableau JSON. Même forme d'élément que /api/findings. Le cap dur de 1000 entrées s'applique (traces pathologiques avec des centaines de clusters N+1).

Paramètre de chemin : trace_id (string, match exact). Le segment est URL-décodé par axum avant comparaison.

Forme de la réponse : même Vec<StoredFinding> que /api/findings. Un tableau vide [] est retourné quand le trace ID est inconnu (l'endpoint ne renvoie pas 404).

Exemple :

bash
curl -sS "http://127.0.0.1:4318/api/findings/trace-n1-sql"
json
[
  {
    "finding": {
      "type": "n_plus_one_sql",
      "severity": "warning",
      "trace_id": "trace-n1-sql",
      "service": "order-svc",
      "source_endpoint": "POST /api/orders/42/submit",
      "pattern": {
        "template": "SELECT * FROM order_item WHERE order_id = ?",
        "occurrences": 6,
        "window_ms": 250,
        "distinct_params": 6
      },
      "suggestion": "Use WHERE ... IN (?) to batch 6 queries into one",
      "first_timestamp": "2025-07-10T14:32:01.000Z",
      "last_timestamp": "2025-07-10T14:32:01.250Z",
      "green_impact": {
        "estimated_extra_io_ops": 5,
        "io_intensity_score": 6.0,
        "io_intensity_band": "high"
      },
      "confidence": "daemon_staging"
    },
    "stored_at_ms": 1776350162450
  }
]

GET /api/explain/{trace_id}

Retourne l'arbre de spans d'une trace encore présente dans la fenêtre de corrélation du daemon (TTL par défaut : 30 secondes après l'arrivée du dernier span de la trace). Utile pour debugger une trace live juste après son émission.

Important : les findings sont retenus dans le ring buffer longtemps après que la trace elle-même ait été évincée de la fenêtre. Cela veut dire que /api/findings/{trace_id} continue à fonctionner pendant des heures après que la trace a disparu, mais que /api/explain/{trace_id} ne fonctionne que pendant la TTL de la fenêtre.

Paramètre de chemin : trace_id (string, match exact).

Forme de la réponse (trace en mémoire) : objet avec un tableau roots. Chaque nœud décrit un span avec :

ChampTypeDescription
span_idstringIdentifiant du span
parent_span_idstring \nullIdentifiant du span parent, null pour les spans racines
servicestringService qui a émis le span
operationstringNom de l'opération (ex. SELECT, GET, POST)
templatestringRequête SQL ou route HTTP normalisée
timestampstringTimestamp de début ISO 8601
duration_usnumberDurée en microsecondes
findingsarrayFindings rattachés à ce span, chacun { type, severity, suggestion, occurrences }
childrenarrayNœuds spans enfants, récursif

Forme de la réponse (trace inconnue ou évincée) : un objet avec un seul champ error.

Exemples :

bash
# Trace encore en mémoire
curl -sS "http://127.0.0.1:4318/api/explain/trace-n1-sql"
json
{
  "roots": [
    {
      "children": [],
      "duration_us": 800,
      "findings": [
        {
          "occurrences": 6,
          "severity": "warning",
          "suggestion": "Use WHERE ... IN (?) to batch 6 queries into one",
          "type": "n_plus_one_sql"
        }
      ],
      "operation": "SELECT",
      "parent_span_id": null,
      "service": "order-svc",
      "span_id": "span-1",
      "template": "SELECT * FROM order_item WHERE order_id = ?",
      "timestamp": "2025-07-10T14:32:01.000Z"
    }
  ]
}
bash
# Trace pas en mémoire (évincée ou jamais vue)
curl -sS "http://127.0.0.1:4318/api/explain/trace-does-not-exist"
json
{
  "error": "trace not found in daemon memory"
}

GET /api/correlations

Retourne les corrélations temporelles cross-trace actives, triées par confiance décroissante. Tableau vide quand [daemon.correlation] enabled = false (défaut). Capé à 1000 entrées.

Paramètres de requête : aucun.

Forme de la réponse : tableau de CrossTraceCorrelation. Chaque entrée contient :

ChampTypeDescription
sourceobjectEndpoint en tête : { finding_type, service, template }
targetobjectEndpoint en queue observé après source dans lag_threshold_ms
co_occurrence_countnumberNombre de co-occurrences dans la fenêtre roulante
source_total_occurrencesnumberOccurrences totales de source dans la fenêtre roulante
confidencenumberRatio co_occurrence_count / source_total_occurrences
median_lag_msnumberLag médian entre source et target
first_seenstringTimestamp ISO 8601 de la première co-occurrence
last_seenstringTimestamp ISO 8601 de la co-occurrence la plus récente

Exemple :

bash
curl -sS "http://127.0.0.1:4318/api/correlations"
json
[
  {
    "source": {
      "finding_type": "redundant_sql",
      "service": "cache-svc",
      "template": "SELECT * FROM settings WHERE key = ?"
    },
    "target": {
      "finding_type": "n_plus_one_sql",
      "service": "order-svc",
      "template": "SELECT * FROM order_item WHERE order_id = ?"
    },
    "co_occurrence_count": 2,
    "source_total_occurrences": 1,
    "confidence": 2.0,
    "median_lag_ms": 0.0,
    "first_seen": "2026-04-16T14:36:02.450Z",
    "last_seen": "2026-04-16T14:36:02.450Z"
  }
]

GET /api/export/report

Snapshot de l'état interne courant du daemon sous forme de JSON Report, avec la même forme que perf-sentinel analyze --format json. Ferme la boucle entre le daemon live et le dashboard HTML perf-sentinel report post-mortem : le rapport HTML peut ingérer un snapshot daemon via HTTP par simple composition shell.

La section analysis reflète les compteurs lifetime du daemon (cumulatifs depuis le démarrage). Le champ green_summary est rafraîchi par l'event loop après chaque batch (régions, top offenders, ratio d'I/O évitables, chiffres CO2, scoring config), donc le snapshot porte une photo CO2 vivante. Le bandeau de chips et le tab GreenOps du dashboard HTML apparaissent naturellement sur les daemons configurés avec Electricity Maps. La quality gate n'est pas recalculée sur le chemin snapshot. Voir 05 · GreenOps & carbon pour le récit complet du chemin d'audit.

Comportement cold-start. Quand le daemon n'a encore traité aucun événement, l'endpoint retourne 200 OK avec une enveloppe Report vide : findings: [], green_summary: GreenSummary::disabled(0), et warnings: ["daemon has not yet processed any events"]. Avant 0.5.16 ce chemin retournait 503 Service Unavailable, ce qui faisait basculer les probes Kubernetes et confondait les scripts CI qui traitent 5xx comme un problème de santé du daemon. L'enveloppe vide permet aux clients de distinguer "cold start" de "événements vus, zéro finding" (ce dernier retourne 200 sans warning et avec analysis.events_processed > 0) sans déclencher un code de statut trompeur. La double garde (events_processed_total > 0 ET traces_analyzed_total > 0) reste préservée en interne pour que le snapshot reste cohérent durant la fenêtre trace_ttl_ms / 2 entre le premier event ingéré et le premier eviction tick.

Métrique Prometheus. Chaque requête incrémente perf_sentinel_export_report_requests_total, les opérateurs peuvent donc dashboarder ou alerter sur la fréquence des snapshots.

Exemple :

bash
# Matérialiser un snapshot daemon live en dashboard HTML
curl -s http://daemon.internal:4318/api/export/report \
    | perf-sentinel report --input - --output report.html

La sous-commande report auto-détecte la forme JSON : un tableau au top-level est traité comme des événements de trace (passés dans normalize + detect + score), un objet au top-level est traité comme un Report pré-calculé (pris tel quel). L'onglet Correlations du dashboard HTML s'active automatiquement quand le Report produit par le daemon porte des correlations non vides.

POST /api/findings/{signature}/ack

Acquitter un finding au runtime. La signature est le canonique <finding_type>:<service>:<sanitized_endpoint>:<sha256-prefix> produit par la même logique de hash que le workflow TOML CI (voir Acquittements). Disponible depuis 0.5.20.

Le daemon maintient un store JSONL append-only à ~/.local/share/perf-sentinel/acks.jsonl par défaut (configurable via [daemon.ack] storage_path). Le store est rejoué et compacté à chaque redémarrage du daemon, donc une boucle de churn ack/unack ne peut pas s'accumuler à l'infini.

Headers :

  • Content-Type: application/json (requis, même avec un body vide).
  • X-User-Id: <identifiant> (optionnel, alimente le champ d'audit by avec priorité sur le body JSON, fallback sur "anonymous").
  • X-API-Key: <secret> (requis uniquement quand [daemon.ack] api_key est défini dans la config daemon, comparaison constant-time).

Body (tous champs optionnels) :

json
{
  "by": "alice@example.com",
  "reason": "différé au prochain trimestre, voir TICKET-1234",
  "expires_at": "2026-08-01T00:00:00Z"
}

Réponses :

StatutCondition
201Ack créé
400La signature ne matche pas le format canonique
401[daemon.ack] api_key est défini, header manquant ou mauvais
409La signature est déjà acquittée (utiliser DELETE d'abord)
503[daemon.ack] enabled = false, le store ack runtime est offline

Exemple :

bash
SIG="n_plus_one_sql:order-svc:_api_v1_orders:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
curl -fsS -X POST "http://127.0.0.1:4318/api/findings/${SIG}/ack" \
  -H "Content-Type: application/json" \
  -H "X-User-Id: alice@example.com" \
  -d '{"reason":"différé au prochain trimestre","expires_at":"2026-08-01T00:00:00Z"}'
# 201 Created

Après un ack réussi, GET /api/findings filtre l'entrée par défaut. Passer ?include_acked=true pour la voir réapparaître avec une annotation acknowledged_by.

DELETE /api/findings/{signature}/ack

Révoquer un ack daemon précédemment créé. Mêmes headers d'auth que POST. Le finding correspondant réapparaît dans GET /api/findings immédiatement.

Réponses :

StatutCondition
204Ack révoqué
400La signature ne matche pas le format canonique
401API key requise et manquante ou mauvaise
404La signature n'est pas actuellement acquittée daemon
503Store ack runtime offline

Note : cet endpoint ne révoque que les acks daemon. Les acks TOML CI sont en lecture seule au runtime et nécessitent une PR contre le fichier .perf-sentinel-acknowledgments.toml pour être supprimés.

GET /api/acks

Retourne le tableau des acks runtime actifs (post-replay, post-filtre d'expiration). Lecture seule, pas d'auth requise (les lectures sur une API loopback sont considérées sûres même quand le daemon impose une clé d'API en écriture).

Réponse : tableau d'objets, un par ack actif :

json
[
  {
    "action": "ack",
    "signature": "n_plus_one_sql:order-svc:_api_v1_orders:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "by": "alice@example.com",
    "reason": "différé au prochain trimestre",
    "at": "2026-05-04T13:30:00Z",
    "expires_at": "2026-08-01T00:00:00Z"
  }
]

Cet endpoint n'expose que les acks JSONL côté daemon. Les acks TOML CI chargés au startup ne sont pas inclus, requêter le fichier TOML directement pour cette vue, ou appeler GET /api/findings?include_acked=true et inspecter le champ acknowledged_by.source pour voir les deux sources unifiées.

Interop TOML et JSONL

Le daemon lit .perf-sentinel-acknowledgments.toml (chemin configurable via [daemon.ack] toml_path) au startup et union ses entrées avec le store JSONL au query time. TOML wins on conflict : quand une signature est acquittée dans les deux, la réponse porte la métadonnée TOML (source: "toml"). Cela garde la baseline CI immutable côté daemon, un SRE ne peut pas accidentellement override ce que l'équipe a validé en review PR.

SourcePersistanceAuditMutable au runtime
TOMLFichier du repogit logNon (PR-only)
Daemonacks.jsonl sur disqueJSONL append + compactionOui (POST/DELETE)

Behavior change en 0.5.20 : filtre par défaut sur /api/findings

GET /api/findings (et les filtres ?service= / ?type= / ?severity=) omettent désormais les findings acquittés par défaut. Passer ?include_acked=true pour restaurer le comportement pré-0.5.20. Le défaut opt-in mire la sémantique CLI 0.5.17 --acknowledgments : un opérateur regardant "qu'est-ce qui est cassé maintenant" ne devrait pas être noyé par des entrées que l'équipe a déjà triées.

Les endpoints /api/findings/{trace_id} et /api/export/report gardent intentionnellement leur shape précédent, les vues per-trace et report complet sont diagnostiques et peuvent avoir besoin de remonter les findings acquittés même dans le chemin par défaut.

Réponses d'erreur

ConditionStatusCorps
trace_id inconnu sur /api/findings/{trace_id}200[]
trace_id inconnu sur /api/explain/{trace_id}200{"error": "trace not found in daemon memory"}
Corrélations désactivées ou correlator inactif200[]
/api/export/report sur daemon cold-start200enveloppe Report vide avec warnings: ["daemon has not yet processed any events"] (avant 0.5.16 : 503)
Paramètre de requête malformé (ex. limit=abc)400erreur en texte brut générée par axum
Chemin inconnu (ex. /api/does-not-exist)404corps vide
Méthode autre que GET405erreur en texte brut générée par axum

L'API n'émet pas de 5xx en fonctionnement normal. Un crash du processus retourne ce que la pile TCP émet (connection reset).

Cas d'usage

Alerting Prometheus sur les findings critiques

Faites tourner un Prometheus Blackbox exporter qui scrape /api/findings?severity=critical&limit=1 et alerte quand le tableau de réponse est non-vide. Exemple de règle AlertManager utilisant un vector_count calculé par une recording rule :

yaml
groups:
  - name: perf-sentinel
    rules:
      - alert: PerfSentinelCriticalFinding
        expr: perf_sentinel_findings_total{severity="critical"} > 0
        for: 2m
        labels:
          severity: page
        annotations:
          summary: "perf-sentinel a détecté un anti-pattern de performance critique"
          description: |
            Compteur de findings critiques: {{ $value }}.
            Interrogez `/api/findings?severity=critical` sur le daemon pour les détails.

L'endpoint Prometheus intégré à /metrics expose déjà perf_sentinel_findings_total{type,severity} comme compteur, donc vous n'avez pas besoin de l'API de requêtage pour compter les alertes. Utilisez l'API de requêtage pour récupérer le payload (template, trace ID, suggestion) que le handler d'alerte inclut dans la notification.

Dashboard Grafana custom via le datasource JSON

Installez le plugin Grafana JSON API datasource, pointez-le vers le daemon et construisez des tableaux par service. Exemple de requête de panel qui retourne les 20 findings les plus récents pour order-svc :

URL :     http://perf-sentinel.internal:4318/api/findings
Méthode : GET
Params :  service=order-svc
          limit=20
Champs :  $.finding.type,
          $.finding.severity,
          $.finding.pattern.template,
          $.finding.pattern.occurrences,
          $.finding.source_endpoint,
          $.stored_at_ms

Couplez cela avec l'endpoint Prometheus /metrics déjà exposé par le daemon pour les tendances time-series et utilisez l'API de requêtage pour la liste de findings concrets sur lesquels l'utilisateur peut cliquer.

Runbook SRE : page sur scraper bloqué

Si votre daemon a un scraper opt-in configuré ([green.scaphandre], [green.cloud], [green.electricity_maps], [pg_stat]), une stagnation dans active_traces ou la croissance de stored_findings est un signal fort que l'ingestion est bloquée. Snippet bash à embarquer dans un runbook on-call :

bash
#!/usr/bin/env bash
set -euo pipefail

DAEMON="${DAEMON:-http://127.0.0.1:4318}"
response=$(curl -sSf --max-time 3 "${DAEMON}/api/status")
uptime=$(echo "$response" | jq -r '.uptime_seconds')
traces=$(echo "$response" | jq -r '.active_traces')
findings=$(echo "$response" | jq -r '.stored_findings')

if [ "$uptime" -gt 300 ] && [ "$traces" -eq 0 ] && [ "$findings" -eq 0 ]; then
  echo "Le daemon perf-sentinel est inactif depuis ${uptime}s sans traces ni findings"
  echo "Vérifier le chemin d'ingestion: endpoint OTLP, config collector, env vars Java agent"
  exit 1
fi

Branchez ceci à PagerDuty ou OpsGenie via l'outil d'escalation on-call de votre choix.

Contrat de stabilité

L'API de requêtage porte une promesse de stabilité à partir de v0.4.1.

Ce qui est stable :

  • Tous les chemins listés dans Vue d'ensemble des endpoints.
  • Tous les champs listés dans les sections d'endpoints ci-dessus. Les noms et formes de champs ne seront pas renommés, retirés ou retypés dans une release mineure.
  • Les valeurs d'enum (finding.type, finding.severity, finding.confidence, io_intensity_band, etc.) : les variantes existantes restent. De nouvelles variantes peuvent être ajoutées dans les releases mineures. Les clients doivent tolérer les valeurs d'enum inconnues sans crasher.
  • Le comportement des cinq réponses d'erreur dans Réponses d'erreur.

Ce qui peut changer dans une release mineure :

  • De nouveaux champs optionnels peuvent être ajoutés à n'importe quel objet JSON.
  • De nouvelles variantes d'enum peuvent être ajoutées.
  • De nouveaux endpoints sous /api/... peuvent être introduits.
  • Les valeurs par défaut (ex. limit=100) peuvent être ajustées si le profilage montre un meilleur défaut, mais le cap dur (1000) ne se réduira pas.

Ce qui requiert une release majeure :

  • Retirer ou renommer un champ.
  • Retyper un champ (ex. transformer un number en string).
  • Réduire le cap dur sur /api/findings?limit=.
  • Changer la surface d'authentification (le contrat actuel est non authentifié, loopback-only par défaut).

Guide pour les clients :

  • Toujours tolérer les champs inconnus dans les objets JSON.
  • Ne jamais parser les variantes d'enum de manière exhaustive sans branche fallback.
  • Pinner la version du daemon dans vos manifestes CI/CD et lire le CHANGELOG.md avant de monter de version.

Voir aussi