Guide de déploiement Helm
Ce sujet existe en version animée : voir les déploiements sur la carte du synoptique →Ce guide décrit le déploiement de Perf Sentinel sur Kubernetes via le chart Helm packagé sous charts/perf-sentinel/. Le chart déploie le daemon (perf-sentinel watch) derrière un Service ClusterIP qui expose OTLP gRPC (4317) et OTLP HTTP plus /metrics plus /api/* (4318).
Pour une alternative sans Helm, voir les manifests bruts dans Instrumentation.
Sommaire
- TL;DR : commande d'installation en un bloc.
- Topologie : pourquoi le chart est sentinel-only, et où placer le sampling du collector par rapport au daemon.
- Installation depuis le registre OCI : chemin d'installation production avec vérification Cosign.
- Artifact Hub : référencement et métadonnées.
- Chaîne d'approvisionnement logicielle : signatures Cosign keyless, provenance SLSA, SBOM, attestation public-good.
- Installation depuis un checkout local : pour les contributeurs et le bisect.
- Couper une nouvelle release de chart : tâche mainteneur, renvoie vers RELEASE-PROCEDURE.
- Modes de workload : trois valeurs de
workload.kindau choix, et les comptes de détections avec plusieurs pods. - Surface de configuration : valeurs du chart pour
.perf-sentinel.toml, plus fragments, secrets, TLS, NetworkPolicy et l'Ingress optionnel. - Observabilité : Prometheus ServiceMonitor, les tableaux de bord Grafana (métriques et la table des findings), alertes et exemplars.
- Mise à jour : flux
helm upgrade. - Désinstallation : flux
helm uninstall. - Exemple bout en bout : exemple complet composant le chart avec le chart upstream OpenTelemetry Collector.
TL;DR
helm install perf-sentinel oci://ghcr.io/institut-du-numerique-responsable/charts/perf-sentinel \
--version 0.9.21 \
--namespace observability --create-namespace
kubectl --namespace observability get pods -l app.kubernetes.io/name=perf-sentinelChaque release publiée est signée Cosign en mode keyless, livrée avec une attestation de provenance de build SLSA v1.0, et livrée avec un SBOM SPDX. Voir Chaîne d'approvisionnement logicielle ci-dessous pour les contrôles avant installation.
Une fois le pod prêt, pointez votre OpenTelemetry Collector vers perf-sentinel.observability.svc.cluster.local:4317 (gRPC) ou :4318 (HTTP). Un exemple complet qui compose Perf Sentinel avec le chart upstream OTel Collector vit sous examples/helm/.
Topologie
Le chart est sentinel-only. Les utilisateurs composent Perf Sentinel avec le chart upstream open-telemetry/opentelemetry-collector plutôt que d'embarquer un collector qui dériverait des releases upstream.
Sampling du collector et ce qui atteint le daemon
La plupart des collectors de production samplent. Si le processeur qui s'en charge se trouve entre les applications et Perf Sentinel, le daemon analyse une fraction du trafic et n'a aucun moyen de le savoir. Une trace samplée qui a été conservée ressemble exactement à une trace complète, et le rapport ne laisse rien deviner du fait que ses chiffres couvrent un dixième des requêtes.
Ce qui survit au sampling et ce qui n'y survit pas :
| Effet d'un sampling amont | |
|---|---|
Détecteurs par trace (n_plus_one, chatty_service, excessive_fanout, serialized_calls, pool_saturation) | Intacts sur les traces qui arrivent. Les politiques head comme tail gardent ou jettent des traces entières, donc une trace conservée contient toujours sa boucle N+1 complète. |
| Couverture | Dégradée. Un pattern présent sur une petite part du trafic peut être entièrement écarté et ne jamais remonter. |
| Comptes absolus (findings, occurrences, totaux Prometheus) | Sous-estimés, silencieusement. Ils décrivent l'échantillon, et rien ne les remet à l'échelle. |
| Ratios (ratio de gaspillage I/O, et les chiffres GreenOps qui en dérivent) | Non biaisés sous un sampler uniforme, qui touche numérateur et dénominateur de la même façon. Les politiques errors et slow d'un tail sampler biaisent la rétention vers les traces lourdes, et le ratio dérive avec elles. |
| Corrélation cross-trace | De fait inactive. [daemon.correlation] min_co_occurrences exige qu'une paire se répète dans la fenêtre, ce qui survit rarement à un échantillon de 10%. |
Donnez à Perf Sentinel son propre pipeline non samplé. Le sampling existe pour borner un coût de stockage, et Perf Sentinel ne stocke rien : il tient une fenêtre par trace en mémoire pendant trace_ttl_ms puis la jette. Répartissez donc depuis le même receiver et n'appliquez tail_sampling que sur la branche qui alimente le magasin de traces :
service:
pipelines:
# Stockage : samplé, parce que Tempo se paie à l'octet retenu.
traces/tempo:
receivers: [otlp]
processors: [k8sattributes, filter/drop_noise, tail_sampling, batch]
exporters: [otlp/tempo]
# Analyse : non samplé, parce que c'est la qualité de détection qui paie.
traces/perf-sentinel:
receivers: [otlp]
processors: [k8sattributes, filter/drop_noise, batch]
exporters: [otlp/perf-sentinel]Gardez le filtre de bruit sur les deux branches. Écarter les health checks, les migrations Liquibase et les spans d'export du collector lui-même retire des findings sur lesquels personne n'agira. Attention aux expressions régulières trop larges à cet endroit. Un motif DDL non ancré comme .*DROP\s+.* écarte aussi des requêtes applicatives qui contiennent seulement le mot.
Si le volume supplémentaire pose problème, restreignez la branche d'analyse par périmètre plutôt que par hasard. Ne routez que les namespaces ou les services sur lesquels vous travaillez, ce qui garde leurs chiffres entiers, au lieu d'un échantillon probabiliste qui rend partiels les chiffres de tous les services. filter/drop_noise retire déjà les spans que Perf Sentinel écarterait de toute façon (pas de db.statement, pas de http.url), donc cette branche porte d'emblée moins que celle du stockage.
Deux contraintes si vous ne pouvez pas éviter un sampling devant le daemon :
- Préférez le tail-based. Il décide par trace entière après coup, donc les traces arrivent complètes, et ses politiques habituelles (garder les erreurs, garder les traces lentes) biaisent la rétention vers l'endroit où vit le gaspillage structurel. Un head-sampling à 1-10% est le pire cas pour la détection.
- Lisez les comptes comme un échantillon, et ne les publiez pas comme des chiffres de trafic complet. Un tail sampler biaise aussi les ratios, puisque garder les erreurs et les traces lentes sur-représente les traces lourdes. Cela compte pour
disclose: un rapport de divulgation publique bâti sur une fenêtre samplée dit faux sur le gaspillage qu'il prétend mesurer. Le paramètre[daemon] sampling_ratedu daemon lève un avertissementtuningdansReport.warning_detailspour cette raison, mais il ne peut pas voir ce qu'un collector a écarté avant l'arrivée des spans.
Quand plusieurs réplicas du daemon se trouvent derrière le pipeline, l'intégrité des traces dépend du routage par trace ID, voir DaemonSet et workload.replicas.
Installation depuis le registre OCI
Le chart est publié en tant qu'artifact OCI sous oci://ghcr.io/institut-du-numerique-responsable/charts/perf-sentinel. Chaque version reçoit une signature Cosign keyless (GitHub OIDC, log de transparence Rekor), une attestation de provenance de build SLSA v1.0 stockée sur l'attestation store du dépôt, et un SBOM SPDX livré à la fois en asset de GitHub Release et en tant qu'attestation signée.
Jusqu'au passage du dépôt à l'Institut du Numérique Responsable, le chart et l'image du daemon étaient publiés sous ghcr.io/robintra. Chaque version publiée là-bas a été copiée vers le nouvel espace avec sa signature : une version épinglée se résout aux deux adresses, mais seule la nouvelle reçoit les prochaines releases.
Pinner une version
helm install perf-sentinel oci://ghcr.io/institut-du-numerique-responsable/charts/perf-sentinel \
--version 0.9.21 \
--namespace observability --create-namespace \
-f my-values.yamlLe version du chart et l'appVersion sont découplés : version désigne la release du chart, appVersion désigne le tag de l'image daemon livrée avec. Une release applicative incrémente les deux ensemble, un correctif chart seul n'incrémente que version et laisse l'appVersion en arrière (cas des 0.9.16, 0.9.18, 0.9.19, 0.9.20 et 0.9.21), donc un --version épinglé donne toujours un appVersion connu. Ne surchargez image.tag que pour faire tourner un build daemon précis avec un autre chart.
Utilisation en subchart ou depuis Argo CD
oci://ghcr.io/institut-du-numerique-responsable/charts/perf-sentinel est l'URL complète du chart, la forme attendue par helm install. Une entrée dependencies: attend au contraire le namespace parent, car Helm concatène name à repository :
dependencies:
- name: perf-sentinel
version: 0.9.21
repository: oci://ghcr.io/institut-du-numerique-responsable/charts # le namespace, pas l'URL du chartMême découpage pour une Application Argo CD : repoURL: ghcr.io/institut-du-numerique-responsable/charts plus chart: perf-sentinel.
Répéter le nom du chart dans repository résout vers charts/perf-sentinel/perf-sentinel, qui n'existe pas. En anonyme, ghcr.io répond 403 denied plutôt que 404 sur un chemin manquant : l'échec ressemble donc à un problème de registre privé alors que c'est un problème de chemin. Pour confirmer que l'artifact est bien public, récupérez un token anonyme et tirez le manifest :
token=$(curl -s "https://ghcr.io/token?scope=repository%3Ainstitut-du-numerique-responsable%2Fcharts%2Fperf-sentinel%3Apull&service=ghcr.io" | jq -r .token)
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $token" \
-H 'Accept: application/vnd.oci.image.manifest.v1+json' \
https://ghcr.io/v2/institut-du-numerique-responsable/charts/perf-sentinel/manifests/0.9.21Artifact Hub
Le chart est indexé sur Artifact Hub, où les utilisateurs peuvent le découvrir, explorer son values schema et consulter le changelog.
L'enregistrement est fait. charts/perf-sentinel/artifacthub-repo.yml porte le repositoryID délivré et chaque release de chart le pousse sur le registre OCI sous le tag réservé artifacthub.io. La procédure, pour référence ou pour la refaire sur un autre registre :
- Connectez-vous sur artifacthub.io avec un compte GitHub.
- Dans le panneau de contrôle, ajoutez un dépôt de type "Helm charts (OCI)" pointant vers
oci://ghcr.io/institut-du-numerique-responsable/charts/perf-sentinel. - Artifact Hub délivre un
repositoryID(UUID). - Placez cet UUID dans
charts/perf-sentinel/artifacthub-repo.yml, committez et poussez. - Taggez une nouvelle release de chart (incrément de patch) pour que le workflow de release pousse le
artifacthub-repo.ymlmis à jour sur le registre OCI sous le tag spécialartifacthub.io. - Artifact Hub scrute le registre et récupère les nouvelles métadonnées en moins de 30 minutes. Le badge "Verified publisher" apparaît au prochain cycle de traitement.
Le statut official est un sujet distinct : il se demande par une issue sur le dépôt artifacthub/hub, une fois que le dépôt porte déjà le badge verified publisher. Aucune annotation de chart ne le confère.
Chaîne d'approvisionnement logicielle
Voir aussi. L'introduction à Sigstore dans la doc supply-chain définit Cosign, Fulcio, Rekor, in-toto, OIDC, SLSA et SBOM utilisés dans cette section.
Chaque release publiée est signée Cosign en mode keyless, livrée avec une attestation de provenance de build SLSA v1.0, et livrée avec un SBOM SPDX attesté sous le prédicat SPDX. Vérifiez au minimum la signature Cosign avant d'installer, et l'ensemble en environnement régulé.
Vérifier la signature Cosign
La vérification Cosign keyless relie chaque release à un run spécifique du workflow GitHub Actions. L'identité du certificat doit correspondre au workflow de release publié, et l'OIDC issuer doit être GitHub Actions :
cosign verify \
--certificate-identity-regexp '^https://github.com/(robintra|Institut-du-Numerique-Responsable)/perf-sentinel/\.github/workflows/helm-release\.yml@refs/tags/chart-v' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/institut-du-numerique-responsable/charts/perf-sentinel:0.9.21Nécessite cosign 3.0 ou plus récent. La signature est un bundle Sigstore attaché au digest du chart comme referrer OCI 1.1, et non un tag sha256-<digest>.sig de l'ancien format. cosign 2.x ne lit pas les referrers et répond Error: no signatures found sur un chart pourtant correctement signé : vérifiez cosign version avant de tirer une conclusion de ce message. Testé sur le chart 0.9.21 avec cosign v3.1.2.
Sous Windows, lancez cette commande depuis PowerShell ou WSL plutôt que depuis Git Bash : MSYS réécrit les échappements antislash de la regex (\. arrive en /.) et la vérification échoue alors sur un trompeur no matching CertificateIdentity. Écrire les échappements sous la forme [.] est équivalent et fonctionne dans tous les shells.
Un run réussi affiche l'entrée du log Rekor et les détails du certificat. Une signature discordante ou absente retourne un code non nul.
Il n'y a pas de fichier .prov, donc helm install --verify n'est pas disponible. Cette absence est délibérée. Le mécanisme de provenance natif de Helm suppose une clé PGP de longue durée conservée en secret de CI, avec la charge de rotation, de révocation et de publication d'empreinte qui va avec. La signature Cosign keyless et l'attestation SLSA répondent à la même question, cet artefact vient-il bien du workflow de release de ce dépôt, sans qu'aucune clé de signature statique existe nulle part. Vérifiez avec la commande cosign verify ci-dessus plutôt qu'avec helm --verify.
Vérifier la provenance de build SLSA
Chaque tarball de chart publié porte une attestation de provenance de build SLSA v1.0 produite par actions/attest-build-provenance et stockée sur l'attestation store du dépôt (pas sur le registre OCI). L'attestation est interrogeable via gh :
gh release download chart-v0.9.21 \
--repo Institut-du-Numerique-Responsable/perf-sentinel \
--pattern 'perf-sentinel-*.tgz'
gh attestation verify perf-sentinel-0.9.21.tgz \
--owner robintra --signer-repo robintra/perf-sentinelLes charts publiés avant le passage du dépôt à l'Institut du Numérique Responsable sont attestés sous le compte d'origine, d'où --owner et --signer-repo. Les charts publiés après ce passage se vérifient avec --repo Institut-du-Numerique-Responsable/perf-sentinel.
Sans les assets de la release, récupérez le chart depuis le registre. helm pull écrit le même tarball que celui couvert par l'attestation, l'artifact OCI lui-même ne porte aucune attestation :
helm pull oci://ghcr.io/institut-du-numerique-responsable/charts/perf-sentinel --version 0.9.21
gh attestation verify perf-sentinel-0.9.21.tgz \
--owner robintra --signer-repo robintra/perf-sentinelLes deux recettes produisent la même assurance. Associez celle que vous choisissez au contrôle de signature Cosign ci-dessus pour confirmer à la fois l'identité du signataire sur l'artifact OCI et la provenance de build sur le tarball.
Vérifier le SBOM
Chaque release livre un SBOM SPDX en tant qu'asset de GitHub Release et en tant qu'attestation signée sur l'attestation store du dépôt.
Le sujet de l'attestation SBOM est le tarball du chart, pas le fichier SBOM, donc vérifiez-la contre le tarball, exactement comme le contrôle de provenance ci-dessus. Le filtre --predicate-type sélectionne l'attestation SBOM SPDX plutôt que celle de provenance de build :
gh release download chart-v0.9.21 --repo Institut-du-Numerique-Responsable/perf-sentinel \
--pattern 'perf-sentinel-*.tgz' \
--pattern 'perf-sentinel-chart-*.spdx.json'
gh attestation verify perf-sentinel-0.9.21.tgz \
--owner robintra --signer-repo robintra/perf-sentinel \
--predicate-type https://spdx.dev/Document/v2.3Le perf-sentinel-chart-0.9.21.spdx.json téléchargé est la copie lisible de ce SBOM attesté. Il capture les dépendances déclarées du chart au moment de la release.
Installation depuis un checkout local
Pour les contributeurs et les utilisateurs qui veulent inspecter, modifier ou faire un bisect sur le chart avant de l'installer, un clone local fonctionne toujours :
git clone https://github.com/Institut-du-Numerique-Responsable/perf-sentinel.git
cd perf-sentinel
# Inspectez ou surchargez les valeurs par défaut avant install.
helm show values ./charts/perf-sentinel > my-values.yaml
helm install perf-sentinel ./charts/perf-sentinel \
--namespace observability --create-namespace \
-f my-values.yamlGardez le chemin OCI pour les installations de production. Le chemin local contourne les contrôles Cosign et SLSA, il ne devrait donc pas être utilisé sur des clusters partagés sauf si vous avez construit le chart vous-même.
Couper une nouvelle release de chart
Publier une nouvelle version du chart est une tâche mainteneur, pas une étape de déploiement. La procédure complète (incrément synchronisé du chart, puis scripts/release-chart.sh chart-vA.B.C, qui exige que l'image daemon soit publiée) est dans Procédure de release.
Modes de workload
Le chart accepte trois valeurs pour workload.kind. Choisissez-en une par installation.
Deployment (par défaut)
Un daemon unique derrière un Service ClusterIP. C'est la topologie recommandée. Perf Sentinel est stateful par trace (la TraceWindow vit en mémoire), donc exécuter un seul daemon et le mettre à l'échelle verticalement est le bon premier mouvement. La topologie shardée est disponible pour des déploiements multi-daemon. Elle repose sur un consistent hashing par trace_id dans le loadbalancingexporter du Collector OTel afin que toutes les spans d'une trace atterrissent sur la même instance daemon.
workload:
kind: Deployment
replicas: 1Scalabilité et état. Les replicas ne partagent jamais d'état. La détection par trace reste correcte entre replicas uniquement avec le load balancing partrace_iddécrit ci-dessus. La corrélation cross-service est mono-processus et ne voit que ce qu'un daemon met en tampon, donc faites-la tourner sur une instance unique qui reçoit tous les services à corréler. Le daemon draine sa fenêtre en vol sur SIGTERM, donc un rolling update ou un scale-down normal ne perd rien. Seul un kill non gracieux (SIGKILL après la période de grâce, OOM) jette la fenêtre, et cela coûte au plustrace_ttl_msde détection de patterns récurrents. Détails dans Limites.
DaemonSet
Rare. Utile uniquement si vous avez une exigence dure d'avoir un daemon sur chaque nœud (par exemple pour remplacer un forwarder de traces node-local existant). Un DaemonSet répartit les traces sur plusieurs nœuds, ce qui casse la détection N+1 sauf si un collector en amont garantit que toutes les spans d'une trace rejoignent le même daemon. La plupart des utilisateurs n'ont pas besoin de ce mode.
Comme cette casse est silencieuse (les groupes passent sous leur seuil et les findings n'apparaissent pas, sans erreur ni métrique), le mode exige une affirmation explicite que ce routage est en place. Le rendu échoue sans elle :
workload:
kind: DaemonSet
daemonset:
# Vrai uniquement si un collector en amont route par trace ID vers ces
# pods, par exemple l'exporter `loadbalancing` d'OTel avec
# `routing_key: traceID`. Un Service ordinaire fait du tourniquet et
# découpe les traces, ce que ce garde-fou attrape précisément.
spanRoutingByTraceId: trueStatefulSet
Le seul mode où les acks runtime (POST /api/findings/{sig}/ack, depuis 0.5.20) fonctionnent. Activer la persistance monte un PVC sur /var/lib/perf-sentinel, et le chart pointe lui-même [daemon.ack] storage_path et [daemon.archive] path dessus, de sorte que la piste d'audit des acks et l'archive de divulgation survivent aux redémarrages et aux replanifications de pod. Les acks TOML CI (.perf-sentinel-acknowledgments.toml) sont en lecture seule au runtime et n'ont pas besoin de PVC, seul le JSONL côté daemon en a besoin.
Monter le TOML d'acks CI : un montage ConfigMap classique suffit. Une ConfigMap projette chaque clé sous forme de symlink (clé -> ..data/clé). Le loader suit un symlink qui résout sous son propre répertoire, c'est-à-dire exactement cette projection, et refuse celui qui résout ailleurs. Ce refus durcit le loader contre un lien hostile pointant vers un fichier sensible (caused by: Acknowledgments file is a symlink resolving outside its own directory, refusing to follow). Montez la ConfigMap comme un répertoire et pointez[daemon.ack] toml_pathsur la clé projetée :yaml volumeMounts: - name: ci-acks mountPath: /etc/perf-sentinel/acksAvectoml_path = "/etc/perf-sentinel/acks/acknowledgments.toml", le daemon relit le fichier chaque minute, donc une modification de la ConfigMap s'applique sans redémarrer le pod.subPathfonctionne toujours et matérialise un vrai fichier, mais il fige le contenu au moment du montage : un changement de ConfigMap demande alors un rollout.
workload:
kind: StatefulSet
replicas: 1
statefulset:
persistence:
enabled: true
size: 5Gi
storageClass: gp3Pour posséder ces tables vous-même, par exemple pour [daemon.ack] toml_path ou [daemon.archive] max_size_mb, posez persistence.manageDaemonPaths: false et écrivez les deux chemins durables sous /var/lib/perf-sentinel dans config.toml. TOML ne peut pas ouvrir deux fois la même table, et une table s'ouvre aussi bien par un en-tête que par une clé pointée (ack.storage_path = ... sous [daemon]) ou une table inline. Tant que manageDaemonPaths vaut true, le chart refuse donc de rendre dès que config.toml mentionne l'une de ces tables sous l'une de ces formes. Le message nomme le drapeau à basculer. Il refuse par excès, parce que l'alternative est une configuration que le daemon ne sait pas parser et un pod en CrashLoop.
[daemon.archive] est entièrement sautée quand config.toml pose [green] enabled = false : le daemon rejette cette combinaison au démarrage, car une archive de fenêtres sans énergie ni carbone rendrait la sortie de disclose dénuée de sens. Déclarer l'archive vous-même avec le scoring green désactivé fait échouer le rendu dans tous les modes de workload, pas seulement sous persistance, puisque le daemon la refuse de toute façon.
Le chemin de montage est figé à /var/lib/perf-sentinel, persistence n'accepte aucune clé mountPath, et l'activer sur un Deployment ou un DaemonSet fait échouer le rendu au lieu de ne rien monter en silence.
En mode Deployment et DaemonSet, les acks runtime sont indisponibles, pas seulement éphémères. Le chemin de stockage par défaut est résolu via dirs::data_local_dir(), et l'image du conteneur est FROM scratch sans HOME ni /etc/passwd, donc le chemin ne peut pas être résolu du tout. Le daemon journalise un WARN au démarrage, reste debout, et les deux routes d'écriture d'ack renvoient 503 Service Unavailable. GET /api/acks n'est gardé que par l'authentification et répond toujours 200 avec une liste vide : ce n'est donc pas une sonde valable pour détecter cet état.
Choisissez le mode de workload en tenant compte de cet arbitrage. Si vos opérateurs doivent acquitter des findings au runtime, depuis le dashboard, la CLI ack ou une alerte à 3h du matin, la topologie par défaut en est incapable et c'est StatefulSet avec persistence.enabled qu'il vous faut installer. La baseline TOML CI ne s'y substitue pas : elle porte les décisions permanentes de l'équipe, revues en pull request et partagées par tous les environnements, pas le report d'astreinte pendant un incident. Elle couvre en revanche le cas où chaque acquittement est une décision durable d'équipe, et elle n'a besoin d'aucun PVC puisqu'elle est en lecture seule au runtime. Voir Flux d’acquittement pour savoir quel acquittement va où.
Comptes de détections avec plusieurs pods
Depuis 0.37.0, chaque daemon compte ses détections par jour UTC dans son propre registre en mémoire ([daemon.detections], actif par défaut). Un seul pod a un seul registre et n'a besoin de rien de plus. Avec plusieurs pods, un Deployment dont workload.replicas dépasse 1 ou un DaemonSet, un PerfSentinelHub qui interroge le Service ClusterIP du chart atteint un pod par requête. Il lit une partie des registres des pods et marque les comptes comme incomplets (multi_instance). Deux montages donnent des comptes complets :
- Le push depuis chaque pod. Activez
[daemon.hub_export]dansconfig.toml. Chaque pod pousse son propre registre sous le mêmesource_id, et le Hub additionne les registres. Cela marche dans tous les modes de workload. - Une source du Hub par pod. Pointez une source du Hub sur l'adresse propre de chaque pod plutôt que sur le Service. Seul
StatefulSetdonne aux pods une adresse stable, par son Service headless :perf-sentinel-0.perf-sentinel-headless.observability.svc.cluster.local:4318pour le premier pod d'une installationworkload.kind=StatefulSet. Passer le StatefulSet à l'échelle demande alors de déclarer les nouveaux pods dans le Hub.
Un redémarrage ouvre un nouveau registre dans tous les modes, et son premier jour est un trou. La liste complète de ce que couvre un compte est dans Limites.
Surface de configuration
Le chart monte une unique ConfigMap sur /etc/perf-sentinel/.perf-sentinel.toml. Éditez le contenu via values.yaml :
config:
toml: |
[thresholds]
n_plus_one_sql_critical_max = 0
io_waste_ratio_max = 0.25
[green]
enabled = true
default_region = "eu-west-3"
[daemon]
listen_address = "0.0.0.0"
environment = "production"Référence complète des champs : Configuration.
Fragments de configuration
config.toml est un document unique. config.fragments est une map de documents TOML supplémentaires, rendus dans une seconde ConfigMap et montés comme répertoire sur /etc/perf-sentinel/.perf-sentinel.d/. Le daemon les fusionne par priorité de nom croissante, puis applique config.toml en dernier comme surcharge finale (Fragments de configuration).
config:
toml: |
[green]
enabled = true
default_region = "eu-west-3"
fragments:
33-green-kepler.toml: |
[green.kepler]
# Un nœud arbitraire par scrape sur un cluster multi-nœuds, voir
# examples/helm/values-green-kepler.yaml.
endpoint = "http://kepler.kepler.svc.cluster.local:28282/metrics"
metric_kind = "container"
[green.kepler.service_mappings]
"order-svc" = "order-svc"C'est ainsi que les fichiers prêts à copier de examples/ atteignent un cluster : gardez le nom de fichier, car son préfixe NN porte déjà l'ordre de fusion. examples/helm/ fournit un overlay de values par backend énergétique, empilable sur les values de base avec un second -f.
Deux règles sont vérifiées au rendu, parce que le daemon les applique au démarrage et que l'image est FROM scratch : un échec de boot ne laisse aucun shell pour aller lire l'erreur.
- Noms.
NN-lowercase-name.toml,NNsur deux chiffres, le reste en[a-z0-9-]sans tiret initial, final ni doublé. Deux fragments ne peuvent pas partager unNN, leur ordre de fusion serait indéfini. - Clés réservées.
listen_port_*et la désactivation de[green]restent dansconfig.toml, toujours.[daemon.ack]et[daemon.archive]ne sont refusées que lorsque la persistance amène le chart à les écrire lui-même, seul cas où un fragment rouvrirait une table déjà ouverte. Sans persistance, ou avecmanageDaemonPaths=false, vous possédez les deux chemins et un fragment convient très bien. Le chart croise ces clés avecservice.ports.*, les probes et le PVC en ne lisant queconfig.toml: un fragment qui en redéfinirait une passerait un contrôle au vert tout en produisant un pod qui écoute là où rien ne route. Le contrôle ramène d'abord à une seule forme les orthographes que TOML autorise. Un en-tête espacé ([ green ]), un nom de clé entre guillemets ou une table inline est donc détecté comme la forme simple, et une clé réservée seulement citée dans un commentaire ne l'est pas.
Éditer un fragment déplace l'annotation checksum/config, donc helm upgrade fait rouler les pods. Le répertoire est monté en entier plutôt que clé par clé, donc ajouter ou retirer un fragment atteint aussi un pod déjà en cours d'exécution.
Aucun secret dans un fragment : il est rendu dans une ConfigMap, lisible par quiconque dispose de get sur le namespace. Utilisez le motif Secret ci-dessous.
Secrets
Le fichier TOML ne doit jamais contenir de secrets (le daemon rejette les champs d'identifiants au chargement de la config). Injectez les valeurs sensibles via des variables d'environnement alimentées par un Secret :
kubectl -n observability create secret generic perf-sentinel-secrets \
--from-literal=PERF_SENTINEL_EMAPS_TOKEN=sk-your-tokenextraEnvFrom:
- secretRef:
name: perf-sentinel-secretsLes valeurs de config adossées à un Secret suivent un seul motif : le Secret entre dans l'environnement du pod, et une variable d'environnement dédiée surcharge le champ de config correspondant quand elle est définie (PERF_SENTINEL_EMAPS_TOKEN pour Electricity Maps, PERF_SENTINEL_ACK_API_KEY, PERF_SENTINEL_INCIDENTS_API_KEY et PERF_SENTINEL_READ_API_KEY pour les trois clés du daemon, et les en-têtes d'auth des scrapers). Voir la section "Variables d'environnement" de Configuration.
Fichiers de calibration et certificats TLS
Les deux passent par extraVolumes plus extraVolumeMounts :
extraVolumes:
- name: tls
secret:
secretName: perf-sentinel-tls
defaultMode: 0400
extraVolumeMounts:
- name: tls
mountPath: /etc/tls
readOnly: true
config:
toml: |
[daemon]
tls_cert_path = "/etc/tls/tls.crt"
tls_key_path = "/etc/tls/tls.key"Store d'acks runtime du daemon
Le daemon 0.5.20 ajoute trois endpoints d'ack runtime (POST / DELETE /api/findings/{signature}/ack et GET /api/acks) sur le port existant de l'API de requêtage. Ils partagent la posture loopback par défaut de /api/findings, mais ils modifient l'état, donc cinq décisions opérateur s'imposent quand le chart est déployé sur un listen_address non-loopback.
Qui peut acquitter des findings. Le chart fait écouter le daemon sur 0.0.0.0 pour que le Service puisse router vers le pod, et garde le store d'acks actif pour que les acks (et les acks TOML commités que le daemon charge avec eux) fonctionnent. Par défaut, le daemon n'a aucune authentification applicative (une écoute hors loopback se contente de journaliser un avertissement au démarrage) : il s'attend à tourner dans un réseau de cluster non exposé, où le Service et la NetworkPolicy forment la frontière. Choisissez l'une des deux façons de restreindre qui peut acquitter :
Par groupe (la réponse fidèle : seuls vos architectes / SRE, avec un vrai by d'audit). Perf Sentinel n'embarque pas d'IAM, donc le contrôle par identité se fait dans un proxy SSO placé en frontal. Déployez la configuration oauth2-proxy + nginx décrite dans API de query, qui autorise les écritures d'ack par groupe SSO, et ajoutez un sélecteur de pair networkPolicy pour que seul le proxy atteigne le daemon. Les lectures (GET /api/findings) restent ouvertes.
Clé partagée grossière (quiconque détient la clé peut acquitter). Créez un Secret Kubernetes dont l'entrée PERF_SENTINEL_ACK_API_KEY est votre clé et exposez-le via extraEnvFrom :
extraEnvFrom:
- secretRef:
name: perf-sentinel-ack # votre Secret, clé PERF_SENTINEL_ACK_API_KEYLa variable d'environnement PERF_SENTINEL_ACK_API_KEY surcharge le champ de config [daemon.ack] api_key, donc la clé vient du Secret et jamais de la ConfigMap. Un Secret monté vide est rejeté au chargement de la config. La clé protège aussi GET /api/acks (la piste d'audit), pas seulement les écritures. Le plancher de 12 caractères s'applique toujours, et 16 sont recommandés.
Les lecteurs qui ne doivent jamais écrire (Grafana, le Hub). Donnez-leur [daemon] read_api_key par son propre Secret, PERF_SENTINEL_READ_API_KEY dans la même liste extraEnvFrom. Elle ouvre GET /api/acks et GET /api/incidents et rien d'autre, et le daemon refuse au démarrage une clé de lecture égale à une clé d'écriture, donc une configuration de dashboard qui fuit ne peut ni acquitter un finding ni fabriquer un incident. Toutes les autres lectures restent ouvertes.
Chaque lecture derrière la clé. Ajoutez require_read_key = true à [daemon] dans config.toml et chaque GET de l'API de requête répond 401 sans [daemon] read_api_key (ou la clé d'écriture que la route prend déjà), voir API de query. Le Secret de la clé de lecture ci-dessus devient alors obligatoire : le daemon refuse de démarrer avec l'interrupteur actif et sans clé de lecture. Chaque lecteur a besoin de la clé, le Hub (AuthHeaderName=X-API-Key), la datasource Grafana ci-dessous, perf-sentinel query, le rapport HTML live et le plugin JetBrains. Ne l'activez pas sur un daemon que lisent les IDE des développeurs tant que le plugin qui y est installé n'envoie pas de secret par endpoint : les versions antérieures n'en envoient aucun et reçoivent 401 à chaque rafraîchissement. /metrics et /health restent ouverts, donc le ServiceMonitor et les sondes n'ont besoin de rien.
Les acks runtime ont besoin d'un PVC pour exister tout court. Sans lui, le chemin de stockage par défaut ne peut pas être résolu dans l'image scratch et les routes d'écriture d'ack renvoient 503. Basculez en mode StatefulSet avec persistence.enabled: true (cf. ci-dessus), ce qui câble [daemon.ack] storage_path sur le PVC pour vous.
Dimensionnez le PVC pour l'archive. Aux valeurs par défaut, l'archive par fenêtre garde un fichier actif plus max_files = 12 fichiers tournés de max_size_mb = 100 chacun, soit environ 1,3 Go, ce que la demande par défaut du chart, 2Gi, couvre avec le store d'acks à côté. Depuis 0.28.0, les divulgations trimestrielles lisent les cumuls quotidiens que le daemon écrit à côté de l'archive par fenêtre, quelques Mo par trimestre, donc la demande par défaut n'a plus à être montée pour elles. La rétention des fenêtres (max_files, max_size_mb) fixe alors la profondeur à laquelle vous pouvez descendre dans le détail des fenêtres, et un jour qu'aucun cumul ne contient, comme un jour antérieur à la mise à jour, est toujours divulgué à partir de ses lignes de fenêtre. Les cumuls suivent [daemon.archive] path, sous la forme de archive-daily-YYYY-MM.ndjson à côté de archive.ndjson, donc ils atterrissent sur le même PVC et le chart n'a besoin d'aucune nouvelle valeur. Un volume plein n'arrête pas le daemon : l'écrivain de l'archive abandonne les fenêtres qu'il ne peut pas écrire et les compte dans perf_sentinel_archive_windows_dropped_total{reason="write_error"}, donc surveillez ce compteur plutôt que la santé du daemon. Montez workload.statefulset.persistence.size au-dessus de 2Gi quand vous donnez à [daemon.archive] un max_files ou un max_size_mb plus grand, et ne descendez en dessous qu'avec persistence.manageDaemonPaths: false et un [daemon.archive] plus petit dans config.toml. Montez-la aussi pour [daemon.incidents] archive_path quand vous pointez ce fichier sur le PVC, comme la note incidents de values.yaml le suggère. L'archive d'incidents est en ajout seul et ne subit jamais de rotation, donc elle grossit aussi longtemps que le daemon tourne et la valeur par défaut de 2Gi ne lui laisse rien. Le volumeClaimTemplate d'un StatefulSet est immuable, donc une release installée sous un chart antérieur garde la demande avec laquelle elle a été créée : redimensionnez le PVC là où la StorageClass le permet, ou réinstallez.
Attention au plancher du securityContext. Le daemon ouvre chaque JSONL durable avec O_NOFOLLOW et le garde accessible au seul propriétaire. Monter le PVC sous un fsGroup ajoute g+rw aux fichiers qui s'y trouvent déjà, et les deux stores le retirent d'eux-mêmes. Le store d'acks réécrit son fichier en 0600 par sa compaction de démarrage, et l'archive d'incidents remet sa propre poignée ouverte au bon mode. Le cas qui échoue encore est un fichier dont l'UID du daemon n'est pas propriétaire, et dont il ne peut donc pas du tout changer le mode. Deux configurations y mènent : runAsUser et fsGroup définis de telle sorte que l'UID du daemon n'est pas propriétaire du point de montage du PVC, ou une politique d'admission mutante (Kyverno, OPA Gatekeeper) qui réécrit fsGroup ou runAsUser sur le pod. Le store d'acks signale alors InsecurePermissions et reste indisponible pendant que le daemon tient debout (les routes d'écriture d'ack renvoient 503, GET /api/acks une liste vide), donc cette moitié est une défaillance non bloquante. Mais [daemon.incidents] archive_path est ouvert au démarrage et fait échouer le daemon franchement, en nommant le mode fautif dans le message. Vérifiez le log au premier rollout. Le répertoire de montage reste lui-même la propriété de root, donc le daemon ne peut pas non plus le ramener à 0700 : il ne le journalise qu'en debug, puisque fsGroup le laisse fermé aux autres utilisateurs.
Charger la baseline TOML CI depuis une ConfigMap. Montez .perf-sentinel-acknowledgments.toml via extraVolumes et pointez [daemon.ack] toml_path dessus pour que le daemon ait une vue unifiée des acks permanents (TOML) et runtime (JSONL). Le POST runtime renvoie 409 Conflict sur les signatures déjà couvertes par un ack TOML actif, ce qui empêche le daemon de masquer silencieusement la baseline validée par l'équipe.
extraVolumes:
- name: ack-toml
configMap:
name: perf-sentinel-acks
extraVolumeMounts:
- name: ack-toml
mountPath: /etc/perf-sentinel/acks
readOnly: true
# Sous persistance StatefulSet, déclarer la table vous-même implique de
# reprendre les deux chemins. Sans persistance, ce drapeau est sans objet.
workload:
statefulset:
persistence:
manageDaemonPaths: false
config:
toml: |
[daemon.ack]
toml_path = "/etc/perf-sentinel/acks/.perf-sentinel-acknowledgments.toml"
storage_path = "/var/lib/perf-sentinel/acks.jsonl"
[daemon.archive]
path = "/var/lib/perf-sentinel/archive.ndjson"Voir API de query et Configuration pour la référence complète des endpoints et le catalogue des champs [daemon.ack].
NetworkPolicy
Le chart peut rendre une NetworkPolicy qui restreint qui peut joindre les ports d'ingestion et de métriques du daemon. Elle est désactivée par défaut et fail-closed : l'activer sans sélecteurs bloque tout ingress, vous devez donc autoriser explicitement les namespaces ou pods qui parlent légitimement à Perf Sentinel, typiquement le collecteur OTel (OTLP 4317 et 4318) et Prometheus (/metrics sur 4318).
networkPolicy:
enabled: true
ingress:
fromNamespaceSelectors:
- matchLabels:
kubernetes.io/metadata.name: observability
fromPodSelectors:
- matchLabels:
app.kubernetes.io/name: otel-collectorLes deux listes de sélecteurs sont combinées en OU : une source d'ingress qui correspond à n'importe quelle entrée de l'une ou l'autre liste est autorisée. Laissez une liste vide pour ignorer cette dimension de correspondance.
Les listes placées directement sous ingress ouvrent les deux ports. Le port 4318 sert aussi /metrics, /health et l'API de requêtage, qui renvoie vos templates SQL et accepte l'écriture des acks. Un collecteur qui n'envoie que de l'OTLP gRPC a donc sa place sous otlpGrpc, qui n'ouvre que le 4317. http n'ouvre que le 4318, pour Prometheus, PerfSentinelHub ou Grafana. Chacun prend les deux mêmes listes, et les trois ensembles s'additionnent.
networkPolicy:
enabled: true
ingress:
otlpGrpc:
fromPodSelectors:
- matchLabels:
app.kubernetes.io/name: otel-collector
http:
fromNamespaceSelectors:
- matchLabels:
kubernetes.io/metadata.name: monitoringIngress
Le chart peut rendre un Ingress devant le Service. Il est désactivé par défaut, et ce défaut est une décision de sécurité plutôt que d'empaquetage : Perf Sentinel n'embarque pas d'IAM, donc le publier met une API non authentifiée sur le réseau. Quiconque atteint l'hôte peut envoyer des traces OTLP en POST, lire /api/findings (vos templates SQL et vos noms d'endpoints) et appeler les endpoints d'écriture d'ack. Le modèle de menace du chart est un réseau de cluster non exposé, borné par le Service et la NetworkPolicy optionnelle.
Deux postures sont prises en charge. Interne uniquement est la plus simple : l'Ingress utilise un contrôleur lui-même injoignable depuis l'extérieur de votre réseau, et la NetworkPolicy autorise les pods de ce contrôleur. La frontière devient l'exposition du contrôleur lui-même, donc vérifiez cette hypothèse au lieu d'en hériter, car un contrôleur partagé porte souvent un écouteur public à côté de l'interne. Authentifiée est requise dès que l'hôte se résout au-delà : placez un proxy SSO en frontal, soit comme backend de l'Ingress, soit comme annotation d'auth du contrôleur, selon les options proxy SSO et clé partagée. Seul le chemin SSO donne un by d'audit par personne sur les acquittements. La clé partagée protège les écritures d'ack et GET /api/acks, et laisse les lectures de findings ouvertes.
Avant de recourir à l'une ou l'autre, vérifiez si vous avez besoin de l'Ingress tout court. La demande habituelle qui le motive, "arrêtez de m'obliger à faire un kubectl port-forward pour voir les findings", trouve sa réponse dans le cluster avec Grafana sur l'API de requête, qui n'expose rien. L'Ingress en vaut la peine pour le rapport HTML complet et le TUI opérateur depuis un poste de travail.
ingress:
enabled: true
className: nginx
annotations:
# Authentifiez au niveau du contrôleur. Sans un mécanisme de ce genre,
# l'API est ouverte à quiconque résout l'hôte.
nginx.ingress.kubernetes.io/auth-url: "https://oauth2-proxy.example.com/oauth2/auth"
nginx.ingress.kubernetes.io/auth-signin: "https://oauth2-proxy.example.com/oauth2/start?rd=$escaped_request_uri"
hosts:
- host: perf-sentinel.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: perf-sentinel-tls
hosts:
- perf-sentinel.example.comservicePortName choisit le port publié vers lequel les règles routent, otlp-http (4318 : OTLP HTTP, l'API de requête et /metrics) par défaut ou otlp-grpc (4317 : OTLP gRPC). Toute autre valeur fait échouer le rendu, puisque le Service ne publie aucun autre port et que l'erreur remonterait sinon en 503 au moment de la requête. Router du gRPC demande aussi un contrôleur configuré pour parler HTTP/2 au backend, soit nginx.ingress.kubernetes.io/backend-protocol: GRPC pour ingress-nginx.
Faites la terminaison TLS au niveau du contrôleur. Le daemon parle HTTP en clair sauf si tls_cert_path et tls_key_path sont posés dans [daemon], et ce chart ne câble pas de certificats vers le backend de l'Ingress.
Une entrée d'hôte sans clé host correspond à tous les hôtes qui atteignent le contrôleur. C'est valide et parfois voulu sur un contrôleur interne, mais sur un contrôleur partagé cela publie l'API bien plus largement que prévu.
Activer un Ingress n'assouplit pas la NetworkPolicy. Si les deux sont actifs, les pods du contrôleur doivent être autorisés comme pair, sinon l'Ingress se résout et chaque requête expire, bloquée par une politique qui la refuse. Autorisez le namespace du contrôleur par son label automatique, que Kubernetes pose sur chaque namespace depuis la 1.21, plutôt que par un label que le chart du contrôleur applique ou non :
networkPolicy:
enabled: true
ingress:
fromNamespaceSelectors:
# ingress-nginx installé dans son propre namespace. Utilisez `traefik`,
# `kube-system`, ou ce que `kubectl get pods -A | grep ingress`
# indique pour votre cluster.
- matchLabels:
kubernetes.io/metadata.name: ingress-nginxUn moyen rapide de confirmer que c'est ce pair qui bloque : avec l'Ingress activé et des requêtes qui expirent, posez networkPolicy.enabled=false le temps d'une mise à jour. Si les requêtes se mettent à passer, c'est le sélecteur qui manque. Réactivez la NetworkPolicy avant de laisser les choses en l'état.
Observabilité
Voir aussi. L'introduction à Prometheus et OpenMetrics définit le scraping, les exemplars et les types Counter/Gauge/Histogram référencés ci-dessous.
ServiceMonitor Prometheus
Quand le Prometheus Operator est installé, basculez serviceMonitor.enabled à true pour scraper /metrics sur le port 4318 :
serviceMonitor:
enabled: true
interval: 15s
scrapeTimeout: 10s
labels:
# Adaptez au sélecteur de votre ressource Prometheus.
release: prometheushonorLabels vaut true par défaut depuis la version 0.17.1 du chart. L'opérateur attache un label de cible nommé service, tiré du nom du Service, et quand honor labels est désactivé Prometheus renomme le label homonyme exposé par la cible : le service du daemon était stocké en exported_service. La variable Service du tableau de bord ne proposait plus que le nom de la release, et son panneau par service réduisait tous les services analysés à une seule ligne. Le daemon n'expose ni job, ni instance, ni namespace, donc rien d'autre ne change de main et Namespace lit toujours ce que le scrape attache. Honor labels tranche une collision et ne remplace rien. Le label service propre au daemon (perf_sentinel_service_io_ops_total, et depuis 0.18.0 perf_sentinel_findings_total, perf_sentinel_slow_duration_seconds et les compteurs d'analyse par service) est conservé là où il est exposé. Toutes les autres séries reçoivent toujours le service de l'opérateur attaché à la cible, et tout ce qui route sur ce label reste intact. Le label grouping que ces séries gagnent en 0.19.0 n'entre en collision avec rien de ce que l'opérateur attache. Il ne s'appelle pas namespace parce que l'opérateur attache bien un label de cible namespace. Honor labels ferait gagner celui du daemon, et la variable Daemon namespace du tableau de bord comme tous les filtres namespace=~"$namespace" se mettraient à sélectionner les namespaces des charges au lieu de l'installation. Un panneau ou une alerte écrits sur la forme produite par le bug, qui filtrent service="<nom complet de la release>" sur la métrique par service, ne renvoient plus rien, car les vrais noms de services ont pris la place. Sur une installation qui a déjà stocké les séries renommées, helm upgrade corrige le scrape suivant et laisse l'historique en l'état.
Dashboards qui scrapent /api/findings
Depuis 0.5.20, GET /api/findings filtre par défaut les findings acquittés. Les dashboards ou règles d'alerte existants qui interrogent l'endpoint et comptent les résultats vont silencieusement passer à côté de findings critiques si ceux-ci ont été acquittés au runtime ou par la baseline TOML CI. Deux options pour câbler un panneau Prometheus ou Grafana sur l'endpoint :
- Passer
?include_acked=trueet s'appuyer sur l'annotationacknowledged_byde la réponse pour filtrer ou colorer les lignes côté client. Garde le compteur visiblement haut quand un ack a atterri tout en laissant l'opérateur voir ce qui est actuellement mis en sourdine. - Garder la forme filtrée par défaut et documenter l'alerte comme "findings actifs uniquement", avec un panneau séparé qui liste
GET /api/ackspour pouvoir passer en revue l'ensemble acquitté.
Les compteurs /metrics (perf_sentinel_findings_total, perf_sentinel_io_waste_ratio) ne sont pas affectés, car ils enregistrent les événements de détection bruts sans aucun filtre d'ack.
Tableau de bord Grafana
Un tableau de bord prêt à l'emploi est fourni dans le dépôt à examples/grafana-dashboard.json (titre perf-sentinel overview, uid perf-sentinel-overview). Ses 29 panneaux couvrent les opérations d'E/S et le ratio de gaspillage, les types de findings par sévérité et dans le temps, le p95 des requêtes lentes, les traces actives, la santé du daemon, la pression mémoire, les plafonds de cardinalité, les scrapes énergie et l'export Hub, plus les jauges d'énergie, de carbone et de marge runtime issues des compteurs /metrics scrapés ci-dessus. Le chart ne l'embarque pas, pour la même raison qu'il n'embarque pas de collecteur : un tableau de bord figé dans le chart dérive du Grafana que vous exploitez déjà. Importez-le de deux façons.
Import manuel : dans Grafana, ouvrez Dashboards puis Import, téléversez le JSON, et mappez l'entrée DS_PROMETHEUS sur votre datasource Prometheus.
Une variante française, examples/FR/grafana-dashboard-FR.json, a les mêmes panneaux et les mêmes requêtes avec titres et descriptions traduits, et s'importe de la même façon. Elle garde l'uid du fichier anglais : l'importer remplace le tableau de bord anglais au lieu d'en ajouter un second. Le tableau de bord des findings décrit plus bas a la sienne.
Une pastille Compatibility termine la première ligne de panneaux. Elle passe à l'orange quand le daemon est antérieur à la 0.20.0, où Service silence n'a rien à tracer. Elle passe au rouge sous la 0.18.0, où les sélecteurs Service et Grouping proposent encore des noms que les panneaux de findings ne peuvent honorer, si bien qu'une vue filtrée se lit comme un service propre. Elle juge le daemon sur la présence de deux métriques dont les panneaux dépendent, perf_sentinel_incidents_total et perf_sentinel_analysis_service_overflow_total, plutôt que sur un numéro de version, parce qu'aucune métrique Perf Sentinel n'en porte. Elle affiche Unknown quand rien ne répond pour les Job et Namespace choisis, ce qui est un daemon éteint ou un label de job qui ne correspond pas, jamais un verdict sur la version. Des panneaux vides sur un daemon à jour relèvent de la configuration et non de l'âge, et la description de la pastille nomme les trois interrupteurs qui les vident.
Quatre variables de template surmontent les panneaux. Job choisit les jobs Prometheus à lire, All par défaut, ce qui compte quand plusieurs daemons sont scrapés par le même Prometheus, staging et production par exemple : n'en garder qu'un les sépare. Namespace restreint les vingt-neuf panneaux à un ou plusieurs namespaces Kubernetes, et vaut All par défaut, la vue globale que le tableau de bord offrait jusqu'ici. Le namespace est celui où tourne chaque daemon, pas celui des charges qu'il analyse, la variable choisit donc une installation et non une tranche du trafic. C'est le seul label que le daemon n'exporte pas : il est attaché par le scrape, donc Prometheus Operator le renseigne en lisant le ServiceMonitor du chart. Un scrape qui n'en attache aucun reste couvert par All, ce qui laisse le tableau de bord inchangé hors Kubernetes. Service filtre les panneaux d'analyse : findings, latence des spans lents et I/O, douze panneaux au total. Elle filtre chaque requête de la ligne depuis la 0.18.0, où perf_sentinel_findings_total et perf_sentinel_slow_duration_seconds ont gagné un label service borné. Grouping, placée avant, restreint les mêmes douze panneaux et la liste des services au regroupement du trafic analysé (k8s.namespace.name, puis service.namespace, par défaut), ce que Namespace ne fait pas. Les autres panneaux mesurent le daemon lui-même (santé, files, réception OTLP, fraîcheur énergie) et restent globaux : aucun service n'émet ces chiffres, aucun filtre service ne pourrait donc les découper. La cardinalité reste maîtrisée par des plafonds par run (128 services sur les findings, 64 sur l'histogramme, débordement replié dans service="_other"). [daemon] per_service_labels = false restaure la forme sans label, et per_grouping_labels = false celle de la 0.18.0. Les plafonds de regroupement comptent les paires (service, grouping) admises après les plafonds de services et ne replient que la moitié regroupement dans grouping="_other".
Tous les panneaux suivent le sélecteur de plage, avec une règle et une exception assumée. Les panneaux de taux utilisent $__rate_interval et les panneaux à fenêtre utilisent $__range : choisir Last 6 hours signifie donc que le classement, la répartition et la table de détail répondent tous sur ces six heures et ne se contredisent jamais. Jusqu'à la 0.10.0, trois panneaux portaient une fenêtre figée dans la requête (1h, 1h, 24h) pendant que tout autour s'adaptait, ce qui donnait l'impression d'un dashboard ignorant à moitié le sélecteur.
L'exception, ce sont les quelques panneaux stat qui affichent une valeur instantanée (Active traces, Daemon health) ou un total depuis le démarrage (Total findings (cumulative), Traces analyzed (cumulative), Ingested I/O ops (cumulative)). Un compteur cumulé depuis le démarrage du daemon est ce qu'ils rapportent, ils le disent donc dans leur titre ou leur description, et ils repartent de zéro au redémarrage du pod.
I/O waste ratio est calculé dans le panneau, en sum(increase(perf_sentinel_service_avoidable_io_ops_total[$__range])) sur sum(increase(perf_sentinel_service_analyzed_io_ops_total[$__range])), les deux compteurs côté analyse de la même passe de scoring sous les mêmes plafonds, plutôt que lu sur la jauge perf_sentinel_io_waste_ratio exportée par le daemon. Cette jauge est un rapport de compteurs cumulés : elle ignore le sélecteur de plage, dilue un problème actuel dans tout ce qui s'est passé depuis le démarrage du pod, et affiche un cadran par réplica. La forme calculée répond sur la plage sélectionnée et pour toute la flotte, avec deux décimales, parce qu'une flotte à 1 % d'I/O évitables sur un million d'opérations mérite d'être vue là où un pourcentage entier l'arrondit à zéro. La jauge exportée reste disponible pour l'alerting.
Chaque série qu'une requête renvoie une fois par réplica porte {{instance}} en légende, et les panneaux qui agrègent sur toute la flotte nomment plutôt ce qu'ils ont agrégé. Au-delà d'un réplica, les légendes répétaient sinon la même entrée une fois par pod (events/s, events/s, events/s), et les panneaux stat affichaient quatre nombres côte à côte sans moyen de savoir lequel venait de quel pod.
La ligne Daemon health (global) porte aussi ce que lisent les alertes livrées et ce que cette documentation appelle des panneaux. Ingest memory pressure est la jauge que lit PerfSentinelMemoryPressureRejecting. Runtime headroom and shedding trace les traces écartées à côté des lots écartés, puisque l'alerte se déclenche sur les traces. Cardinality caps (overflow) regroupe les six compteurs de plafond par run que les règles livrées laissent de côté. Energy scrape outcome place les taux de succès et d'échec à côté des jauges de fraîcheur. Daemon memory (RSS) est le chiffre du collecteur de processus que le garde-fou mémoire maintient sous la limite, et Hub export (pending and dropped) surveille le tampon d'export borné. OTLP span intake ventile les spans filtrés par raison, missing_db_statement et missing_http_url, les deux trous d'instrumentation qu'aucune règle livrée n'attrape, se lisent donc là. Dans la ligne d'analyse, Findings rate by type est la seule série temporelle sur perf_sentinel_findings_total et le seul panneau avec les exemplars activés : le clic vers une trace demande la configuration décrite sous Exemplars. Le tableau de bord se rafraîchit toutes les minutes et pointe vers perf-sentinel findings par le tag perf-sentinel partagé, qui pointe en retour.
Import par sidecar (kube-prometheus-stack et similaires) : chargez le JSON dans une ConfigMap étiquetée pour que le sidecar Grafana la découvre automatiquement.
kubectl -n observability create configmap perf-sentinel-grafana \
--from-file=perf-sentinel-overview.json=examples/grafana-dashboard.json
kubectl -n observability label configmap perf-sentinel-grafana \
grafana_dashboard=1La clé d'étiquette (grafana_dashboard ici) doit correspondre au dashboards.sidecar.label configuré pour votre sidecar Grafana.
Grafana sur l'API de requête (table des findings)
Le tableau de bord ci-dessus lit Prometheus, qui répond à "combien de findings, de quel type, depuis quel service". Depuis la 0.18.0, perf_sentinel_findings_total porte type, severity et un label service borné par un plafond de 128 services (le débordement se replie dans service="_other") et, depuis la 0.19.0, un label grouping borné par ses propres plafonds. Un label par endpoint reste hors de /metrics parce que la cardinalité des endpoints n'est pas bornée. Quelle opération sur quel endpoint vit derrière l'API de requête.
Un second tableau de bord la lit directement par le plugin Infinity (yesoreyeram-infinity-datasource, qui n'est pas livré avec Grafana : installez-le d'abord et épinglez sa version là où vous le provisionnez) :
examples/grafana-infinity-datasource.yaml, la datasource provisionnée. Renseignez le namespace dans l'URL, et le port aussi si vous avez déplacéservice.ports.otlpHttp.port: le chart recoupe cette valeur avec[daemon] listen_port_http, mais ce fichier est hors de ce contrôle. Son second bloc, optionnel, pointe sur un PerfSentinelHub pour la rangéeHistory (Hub)décrite plus bas.examples/grafana-findings-dashboard.json, titreperf-sentinel findings, uidperf-sentinel-findings: la ligne d'état du daemon, une table filtrable des findings, les corrélations entre services que le daemon garde tant que[daemon.correlation]est activé, les acquittements à chaud, la santé des backends énergie et, depuis la 0.20.0, les incidents postés par votre alerting avec les findings gelés pour chacun. Une pastilleCompatibilityà côté de la ligne d'état passe au rouge sous la 0.21.0, où ce tableau de bord affiche des données fausses.groupingetoffsetn'existaient pas comme paramètres de requête, et l'API ignore ce qu'elle ne connaît pas, si bien que le sélecteurGroupingnomme un déploiement pendant que la table les liste tous et queSkip rowsrenvoie la même page à chaque valeur. Elle lit laversionque le daemon déclare, queGET /api/statusrenvoie depuis la 0.4.0.
Ni port-forward, ni Ingress. C'est le backend de Grafana qui fait la requête, donc un Grafana dans le cluster atteint le Service par le réseau du cluster. C'est la réponse à "il me faut un kubectl port-forward chaque fois que je veux voir les findings", et rien n'est exposé hors du cluster.
Deux points à régler avant de le mettre en service. D'abord, le daemon n'embarque pas d'IAM : qui ouvre le dossier de ce tableau de bord lit vos templates SQL et vos noms d'endpoints, réservez donc le dossier aux personnes autorisées à les voir, ce qui suppose un dossier à lui. Dans un dossier partagé avec les tableaux de bord du cluster et des bases, le restreindre restreint aussi tous les autres : la réponse y est un dossier dédié plutôt qu'un dossier commun plus fermé. Ensuite, si networkPolicy.enabled=true, ajoutez Grafana comme pair sous networkPolicy.ingress.fromNamespaceSelectors ou .fromPodSelectors, sinon la datasource expire sans erreur utile, parce qu'une NetworkPolicy refuse en silence.
La table affiche une ligne par problème distinct plutôt qu'une par détection, puisque /api/findings replie par la signature qu'utilisent les acquittements, et sa colonne Retenues est le compte de ce repli. Il compte les détections encore retenues dans le tampon circulaire du daemon, il baisse donc quand les plus anciennes expirent et repart de zéro au redémarrage du daemon. La sévérité se filtre par l'en-tête de colonne, ainsi aucune requête ne peut demander à l'API une sévérité qu'elle ne connaît pas. Outre Max rows, cinq variables changent la requête. Grouping et Service la resserrent côté daemon par les paramètres grouping et service. Leurs valeurs sont les label_values de perf_sentinel_findings_total, donc le tableau de bord lie aussi une datasource Prometheus. Ce sont les mêmes labels que filtre le tableau de bord d'aperçu, listés depuis Prometheus plutôt que lus sur une page peut-être déjà tronquée. Leur All envoie un blanc, que l'API lit comme une absence de filtre. Finding type la resserre par le paramètre type de l'API, l'un des douze types listés sous les libellés qu'affiche la colonne Type, et son All envoie le même blanc. Un type rare comme Slow SQL n'est donc pas coupé par Max rows avant que l'en-tête de colonne puisse le filtrer. Skip rows est l'offset de l'API, un listing au-delà du plafond de 1000 lignes se lit donc par tranches, 0 puis le Max rows de la page précédente. L'anneau continue de bouger entre deux requêtes, une ligne peut donc franchir une frontière de tranche, et resserrer d'abord est ce qui fait tenir la plupart des pages sous le plafond. Include acked : l'API laisse par défaut les findings acquittés de côté, un critique acquitté est donc absent sans que rien ne le dise. true les redemande avec une colonne Acked via qui nomme la source (toml pour la baseline CI, daemon pour le stockage à chaud). Suggestion est le correctif propre au cadriciel ou au courtier que le daemon a déduit de la trace quand il y en a un, l'indication générique sinon, et Fix for nomme cette technologie, vide quand l'indication est générique. Code est le site d'appel tiré des attributs code.* du span, la fonction puis fichier:ligne comme la CLI les affiche, la fonction seule quand l'instrumentation n'envoie ni fichier ni ligne, fichier:ligne seul quand elle n'envoie pas de fonction, vide quand elle n'en envoie aucun. Un chemin long se lit en entier par le bouton d'inspection de la cellule. Les types s'affichent par leur libellé, N+1 SQL plutôt que n_plus_one_sql, sur les deux tableaux de bord. Une colonne Grouping nomme le déploiement auquel appartient chaque ligne, le premier [detection] grouping_attributes que portaient ses spans. Le tableau de bord déclare le plugin Infinity et la datasource Prometheus dans __inputs et __requires, la boîte d'import demande donc laquelle de chaque lier au lieu d'importer des panneaux qui n'ont rien à interroger. Le 2.0.0 de __requires est le plancher que vérifie cette boîte d'import, pas une garantie sur les majeures suivantes. Après une montée de version majeure du plugin, vérifiez que toutes les tables se remplissent encore : leurs colonnes viennent du parseur backend, et un changement de parseur vide une table sans erreur nulle part.
La table Correlations sous les findings lit GET /api/correlations : une ligne par paire orientée de findings qui se sont déclenchés ensemble entre services, la source d'abord et la cible dans les [daemon.correlation] lag_threshold_ms. Chaque ligne porte le ratio Confidence, les deux comptes qui le fondent, le décalage médian, et Source trace et Target trace, les deux côtés de la dernière co-occurrence. La table reste vide tant que [daemon.correlation] enabled n'est pas à true. Une paire vit une fenêtre (window_minutes) après sa dernière co-occurrence et rien ne la sauvegarde, si bien qu'un redémarrage vide la table. La route ne prend aucun paramètre, Grouping, Service et Finding type ne la resserrent donc pas et ce sont les en-têtes de colonne qui filtrent.
Deux tables en bas lisent GET /api/incidents, la route que POST /api/incidents remplit depuis un webhook Alertmanager (activée explicitement par [daemon.incidents], voir API de query). Pour des règles et un receiver prêts à copier, voir examples/incident-alerts-prometheus-operator.yaml ou examples/incident-alerts-victoriametrics-operator.yaml. Incidents liste les incidents du plus récent au plus ancien avec la fenêtre de capture, le nombre de findings gelés pour chacun, et une colonne Capture. Cette colonne lit oldest_finding_ms contre le début de la fenêtre comme le fait Runbook : complete quand l'anneau atteignait encore en dessous de la fenêtre, partial quand il en avait déjà évincé une partie, empty ring quand rien n'était retenu. Cliquer une ligne renseigne la variable Incident, et Incident findings montre alors les lignes gelées pour cet incident, repliées sur la seule fenêtre. Cette table a une colonne First seen à la place de Acked via : une première apparition postérieure au début de l'incident est une ligne qui ne s'est déclenchée qu'après le redémarrage. Le sélecteur de temps ne filtre aucune des deux tables, puisque la route n'a pas de filtre temporel. Incidents lit une page de 50 incidents après le nombre que choisit la variable Incident skip rows (de 0 à 950, par pas de 50), donc 0 montre les 50 plus récents et les valeurs suivantes remontent un anneau d'au plus 1000. Elle les demande avec findings=false, chaque incident portant finding_count à la place de ses findings figés, si bien que la réponse reste petite quel que soit le nombre de findings que la page a figés. Un daemon antérieur au paramètre l'ignore et envoie les findings, que la table compte alors. Incident findings demande à la route le seul incident que nomme la variable Incident (id), quelle que soit la page affichée au-dessus. Un daemon antérieur à 0.24.0 ignore id, et la table trouve alors l'incident sur cette même page.
Les deux routes sont protégées, la datasource envoie donc désormais une clé. examples/grafana-infinity-datasource.yaml place [daemon] read_api_key dans l'en-tête X-API-Key (auth_method: apiKey dans les termes d'Infinity). Grafana remplit l'en-tête depuis PERF_SENTINEL_READ_API_KEY dans son propre environnement via $__env{}, la clé vient donc d'un Secret monté dans le pod Grafana et ne figure jamais dans le fichier. C'est la clé de lecture, jamais la clé d'écriture de [daemon.incidents] : un dossier Grafana ne doit pas pouvoir fabriquer un incident, et le daemon refuse au démarrage une clé de lecture égale à une clé d'écriture. Le même en-tête ouvre GET /api/acks pour la table des acquittements dès que [daemon.ack] api_key est renseignée, chaque route dès que [daemon] require_read_key = true, et les routes non protégées l'ignorent, une seule datasource sert donc tous les panneaux. Avec un en-tête configuré, Infinity exige allowedHosts, que le fichier épingle déjà sur l'URL du daemon.
Historique et acks par le Hub (depuis 0.24.0)
Les tables ci-dessus lisent le ring du daemon, borné en volume et non en durée, donc le sélecteur de temps ne peut pas remonter. Là où un PerfSentinelHub est déployé (0.3.0 et suivants), la rangée repliée History (Hub) lit le Hub à la place. Elle appelle le GET /api/findings du Hub avec la plage du sélecteur (from, to), la variable Environment, et les filtres Grouping, Service et Finding type de la table en direct. La signature ne porte pas de regroupement, donc le Hub compare Grouping à chaque regroupement où il a vu le finding, et un Hub antérieur à ce filtre l'ignore. Findings history liste une ligne par problème que le Hub a vu dans cet environnement un jour de la plage, à l'horloge du Hub en UTC, avec First seen, Last seen et Status calculés pour ce seul environnement. Elle n'a pas de colonnes d'occurrences : les comptes du daemon sont relatifs à son ring et ne s'additionnent pas sur des mois, et perf_sentinel_findings_total garde les volumes dans Prometheus. L'historique par jour commence le jour où le Hub est passé en 0.3.0, et le Hub le garde pendant Hub:Retention, 180 jours par défaut.
La rangée a besoin de trois choses, dont le reste du dashboard se passe :
- Une seconde datasource Infinity pointée sur le Hub, le second bloc de
examples/grafana-infinity-datasource.yaml. LeGET /api/findingsdu Hub ne prend pas de clé sauf si le contrôle d'accès du Hub est actif (hub.access.enabled), donc sans lui gardez le Hub sur le réseau du cluster. Avec lui, la route exige un jeton machine portantfindings.read, déclaré danshub.access.machineTokensdu Hub avec sa valeur dans un Secret (ledocs/ACCESS-CONTROL.mddu Hub), que la datasource envoie enAuthorization: BearerdepuisPERF_SENTINEL_HUB_TOKENdans l'environnement de Grafana. Sans le jeton, chaque panneau de la rangée répond401, et403quand aucun grant ne donnefindings.readau groupe du jeton, alors qu'un jeton dont le groupe figure danshub.access.adminGroupslit tout. Un Hub sans contrôle d'accès ignore l'en-tête, donc le même bloc sert dans les deux cas. Quiconque peut interroger la datasource lit ce que son jeton atteint : donnez-lui le jeton d'une seule équipe, autorisé sur les seuls projets de cette équipe. La variableperf-sentinel Hubne propose que les datasources dont le nom contientHub, pour que la rangée ne se lie jamais au daemon par accident. Sans datasource, la rangée est en erreur quand on l'ouvre et rien d'autre ne change. - Un Prometheus qui scrape le
/metricsdu Hub : la variableEnvironmentliste le labelenvironmentdeperf_sentinel_hub_findings.Alllit toute la flotte, ou, avec le contrôle d'accès actif, chaque finding que le jeton atteint. Le contrôle d'accès exigehub.metricsPort, et/metricsne répond alors que sur ce port : la configuration de scrape doit le viser, sinon la variable ne liste rien. La variable liste chaque environnement que le Hub connaît, pas seulement ceux du jeton : un environnement où le jeton n'atteint aucune source répond400. - La variable
Hub URL, l'origine du lanceur du Hub telle que votre navigateur l'atteint.
Ack, la première colonne de Findings, Incident findings et Findings history, ouvre dans un nouvel onglet la page d'ack du Hub pour cette signature (<Hub URL>/?ack=<signature>), où un ack ou une révocation est relayé aux daemons qui portent le finding. La rangée d'historique ajoute son environnement, pour que cette page s'ouvre avec les sources de cet environnement cochées. Le lien a besoin de Hub URL, d'un Hub qui détient une clé d'ack pour la source (le Configuration du Hub), et d'un finding que le Hub connaît déjà, ce qui prend quelques secondes avec [daemon.hub_export] et un cycle d'interrogation sinon. Sans Hub les liens ne mènent nulle part : acquittez depuis la CLI, le rapport HTML ou le TUI, voir Flux d’acquittement.
Acked on et Acked by viennent du miroir que le Hub tient des acks de chaque daemon, baseline CI comprise, ce qui demande des daemons à partir de 0.24.0. Acked via est l'annotation du daemon lui-même à sa dernière observation.
Règles d'alerte (PrometheusRule)
Le chart fournit une PrometheusRule avec les alertes qui comptent. Elle est conditionnée comme le ServiceMonitor et désactivée par défaut :
prometheusRule:
enabled: true
labels:
# Adaptez au ruleSelector de votre ressource Prometheus.
release: prometheus
# N'ajoutez les alertes de péremption des scrapers d'énergie par backend,
# et celles des sondes CPU des hôtes, que lorsqu'un backend énergie
# (Alumet, Scaphandre, Kepler, Redfish, cloud_energy) est configuré.
energyScrapers: false
scraperStaleSeconds: 120Le groupe par défaut perf-sentinel.rules porte cinq règles. Chacune se déclenche sur une donnée que le daemon a perdue sans pouvoir la retrouver : le daemon qui n'est plus collecté (up{job="<nom complet de la release>"} == 0), l'ingestion abandonnée sur un canal saturé, l'ingestion refusée sous pression mémoire, les traces délestées avant analyse, et une fenêtre d'archive de divulgation perdue. La description de chaque alerte nomme le paramètre [daemon] à relever. Ajoutez les vôtres via prometheusRule.additionalRules, passées telles quelles dans le même groupe, sans fork.
La saturation de file, l'éviction de paires du corrélateur et le débordement de cardinalité de services ne sont pas des alertes. Chacune se déclencherait sur un état que le daemon atteint en fonctionnant normalement, et chacune est déjà un panneau du tableau de bord Grafana livré. La saturation en particulier prédisait l'alerte de délestage, donc un incident produisait deux notifications et la première ne portait aucun remède que la seconde n'avait pas.
PerfSentinelDown lit le nom de job que l'opérateur Prometheus dérive du Service, c'est-à-dire le nom complet de la release. Collecter avec votre propre scrape_config sous un autre job_name laisse cette règle muette. Dans ce cas, redéfinissez-la via additionalRules.
PodDisruptionBudget
Le défaut est mono-réplique, où un PDB a peu d'effet : maxUnavailable: 1 autorise quand même l'éviction et minAvailable: 1 bloquerait chaque drain de nœud. Quand la collecte sans interruption compte (par exemple des données carbone sans trou pour disclose), passez à une topologie shardée multi-répliques avec minAvailable, et utilisez le mode StatefulSet pour que les fenêtres archivées survivent aux redémarrages.
podDisruptionBudget:
enabled: true
maxUnavailable: 1Exemplars
Perf Sentinel émet des exemplars Prometheus sur perf_sentinel_findings_total, perf_sentinel_io_waste_ratio et perf_sentinel_slow_duration_seconds. Activez le stockage des exemplars côté Prometheus :
prometheus:
prometheusSpec:
enableFeatures:
- exemplar-storagePuis configurez Grafana pour cliquer de la métrique vers la trace :
datasources:
- name: Prometheus
type: prometheus
jsonData:
exemplarTraceIdDestinations:
- name: trace_id
datasourceUid: tempoSans le Prometheus Operator
Si vous utilisez un Prometheus standard sans l'opérateur, ajoutez une entrée de scrape statique :
scrape_configs:
- job_name: perf-sentinel
kubernetes_sd_configs:
- role: endpoints
namespaces:
names: [observability]
relabel_configs:
- source_labels: [__meta_kubernetes_service_label_app_kubernetes_io_name]
regex: perf-sentinel
action: keep
- source_labels: [__meta_kubernetes_endpoint_port_name]
regex: otlp-http
action: keep
- source_labels: [__meta_kubernetes_namespace]
target_label: namespaceLe rôle est endpoints parce que __meta_kubernetes_endpoint_port_name n'existe que là, et un keep sur un label que le rôle ne pose jamais écarte toutes les cibles. La dernière règle est ce que lit la variable Namespace du tableau de bord : Prometheus Operator attache ce label de lui-même, mais un scrape écrit à la main doit le demander.
Mise à jour
helm upgrade perf-sentinel ./charts/perf-sentinel \
--namespace observability \
-f my-values.yamlLe daemon ne recharge pas sa config à chaud, donc les modifications de config.toml exigent un redémarrage du pod. Le chart gère cela automatiquement : une annotation checksum/config sur le pod template calcule un hash de la ConfigMap rendue, donc chaque édition de config modifie l'annotation et déclenche un rolling restart. Aucun kubectl rollout restart manuel n'est nécessaire.
Lors du passage du chart à un nouveau appVersion, épinglez image.tag explicitement et relisez CHANGELOG.md pour repérer les changements de config incompatibles. Le chart ne valide pas encore que la version du daemon corresponde à la version du chart, cette vérification incombe donc à l'opérateur.
Désinstallation
helm uninstall perf-sentinel --namespace observabilityCela supprime le Deployment, le Service, la ConfigMap, le ServiceAccount et (quand ils sont créés) le ServiceMonitor et la NetworkPolicy. Le mode StatefulSet avec persistance conserve les PersistentVolumeClaims sous-jacents par défaut, conformément à la sémantique Kubernetes. Supprimez-les explicitement si vous voulez nettoyer l'état :
kubectl --namespace observability delete pvc \
-l app.kubernetes.io/instance=perf-sentinelExemple bout en bout
examples/helm/ fournit deux fichiers de valeurs qui composent le chart perf-sentinel avec le chart upstream OTel Collector dans une topologie fanout Zipkin + OTLP vers Tempo et Perf Sentinel. Suivez le README de ce répertoire pour la recette complète d'installation et de vérification.