feat(stt): enrichit les sondes (host_health, cluster_status, metrics) (#52)

- host_health : ajoute CPU%, disque /%, température CPU, uptime ; pour gpu-01
  un bloc GPU complet (température, utilisation, VRAM%, puissance via
  rocm_scraper) en plus de llama-server. Requêtes concurrentes (asyncio.gather).
- cluster_status : ajoute le nb de nœuds k8s Ready et une ligne CrashLoopBackOff
  (pods en boucle de crash) — complète le filtrage des pods terminés.
- metrics_block (contexte grafana) : ajoute le résumé GPU (util/temp/VRAM).
- Toutes les PromQL validées contre le Prometheus in-cluster.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ALI YESILKAYA 2026-06-22 23:15:49 +02:00 committed by GitHub
parent 421ec77ef9
commit 0d09dec3dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 88 additions and 34 deletions

View file

@ -12,6 +12,7 @@ Aucune action d'écriture ici : le volet « agir sur le homelab » passe par `ag
from __future__ import annotations
import asyncio
from dataclasses import dataclass
import httpx
@ -23,6 +24,15 @@ from stt_server.config import settings
_HOST_JOBS = {"gpu-01": "gpu-01-node", "storage-01": "storage-01"}
async def _q1(client: httpx.AsyncClient, expr: str) -> float | None:
"""Valeur scalaire d'une requête PromQL (1re série) ou None. Ne lève jamais."""
try:
rows = await sources._prom_query(client, expr)
except httpx.HTTPError:
return None
return rows[0][1] if rows else None
@dataclass
class ToolDeps:
"""Dépendances injectées aux exécuteurs (client HTTP partagé, RAG, vecteur de requête)."""
@ -66,31 +76,53 @@ async def _host_health(args: dict, deps: ToolDeps) -> str:
job = _HOST_JOBS.get(host)
if not job:
return f"Hôte inconnu : {host!r} (attendu : {', '.join(_HOST_JOBS)})."
try:
up = await sources._prom_query(deps.client, f'up{{job="{job}"}}')
load = await sources._prom_query(deps.client, f'node_load1{{job="{job}"}}')
mem = await sources._prom_query(
deps.client,
f'100*node_memory_MemAvailable_bytes{{job="{job}"}}'
f'/node_memory_MemTotal_bytes{{job="{job}"}}',
)
except httpx.HTTPError:
return "Données indisponibles : Prometheus injoignable."
if not up:
c = deps.client
# Sondes hôte (concurrentes) : up, CPU%, RAM libre%, disque /%, temp CPU max, uptime h.
up, cpu, mem, disk, temp, uptime = await asyncio.gather(
_q1(c, f'up{{job="{job}"}}'),
_q1(c, f'100-100*avg(rate(node_cpu_seconds_total{{job="{job}",mode="idle"}}[5m]))'),
_q1(c, f'100*node_memory_MemAvailable_bytes{{job="{job}"}}'
f'/node_memory_MemTotal_bytes{{job="{job}"}}'),
_q1(c, f'100-100*node_filesystem_avail_bytes{{job="{job}",mountpoint="/"}}'
f'/node_filesystem_size_bytes{{job="{job}",mountpoint="/"}}'),
_q1(c, f'max(node_hwmon_temp_celsius{{job="{job}"}})'),
_q1(c, f'(node_time_seconds{{job="{job}"}}-node_boot_time_seconds{{job="{job}"}})/3600'),
)
if up is None:
return f"{host} : aucune métrique (cible Prometheus absente ou hôte éteint)."
online = up[0][1] == 1
parts = [f"{host} : {'en ligne' if online else 'HORS LIGNE'}"]
if load:
parts.append(f"charge 1 min {round(load[0][1], 2)}")
if mem:
parts.append(f"RAM libre {round(mem[0][1])}%")
parts = [f"{host} : {'en ligne' if up == 1 else 'HORS LIGNE'}"]
if cpu is not None:
parts.append(f"CPU {round(cpu)}%")
if mem is not None:
parts.append(f"RAM libre {round(mem)}%")
if disk is not None:
parts.append(f"disque / {round(disk)}% utilisé")
if temp is not None:
parts.append(f"temp CPU {round(temp)}°C")
if uptime is not None:
parts.append(f"uptime {round(uptime)} h")
if host == "gpu-01":
try:
llama = await sources._prom_query(deps.client, 'up{job="llama-server-gpu"}')
if llama:
parts.append("llama-server " + ("up" if llama[0][1] == 1 else "DOWN"))
except httpx.HTTPError:
pass
# Métriques GPU (rocm_scraper → textfile collector node_exporter).
gtemp, gutil, gvram, gpow, llama = await asyncio.gather(
_q1(c, "rocm_gpu_temperature_celsius"),
_q1(c, "rocm_gpu_utilization_percent"),
_q1(c, "100*rocm_vram_used_bytes/rocm_vram_total_bytes"),
_q1(c, "rocm_gpu_power_watts"),
_q1(c, 'up{job="llama-server-gpu"}'),
)
gpu = []
if gutil is not None:
gpu.append(f"util {round(gutil)}%")
if gtemp is not None:
gpu.append(f"{round(gtemp)}°C")
if gvram is not None:
gpu.append(f"VRAM {round(gvram)}%")
if gpow is not None:
gpu.append(f"{round(gpow)} W")
if gpu:
parts.append("GPU (" + ", ".join(gpu) + ")")
if llama is not None:
parts.append("llama-server " + ("up" if llama == 1 else "DOWN"))
return ", ".join(parts) + "."
@ -179,9 +211,10 @@ _TOOLS: dict[str, tuple[dict, object]] = {
"type": "function",
"function": {
"name": "host_health",
"description": "État de santé LIVE d'une machine hors-cluster "
"(en ligne ?, charge CPU, RAM libre ; pour gpu-01 aussi l'état de llama-server). "
"À utiliser pour « est-ce que gpu-01 / storage-01 tourne bien ? ».",
"description": "État de santé LIVE d'une machine hors-cluster (en ligne ?, CPU, "
"RAM libre, disque, température, uptime ; pour gpu-01 aussi GPU température/"
"utilisation/VRAM/puissance et l'état de llama-server). À utiliser pour "
"« est-ce que gpu-01 / storage-01 tourne bien ? » ou une métrique machine précise.",
"parameters": {
"type": "object",
"properties": {