grafana : un tableau de synthèse, au site et chez les locataires

- tableau-synthese : faut-il agir, marge, activité, où regarder ensuite ;
  hyperviseurs au site seulement ; chaque panneau dit quoi faire
- CHANGELOG 70

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-10-04 01:12:00 -04:00
parent b3debd6ca9
commit 05ec7a8743
3 changed files with 262 additions and 0 deletions

View file

@ -1,5 +1,36 @@
# CHANGELOG — Set-OPS
## 2026-10-04 (70) — Un tableau de synthèse, au site et chez les locataires
**Demande de l'exploitant** : la seconde moitié de « rendre les tableaux plus parlants » — un
tableau d'accueil. Contenu arrêté avec lui avant d'écrire ; site ET locataires.
**Fait** : `tableau-synthese.json.j2` (« Set-OPS — Synthèse », `setops-synthese`), quatre
rangées, quatre questions :
1. **Faut-il agir ?** — tuiles colorées : matériel des hyperviseurs (niveau 0/1/2 tiré du
même catalogue que le tableau Matériel et la sonde Icinga), swap le plus plein des
hyperviseurs, disque le plus plein des machines, correctifs de sécurité en attente,
redémarrages en attente, machines muettes ;
2. **Reste-t-il de la marge ?** — mémoire et swap des hyperviseurs, disque et mémoire par
machine (de la plus pleine à la moins pleine) ;
3. **Que se passe-t-il ?** — processeur et trafic réseau (interfaces physiques) ;
4. **Où regarder ensuite ?** — liens vers Matériel, Machines, PostgreSQL, Journaux et la vigie.
Chaque panneau dit ce qu'il mesure, ses seuils et quoi faire quand sa couleur change.
**Un gabarit, deux visages** : au site (`setops_materiel` présent), il ajoute les hyperviseurs ;
chez un locataire, il ne voit que ses machines. Le nom des machines vient de
`node_uname_info` : les locataires n'ont pas l'étiquette `hote` du site.
**Éprouvé avant de poser** : chaque requête exécutée sur le vrai Prometheus, au site et chez
Chezlepro. Une erreur trouvée ainsi : la tuile matériel affichait « 3 » — `count(x) > 0` garde
la valeur du compte ; elle rend désormais un niveau. Ce que la synthèse dit au premier
regard, au site : matériel « à planifier » (disque de gandalf), **swap de vishnu à 98,6 %**,
65 correctifs de sécurité en attente, 9 redémarrages en attente.
**Appliqué** au site, puis aux deux locataires (17 et 14 panneaux), sans erreur de chargement.
**Validation** : `ansible-lint` (0 violation), `make verifier` conforme (83/83).
## 2026-10-04 (69) — Le tableau Matériel dit quoi faire
**Demande de l'exploitant** : rendre les tableaux Grafana plus parlants — « phrases et

View file

@ -335,6 +335,21 @@
when: setops_materiel is defined
notify: Redemarrer grafana
# --- LA SYNTHESE (2026-10-04) — la porte d'entree, au site comme chez un locataire ------
#
# Le meme gabarit partout : il ajoute les hyperviseurs quand `setops_materiel` existe (au
# site), et ne voit que les machines de l'ecosysteme sinon. Pas de prefixe `setops-role-` :
# la moisson des tableaux orphelins ne doit pas le voir.
- name: Assembler le tableau de synthèse
ansible.builtin.template:
src: tableau-synthese.json.j2
dest: "{{ serveur_grafana_dashboards_dir }}/setops-synthese.json"
owner: root
group: grafana
mode: "0640"
validate: "python3 -c \"import json,sys; json.load(open(sys.argv[1]))\" %s"
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 :

View file

@ -0,0 +1,216 @@
{#- GENERE par Set-OPS (role serveur_grafana). Ne pas editer a la main.
LE TABLEAU DE SYNTHESE (2026-10-04) — la porte d'entree de l'observatoire.
Quatre rangees, quatre questions : faut-il agir ? reste-t-il de la marge ? que se
passe-t-il ? ou regarder ensuite ? Chaque panneau dit ce qu'il mesure, ou sont ses
seuils, et QUOI FAIRE quand sa couleur change — meme patron que le tableau Materiel.
LE MEME GABARIT AU SITE ET CHEZ UN LOCATAIRE. Au site, `setops_materiel` existe : la
synthese ajoute les hyperviseurs (tuile materiel, memoire et swap) et regarde leur
activite. Chez un locataire, elle ne voit que ses propres machines.
LE NOM DES MACHINES vient de `node_uname_info` (joint sur `instance`) : les locataires
n'ont pas l'etiquette `hote` du site, et une adresse IP ne parle a personne.
-#}
{%- set fabric = setops_materiel is defined -%}
{#- La tuile materiel rend un NIVEAU (0, 1, 2), pas un compte : `count(x) > 0` garde la
valeur du compte — trois secteurs en avertissement s'affichaient « 3 ». -#}
{%- set cat = setops_materiel | default({"temperatures": [], "controles": []}) -%}
{%- set nom = ' * on(instance) group_left(nodename) node_uname_info' -%}
{%- set ds = {"type": "prometheus", "uid": "${ds_prom}"} -%}
{%- set panneaux = [] -%}
{%- set y = namespace(v=0, id=0) -%}
{#- ---- MODE D'EMPLOI ---------------------------------------------------------------- #}
{%- set y.id = y.id + 1 -%}
{%- set _ = panneaux.append({
"type": "text", "id": y.id, "title": "",
"gridPos": {"h": 3, "w": 24, "x": 0, "y": y.v},
"options": {"mode": "markdown", "content":
"**Synthèse " ~ ("du site" if fabric else "de l'écosystème") ~ ".** Quatre rangées, quatre questions : **faut-il agir ?** — **reste-t-il de la marge ?** — **que se passe-t-il ?** — **où regarder ensuite ?** "
~ "Vert : rien à faire. Orange : à planifier. Rouge : à traiter. Survoler le titre d'un panneau (ⓘ) dit ce qu'il mesure et quoi faire s'il change de couleur."}}) -%}
{%- set y.v = y.v + 3 -%}
{#- ---- RANGEE 1 : FAUT-IL AGIR ? ----------------------------------------------------- #}
{%- set y.id = y.id + 1 -%}
{%- set _ = panneaux.append({"type": "row", "id": y.id, "title": "Faut-il agir ?", "collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": y.v}}) -%}
{%- set y.v = y.v + 1 -%}
{%- set tuiles = [] -%}
{%- if fabric -%}
{%- set crit = [] -%}
{%- set avert = [] -%}
{%- for c in cat.controles -%}
{%- set _ = (crit if c.niveau == "crit" else avert).append(c.expr) -%}
{%- endfor -%}
{%- for f in cat.temperatures -%}
{%- if f.crit is not none -%}{%- set _ = crit.append("(" ~ f.expr_juge ~ ") > " ~ f.crit) -%}{%- endif -%}
{%- if f.avert is not none -%}{%- set _ = avert.append("(" ~ f.expr_juge ~ ") > " ~ f.avert) -%}{%- endif -%}
{%- endfor -%}
{%- set _ = tuiles.append({
"titre": "Matériel des hyperviseurs",
"expr": "(count((" ~ (crit | join(") or (")) ~ ")) > 0) * 0 + 2 or (count((" ~ (avert | join(") or (")) ~ ")) > 0) * 0 + 1 or vector(0)",
"unite": "none",
"seuils": [{"color": "green", "value": none}, {"color": "orange", "value": 1}, {"color": "red", "value": 2}],
"mappings": [{"type": "value", "options": {"0": {"text": "Rien à faire"}, "1": {"text": "À planifier"}, "2": {"text": "À traiter"}}}],
"desc": "Le pire verdict du tableau Matériel : températures et contrôles des disques, ventilateurs et NVMe, avec les mêmes seuils que le service Icinga « materiel ».\n\n**Si c'est orange ou rouge** : ouvrir le tableau « Set-OPS — Matériel » ; la tuile colorée y dit quel équipement, et sa description dit quoi faire."}) -%}
{%- set _ = tuiles.append({
"titre": "Swap le plus plein (hyperviseurs)",
"expr": "max(100 * (1 - node_memory_SwapFree_bytes{job=\"fabric\"} / node_memory_SwapTotal_bytes{job=\"fabric\"}))",
"unite": "percent",
"seuils": [{"color": "green", "value": none}, {"color": "orange", "value": 50}, {"color": "red", "value": 90}],
"desc": "Le swap le plus rempli des trois hyperviseurs. Un hyperviseur qui swappe ralentit toutes les VM qu'il porte, sans aucune erreur visible.\n\n**Seuils** : orange à 50 %, rouge à 90 %.\n\n**Si c'est orange ou rouge** : la mémoire allouée aux VM dépasse ce que la machine peut tenir. Réduire la RAM des VM qui en ont trop (voir « Mémoire utilisée par machine »), arrêter les VM inutiles, ou déplacer une VM vers l'hyperviseur le moins chargé."}) -%}
{%- endif -%}
{%- set _ = tuiles.append({
"titre": "Disque le plus plein (machines)",
"expr": "max(100 * (1 - node_filesystem_avail_bytes{job=\"node\",mountpoint=\"/\"} / node_filesystem_size_bytes{job=\"node\",mountpoint=\"/\"}))",
"unite": "percent",
"seuils": [{"color": "green", "value": none}, {"color": "orange", "value": 80}, {"color": "red", "value": 90}],
"desc": "Le disque système le plus rempli parmi les machines de cet écosystème.\n\n**Seuils** : orange à 80 %, rouge à 90 % — les mêmes que la sonde Icinga « disque ».\n\n**Si c'est orange ou rouge** : trouver la machine dans « Disque utilisé par machine », plus bas ; chercher ce qui grossit (journaux, cache apt, sauvegardes locales) ou agrandir son disque."}) -%}
{%- set _ = tuiles.append({
"titre": "Correctifs de sécurité en attente",
"expr": "sum(apt_upgrades_pending{origin=~\"(?i).*securit.*\"}) or vector(0)",
"unite": "none",
"seuils": [{"color": "green", "value": none}, {"color": "orange", "value": 1}],
"desc": "Nombre de paquets de sécurité Debian prêts à être installés, toutes machines confondues" ~ (" (hyperviseurs compris)" if fabric else "") ~ ".\n\n**Seuil** : orange dès 1.\n\n**Si c'est orange** : les machines du socle les appliquent d'elles-mêmes dans la nuit ; si le compte ne redescend pas d'ici le lendemain, la sonde Icinga « correctifs » dit lesquelles bloquent." ~ (" Les hyperviseurs, eux, se mettent à jour à la main." if fabric else "")}) -%}
{%- set _ = tuiles.append({
"titre": "Redémarrages en attente",
"expr": "count(node_reboot_required == 1) or vector(0)",
"unite": "none",
"seuils": [{"color": "green", "value": none}, {"color": "orange", "value": 1}],
"desc": "Machines dont une mise à jour (noyau, bibliothèque système) ne prendra effet qu'au prochain redémarrage.\n\n**Seuil** : orange dès 1.\n\n**Si c'est orange** : planifier les redémarrages, une machine à la fois, en dehors des heures d'usage. Tant qu'elles ne redémarrent pas, le correctif est installé mais pas actif."}) -%}
{%- set _ = tuiles.append({
"titre": "Machines muettes",
"expr": "count(up == 0) or vector(0)",
"unite": "none",
"seuils": [{"color": "green", "value": none}, {"color": "red", "value": 1}],
"desc": "Cibles que Prometheus n'arrive plus à interroger : machine arrêtée, exportateur tombé, ou flux coupé.\n\n**Seuil** : rouge dès 1.\n\n**Si c'est rouge** : ce tableau ne voit plus cette machine, et tous ses chiffres sont figés. Vérifier qu'elle tourne, puis la sonde Icinga « collecte », qui la nomme."}) -%}
{%- set largeur = 24 // (tuiles | length) -%}
{%- for t in tuiles -%}
{%- set y.id = y.id + 1 -%}
{%- set _ = panneaux.append({
"type": "stat", "id": y.id, "title": t.titre, "description": t.desc, "datasource": ds,
"gridPos": {"h": 5, "w": largeur, "x": loop.index0 * largeur, "y": y.v},
"fieldConfig": {"defaults": {"unit": t.unite, "decimals": 0, "noValue": "—",
"mappings": t.mappings | default([]),
"thresholds": {"mode": "absolute", "steps": t.seuils}}, "overrides": []},
"options": {"colorMode": "background", "graphMode": "none", "justifyMode": "center",
"textMode": "value", "orientation": "auto",
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}},
"targets": [{"refId": "A", "datasource": ds, "expr": t.expr}]}) -%}
{%- endfor -%}
{%- set y.v = y.v + 5 -%}
{#- ---- RANGEE 2 : RESTE-T-IL DE LA MARGE ? ----------------------------------------- #}
{%- set y.id = y.id + 1 -%}
{%- set _ = panneaux.append({"type": "row", "id": y.id, "title": "Reste-t-il de la marge ?", "collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": y.v}}) -%}
{%- set y.v = y.v + 1 -%}
{%- set w = 8 if fabric else 12 -%}
{%- set x = namespace(v=0) -%}
{%- if fabric -%}
{%- set y.id = y.id + 1 -%}
{%- set _ = panneaux.append({
"type": "timeseries", "id": y.id, "title": "Mémoire et swap des hyperviseurs", "datasource": ds,
"description": "Pour chaque hyperviseur : la part de sa RAM utilisée, et celle de son swap.\n\n**Comment le lire** : une RAM plate autour de 80 % avec un swap qui monte veut dire que les VM réclament plus que la machine n'a. Le swap n'est pas une marge, c'est un ralentissement.\n\n**Seuils** (pointillés) : 80 % et 90 %.\n\n**Si une courbe de swap reste haute** : réduire la RAM allouée aux VM de cet hyperviseur, ou en déplacer une.",
"gridPos": {"h": 9, "w": w, "x": x.v, "y": y.v},
"fieldConfig": {"defaults": {"unit": "percent", "min": 0, "max": 100, "decimals": 0, "color": {"mode": "palette-classic"},
"thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": none}, {"color": "orange", "value": 80}, {"color": "red", "value": 90}]},
"custom": {"drawStyle": "line", "lineWidth": 2, "fillOpacity": 5, "showPoints": "never", "spanNulls": true, "thresholdsStyle": {"mode": "dashed"}}},
"overrides": []},
"options": {"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["lastNotNull", "max"]}, "tooltip": {"mode": "multi", "sort": "desc"}},
"targets": [
{"refId": "A", "datasource": ds, "legendFormat": "{{nodename}} · RAM",
"expr": "100 * (1 - node_memory_MemAvailable_bytes{job=\"fabric\"} / node_memory_MemTotal_bytes{job=\"fabric\"})" ~ nom},
{"refId": "B", "datasource": ds, "legendFormat": "{{nodename}} · swap",
"expr": "100 * (1 - node_memory_SwapFree_bytes{job=\"fabric\"} / node_memory_SwapTotal_bytes{job=\"fabric\"})" ~ nom}]}) -%}
{%- set x.v = x.v + w -%}
{%- endif -%}
{%- for p in [
{"titre": "Disque utilisé par machine", "seuils": [80, 90],
"expr": "100 * (1 - node_filesystem_avail_bytes{job=\"node\",mountpoint=\"/\"} / node_filesystem_size_bytes{job=\"node\",mountpoint=\"/\"})",
"desc": "Part utilisée du disque système de chaque machine, de la plus pleine à la moins pleine.\n\n**Seuils** : orange à 80 %, rouge à 90 %.\n\n**Si une barre passe à l'orange** : se connecter à la machine et chercher ce qui grossit (`du -xh / | sort -h | tail`) — le plus souvent des journaux ou un cache ; sinon, agrandir son disque."},
{"titre": "Mémoire utilisée par machine", "seuils": [85, 95],
"expr": "100 * (1 - node_memory_MemAvailable_bytes{job=\"node\"} / node_memory_MemTotal_bytes{job=\"node\"})",
"desc": "Part de la RAM que chaque machine utilise vraiment (cache disque exclu).\n\n**Comment le lire** : une barre très courte sur une machine qui a beaucoup de RAM, c'est de la mémoire qu'on pourrait rendre à l'hyperviseur" ~ (" — et c'est exactement ce qui manque quand le swap des hyperviseurs monte." if fabric else ".") ~ "\n\n**Seuils** : orange à 85 %, rouge à 95 %.\n\n**Si une barre passe au rouge** : la machine manque de mémoire ; lui en donner un peu plus, ou alléger ce qu'elle fait tourner."}
] -%}
{%- set y.id = y.id + 1 -%}
{%- set _ = panneaux.append({
"type": "bargauge", "id": y.id, "title": p.titre, "description": p.desc, "datasource": ds,
"gridPos": {"h": 9, "w": w, "x": x.v, "y": y.v},
"fieldConfig": {"defaults": {"unit": "percent", "min": 0, "max": 100, "decimals": 0,
"thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": none}, {"color": "orange", "value": p.seuils[0]}, {"color": "red", "value": p.seuils[1]}]}},
"overrides": []},
"options": {"displayMode": "gradient", "orientation": "horizontal", "showUnfilled": true, "valueMode": "color",
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}},
"targets": [{"refId": "A", "datasource": ds, "legendFormat": "{{nodename}}",
"expr": "sort_desc(" ~ p.expr ~ nom ~ ")"}]}) -%}
{%- set x.v = x.v + w -%}
{%- endfor -%}
{%- set y.v = y.v + 9 -%}
{#- ---- RANGEE 3 : QUE SE PASSE-T-IL ? ---------------------------------------------- #}
{%- set y.id = y.id + 1 -%}
{%- set _ = panneaux.append({"type": "row", "id": y.id, "title": "Que se passe-t-il ?", "collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": y.v}}) -%}
{%- set y.v = y.v + 1 -%}
{%- set job = 'job="fabric"' if fabric else 'job="node"' -%}
{%- set qui = "hyperviseur" if fabric else "machine" -%}
{%- set y.id = y.id + 1 -%}
{%- set _ = panneaux.append({
"type": "timeseries", "id": y.id, "title": "Processeur par " ~ qui, "datasource": ds,
"description": "Part du processeur occupée, par " ~ qui ~ ".\n\n**Comment le lire** : une courbe basse et plate est normale" ~ (" — 72 cœurs pour la flotte, occupés à quelques pour cent" if fabric else "") ~ ". Les pics racontent ce qui se passe : une reconstruction, une sauvegarde, un déploiement. Une courbe qui reste haute sans raison mérite qu'on cherche quelle machine consomme.\n\n**Si une courbe reste au-dessus de 80 %** : ouvrir le tableau « client metrique » pour voir quelle machine en est la cause.",
"gridPos": {"h": 8, "w": 12, "x": 0, "y": y.v},
"fieldConfig": {"defaults": {"unit": "percent", "min": 0, "decimals": 0, "color": {"mode": "palette-classic"},
"custom": {"drawStyle": "line", "lineWidth": 1, "fillOpacity": 10, "showPoints": "never", "spanNulls": true}}, "overrides": []},
"options": {"legend": {"displayMode": "table", "placement": "right", "calcs": ["mean", "max"]}, "tooltip": {"mode": "multi", "sort": "desc"}},
"targets": [{"refId": "A", "datasource": ds, "legendFormat": "{{nodename}}",
"expr": "100 * (1 - avg by (instance) (rate(node_cpu_seconds_total{mode=\"idle\"," ~ job ~ "}[5m])))" ~ nom}]}) -%}
{%- set y.id = y.id + 1 -%}
{%- set _ = panneaux.append({
"type": "timeseries", "id": y.id, "title": "Trafic réseau par " ~ qui, "datasource": ds,
"description": "Débit entrant et sortant additionnés, par " ~ qui ~ " (interfaces physiques seulement).\n\n**Comment le lire** : " ~ ("le trafic courant est surtout celui de Ceph et des sauvegardes ; les grands pics marquent une reconstruction ou un déplacement de VM." if fabric else "les pics marquent les sauvegardes de la nuit, les mises à jour et les échanges entre services.") ~ "\n\n**Si un pic dure sans explication** : croiser avec le tableau « Journaux de la flotte » à la même heure.",
"gridPos": {"h": 8, "w": 12, "x": 12, "y": y.v},
"fieldConfig": {"defaults": {"unit": "Bps", "min": 0, "decimals": 1, "color": {"mode": "palette-classic"},
"custom": {"drawStyle": "line", "lineWidth": 1, "fillOpacity": 10, "showPoints": "never", "spanNulls": true}}, "overrides": []},
"options": {"legend": {"displayMode": "table", "placement": "right", "calcs": ["mean", "max"]}, "tooltip": {"mode": "multi", "sort": "desc"}},
"targets": [{"refId": "A", "datasource": ds, "legendFormat": "{{nodename}}",
"expr": "sum by (instance) (rate(node_network_receive_bytes_total{" ~ job ~ ",device=~\"(en|eth).*\"}[5m]) + rate(node_network_transmit_bytes_total{" ~ job ~ ",device=~\"(en|eth).*\"}[5m]))" ~ nom}]}) -%}
{%- set y.v = y.v + 8 -%}
{#- ---- RANGEE 4 : OU REGARDER ENSUITE ? -------------------------------------------- #}
{%- set y.id = y.id + 1 -%}
{%- set _ = panneaux.append({"type": "row", "id": y.id, "title": "Où regarder ensuite ?", "collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": y.v}}) -%}
{%- set y.v = y.v + 1 -%}
{%- set liens = [] -%}
{%- if fabric -%}{%- set _ = liens.append("- [**Matériel**](/d/setops-materiel) — températures, ventilateurs, disques des hyperviseurs et de la frontière.") -%}{%- endif -%}
{%- set _ = liens.append("- [**Machines**](/d/setops-client-metrique) — processeur, mémoire et disque de chaque machine, en détail.") -%}
{%- set _ = liens.append("- [**PostgreSQL**](/d/setops-postgresql) — bases de données : taille, connexions, requêtes.") -%}
{%- set _ = liens.append("- [**Journaux de la flotte**](/d/setops-journaux) — ce que les machines ont écrit, et quand.") -%}
{%- set _ = liens.append("- [**Vigie**](https://vigie." ~ domaine_interne ~ ") — les verdicts d'Icinga : ce qui est cassé, service par service.") -%}
{%- set y.id = y.id + 1 -%}
{%- set _ = panneaux.append({
"type": "text", "id": y.id, "title": "",
"gridPos": {"h": 5, "w": 24, "x": 0, "y": y.v},
"options": {"mode": "markdown", "content": liens | join("\n")}}) -%}
{{ {
"uid": "setops-synthese",
"title": "Set-OPS — Synthèse",
"description": "La porte d'entrée de l'observatoire : faut-il agir, reste-t-il de la marge, que se passe-t-il, où regarder ensuite.",
"tags": ["set-ops", "derive", "synthese"],
"timezone": "browser",
"refresh": "1m",
"time": {"from": "now-7d", "to": "now"},
"schemaVersion": 39,
"version": 1,
"graphTooltip": 1,
"templating": {"list": [
{"name": "ds_prom", "label": "Source Prometheus", "type": "datasource", "query": "prometheus", "hide": 2, "current": {}}
]},
"panels": panneaux
} | tojson }}