grafana : les panneaux declares deviennent des tableaux
La moitie qui manquait. Les roles declaraient leurs panneaux, Prometheus en derivait ses cibles, les expressions repondaient, et rien ne les assemblait. serveur_grafana lit les memes meta/metriques.yml que serveur_prometheus et en assemble un tableau par role — la raison du panneau devient sa description dans Grafana. Moisson des tableaux orphelins, prefixe setops-role- pour ne jamais toucher un tableau ecrit a la main, JSON valide avant d etre pose (Grafana ne tombe pas sur un tableau illisible : il le saute en silence). client_metrique declare quatre panneaux sans exportateur — le cas symetrique. Celui qui compte : la memoire disponible, depuis que le ballon est actif. Eprouve sur le site : les deux tableaux charges dans le stockage unifie, un temoin orphelin retire, les quatre panneaux de flotte a 10 series chacun. Un panneau etait faux — /var/lib/lxcfs rapporte toujours zero octet libre et un min() en meurt. Corrige en liste blanche : un montage inconnu manque au graphe, ce qui se voit, au lieu de l ecraser. P77 garde la table des unites : une unite inventee retombe sur short, lisible et fausse. 76 OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
This commit is contained in:
parent
baa9d12054
commit
311f33d6f6
10 changed files with 522 additions and 5 deletions
|
|
@ -188,7 +188,7 @@ Si `ansible-lint` n’est pas disponible, le signaler clairement. Ne pas invente
|
|||
## Écrire, puis relire (D-68)
|
||||
|
||||
`--syntax-check` et `ansible-lint` prouvent que le dépôt est cohérent **avec lui-même**.
|
||||
C'est aussi ce que font les 76 preuves de `make prouver` : elles lisent le dépôt, sans le
|
||||
C'est aussi ce que font les 77 preuves de `make prouver` : elles lisent le dépôt, sans le
|
||||
moindre appel réseau. **Aucune ne demande au système déployé s'il ressemble à ce que le
|
||||
dépôt annonce.**
|
||||
|
||||
|
|
|
|||
91
CHANGELOG.md
91
CHANGELOG.md
|
|
@ -1,5 +1,96 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-09-14 (15) — Le generateur de tableaux : les panneaux declares deviennent des tableaux
|
||||
|
||||
Les roles declaraient leurs panneaux dans `meta/metriques.yml`, Prometheus en derivait deja
|
||||
ses cibles, les expressions repondaient — et RIEN ne les assemblait. Un panneau declare que
|
||||
personne ne peut regarder n'est pas une observabilite, c'est une intention.
|
||||
|
||||
### Ce qui a ete construit
|
||||
|
||||
`serveur_grafana` lit desormais les memes `meta/metriques.yml` que `serveur_prometheus`, et
|
||||
en assemble **un tableau par role**. Exactement le patron du voisin : le role declare, le
|
||||
moteur derive.
|
||||
|
||||
meta/metriques.yml --exportateur--> cible de scrutation (serveur_prometheus)
|
||||
--panneaux-----> tableau Grafana (serveur_grafana)
|
||||
|
||||
UN TABLEAU PAR ROLE, pas un grand. La question qu'on se pose est « comment va
|
||||
PostgreSQL », pas « comment va la flotte » — celle-la est deja repondue par Icinga. Le
|
||||
role est l'unite qui declare, donc l'unite qui s'affiche.
|
||||
|
||||
LA `raison` DEVIENT LA DESCRIPTION DU PANNEAU. C'est le seul champ qui ne produit aucun
|
||||
pixel, et le plus important : Grafana l'affiche au survol du titre. Sans elle, celui qui
|
||||
regarde six mois plus tard voit une courbe sans savoir ce qu'elle annonce.
|
||||
|
||||
LA MOISSON, comme pour les sondes orphelines. Un tableau qu'aucun role ne declare plus est
|
||||
retire. Un tableau orphelin est moins grave qu'une sonde orpheline — il n'echoue pas, il
|
||||
MENT : il affiche « aucune donnee » pour un service disparu, et celui qui regarde croit a
|
||||
une panne. Le prefixe `setops-role-` protege `journaux-flotte.json`, ecrit a la main.
|
||||
|
||||
LE JSON EST VALIDE AVANT D'ETRE POSE. Grafana n'echoue pas sur un tableau illisible : il
|
||||
le SAUTE, en silence. Sans `validate:`, une expression mal repliee ferait perdre un tableau
|
||||
sans qu'aucune tache ne devienne rouge.
|
||||
|
||||
### Un second rôle qui déclare, et il donne des données tout de suite
|
||||
|
||||
`client_metrique` declare **quatre panneaux sans exportateur** — le cas symetrique de
|
||||
PostgreSQL. Le job `node` est universel et ecrit une fois dans `prometheus.yml.j2` ; le
|
||||
deriver le ferait exister deux fois.
|
||||
|
||||
Le panneau qui compte : **memoire disponible en part du total**. Depuis que le ballon est
|
||||
actif, l'hyperviseur reprend de la memoire a une VM qui n'en a pas besoin, 100 Mio par
|
||||
cycle. La VM ne voit pas son plafond bouger, elle voit sa marge fondre. Aucun verdict ne
|
||||
peut se poser la-dessus : il n'y a pas de seuil juste, c'est la PENTE qui dit si le
|
||||
plancher a ete pris trop bas.
|
||||
|
||||
### Eprouve sur le site, de bout en bout
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| fichiers assembles | `setops-role-serveur_postgresql.json`, `setops-role-client_metrique.json` |
|
||||
| Grafana les a-t-il charges ? | oui — `setops-postgresql` et `setops-client-metrique` dans le stockage unifie |
|
||||
| moisson | un `setops-role-serveur_fantome.json` pose a la main a ete retire, les deux autres conserves |
|
||||
| les panneaux repondent-ils ? | les 4 de `client_metrique` : 10 series chacun, donnees reelles |
|
||||
|
||||
**Le journal ne prouvait rien.** `finished to provision dashboards` s'ecrit aussi quand
|
||||
Grafana saute un fichier. La verification est allee lire ce que Grafana a VRAIMENT
|
||||
enregistre — et Grafana 13 range les tableaux dans le **stockage unifie** (table
|
||||
`resource`), plus dans la table `dashboard`, qui est vide et le restera. Une premiere
|
||||
lecture y a vu « zero tableau » ; c'etait la mauvaise table.
|
||||
|
||||
### Un panneau faux, trouve en le regardant
|
||||
|
||||
« Espace libre — le point de montage le plus serre » rendait **0 octet** pour les trois
|
||||
hyperviseurs. Le coupable : `/var/lib/lxcfs`, un systeme FUSE virtuel qui rapporte toujours
|
||||
zero, et qui n'est pas en lecture seule — aucun filtre malin ne l'ecartait.
|
||||
|
||||
**Un `min()` est impitoyable** : un seul montage pathologique rend le panneau inutile pour
|
||||
toujours, et sa courbe plate ressemble a un disque plein. La pire sorte de faux — lisible,
|
||||
alarmant, et faux.
|
||||
|
||||
Corrige en **liste blanche** (`ext4|xfs|btrfs|zfs`) plutot qu'en liste noire. Une liste
|
||||
noire suit ce qui existe, donc prend du retard. Une liste blanche ignore par defaut : un
|
||||
montage inconnu MANQUE au graphe, ce qui se voit, au lieu de l'ecraser, ce qui ne se voit
|
||||
pas. Apres correction : min 28 Go, max 401 Go.
|
||||
|
||||
### La garde
|
||||
|
||||
**P77** exige de chaque panneau un titre, une expression, une unite et une raison — et que
|
||||
l'unite figure dans la table de traduction de `serveur_grafana`. C'est encore une liste qui
|
||||
en suit une autre : une unite inventee ne casse rien, le panneau retombe sur `short`, et un
|
||||
graphe d'octets gradue en unites brutes reste parfaitement lisible et parfaitement faux.
|
||||
Eprouvee dans les deux sens.
|
||||
|
||||
`docs/supervision-conception.md` porte desormais la moitie « metriques » a cote de la
|
||||
moitie « sondes ».
|
||||
|
||||
### Ce qui reste
|
||||
|
||||
Le tableau de PostgreSQL est assemble, charge, et **vide** : l'exportateur exige
|
||||
`vault_pg_exportateur` dans la voute du SITE, qui n'y est pas. Ajouter un secret a cette
|
||||
voute-la n'est pas un geste a poser sans le dire.
|
||||
|
||||
## 2026-09-14 (14) — L'echappatoire du wiki etait documentee cinq fois et n'existait pas
|
||||
|
||||
En publiant les huit fiches neuves, `make wiki-publier` a refuse : l'ecosysteme monte est
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
> [`docs/audit/affirmations.md`](affirmations.md).
|
||||
|
||||
- **Instance** : `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance` — inventaire `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance/inventories/principal/hosts.yml`
|
||||
- **Verdict** : ✅ CONFORME (75 OK · 0 echec · 1 saute)
|
||||
- **Verdict** : ✅ CONFORME (76 OK · 0 echec · 1 saute)
|
||||
|
||||
## Preuves
|
||||
|
||||
|
|
@ -69,7 +69,7 @@
|
|||
| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 9 role(s) applique(s), aucun secret de tenant reclame. |
|
||||
| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 13 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 13. |
|
||||
| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. |
|
||||
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (76 preuves, 68 roles, 41 groupes). |
|
||||
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (77 preuves, 68 roles, 41 groupes). |
|
||||
| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. |
|
||||
| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). |
|
||||
| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `aac74f6` (publie le 2026-09-14). |
|
||||
|
|
@ -88,7 +88,8 @@
|
|||
| P73 | Le locataire designe les services de son site REEL | — | ✅ OK | CONFORME : 6 intrant(s) du locataire concordent avec ce que le site expose (1 non declare(s), donc derive(s) ou non utilise(s)). |
|
||||
| P74 | Gabarit dore : une seule declaration, au plan du site | — | ✅ OK | Gabarit declare une seule fois : VMID 9006 « modeleSetOPS-minimal », precedent 99998. |
|
||||
| P75 | Les parametres de clonage traversent les trois maillons | — | ✅ OK | 14 parametre(s) de clonage, tous emis par l'inventaire. |
|
||||
| P76 | Tout gabarit de role se rend vraiment | — | ✅ OK | 151 gabarits de role : tous se rendent. |
|
||||
| P76 | Tout gabarit de role se rend vraiment | — | ✅ OK | 152 gabarits de role : tous se rendent. |
|
||||
| P77 | Panneaux declares : assemblables, et gradues | — | ✅ OK | 8 panneau(x) declare(s) dans 2 role(s), tous avec titre, expression, raison et une unite que la table sait traduire. |
|
||||
|
||||
## Couverture des affirmations ✅ du registre
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ make placement-plan # chaque VM est-elle là où le plan la met
|
|||
|
||||
## Le trou qu'il comble
|
||||
|
||||
`scripts/prouver.py` porte 76 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
|
||||
`scripts/prouver.py` porte 77 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
|
||||
dépôt. Zéro appel réseau, zéro SSH, zéro `ansible`. Elles établissent que le dépôt est
|
||||
cohérent **avec lui-même** — que les handlers existent, que les intrants ont un
|
||||
propriétaire, que rien n'est codé en dur.
|
||||
|
|
|
|||
|
|
@ -108,6 +108,62 @@ Une **métrique à seuil** — durée de collecte, volume de journaux, taux d'oc
|
|||
appartient à Prometheus et Grafana. Icinga répond à une seule question : *est-ce cassé ?*
|
||||
Mélanger les deux rendrait les deux moins lisibles.
|
||||
|
||||
## L'autre moitié : `meta/metriques.yml`
|
||||
|
||||
Cette phrase appelle son pendant, et il porte le même patron — **le rôle déclare, le
|
||||
moteur dérive**.
|
||||
|
||||
| fichier | ce qu'il produit | la question |
|
||||
| --- | --- | --- |
|
||||
| `meta/supervision.yml` | une **sonde** → un verdict, avec un TTL | *est-ce cassé ?* |
|
||||
| `meta/metriques.yml` | un **exportateur** → une série ; des **panneaux** → un tableau | *depuis quand, et vers où ?* |
|
||||
|
||||
**Deux fichiers, deux consommateurs.** `serveur_prometheus` lit l'`exportateur` pour en
|
||||
tirer sa cible de scrutation ; `serveur_grafana` lit les `panneaux` pour en assembler un
|
||||
tableau par rôle. Les deux moitiés sont indépendantes : un rôle peut déclarer un
|
||||
exportateur sans panneau (des séries collectées, regardées ailleurs), et des panneaux sans
|
||||
exportateur (`client_metrique` — le job `node` est universel, écrit une fois, et le
|
||||
dériver le ferait exister deux fois).
|
||||
|
||||
**Le flux n'est pas dérivé d'ici, et c'est voulu.** Le port de l'exportateur doit s'ouvrir
|
||||
depuis l'observatoire, et c'est `meta/flux.yml` qui le déclare — là où vivent déjà tous les
|
||||
flux du rôle. Deux fichiers pour un même fait finiraient par diverger.
|
||||
|
||||
### Le critère d'un panneau
|
||||
|
||||
*Une série a sa place si elle **précède** un verdict, ou si elle n'en aura **jamais**.*
|
||||
|
||||
Ce qui bascule d'un coup appartient à Icinga ; ce qui dérive lentement n'a que le graphe
|
||||
pour se faire voir. Une base qui passe de 20 à 60 connexions en trois semaines n'a rien
|
||||
cassé — elle annonce la date où elle cassera.
|
||||
|
||||
### La `raison` devient la description du panneau
|
||||
|
||||
C'est le seul champ qui ne produit aucun pixel de graphe, et c'est le plus important.
|
||||
Grafana l'affiche au survol du titre. Sans elle, celui qui regarde six mois plus tard voit
|
||||
une courbe sans savoir ce qu'elle annonce.
|
||||
|
||||
### Agréger, toujours
|
||||
|
||||
node_exporter publie une série par interface, par point de montage, par cœur : **339
|
||||
séries** pour le seul trafic réseau du site, mesuré le 2026-09-14. Un panneau qui les
|
||||
montre toutes ne montre rien. Et se méfier de `min()` / `max()` : un seul montage
|
||||
pathologique — `/var/lib/lxcfs`, qui rapporte toujours zéro — suffit à rendre un panneau
|
||||
faux **pour toujours**, avec une courbe plate parfaitement lisible. Filtrer par **liste
|
||||
blanche** : un montage inconnu manque au graphe, ce qui se voit, au lieu de l'écraser, ce
|
||||
qui ne se voit pas.
|
||||
|
||||
### Ce que les gardes tiennent
|
||||
|
||||
**P77** exige de chaque panneau un titre, une expression, une unité et une raison — et que
|
||||
l'unité figure dans la table de traduction de `serveur_grafana`. Une unité inventée ne
|
||||
casse rien : le panneau retombe sur `short`, et un graphe d'octets gradué en unités brutes
|
||||
reste parfaitement lisible et parfaitement faux.
|
||||
|
||||
Ce que P77 **ne** dit pas : si l'expression répond. Aucune lecture statique ne le dira. Un
|
||||
panneau se prouve comme une sonde — en le regardant rendre des données, et en le notant au
|
||||
CHANGELOG.
|
||||
|
||||
## Chaque sonde vient avec son contrôle négatif
|
||||
|
||||
Une sonde qu'on n'a jamais vue échouer n'est pas une sonde, c'est une habitude. Dix-neuf
|
||||
|
|
|
|||
85
roles/client_metrique/meta/metriques.yml
Normal file
85
roles/client_metrique/meta/metriques.yml
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
---
|
||||
# Metriques derivees du role. Le pendant de `meta/supervision.yml`, et son contraire.
|
||||
#
|
||||
# `supervision.yml` -> une SONDE rend un VERDICT avec un TTL. « est-ce casse ? »
|
||||
# `metriques.yml` -> un EXPORTATEUR expose une SERIE. « depuis quand,
|
||||
# et vers ou ? »
|
||||
#
|
||||
# PAS D'`exportateur:` ICI, ET C'EST VOULU. Ce role EST celui qui pose node_exporter, et
|
||||
# le job `node` est ecrit en dur dans `prometheus.yml.j2` parce qu'il est UNIVERSEL : il
|
||||
# vise toute la flotte, pas les porteurs d'un role. Le deriver d'ici le ferait exister
|
||||
# deux fois, et deux ecritures d'un meme fait finissent par diverger.
|
||||
#
|
||||
# Ce fichier n'apporte donc que des `panneaux` — la moitie que personne ne portait. C'est
|
||||
# le cas symetrique de celui que le moteur sait deja traiter : un role peut declarer un
|
||||
# exportateur sans panneau, il peut declarer des panneaux sans exportateur.
|
||||
#
|
||||
# LE CRITERE, LE MEME QUE PARTOUT : une serie a sa place ici si elle PRECEDE un verdict ou
|
||||
# si elle n'en aura jamais. Ce qui bascule d'un coup appartient a Icinga ; ce qui derive
|
||||
# lentement n'a que le graphe pour se faire voir.
|
||||
#
|
||||
# TOUT EST AGREGE PAR `instance`. node_exporter publie une serie par interface, par point
|
||||
# de montage, par coeur : 339 series pour le seul trafic reseau du site, mesure le
|
||||
# 2026-09-14. Un panneau qui les montre toutes ne montre rien.
|
||||
panneaux:
|
||||
- titre: "Mémoire disponible (part du total)"
|
||||
expr: >-
|
||||
sum by (instance) (node_memory_MemAvailable_bytes)
|
||||
/ clamp_min(sum by (instance) (node_memory_MemTotal_bytes), 1)
|
||||
unite: ratio
|
||||
raison: >-
|
||||
CELUI-LA COMPTE DEPUIS QUE LE BALLON EST ACTIF. L'hyperviseur reprend de la memoire
|
||||
a une VM qui n'en a pas besoin — silencieusement, 100 Mio par cycle. La VM ne voit
|
||||
pas son plafond bouger : elle voit sa marge fondre. Aucun verdict ne peut se poser
|
||||
la-dessus, parce qu'il n'y a pas de seuil juste : c'est la PENTE qui dit si le
|
||||
plancher a ete pris trop bas.
|
||||
|
||||
- titre: "Espace libre — le point de montage le plus serré"
|
||||
# UNE LISTE BLANCHE DE SYSTEMES DE FICHIERS REELS, ET PAS UNE LISTE NOIRE.
|
||||
#
|
||||
# La premiere ecriture excluait `tmpfs|devtmpfs|overlay|squashfs`. Mesure du
|
||||
# 2026-09-14 sur la flotte : le panneau rendait **0 octet libre** pour les trois
|
||||
# hyperviseurs. Le coupable etait `/var/lib/lxcfs`, un systeme FUSE virtuel qui
|
||||
# rapporte toujours zero — et qui n'est pas en lecture seule, donc aucun filtre
|
||||
# malin ne l'ecartait.
|
||||
#
|
||||
# UN `min()` EST IMPITOYABLE : un seul montage pathologique suffit a rendre le
|
||||
# panneau inutile POUR TOUJOURS, et sa courbe plate ressemble a un disque plein.
|
||||
# C'est la pire sorte de faux : lisible, alarmant, et faux.
|
||||
#
|
||||
# Une liste noire suit ce qui existe — elle prend donc du retard des qu'un systeme de
|
||||
# fichiers nouveau apparait. Une liste blanche, elle, ignore par defaut : un montage
|
||||
# inconnu manque au graphe, ce qui se voit, au lieu de l'ecraser, ce qui ne se voit
|
||||
# pas. Mesure du 2026-09-14 : la flotte ne monte que ext4, vfat, tmpfs, fuse et
|
||||
# fuse.lxcfs.
|
||||
expr: >-
|
||||
min by (instance) (node_filesystem_avail_bytes{fstype=~"ext4|xfs|btrfs|zfs"})
|
||||
unite: octets
|
||||
raison: >-
|
||||
La sonde `sante` crie quand un seuil est franchi. Ce panneau dit ce qu'elle ne peut
|
||||
pas dire : DANS COMBIEN DE TEMPS. Un disque qui perd 2 Go par semaine n'a rien
|
||||
casse — il annonce la date ou il cassera, et c'est la seule chose qu'on ne peut pas
|
||||
mesurer apres coup.
|
||||
|
||||
- titre: "Charge par cœur"
|
||||
expr: >-
|
||||
node_load1
|
||||
/ on(instance) group_left count by (instance) (node_cpu_seconds_total{mode="idle"})
|
||||
unite: nombre
|
||||
raison: >-
|
||||
NORMALISEE, sinon elle ne se compare pas : une charge de 4 est confortable sur huit
|
||||
coeurs et desastreuse sur deux. Au-dessus de 1, la machine attend plus qu'elle ne
|
||||
travaille. C'est le contexte des trois autres — une memoire qui fond a charge
|
||||
CONSTANTE ne dit pas la meme chose qu'une memoire qui fond parce que le travail a
|
||||
double.
|
||||
|
||||
- titre: "Trafic réseau entrant"
|
||||
expr: >-
|
||||
sum by (instance) (rate(node_network_receive_bytes_total{device!="lo"}[5m]))
|
||||
unite: octets_par_seconde
|
||||
raison: >-
|
||||
Il n'aura JAMAIS de verdict, et c'est pourquoi il est la. Aucun seuil de trafic
|
||||
n'est juste : ce qui se lit, c'est le changement de FORME — un plateau la ou il y
|
||||
avait des vagues, un creux aux heures ouvrables. Une sauvegarde qui ne part plus et
|
||||
un flux qu'on vient de fermer se ressemblent ici, et ne se voient nulle part
|
||||
ailleurs.
|
||||
|
|
@ -77,3 +77,31 @@ serveur_grafana_sonde_url: "http://127.0.0.1:{{ serveur_grafana_port }}"
|
|||
# DEGRADE, JAMAIS DEVINE : pas de cache d'amorcage declare, pas de reecriture.
|
||||
serveur_grafana_depot_schema: >-
|
||||
{{ 'http' if (artefacts_amorcage | default('') | string | length > 0) else 'https' }}
|
||||
|
||||
# --- LES TABLEAUX DERIVES DES ROLES (2026-09-14) --------------------------------------
|
||||
#
|
||||
# TRADUCTION DES UNITES : ce qu'un role ECRIT dans son `meta/metriques.yml` vers ce que
|
||||
# Grafana COMPREND. Un role parle sa langue — « octets », « connexions » — et n'a pas a
|
||||
# connaitre la nomenclature de Grafana ; c'est le moteur qui traduit, comme partout ici.
|
||||
#
|
||||
# UNE UNITE ABSENTE DE CETTE TABLE retombe sur `short`, et le graphe reste lisible. Mais
|
||||
# elle ne passe pas inapercue : P77 refuse une unite que cette table ne connait pas.
|
||||
serveur_grafana_unites:
|
||||
octets: bytes
|
||||
octets_par_seconde: Bps
|
||||
secondes: s
|
||||
millisecondes: ms
|
||||
ratio: percentunit
|
||||
pourcent: percent
|
||||
connexions: short
|
||||
requetes: short
|
||||
messages: short
|
||||
tps: ops
|
||||
operations_par_seconde: ops
|
||||
nombre: short
|
||||
|
||||
# LE PREFIXE DES TABLEAUX DERIVES. Il sert a la MOISSON : tout fichier qui le porte et
|
||||
# qu'aucun role ne declare plus est retire. Sans ce marqueur on ne saurait pas distinguer
|
||||
# un tableau derive d'un tableau ecrit a la main (`journaux-flotte.json`), et on effacerait
|
||||
# le travail de quelqu'un.
|
||||
serveur_grafana_tableaux_prefixe: "setops-role-"
|
||||
|
|
|
|||
|
|
@ -245,6 +245,109 @@
|
|||
directory_mode: "0750"
|
||||
notify: Redemarrer grafana
|
||||
|
||||
# --- UN TABLEAU PAR ROLE QUI DECLARE DES PANNEAUX (2026-09-14) -------------------------
|
||||
#
|
||||
# LA MOITIE QUI MANQUAIT. Les roles declaraient leurs panneaux dans `meta/metriques.yml`,
|
||||
# Prometheus en derivait deja ses cibles, les expressions repondaient — et RIEN ne les
|
||||
# assemblait. Un panneau declare que personne ne peut regarder n'est pas une
|
||||
# observabilite, c'est une intention.
|
||||
#
|
||||
# EXACTEMENT LE PATRON DE `serveur_prometheus`, qui lit les memes fichiers pour en tirer
|
||||
# ses cibles de scrutation. Les deux moities de `metriques.yml` trouvent ainsi leur
|
||||
# consommateur : l'`exportateur` chez Prometheus, les `panneaux` ici.
|
||||
#
|
||||
# `run_once` ET `delegate_to: localhost` : les declarations vivent dans le DEPOT, sur le
|
||||
# controleur — pas sur la machine. On les lit une fois, on les rend pour tout le monde.
|
||||
- name: Relever les metriques que les roles declarent
|
||||
ansible.builtin.find:
|
||||
paths: "{{ role_path }}/.."
|
||||
patterns: metriques.yml
|
||||
recurse: true
|
||||
depth: 3
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
run_once: true
|
||||
check_mode: false
|
||||
register: serveur_grafana_metas
|
||||
|
||||
- name: Lire chaque declaration de metriques
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ item.path }}"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
run_once: true
|
||||
check_mode: false
|
||||
loop: "{{ serveur_grafana_metas.files }}"
|
||||
loop_control:
|
||||
label: "{{ item.path | dirname | dirname | basename }}"
|
||||
register: serveur_grafana_declarations
|
||||
|
||||
# CE QUI SERA POSE, CALCULE UNE FOIS. On ne garde que les roles qui declarent vraiment des
|
||||
# `panneaux` : un `metriques.yml` peut n'avoir qu'un `exportateur` — il aura une cible
|
||||
# Prometheus et aucun tableau, et c'est un etat legitime, pas un oubli.
|
||||
- name: Retenir les roles qui ont des panneaux a montrer
|
||||
ansible.builtin.set_fact:
|
||||
serveur_grafana_tableaux: >-
|
||||
{{ serveur_grafana_declarations.results
|
||||
| map(attribute='item.path') | map('dirname') | map('dirname') | map('basename')
|
||||
| zip(serveur_grafana_declarations.results
|
||||
| map(attribute='content') | map('b64decode') | map('from_yaml'))
|
||||
| selectattr(1, 'mapping')
|
||||
| selectattr('1.panneaux', 'defined')
|
||||
| rejectattr('1.panneaux', 'none')
|
||||
| list }}
|
||||
run_once: true
|
||||
|
||||
- name: Assembler le tableau de chaque role
|
||||
ansible.builtin.template:
|
||||
src: tableau-role.json.j2
|
||||
dest: "{{ serveur_grafana_dashboards_dir }}/{{ serveur_grafana_tableaux_prefixe }}{{ item.0 }}.json"
|
||||
owner: root
|
||||
group: grafana
|
||||
mode: "0640"
|
||||
# LE JSON EST VALIDE AVANT D'ETRE POSE. Grafana n'echoue pas sur un tableau illisible :
|
||||
# il le SAUTE, en silence, et le tableau disparait simplement de la liste. Sans cette
|
||||
# validation, une expression PromQL mal repliee ferait perdre un tableau sans qu'aucune
|
||||
# tache ne devienne rouge.
|
||||
validate: "python3 -c \"import json,sys; json.load(open(sys.argv[1]))\" %s"
|
||||
loop: "{{ serveur_grafana_tableaux }}"
|
||||
loop_control:
|
||||
label: "{{ item.0 }} ({{ item.1.panneaux | length }} panneau(x))"
|
||||
vars:
|
||||
tableau_role: "{{ item.0 }}"
|
||||
tableau_panneaux: "{{ item.1.panneaux }}"
|
||||
notify: Redemarrer grafana
|
||||
|
||||
# --- UN ROLE QU'ON RETIRE DOIT POUVOIR DEFAIRE CE QU'IL A FAIT ------------------------
|
||||
#
|
||||
# Meme lecon que les sondes orphelines de `client_sante`, et elle s'est deja payee ici :
|
||||
# quand un ecosysteme cesse de declarer un service, plus rien ne retire ce qu'on avait pose
|
||||
# pour lui. Un tableau orphelin est moins grave qu'une sonde orpheline — il n'echoue pas,
|
||||
# il MENT : il affiche « aucune donnee » pour un service qui n'existe plus, et celui qui
|
||||
# regarde croit a une panne.
|
||||
#
|
||||
# LE PREFIXE EST CE QUI PROTEGE `journaux-flotte.json` : il n'en porte pas, donc la moisson
|
||||
# ne le voit pas. On n'efface jamais un tableau ecrit a la main.
|
||||
- name: Relever les tableaux derives reellement poses
|
||||
ansible.builtin.find:
|
||||
paths: "{{ serveur_grafana_dashboards_dir }}"
|
||||
patterns: "{{ serveur_grafana_tableaux_prefixe }}*.json"
|
||||
register: serveur_grafana_poses
|
||||
|
||||
- name: Retirer les tableaux qu'aucun role ne declare plus
|
||||
ansible.builtin.file:
|
||||
path: "{{ item.path }}"
|
||||
state: absent
|
||||
loop: "{{ serveur_grafana_poses.files }}"
|
||||
loop_control:
|
||||
label: "{{ item.path | basename }}"
|
||||
when: >-
|
||||
(item.path | basename
|
||||
| regex_replace('^' ~ serveur_grafana_tableaux_prefixe, '')
|
||||
| regex_replace('\.json$', ''))
|
||||
not in (serveur_grafana_tableaux | map(attribute=0) | list)
|
||||
notify: Redemarrer grafana
|
||||
|
||||
- name: Assurer le repertoire de drop-in systemd
|
||||
ansible.builtin.file:
|
||||
path: /etc/systemd/system/grafana-server.service.d
|
||||
|
|
|
|||
77
roles/serveur_grafana/templates/tableau-role.json.j2
Normal file
77
roles/serveur_grafana/templates/tableau-role.json.j2
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
{#- GENERE par Set-OPS (role serveur_grafana). Ne pas editer a la main.
|
||||
|
||||
UN TABLEAU PAR ROLE, ASSEMBLE DEPUIS SON `meta/metriques.yml`.
|
||||
|
||||
C'EST LA MOITIE QUI MANQUAIT. Les roles declaraient leurs panneaux, Prometheus
|
||||
collectait les series, les expressions repondaient — et rien ne les assemblait. Un
|
||||
panneau declare que personne ne peut regarder n'est pas une observabilite, c'est une
|
||||
intention.
|
||||
|
||||
POURQUOI UN TABLEAU PAR ROLE ET PAS UN GRAND. Parce que la question qu'on se pose est
|
||||
« comment va PostgreSQL », pas « comment va la flotte » — celle-la est deja repondue
|
||||
par Icinga, et par le tableau des journaux. Un tableau par role suit la meme ligne de
|
||||
partage que tout le reste ici : le role est l'unite qui declare, donc l'unite qui
|
||||
s'affiche.
|
||||
|
||||
LA `raison` DEVIENT LA DESCRIPTION DU PANNEAU. Elle est le seul champ obligatoire qui
|
||||
ne produit aucun pixel de graphe, et c'est le plus important : sans elle, celui qui
|
||||
regarde a six mois de distance voit une courbe sans savoir ce qu'elle annonce. Grafana
|
||||
l'affiche au survol du titre.
|
||||
-#}
|
||||
{%- set roles_unites = serveur_grafana_unites -%}
|
||||
{
|
||||
"uid": {{ ("setops-" ~ tableau_role | replace("serveur_", "") | replace("_", "-")) | truncate(40, true, "") | tojson }},
|
||||
"title": {{ ("Set-OPS — " ~ tableau_role | replace("serveur_", "") | replace("_", " ")) | tojson }},
|
||||
"tags": ["set-ops", "derive", {{ tableau_role | tojson }}],
|
||||
"timezone": "browser",
|
||||
"refresh": "1m",
|
||||
"time": { "from": "now-6h", "to": "now" },
|
||||
"schemaVersion": 39,
|
||||
"version": 1,
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"name": "ds_prom",
|
||||
"label": "Source Prometheus",
|
||||
"type": "datasource",
|
||||
"query": "prometheus",
|
||||
"hide": 2,
|
||||
"current": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"panels": [
|
||||
{%- for p in tableau_panneaux %}
|
||||
{
|
||||
"id": {{ loop.index }},
|
||||
"type": "timeseries",
|
||||
"title": {{ p.titre | tojson }},
|
||||
{#- LA RAISON, TELLE QUELLE. On ne la resume pas : c'est le texte que le role a
|
||||
ecrit pour expliquer ce que la courbe annonce. #}
|
||||
"description": {{ (p.raison | default("") | trim) | tojson }},
|
||||
"datasource": { "type": "prometheus", "uid": "${ds_prom}" },
|
||||
{#- DEUX PAR RANGEE. Une expression PromQL rend souvent plusieurs series ; a pleine
|
||||
largeur on ne compare rien, a un quart de largeur on ne lit rien. #}
|
||||
"gridPos": { "h": 8, "w": 12, "x": {{ 0 if loop.index0 % 2 == 0 else 12 }}, "y": {{ (loop.index0 // 2) * 8 }} },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
{#- L'UNITE VIENT DU ROLE, TRADUITE EN UNITE GRAFANA. Un `unite:` inconnu
|
||||
retombe sur `short` — et P77 refuse de laisser ce cas passer inapercu. #}
|
||||
"unit": {{ roles_unites.get(p.unite | default("") | string, "short") | tojson }},
|
||||
"custom": { "drawStyle": "line", "fillOpacity": 10, "showPoints": "never" }
|
||||
}
|
||||
},
|
||||
"options": { "legend": { "displayMode": "list", "placement": "bottom" } },
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"datasource": { "type": "prometheus", "uid": "${ds_prom}" },
|
||||
{#- L'EXPRESSION EST CELLE DU ROLE, non reecrite. Repliee sur une ligne parce
|
||||
que `>-` en YAML laisse des retours que JSON refuse. #}
|
||||
"expr": {{ (p.expr | trim | replace("\n", " ")) | regex_replace("\\s+", " ") | tojson }}
|
||||
}
|
||||
]
|
||||
}{{ "," if not loop.last else "" }}
|
||||
{%- endfor %}
|
||||
]
|
||||
}
|
||||
|
|
@ -2105,6 +2105,80 @@ def preuve_carte_dit_vrai() -> tuple[bool, str]:
|
|||
f"{len(_carte_chiffres_mesures())} chiffres correspondent a la mesure.")
|
||||
|
||||
|
||||
def preuve_panneaux_assembles() -> tuple[bool, str]:
|
||||
"""Chaque panneau declare porte de quoi etre assemble, et son unite est traduisible.
|
||||
|
||||
LES DEUX MOITIES DE `meta/metriques.yml` ONT CHACUNE LEUR CONSOMMATEUR : l'
|
||||
`exportateur` devient une cible de scrutation chez `serveur_prometheus`, les
|
||||
`panneaux` deviennent un tableau chez `serveur_grafana`. P64 fait ce travail pour les
|
||||
sondes ; celle-ci le fait pour les series.
|
||||
|
||||
CE QU'ELLE EXIGE DE CHAQUE PANNEAU. Un `titre`, une `expr`, une `unite`, et une
|
||||
`raison`. La `raison` est le seul champ qui ne produit aucun pixel, et c'est le plus
|
||||
important : elle devient la description du panneau dans Grafana. Sans elle, celui qui
|
||||
regarde a six mois de distance voit une courbe sans savoir ce qu'elle annonce.
|
||||
|
||||
ET SURTOUT : L'UNITE DOIT ETRE TRADUISIBLE. `serveur_grafana` tient une table qui
|
||||
traduit la langue des roles — « octets », « connexions » — vers celle de Grafana.
|
||||
C'est UNE LISTE QUI EN SUIT UNE AUTRE, et une liste qui suit une autre prend du
|
||||
retard : un role qui invente une unite ne casse rien, son panneau retombe simplement
|
||||
sur `short`. Un graphe d'octets gradue en unites brutes reste parfaitement lisible et
|
||||
parfaitement faux.
|
||||
|
||||
CE QU'ELLE NE FAIT PAS. Elle ne dit pas si l'expression REPOND — aucune lecture
|
||||
statique ne le dira, il faut un Prometheus qui a des donnees. C'est au controle de
|
||||
chaque panneau, trace au CHANGELOG, comme le controle negatif d'une sonde.
|
||||
"""
|
||||
roles = RACINE / "roles"
|
||||
table = roles / "serveur_grafana" / "defaults" / "main.yml"
|
||||
try:
|
||||
unites = set((yaml.safe_load(table.read_text(encoding="utf-8")) or {})
|
||||
.get("serveur_grafana_unites") or {})
|
||||
except (OSError, yaml.YAMLError) as e:
|
||||
return False, f"Table des unites de serveur_grafana illisible ({e})."
|
||||
if not unites:
|
||||
return False, ("`serveur_grafana_unites` est vide : aucun panneau ne pourrait "
|
||||
"etre gradue.")
|
||||
|
||||
fautes, mesures = [], []
|
||||
for meta in sorted(roles.glob("*/meta/metriques.yml")):
|
||||
role = meta.parent.parent.name
|
||||
try:
|
||||
data = yaml.safe_load(meta.read_text(encoding="utf-8")) or {}
|
||||
except yaml.YAMLError as e:
|
||||
fautes.append(f"{role} : `meta/metriques.yml` illisible ({e})")
|
||||
continue
|
||||
panneaux = data.get("panneaux")
|
||||
expo = data.get("exportateur")
|
||||
if not panneaux and not expo:
|
||||
fautes.append(f"{role} : `meta/metriques.yml` ne declare ni exportateur "
|
||||
f"ni panneau — personne ne le lira")
|
||||
continue
|
||||
for p in (panneaux or []):
|
||||
titre = p.get("titre") or "(sans titre)"
|
||||
for champ in ("titre", "expr", "unite", "raison"):
|
||||
if not str(p.get(champ) or "").strip():
|
||||
fautes.append(f"{role}/{titre} : `{champ}` manquant"
|
||||
+ (" — un panneau dont personne ne sait ce qu'il "
|
||||
"annonce n'est pas une observabilite"
|
||||
if champ == "raison" else ""))
|
||||
unite = str(p.get("unite") or "").strip()
|
||||
if unite and unite not in unites:
|
||||
fautes.append(f"{role}/{titre} : unite « {unite} » absente de "
|
||||
f"`serveur_grafana_unites` — le panneau retomberait sur "
|
||||
f"`short`, lisible et faux")
|
||||
mesures.append(f"{role}/{titre}")
|
||||
|
||||
if fautes:
|
||||
return False, "Panneaux declares :\n - " + "\n - ".join(fautes)
|
||||
if not mesures:
|
||||
return True, ("Aucun panneau declare — rien a assembler "
|
||||
"(cf. docs/supervision-conception.md).")
|
||||
return True, (f"{len(mesures)} panneau(x) declare(s) dans "
|
||||
f"{len({m.split('/')[0] for m in mesures})} role(s), tous avec titre, "
|
||||
f"expression, raison et une unite que la table sait traduire.")
|
||||
|
||||
|
||||
def preuve_gabarits_shell_rendent() -> tuple[bool, str]:
|
||||
"""Aucun gabarit shell ne contient de sequence que Jinja lirait autrement.
|
||||
|
||||
|
|
@ -3875,6 +3949,8 @@ PREUVES: list[dict] = [
|
|||
"refs": [], "func": preuve_parametres_clone_traversent},
|
||||
{"id": "P76", "titre": "Tout gabarit de role se rend vraiment",
|
||||
"refs": [], "func": preuve_gabarits_shell_rendent},
|
||||
{"id": "P77", "titre": "Panneaux declares : assemblables, et gradues",
|
||||
"refs": [], "func": preuve_panneaux_assembles},
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue