mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-08 17:44:42 +02:00
feat(stt-client): page détail des services en pleine page + métriques live
La page détail d'un service (panneau « Services » du HUD) passe en PLEINE
PAGE : le drawer prend tout l'écran, le titre devient le nom du service, et
le contenu s'organise en 2 colonnes sur grand écran (données à gauche, état
+ actions à droite).
Nouvelle section « Métriques » par service, config-driven (comme `components`) :
- `[[services.metrics]]` → {label, prom, unit, hide_if_empty} évalué par le
StatusPoller (dédup + parallèle avec les composants).
- Défauts via `config._pod_metrics(ns, pod_re)` pour les 7 services : redémarrages
+ uptime (kube-state-metrics, toujours dispo) ; mémoire + CPU (cAdvisor,
`hide_if_empty` → carte masquée si non scrappé).
- Formatage HUD par unité : duration (8 j 9 h), bytes (497 Mo), cpu (30 mcœur),
count.
Client 0.19.0. PromQL vérifié live (les 4 métriques renvoient des valeurs réelles
sur les 7 services). Rendu pleine page + formats validés via Playwright.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
4bf4655a83
commit
0f732f0631
6 changed files with 182 additions and 26 deletions
|
|
@ -214,6 +214,7 @@ au prompt système (`STT_DISABLE_THINKING`, défaut `true` ; inoffensif hors Qwe
|
|||
| — UI/HUD | `stt/client/stt/ui/` + `hud/` | HTTP statique + websocket bidirectionnel (états → HUD ; réglages **et messages texte** → backend) ; HUD embarqué dans le package |
|
||||
| — portail | `stt/client/stt/portal/` | `registry.py` (services config-driven `[[services]]`) + `health.py` (StatusPoller). Panneau « Services » du HUD : tuiles + **pages détail** (description, **santé live**, composants, alertes, **aperçu enrichi**, bouton Ouvrir). Santé = probe HTTP + **Prometheus** `up{}`/kube-state-metrics + **Alertmanager**. Ouvre l'URL dans le navigateur normal |
|
||||
| — aperçu enrichi (≥ 0.18.0) | `hud/index.html` (`renderServiceData`) + `ui/app.py` (`service-detail`) | Aperçu live spécifique au service dans sa page détail : **Ghostfolio** → valeur + performance + positions (alloc % et P/L par ligne, via `POST /v1/portfolio/details` du serveur, jeton client) ; **Alertmanager** → **toutes** les alertes actives (rendu HUD à partir de `all_alerts` poussé par le poller, auto-rafraîchi). Extensible : `DETAIL_PROVIDERS` (`server` = requête backend \| `alerts` = liste locale) |
|
||||
| — page détail pleine page + métriques (≥ 0.19.0) | `hud/index.html` (`renderMetrics`, `is-detail`) + `registry.Metric` + `health.py` | La page détail passe en **pleine page** (le drawer prend tout l'écran, 2 colonnes sur grand écran : données ⟂ état). Section **Métriques** par service (config `[[services.metrics]]` → `{label, prom, unit, hide_if_empty}`) évaluée par le `StatusPoller` : redémarrages, uptime (kube-state-metrics) + mémoire, CPU (cAdvisor, masqués si absents). Défauts via `config._pod_metrics(ns, pod_re)`. Formatage HUD par unité (`duration`/`bytes`/`cpu`/`count`) |
|
||||
| — intentions vocales | `stt/client/stt/portal/intents.py` + `ghostfolio.py` | lecture seule, court-circuitent le LLM (interceptées dans `engine._respond`) : « ouvre <service> », « état du cluster/services » (résumé santé parlé), « combien sur mon ghostfolio » (API Ghostfolio, `[ghostfolio] access_token`) |
|
||||
| — contexte HUD | `stt/client/stt/ui/app.py` + `hud/` | sélecteur de contexte (Réglages) envoyé par requête + **visualiseur** (drawer) affichant le contexte assemblé renvoyé par le serveur ; affichage de la version installée |
|
||||
| STT-server | `stt/server/` (conteneur) | FastAPI : `/healthz`, `/v1/ask` ; `brain.py` → LiteLLM |
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "stt"
|
||||
version = "0.18.0"
|
||||
version = "0.19.0"
|
||||
description = "STT — client vocal Jarvis du homelab Funk (voix + HUD, parle au STT-server)"
|
||||
requires-python = ">=3.11"
|
||||
readme = "README.md"
|
||||
|
|
|
|||
|
|
@ -29,6 +29,29 @@ def resolve_model(name: str) -> str:
|
|||
"""Nom court → alias LiteLLM (ou renvoie tel quel si déjà un alias)."""
|
||||
return MODEL_PRESETS.get(name, name)
|
||||
|
||||
|
||||
def _pod_metrics(ns: str, pod_re: str) -> list[dict]:
|
||||
"""Jeu de métriques standard pour les pods d'un service (page détail HUD).
|
||||
|
||||
Redémarrages + uptime : kube-state-metrics (toujours scrappé). Mémoire + CPU :
|
||||
cAdvisor/kubelet (`hide_if_empty` → la carte disparaît si non disponible). Les
|
||||
requêtes s'agrègent à une seule valeur (sum/max) → le poller prend la 1re.
|
||||
"""
|
||||
sel = f'namespace="{ns}",pod=~"{pod_re}"'
|
||||
csel = f'namespace="{ns}",pod=~"{pod_re}",container!="",container!="POD"'
|
||||
return [
|
||||
{"label": "Redémarrages",
|
||||
"prom": f"sum(kube_pod_container_status_restarts_total{{{sel}}})", "unit": "count"},
|
||||
{"label": "Démarré depuis",
|
||||
"prom": f"time()-max(kube_pod_start_time{{{sel}}})", "unit": "duration"},
|
||||
{"label": "Mémoire",
|
||||
"prom": f"sum(container_memory_working_set_bytes{{{csel}}})",
|
||||
"unit": "bytes", "hide_if_empty": True},
|
||||
{"label": "CPU",
|
||||
"prom": f"sum(rate(container_cpu_usage_seconds_total{{{csel}}}[5m]))",
|
||||
"unit": "cpu", "hide_if_empty": True},
|
||||
]
|
||||
|
||||
DEFAULT_CONFIG: dict[str, Any] = {
|
||||
"server": {
|
||||
# STT-server (in-cluster) — joignable via Traefik / wildcard *.lab.local
|
||||
|
|
@ -109,6 +132,7 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
|||
"description": "Suivi de portefeuille et d'investissements (actions, crypto). "
|
||||
"Base PostgreSQL sur storage-01 + cache Redis in-cluster.",
|
||||
"health_url": "http://ghostfolio.lab.local/api/v1/health",
|
||||
"metrics": _pod_metrics("ai", "ghostfolio-.+"),
|
||||
"components": [
|
||||
{"name": "Application", "prom": 'kube_pod_status_ready{namespace="ai",pod=~"ghostfolio-.+",pod!~"ghostfolio-redis-.+",condition="true"}'},
|
||||
{"name": "Redis (cache)", "prom": 'kube_pod_status_ready{namespace="ai",pod=~"ghostfolio-redis-.+",condition="true"}'},
|
||||
|
|
@ -123,6 +147,7 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
|||
"description": "Tableaux de bord et visualisation des métriques du homelab "
|
||||
"(Prometheus comme source de données).",
|
||||
"health_url": "http://grafana.lab.local/api/health",
|
||||
"metrics": _pod_metrics("monitoring", ".*grafana.*"),
|
||||
"components": [
|
||||
{"name": "Pod", "prom": 'kube_pod_status_ready{namespace="monitoring",pod=~".*grafana.*",condition="true"}'},
|
||||
],
|
||||
|
|
@ -135,6 +160,7 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
|||
"description": "Déploiement continu GitOps : synchronise les workloads k8s "
|
||||
"depuis le dépôt Git (pattern App-of-Apps).",
|
||||
"health_url": "http://argocd.lab.local/healthz",
|
||||
"metrics": _pod_metrics("argocd", ".+"),
|
||||
"components": [
|
||||
{"name": "Pods (server, repo, redis…)", "prom": 'kube_pod_status_ready{namespace="argocd",condition="true"}'},
|
||||
],
|
||||
|
|
@ -147,6 +173,7 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
|||
"description": "Automatisation et workflows (alertes, rapport horaire, worker "
|
||||
"doc Hermès). Base PostgreSQL sur storage-01.",
|
||||
"health_url": "http://n8n.lab.local/healthz",
|
||||
"metrics": _pod_metrics("ai", "n8n-.+"),
|
||||
"components": [
|
||||
{"name": "Application", "prom": 'kube_pod_status_ready{namespace="ai",pod=~"n8n-.+",condition="true"}'},
|
||||
{"name": "PostgreSQL — storage-01", "prom": 'up{job="storage-01"}'},
|
||||
|
|
@ -160,6 +187,7 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
|||
"description": "Interface de chat web vers les LLM du homelab "
|
||||
"(backend LiteLLM sur storage-01).",
|
||||
"health_url": "http://openwebui.lab.local/health",
|
||||
"metrics": _pod_metrics("ai", "open-webui-.+"),
|
||||
"components": [
|
||||
{"name": "Application", "prom": 'kube_pod_status_ready{namespace="ai",pod=~"open-webui-.+",condition="true"}'},
|
||||
{"name": "LiteLLM — storage-01", "prom": 'up{job="storage-01"}'},
|
||||
|
|
@ -173,6 +201,7 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
|||
"description": "Collecte et stockage des métriques (TSDB) — scrute hôtes, "
|
||||
"nœuds k8s et services.",
|
||||
"health_url": "http://prometheus.lab.local/-/healthy",
|
||||
"metrics": _pod_metrics("monitoring", "prometheus-.+"),
|
||||
"components": [
|
||||
{"name": "Pod", "prom": 'kube_pod_status_ready{namespace="monitoring",pod=~"prometheus-.+",condition="true"}'},
|
||||
],
|
||||
|
|
@ -185,6 +214,7 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
|||
"description": "Routage et notification des alertes Prometheus "
|
||||
"(→ webhook Hermès, n8n, e-mail).",
|
||||
"health_url": "http://alertmanager.lab.local/-/healthy",
|
||||
"metrics": _pod_metrics("monitoring", "alertmanager-.+"),
|
||||
"components": [
|
||||
{"name": "Pod", "prom": 'kube_pod_status_ready{namespace="monitoring",pod=~"alertmanager-.+",condition="true"}'},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -780,6 +780,27 @@
|
|||
.svc-data-row .svc-data-val[data-tone="bad"] { color: #d23a52; }
|
||||
.svc-data-row .svc-data-val[data-tone="warn"] { color: #c5841f; }
|
||||
.svc-data-empty { font-size: 13px; color: var(--ink-soft); margin: 4px 0 0; }
|
||||
.svc-stat .svc-stat-sub { display: block; font-size: 11px; color: var(--ink-soft); margin-top: 2px; font-weight: 500; }
|
||||
|
||||
/* ============================================================
|
||||
PORTAIL — page détail PLEINE PAGE (le drawer prend tout l'écran)
|
||||
============================================================ */
|
||||
#servicesDrawer.is-detail { width: 100vw; max-width: 100vw; }
|
||||
#servicesDrawer.is-detail .body { padding: 0; }
|
||||
/* contenu centré et large, façon « vraie page » */
|
||||
#svcDetail { max-width: 1040px; margin: 0 auto; padding: 18px 28px 40px; }
|
||||
/* 2 colonnes sur grand écran : données à gauche, état/actions à droite */
|
||||
.svc-detail-grid { display: block; }
|
||||
@media (min-width: 820px) {
|
||||
.svc-detail-grid {
|
||||
display: grid; grid-template-columns: 1.4fr 1fr;
|
||||
gap: 0 40px; align-items: start;
|
||||
}
|
||||
.svc-col-side .svc-section:first-child { padding-top: 0; }
|
||||
.svc-col-side .svc-section:first-child .flabel { margin-top: 0; }
|
||||
}
|
||||
/* en pleine page, les cartes de stats peuvent s'étaler davantage */
|
||||
#servicesDrawer.is-detail .svc-stat { flex-basis: 150px; }
|
||||
|
||||
/* Badge alertes global (en tête de la liste) */
|
||||
.svc-alert-badge {
|
||||
|
|
@ -1120,31 +1141,44 @@
|
|||
</div>
|
||||
<p class="svc-detail-desc hint" id="svcDetailDesc"></p>
|
||||
|
||||
<!-- Aperçu enrichi spécifique au service (portefeuille Ghostfolio, toutes les
|
||||
alertes Alertmanager…). Cartes de stats + lignes de données. -->
|
||||
<div class="svc-section" id="svcDataWrap" hidden>
|
||||
<span class="flabel" id="svcDataLabel">Aperçu</span>
|
||||
<div class="svc-stats" id="svcStats"></div>
|
||||
<div class="svc-data-rows" id="svcDataRows"></div>
|
||||
<p class="svc-data-empty hint" id="svcDataEmpty" hidden></p>
|
||||
</div>
|
||||
<!-- Pleine page : 2 colonnes sur grand écran (données à gauche, état à droite) -->
|
||||
<div class="svc-detail-grid">
|
||||
<div class="svc-col-main">
|
||||
<!-- Aperçu enrichi spécifique au service (portefeuille Ghostfolio, toutes
|
||||
les alertes Alertmanager…). Cartes de stats + lignes de données. -->
|
||||
<div class="svc-section" id="svcDataWrap" hidden>
|
||||
<span class="flabel" id="svcDataLabel">Aperçu</span>
|
||||
<div class="svc-stats" id="svcStats"></div>
|
||||
<div class="svc-data-rows" id="svcDataRows"></div>
|
||||
<p class="svc-data-empty hint" id="svcDataEmpty" hidden></p>
|
||||
</div>
|
||||
|
||||
<div class="svc-section" id="svcDetailCompWrap" hidden>
|
||||
<span class="flabel">Composants</span>
|
||||
<div id="svcCompList"></div>
|
||||
</div>
|
||||
<!-- Métriques live des pods (redémarrages, uptime, mémoire, CPU) -->
|
||||
<div class="svc-section" id="svcMetricsWrap" hidden>
|
||||
<span class="flabel">Métriques</span>
|
||||
<div class="svc-stats" id="svcMetrics"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="svc-section" id="svcDetailAlertsWrap">
|
||||
<span class="flabel">Alertes</span>
|
||||
<div id="svcAlertList"></div>
|
||||
</div>
|
||||
<div class="svc-col-side">
|
||||
<div class="svc-section" id="svcDetailCompWrap" hidden>
|
||||
<span class="flabel">Composants</span>
|
||||
<div id="svcCompList"></div>
|
||||
</div>
|
||||
|
||||
<div class="svc-section">
|
||||
<span class="flabel">Infos</span>
|
||||
<div class="svc-info" id="svcInfo"></div>
|
||||
</div>
|
||||
<div class="svc-section" id="svcDetailAlertsWrap">
|
||||
<span class="flabel">Alertes</span>
|
||||
<div id="svcAlertList"></div>
|
||||
</div>
|
||||
|
||||
<button class="btn svc-open-btn" id="svcOpenBtn">↗ Ouvrir le service</button>
|
||||
<div class="svc-section">
|
||||
<span class="flabel">Infos</span>
|
||||
<div class="svc-info" id="svcInfo"></div>
|
||||
</div>
|
||||
|
||||
<button class="btn svc-open-btn" id="svcOpenBtn">↗ Ouvrir le service</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
|
@ -1551,12 +1585,16 @@ function updateAlertBadge() {
|
|||
svcAlertBadge.textContent = `⚠ ${alertsTotal} alerte${s} active${s} dans le homelab`;
|
||||
}
|
||||
|
||||
/* ----- Navigation liste ⟷ détail ----- */
|
||||
/* ----- Navigation liste ⟷ détail (la vue détail passe en PLEINE PAGE) ----- */
|
||||
const servicesDrawerEl = document.getElementById('servicesDrawer');
|
||||
const servicesTitleEl = document.getElementById('servicesTitle');
|
||||
function showDetail(id) {
|
||||
if (!servicesById[id]) return;
|
||||
const s = servicesById[id]; if (!s) return;
|
||||
detailId = id;
|
||||
svcHome.hidden = true;
|
||||
svcDetail.hidden = false;
|
||||
servicesDrawerEl.classList.add('is-detail'); // drawer → pleine page
|
||||
servicesTitleEl.textContent = s.name || id; // titre = nom du service
|
||||
svcDetail.parentElement.scrollTop = 0;
|
||||
renderDetail(id);
|
||||
}
|
||||
|
|
@ -1564,6 +1602,8 @@ function showHome() {
|
|||
detailId = null;
|
||||
svcDetail.hidden = true;
|
||||
svcHome.hidden = false;
|
||||
servicesDrawerEl.classList.remove('is-detail');
|
||||
servicesTitleEl.textContent = 'Services';
|
||||
}
|
||||
|
||||
function renderDetail(id) {
|
||||
|
|
@ -1583,8 +1623,9 @@ function renderDetail(id) {
|
|||
desc.textContent = s.description || '';
|
||||
desc.hidden = !s.description;
|
||||
|
||||
// Aperçu enrichi (portefeuille, alertes…)
|
||||
// Aperçu enrichi (portefeuille, alertes…) + métriques live des pods
|
||||
renderServiceData(id);
|
||||
renderMetrics(st);
|
||||
|
||||
// Composants
|
||||
const comps = st.components || [];
|
||||
|
|
@ -1636,6 +1677,51 @@ const svcDataLabel = document.getElementById('svcDataLabel');
|
|||
const svcStats = document.getElementById('svcStats');
|
||||
const svcDataRows = document.getElementById('svcDataRows');
|
||||
const svcDataEmpty = document.getElementById('svcDataEmpty');
|
||||
const svcMetricsWrap = document.getElementById('svcMetricsWrap');
|
||||
const svcMetrics = document.getElementById('svcMetrics');
|
||||
|
||||
/* Formatage d'une métrique selon son unité (poller → page détail) */
|
||||
function fmtMetric(value, unit) {
|
||||
if (value == null || isNaN(value)) return '—';
|
||||
const v = Number(value);
|
||||
if (unit === 'duration') return fmtDuration(v);
|
||||
if (unit === 'bytes') return fmtBytes(v);
|
||||
if (unit === 'cpu') return v < 1 ? Math.round(v * 1000) + ' mcœur' : v.toFixed(2).replace('.', ',') + ' cœur';
|
||||
if (unit === 'percent') return Math.round(v) + ' %';
|
||||
if (unit === 'count') return String(Math.round(v));
|
||||
return Math.round(v).toLocaleString('fr-FR');
|
||||
}
|
||||
function fmtDuration(sec) {
|
||||
sec = Math.max(0, Math.round(sec));
|
||||
const d = Math.floor(sec / 86400), h = Math.floor((sec % 86400) / 3600), m = Math.floor((sec % 3600) / 60);
|
||||
if (d > 0) return `${d} j ${h} h`;
|
||||
if (h > 0) return `${h} h ${m} min`;
|
||||
if (m > 0) return `${m} min`;
|
||||
return `${sec} s`;
|
||||
}
|
||||
function fmtBytes(b) {
|
||||
if (b >= 1073741824) return (b / 1073741824).toFixed(1).replace('.', ',') + ' Go';
|
||||
if (b >= 1048576) return Math.round(b / 1048576) + ' Mo';
|
||||
if (b >= 1024) return Math.round(b / 1024) + ' Ko';
|
||||
return Math.round(b) + ' o';
|
||||
}
|
||||
|
||||
/* Métriques live des pods (redémarrages, uptime, mémoire, CPU) */
|
||||
function renderMetrics(st) {
|
||||
const metrics = (st && Array.isArray(st.metrics)) ? st.metrics : [];
|
||||
svcMetrics.innerHTML = '';
|
||||
svcMetricsWrap.hidden = metrics.length === 0;
|
||||
for (const m of metrics) {
|
||||
const c = document.createElement('div');
|
||||
c.className = 'svc-stat';
|
||||
// redémarrages > 0 = avertissement léger ; sinon neutre
|
||||
if (m.unit === 'count' && m.label && m.label.toLowerCase().includes('redémarr') && Number(m.value) > 0) c.dataset.tone = 'bad';
|
||||
c.innerHTML = `<span class="svc-stat-label"></span><span class="svc-stat-value"></span>`;
|
||||
c.querySelector('.svc-stat-label').textContent = m.label;
|
||||
c.querySelector('.svc-stat-value').textContent = fmtMetric(m.value, m.unit);
|
||||
svcMetrics.appendChild(c);
|
||||
}
|
||||
}
|
||||
|
||||
const fmtMoney = (v, cur) => {
|
||||
if (v == null || isNaN(v)) return '—';
|
||||
|
|
|
|||
|
|
@ -143,8 +143,10 @@ class StatusPoller:
|
|||
s.id: pool.submit(probe_http, s.probe_url, self.timeout)
|
||||
for s in self.services
|
||||
}
|
||||
# 2) requêtes Prometheus distinctes (déduplique les exprs identiques)
|
||||
# 2) requêtes Prometheus distinctes (déduplique les exprs identiques) —
|
||||
# composants (1/0) ET métriques chiffrées de page détail.
|
||||
exprs = {c.prom for s in self.services for c in s.components if c.prom}
|
||||
exprs |= {m.prom for s in self.services for m in s.metrics if m.prom}
|
||||
prom_futs = {
|
||||
e: pool.submit(query_prometheus, self.prom_url, e, self.timeout)
|
||||
for e in exprs
|
||||
|
|
@ -163,6 +165,14 @@ class StatusPoller:
|
|||
"state": _component_state(prom.get(c.prom)) if c.prom else "info"}
|
||||
for c in s.components
|
||||
]
|
||||
# Métriques chiffrées : 1re valeur de la série agrégée (None si absente).
|
||||
metrics_out = []
|
||||
for m in s.metrics:
|
||||
vals = prom.get(m.prom)
|
||||
value = vals[0] if vals else None
|
||||
if value is None and m.hide_if_empty:
|
||||
continue # ex. cAdvisor non scrappé → on n'affiche pas la carte
|
||||
metrics_out.append({"label": m.label, "value": value, "unit": m.unit})
|
||||
svc_alerts = [a for a in (alerts or []) if _alert_matches(a, s.alerts_match)]
|
||||
degraded = probe["state"] == "up" and any(c["state"] == "down" for c in comps)
|
||||
if probe["state"] == "down":
|
||||
|
|
@ -178,6 +188,7 @@ class StatusPoller:
|
|||
"latency_ms": probe["latency_ms"],
|
||||
"health": health,
|
||||
"components": comps,
|
||||
"metrics": metrics_out,
|
||||
"alerts": svc_alerts,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,23 @@ class Component:
|
|||
prom: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Metric:
|
||||
"""Chiffre live affiché sur la page détail (redémarrages, uptime, mémoire…).
|
||||
|
||||
`prom` : requête PromQL qui DOIT s'agréger à une seule valeur (sum/max/…). Le
|
||||
StatusPoller prend la première valeur retournée.
|
||||
`unit` : formatage côté HUD — "count" | "duration" (secondes) | "bytes" |
|
||||
"cpu" (cœurs) | "percent" | "" (nombre brut).
|
||||
`hide_if_empty` : carte masquée si la requête ne renvoie aucune série (ex. cAdvisor
|
||||
absent) → pas de carte « — » disgracieuse.
|
||||
"""
|
||||
label: str
|
||||
prom: str
|
||||
unit: str = ""
|
||||
hide_if_empty: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Service:
|
||||
id: str
|
||||
|
|
@ -43,6 +60,7 @@ class Service:
|
|||
description: str = ""
|
||||
health_url: str = "" # endpoint sondé (défaut : url)
|
||||
components: tuple[Component, ...] = ()
|
||||
metrics: tuple[Metric, ...] = () # chiffres live (page détail)
|
||||
# Une alerte Alertmanager est « pertinente » si TOUS ces (label, regex) matchent.
|
||||
alerts_match: tuple[tuple[str, str], ...] = ()
|
||||
|
||||
|
|
@ -85,6 +103,15 @@ def load_services(cfg: dict) -> list[Service]:
|
|||
for c in (raw.get("components") or [])
|
||||
if isinstance(c, dict) and str(c.get("name") or "").strip()
|
||||
)
|
||||
metrics = tuple(
|
||||
Metric(label=str(m.get("label") or "").strip(),
|
||||
prom=str(m.get("prom") or "").strip(),
|
||||
unit=str(m.get("unit") or "").strip(),
|
||||
hide_if_empty=bool(m.get("hide_if_empty")))
|
||||
for m in (raw.get("metrics") or [])
|
||||
if isinstance(m, dict) and str(m.get("label") or "").strip()
|
||||
and str(m.get("prom") or "").strip()
|
||||
)
|
||||
alerts_match = tuple(
|
||||
(str(k), str(v))
|
||||
for k, v in (raw.get("alerts_match") or {}).items()
|
||||
|
|
@ -99,6 +126,7 @@ def load_services(cfg: dict) -> list[Service]:
|
|||
description=str(raw.get("description") or "").strip(),
|
||||
health_url=str(raw.get("health_url") or "").strip(),
|
||||
components=components,
|
||||
metrics=metrics,
|
||||
alerts_match=alerts_match,
|
||||
)
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue