Feat : Graph data expand
Some checks failed
Build and Push Docker Images / docker (push) Failing after 8s
Some checks failed
Build and Push Docker Images / docker (push) Failing after 8s
This commit is contained in:
Binary file not shown.
@@ -9,6 +9,7 @@ import json
|
||||
import os
|
||||
import secrets
|
||||
import sqlite3
|
||||
import time
|
||||
from collections import deque
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, timedelta, timezone
|
||||
@@ -134,6 +135,26 @@ def init_db() -> None:
|
||||
except Exception:
|
||||
pass # colonne déjà présente
|
||||
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS vps_stats (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
vps_id TEXT NOT NULL,
|
||||
ts INTEGER NOT NULL,
|
||||
cpu REAL NOT NULL,
|
||||
ram_percent REAL NOT NULL,
|
||||
ram_used INTEGER NOT NULL,
|
||||
ram_total INTEGER NOT NULL,
|
||||
net_sent_per_sec REAL NOT NULL DEFAULT 0,
|
||||
net_recv_per_sec REAL NOT NULL DEFAULT 0,
|
||||
net_bytes_sent INTEGER NOT NULL DEFAULT 0,
|
||||
net_bytes_recv INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
""")
|
||||
conn.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_vps_stats
|
||||
ON vps_stats(vps_id, ts DESC)
|
||||
""")
|
||||
|
||||
|
||||
init_db()
|
||||
|
||||
@@ -250,16 +271,34 @@ async def agent_post(vps: dict, path: str, payload: dict | None = None):
|
||||
|
||||
|
||||
async def fetch_vps_status(vps: dict) -> dict:
|
||||
"""Interroge un agent et retourne son état complet."""
|
||||
"""Interroge un agent et retourne son état complet.
|
||||
|
||||
Les données système (CPU, RAM, réseau) proviennent en priorité du ring buffer
|
||||
alimenté par le collecteur toutes les 5 s — même source que le modal de stats.
|
||||
Si le buffer est encore vide (premier démarrage), un appel direct est fait.
|
||||
"""
|
||||
try:
|
||||
containers_res, system_res = await asyncio.gather(
|
||||
agent_get(vps, "/containers"),
|
||||
agent_get(vps, "/system"),
|
||||
return_exceptions=True,
|
||||
)
|
||||
if isinstance(containers_res, Exception):
|
||||
raise containers_res
|
||||
system = system_res if not isinstance(system_res, Exception) else None
|
||||
containers_res = await agent_get(vps, "/containers")
|
||||
|
||||
# Source unique : ring buffer → carte et modal affichent exactement la même valeur
|
||||
history = _stats_history.get(vps["id"])
|
||||
if history:
|
||||
last = history[-1]
|
||||
system = {
|
||||
"cpu_percent": last["cpu"],
|
||||
"ram_used": last["ram_used"],
|
||||
"ram_total": last["ram_total"],
|
||||
"ram_percent": last["ram_percent"],
|
||||
"net_sent_per_sec": last["net_sent_per_sec"],
|
||||
"net_recv_per_sec": last["net_recv_per_sec"],
|
||||
}
|
||||
else:
|
||||
# Buffer vide (premier démarrage) — appel direct en fallback
|
||||
try:
|
||||
system = await agent_get(vps, "/system")
|
||||
except Exception:
|
||||
system = None
|
||||
|
||||
return {
|
||||
"id": vps["id"],
|
||||
"name": vps["name"],
|
||||
@@ -287,12 +326,15 @@ async def fetch_vps_status(vps: dict) -> dict:
|
||||
# ─── Collecteur de stats (tâche de fond) ─────────────────────────────────────
|
||||
|
||||
async def _fetch_system_stat(vps: dict) -> None:
|
||||
"""Collecte un point de stats système pour un VPS et l'insère dans le ring buffer."""
|
||||
"""Collecte un point de stats système pour un VPS et le persiste (ring buffer + SQLite)."""
|
||||
try:
|
||||
data = await agent_get(vps, "/system")
|
||||
now = int(time.time())
|
||||
|
||||
# Ring buffer en mémoire (source pour la carte VPS — valeur courante)
|
||||
buf = _stats_history.setdefault(vps["id"], deque(maxlen=_STATS_MAX_POINTS))
|
||||
buf.append({
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"ts": datetime.fromtimestamp(now, tz=timezone.utc).isoformat(),
|
||||
"cpu": data["cpu_percent"],
|
||||
"ram_percent": data["ram_percent"],
|
||||
"ram_used": data["ram_used"],
|
||||
@@ -302,6 +344,21 @@ async def _fetch_system_stat(vps: dict) -> None:
|
||||
"net_bytes_sent": data.get("net_bytes_sent", 0),
|
||||
"net_bytes_recv": data.get("net_bytes_recv", 0),
|
||||
})
|
||||
|
||||
# Persistance SQLite (historique long terme)
|
||||
with get_db() as conn:
|
||||
conn.execute("""
|
||||
INSERT INTO vps_stats
|
||||
(vps_id, ts, cpu, ram_percent, ram_used, ram_total,
|
||||
net_sent_per_sec, net_recv_per_sec, net_bytes_sent, net_bytes_recv)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
vps["id"], now,
|
||||
data["cpu_percent"], data["ram_percent"],
|
||||
data["ram_used"], data["ram_total"],
|
||||
data["net_sent_per_sec"], data["net_recv_per_sec"],
|
||||
data.get("net_bytes_sent", 0), data.get("net_bytes_recv", 0),
|
||||
))
|
||||
except Exception:
|
||||
pass # VPS hors ligne ou agent non mis à jour — ignoré silencieusement
|
||||
|
||||
@@ -321,6 +378,16 @@ async def _stats_collector() -> None:
|
||||
@app.on_event("startup")
|
||||
async def startup_event() -> None:
|
||||
asyncio.create_task(_stats_collector())
|
||||
asyncio.create_task(_cleanup_old_stats())
|
||||
|
||||
|
||||
async def _cleanup_old_stats() -> None:
|
||||
"""Supprime les statistiques de plus de 31 jours (s'exécute toutes les heures)."""
|
||||
while True:
|
||||
await asyncio.sleep(3600)
|
||||
cutoff = int(time.time()) - 31 * 24 * 3600
|
||||
with get_db() as conn:
|
||||
conn.execute("DELETE FROM vps_stats WHERE ts < ?", (cutoff,))
|
||||
|
||||
|
||||
# ─── Routes Auth ──────────────────────────────────────────────────────────────
|
||||
@@ -415,11 +482,55 @@ def edit_vps(vps_id: str, body: VpsUpdateRequest, _: Annotated[dict, Depends(get
|
||||
|
||||
|
||||
@app.get("/api/vps/{vps_id}/stats")
|
||||
def get_vps_stats(vps_id: str, _: Annotated[dict, Depends(get_current_user)]):
|
||||
"""Retourne l'historique des stats système d'un VPS (ring buffer en mémoire)."""
|
||||
def get_vps_stats(
|
||||
vps_id: str,
|
||||
duration: int = 600, # secondes, défaut 10 min
|
||||
_: Annotated[dict, Depends(get_current_user)] = None,
|
||||
):
|
||||
"""Retourne l'historique des stats système d'un VPS avec downsampling automatique.
|
||||
|
||||
Le paramètre `duration` (en secondes) détermine la fenêtre temporelle.
|
||||
La réponse contient toujours ~300 points maximum (agrégation par bucket SQL).
|
||||
"""
|
||||
if not any(v["id"] == vps_id for v in load_vps()):
|
||||
raise HTTPException(status_code=404, detail="VPS introuvable")
|
||||
return list(_stats_history.get(vps_id, deque()))
|
||||
|
||||
duration = max(60, min(duration, 31 * 24 * 3600)) # clamp 1 min – 31 j
|
||||
since = int(time.time()) - duration
|
||||
bucket = max(5, duration // 300) # ≤ 300 points retournés
|
||||
|
||||
with get_db() as conn:
|
||||
rows = conn.execute("""
|
||||
SELECT
|
||||
(ts / :b) * :b AS ts,
|
||||
AVG(cpu) AS cpu,
|
||||
AVG(ram_percent) AS ram_percent,
|
||||
CAST(AVG(ram_used) AS INTEGER) AS ram_used,
|
||||
MAX(ram_total) AS ram_total,
|
||||
AVG(net_sent_per_sec) AS net_sent_per_sec,
|
||||
AVG(net_recv_per_sec) AS net_recv_per_sec,
|
||||
MAX(net_bytes_sent) AS net_bytes_sent,
|
||||
MAX(net_bytes_recv) AS net_bytes_recv
|
||||
FROM vps_stats
|
||||
WHERE vps_id = :vps_id AND ts >= :since
|
||||
GROUP BY (ts / :b)
|
||||
ORDER BY ts ASC
|
||||
""", {"b": bucket, "vps_id": vps_id, "since": since}).fetchall()
|
||||
|
||||
return [
|
||||
{
|
||||
"ts": datetime.fromtimestamp(int(row["ts"]), tz=timezone.utc).isoformat(),
|
||||
"cpu": row["cpu"],
|
||||
"ram_percent": row["ram_percent"],
|
||||
"ram_used": row["ram_used"],
|
||||
"ram_total": row["ram_total"],
|
||||
"net_sent_per_sec": row["net_sent_per_sec"],
|
||||
"net_recv_per_sec": row["net_recv_per_sec"],
|
||||
"net_bytes_sent": row["net_bytes_sent"],
|
||||
"net_bytes_recv": row["net_bytes_recv"],
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
|
||||
|
||||
@app.get("/api/status")
|
||||
|
||||
Reference in New Issue
Block a user