From cd99ed12eb2c6849b3e63403a29b2083ab857cd8 Mon Sep 17 00:00:00 2001 From: Mathis Date: Thu, 26 Feb 2026 12:43:43 +0100 Subject: [PATCH] V1.0 de GameurBot (Metrics surveillance des perfs) --- README.md | 192 +++++++++++++ __pycache__/status_engine.cpython-312.pyc | Bin 0 -> 12603 bytes agent.py | 138 +++++++++ agent_output.log | 0 agent_precision.log | 14 + agent_verification.log | 61 ++++ bot.py | 327 ++++++++++++++++++++++ bot_output.log | 28 ++ bot_precision.log | 41 +++ bot_verification.log | 135 +++++++++ config.example.json | 13 + config.json | 17 ++ requirements.txt | 3 + status_engine.py | 279 ++++++++++++++++++ 14 files changed, 1248 insertions(+) create mode 100644 README.md create mode 100644 __pycache__/status_engine.cpython-312.pyc create mode 100644 agent.py create mode 100644 agent_output.log create mode 100644 agent_precision.log create mode 100644 agent_verification.log create mode 100644 bot.py create mode 100644 bot_output.log create mode 100644 bot_precision.log create mode 100644 bot_verification.log create mode 100644 config.example.json create mode 100644 config.json create mode 100644 requirements.txt create mode 100644 status_engine.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..3efa09d --- /dev/null +++ b/README.md @@ -0,0 +1,192 @@ +# 🖥️ Discord Status Bot + +Bot Discord de monitoring serveur en temps réel. +Affiche et met à jour automatiquement un message épinglé dans chaque serveur Discord +avec l'état des ressources, la cause des ralentissements, et une ETA de retour à la normale. + +--- + +## Architecture + +``` +[Serveur de jeux] [n'importe où] + agent.py ──── HTTP POST ────▶ bot.py + (psutil) /metrics ├── serveur aiohttp (port 8765) + ├── StatusEngine (analyse) + └── discord.py (mise à jour embed épinglé) + │ + ┌─────────┼─────────┐ + Guilde 1 Guilde 2 Guilde 3 ... +``` + +--- + +## Installation + +### 1. Dépendances + +```bash +pip install -r requirements.txt +``` + +### 2. Configuration + +```bash +cp config.example.json config.json +``` + +Remplis `config.json` : + +| Clé | Description | +|-----------------|--------------------------------------------------------------------| +| `discord_token` | Token de ton bot (https://discord.com/developers/applications) | +| `api_secret` | Mot de passe partagé entre le bot et l'agent (chaîne aléatoire) | +| `api_port` | Port HTTP du bot (défaut : 8765) | + +> ⚠️ Le même `api_secret` doit être renseigné dans `agent.py` (`API_SECRET`) et dans `config.json`. + +### 3. Adapter l'agent + +Dans `agent.py`, modifie : + +```python +BOT_API_URL = "http://ADRESSE_DU_BOT:8765/metrics" +API_SECRET = "le_meme_secret_que_dans_config.json" +``` + +Si le bot et l'agent tournent sur la même machine, laisse `http://localhost:8765/metrics`. + +Si le bot est sur une autre machine (VPS dédié Discord), utilise son IP/domaine. + +### 4. Ajouter les mots-clés de tes services + +Dans `agent.py`, `SERVICE_KEYWORDS` : + +```python +SERVICE_KEYWORDS = { + "minecraft": ["java", "minecraft", "paper"], + "satisfactory": ["factoryserver"], + "ollama": ["ollama"], + # Ajoute tes propres services ici + "mon_jeu": ["mon_process_name"], +} +``` + +--- + +## Lancement + +### Bot (à lancer en premier) + +```bash +python bot.py +``` + +Ou en service systemd : + +```ini +[Unit] +Description=Discord Status Bot +After=network.target + +[Service] +WorkingDirectory=/chemin/vers/discord-status-bot +ExecStart=/usr/bin/python3 bot.py +Restart=always + +[Install] +WantedBy=multi-user.target +``` + +### Agent (sur le serveur de jeux) + +```bash +python agent.py +``` + +Idem, peut être lancé en service systemd. + +--- + +## Configuration Discord + +### Créer le bot Discord + +1. Aller sur https://discord.com/developers/applications +2. **New Application** → donne-lui un nom +3. Onglet **Bot** → **Reset Token** → copier le token dans `config.json` +4. Activer **Server Members Intent** et **Message Content Intent** si nécessaire +5. Onglet **OAuth2 > URL Generator** : + - Scopes : `bot`, `applications.commands` + - Bot Permissions : `Send Messages`, `Manage Messages`, `Embed Links`, `Read Message History` +6. Copier l'URL générée et l'ouvrir pour inviter le bot sur tes serveurs + +### Configurer chaque serveur Discord + +Une fois le bot invité sur un serveur et lancé localement : + +1. Va dans le salon où tu veux le tableau de bord +2. Tape `/setup` +3. Le bot envoie et épingle le premier message de statut +4. Répète sur chaque serveur Discord + +--- + +## Commandes slash disponibles + +| Commande | Description | Permissions | +|--------------|--------------------------------------------------|-------------| +| `/status` | Affiche l'état actuel (message éphémère) | Tous | +| `/services` | Détail de chaque service détecté | Tous | +| `/setup` | Configure le salon actuel comme tableau de bord | Admin | + +--- + +## Personnalisation + +### Seuils d'alerte + +Dans `status_engine.py`, modifie `THRESHOLDS` : + +```python +THRESHOLDS = { + "cpu": {"warning": 70, "critical": 85}, + "ram": {"warning": 80, "critical": 90}, + "swap": {"warning": 40, "critical": 70}, +} +``` + +### Fréquence de mise à jour + +- **Agent** → `PUSH_INTERVAL` dans `agent.py` (défaut : 15s) +- **Bot** → `@tasks.loop(seconds=20)` dans `bot.py` + +--- + +## Exemple de rendu + +``` +🔴 Serveur — Surchargé + +📊 Ressources +CPU ████████░░ 82% 🌡️ 74°C +RAM █████████░ 91% (14.6/16.0 GB) +SWAP ████░░░░░░ 43% + +📈 Charge (1m / 5m / 15m) : 7.42 / 6.80 / 5.12 + +🎮 Services actifs +▸ ⛏️ Minecraft 🔥 — CPU 45% | RAM 4.2 GB +▸ 🤖 Ollama (IA) 🔥 — CPU 38% | RAM 8.1 GB +▸ 🏭 Satisfactory ⚡ — CPU 12% | RAM 2.1 GB + +🔍 Pourquoi ça rame ? +• CPU à 82% — principal consommateur : Minecraft (45%) +• RAM à 91% (14.6/16.0 GB) — Ollama (IA) occupe 8.1 GB +• 💡 Conflit probable : Ollama (IA) et Minecraft, Satisfactory se partagent les ressources + +⏱️ Retour à la normale +⏱️ Retour à la normale dans ~8 min + +Dernière mise à jour 26/02/2026 21:34 +``` diff --git a/__pycache__/status_engine.cpython-312.pyc b/__pycache__/status_engine.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bdb04fb7a352c4d36d4cba81f5d8c652f9efa1e4 GIT binary patch literal 12603 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 xIHxUeepH!$t1{4Su+TI<%2k<)B dict: + """Parcourt les processus et agrège l'usage par service.""" + buckets: dict[str, dict] = {k: {"running": False, "cpu": 0.0, "ram_mb": 0.0, "pids": []} + for k in SERVICE_KEYWORDS} + + for proc in psutil.process_iter(["pid", "name", "cmdline", "cpu_percent", "memory_info", "status"]): + try: + name = (proc.info["name"] or "").lower() + cmdline = " ".join(proc.info["cmdline"] or []).lower() + text = name + " " + cmdline + + for svc, keywords in SERVICE_KEYWORDS.items(): + if any(kw in text for kw in keywords): + b = buckets[svc] + b["running"] = True + b["cpu"] += proc.info["cpu_percent"] or 0.0 + b["ram_mb"] += (proc.info["memory_info"].rss / 1024 / 1024) if proc.info["memory_info"] else 0.0 + b["pids"].append(proc.info["pid"]) + break # un process ne compte que pour un service + + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + continue + + # Nettoyage : on retire les pids de la réponse (inutiles pour le bot) + for svc in buckets: + buckets[svc].pop("pids", None) + + return buckets + + +async def collect() -> dict: + """Construit le payload complet à envoyer au bot.""" + cpu = psutil.cpu_percent(interval=1) # bloquant 1s — thread OK en async + mem = psutil.virtual_memory() + swap = psutil.swap_memory() + load = psutil.getloadavg() # (1m, 5m, 15m) + temps = {} + + try: + sensor_data = psutil.sensors_temperatures() + if sensor_data: + # On prend la temp CPU principale si dispo + for key in ("coretemp", "k10temp", "cpu_thermal"): + if key in sensor_data: + temps["cpu_c"] = round(sensor_data[key][0].current, 1) + break + except AttributeError: + pass # Windows / pas de capteurs + + services = scan_services() + + return { + "timestamp": time.time(), + "api_key": API_SECRET, + "system": { + "cpu_percent": round(cpu, 1), + "cpu_count": psutil.cpu_count(logical=True), + "load_avg_1m": round(load[0], 2), + "load_avg_5m": round(load[1], 2), + "load_avg_15m": round(load[2], 2), + "ram_total_gb": round(mem.total / 1024**3, 2), + "ram_used_gb": round(mem.used / 1024**3, 2), + "ram_percent": mem.percent, + "swap_total_gb": round(swap.total / 1024**3, 2), + "swap_used_gb": round(swap.used / 1024**3, 2), + "swap_percent": swap.percent, + **temps, + }, + "services": services, + } + +# ─── Boucle principale ──────────────────────────────────────────────────────── + +async def run(): + # Premier appel cpu_percent sans intervalle pour initialiser le compteur + psutil.cpu_percent(interval=None) + await asyncio.sleep(0.5) + + print(f"[Agent] Démarrage — push vers {BOT_API_URL} toutes les {PUSH_INTERVAL}s") + + async with aiohttp.ClientSession() as session: + while True: + try: + payload = await collect() + async with session.post( + BOT_API_URL, + json=payload, + timeout=aiohttp.ClientTimeout(total=8) + ) as resp: + status_str = "✓" if resp.status == 200 else f"✗ HTTP {resp.status}" + ts = datetime.now().strftime("%H:%M:%S") + print(f"[{ts}] CPU={payload['system']['cpu_percent']}% " + f"RAM={payload['system']['ram_percent']}% → {status_str}") + + except aiohttp.ClientConnectorError: + print(f"[{datetime.now().strftime('%H:%M:%S')}] ✗ Impossible de joindre le bot — il est lancé ?") + except Exception as exc: + print(f"[{datetime.now().strftime('%H:%M:%S')}] ✗ Erreur inattendue : {exc}") + + await asyncio.sleep(PUSH_INTERVAL) + + +if __name__ == "__main__": + asyncio.run(run()) diff --git a/agent_output.log b/agent_output.log new file mode 100644 index 0000000..e69de29 diff --git a/agent_precision.log b/agent_precision.log new file mode 100644 index 0000000..23ce826 --- /dev/null +++ b/agent_precision.log @@ -0,0 +1,14 @@ +[Agent] Démarrage — push vers http://localhost:8765/metrics toutes les 5s +[12:25:55] CPU=12.1% RAM=32.5% → ✓ +[12:26:01] CPU=3.6% RAM=32.6% → ✓ +[12:26:07] CPU=5.3% RAM=32.7% → ✓ +[12:26:13] CPU=4.5% RAM=32.5% → ✓ +[12:26:19] CPU=8.6% RAM=32.5% → ✓ +[12:26:25] CPU=6.5% RAM=32.9% → ✓ +[12:26:31] CPU=20.8% RAM=34.2% → ✓ +[12:26:37] CPU=8.1% RAM=33.0% → ✓ +[12:26:43] CPU=3.7% RAM=32.8% → ✓ +[12:26:50] CPU=4.2% RAM=32.3% → ✓ +[12:26:56] CPU=5.7% RAM=32.2% → ✓ +[12:27:02] CPU=15.0% RAM=32.2% → ✓ +[12:27:08] CPU=19.7% RAM=32.9% → ✓ diff --git a/agent_verification.log b/agent_verification.log new file mode 100644 index 0000000..d630171 --- /dev/null +++ b/agent_verification.log @@ -0,0 +1,61 @@ +[Agent] Démarrage — push vers http://localhost:8765/metrics toutes les 5s +[12:19:48] CPU=24.6% RAM=71.4% → ✓ +[12:19:54] CPU=18.3% RAM=71.7% → ✓ +[12:20:00] CPU=14.2% RAM=71.4% → ✓ +[12:20:06] CPU=14.7% RAM=71.3% → ✓ +[12:20:12] CPU=15.4% RAM=71.2% → ✓ +[12:20:18] CPU=17.9% RAM=71.2% → ✓ +[12:20:24] CPU=16.5% RAM=71.3% → ✓ +[12:20:30] CPU=16.5% RAM=71.5% → ✓ +[12:20:36] CPU=21.5% RAM=71.2% → ✓ +[12:20:42] CPU=31.4% RAM=71.0% → ✓ +[12:20:49] CPU=38.2% RAM=71.2% → ✓ +[12:20:55] CPU=55.5% RAM=35.2% → ✓ +[12:21:01] CPU=31.1% RAM=35.4% → ✓ +[12:21:07] CPU=15.2% RAM=35.0% → ✓ +[12:21:13] CPU=16.2% RAM=34.4% → ✓ +[12:21:19] CPU=9.1% RAM=31.9% → ✓ +[12:21:25] CPU=9.8% RAM=32.1% → ✓ +[12:21:31] CPU=4.9% RAM=31.8% → ✓ +[12:21:37] CPU=5.1% RAM=31.6% → ✓ +[12:21:43] CPU=4.9% RAM=31.6% → ✓ +[12:21:49] CPU=5.6% RAM=31.6% → ✓ +[12:21:55] CPU=14.8% RAM=32.2% → ✓ +[12:22:01] CPU=13.4% RAM=32.1% → ✓ +[12:22:08] CPU=8.7% RAM=32.5% → ✓ +[12:22:14] CPU=4.3% RAM=32.2% → ✓ +[12:22:20] CPU=4.5% RAM=31.8% → ✓ +[12:22:26] CPU=8.6% RAM=31.9% → ✓ +[12:22:32] CPU=5.2% RAM=31.7% → ✓ +[12:22:38] CPU=6.3% RAM=31.6% → ✓ +[12:22:44] CPU=2.4% RAM=31.6% → ✓ +[12:22:50] CPU=3.7% RAM=31.6% → ✓ +[12:22:56] CPU=9.3% RAM=31.1% → ✓ +[12:23:02] CPU=2.6% RAM=31.0% → ✓ +[12:23:08] CPU=15.7% RAM=31.2% → ✓ +[12:23:14] CPU=7.5% RAM=31.4% → ✓ +[12:23:20] CPU=2.5% RAM=31.3% → ✓ +[12:23:27] CPU=4.6% RAM=32.1% → ✓ +[12:23:33] CPU=9.5% RAM=31.4% → ✓ +[12:23:39] CPU=7.6% RAM=31.4% → ✓ +[12:23:45] CPU=8.3% RAM=31.2% → ✓ +[12:23:51] CPU=10.3% RAM=31.2% → ✓ +[12:23:57] CPU=15.0% RAM=31.2% → ✓ +[12:24:03] CPU=11.1% RAM=31.8% → ✓ +[12:24:09] CPU=10.7% RAM=31.7% → ✓ +[12:24:15] CPU=20.2% RAM=34.4% → ✓ +[12:24:21] CPU=15.6% RAM=34.0% → ✓ +[12:24:27] CPU=9.6% RAM=33.5% → ✓ +[12:24:34] CPU=5.1% RAM=32.1% → ✓ +[12:24:40] CPU=8.1% RAM=31.5% → ✓ +[12:24:46] CPU=3.0% RAM=31.4% → ✓ +[12:24:52] CPU=2.7% RAM=31.4% → ✓ +[12:24:58] CPU=3.0% RAM=31.4% → ✓ +[12:25:05] CPU=3.4% RAM=31.9% → ✓ +[12:25:11] CPU=6.4% RAM=31.6% → ✓ +[12:25:17] CPU=7.8% RAM=32.2% → ✓ +[12:25:23] CPU=19.9% RAM=34.0% → ✓ +[12:25:29] CPU=4.0% RAM=32.9% → ✓ +[12:25:35] CPU=6.9% RAM=32.4% → ✓ +[12:25:41] CPU=19.0% RAM=32.3% → ✓ +[12:25:47] CPU=18.7% RAM=32.3% → ✓ diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..f83a857 --- /dev/null +++ b/bot.py @@ -0,0 +1,327 @@ +""" +bot.py — Bot Discord de monitoring serveur. +Lance simultanément : + • Un serveur HTTP (aiohttp) qui reçoit les métriques de l'agent + • Le bot Discord qui met à jour les messages épinglés dans les guildes configurées + +Lancement : python bot.py +""" + +import asyncio +import json +import os +import logging +from datetime import datetime, timezone +from pathlib import Path + +import discord +from discord.ext import commands, tasks +from aiohttp import web + +from status_engine import StatusEngine, AnalysisResult, STATUS_OK, STATUS_WARNING, STATUS_CRITICAL + +# ─── Logging ────────────────────────────────────────────────────────────────── + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + datefmt="%H:%M:%S", +) +log = logging.getLogger("bot") + +# ─── Config ─────────────────────────────────────────────────────────────────── + +CONFIG_PATH = Path("config.json") + +def load_config() -> dict: + with open(CONFIG_PATH, encoding="utf-8") as f: + return json.load(f) + +def save_config(cfg: dict): + with open(CONFIG_PATH, "w", encoding="utf-8") as f: + json.dump(cfg, f, indent=2, ensure_ascii=False) + +# ─── Bot ────────────────────────────────────────────────────────────────────── + +class StatusBot(commands.Bot): + + def __init__(self, cfg: dict): + intents = discord.Intents.default() + intents.guilds = True + super().__init__(command_prefix="!", intents=intents) + + self.cfg = cfg + self.engine = StatusEngine() + + # guild_id (str) → {"channel_id": int, "message_id": int | None} + self.targets: dict[str, dict] = cfg.get("guilds", {}) + + self._api_runner: web.AppRunner | None = None + + # ── Lifecycle ───────────────────────────────────────────────────────────── + + async def setup_hook(self): + # On lance l'API et la boucle AVANT la synchro des commandes (qui peut être longue) + asyncio.create_task(self._start_api_server()) + self.update_loop.start() + log.info("Services de monitoring démarrés.") + + await self.tree.sync() + log.info("Slash commands synced.") + + async def on_ready(self): + log.info(f"Connecté en tant que {self.user} (ID {self.user.id})") + await self.change_presence( + activity=discord.Activity(type=discord.ActivityType.watching, name="l'état du serveur") + ) + + # ── API HTTP (réception métriques) ──────────────────────────────────────── + + async def _start_api_server(self): + app = web.Application() + app.router.add_post("/metrics", self._handle_metrics) + runner = web.AppRunner(app) + await runner.setup() + site = web.TCPSite(runner, "0.0.0.0", self.cfg.get("api_port", 8765)) + await site.start() + self._api_runner = runner + log.info(f"API HTTP en écoute sur le port {self.cfg.get('api_port', 8765)}") + + async def _handle_metrics(self, request: web.Request) -> web.Response: + try: + data = await request.json() + except Exception: + return web.Response(status=400, text="JSON invalide") + + if data.get("api_key") != self.cfg.get("api_secret"): + return web.Response(status=403, text="Clé API invalide") + + log.info(f"Métriques reçues de l'agent (CPU: {data.get('system', {}).get('cpu_percent') or '?'}%)") + self.engine.push(data) + return web.Response(status=200, text="ok") + + # ── Boucle de mise à jour ───────────────────────────────────────────────── + + @tasks.loop(seconds=20) + async def update_loop(self): + if not self.engine.has_data: + return + + result = self.engine.analyse() + if result is None: + return + + embed = self._build_embed(result) + + for guild_id_str, target in self.targets.items(): + if not isinstance(target, dict): + continue + + channel_id = target.get("channel_id") + if not channel_id: + continue + + channel = self.get_channel(channel_id) + if not channel: + try: + channel = await self.fetch_channel(channel_id) + except Exception as exc: + log.warning(f"Impossible de récupérer le salon {channel_id}: {exc}") + continue + + msg_id = target.get("message_id") + + try: + if msg_id: + # Mise à jour du message existant + msg = await channel.fetch_message(msg_id) + await msg.edit(embed=embed) + else: + # Premier envoi : on envoie et on épingle + msg = await channel.send(embed=embed) + await msg.pin() + target["message_id"] = msg.id + save_config(self.cfg) + log.info(f"Message épinglé dans #{channel.name} ({guild_id_str})") + + except discord.NotFound: + # Le message a été supprimé — on recrée + target["message_id"] = None + save_config(self.cfg) + except discord.Forbidden: + log.warning(f"Permissions insuffisantes dans #{channel.name}") + except Exception as exc: + log.error(f"Erreur update guilde {guild_id_str}: {exc}") + + @update_loop.before_loop + async def before_loop(self): + await self.wait_until_ready() + + # ── Construction de l'embed ─────────────────────────────────────────────── + + def _build_embed(self, r: AnalysisResult) -> discord.Embed: + COLOR = { + STATUS_OK: 0x2ECC71, # vert + STATUS_WARNING: 0xF39C12, # orange + STATUS_CRITICAL: 0xE74C3C, # rouge + } + + TITLE = { + STATUS_OK: "🟢 Serveur — Tout est normal", + STATUS_WARNING: "🟡 Serveur — Charge élevée", + STATUS_CRITICAL: "🔴 Serveur — Surchargé", + } + + embed = discord.Embed( + title=TITLE[r.status], + color=COLOR[r.status], + timestamp=datetime.now(timezone.utc), + ) + embed.set_footer(text="Dernière mise à jour") + + # ── Ressources globales ── + cpu_bar = self._bar(r.cpu, 100) + ram_bar = self._bar(r.ram_pct, 100) + swap_bar = self._bar(r.swap_pct, 100) + + temp_str = f" 🌡️ {r.cpu_temp}°C" if r.cpu_temp else "" + + resources = ( + f"`CPU ` {cpu_bar} **{r.cpu:.0f}%**{temp_str}\n" + f"`RAM ` {ram_bar} **{r.ram_pct:.0f}%** " + f"({r.ram_used_gb:.1f}/{r.ram_total_gb:.1f} GB)\n" + ) + if r.swap_pct > 5: + resources += f"`SWAP` {swap_bar} **{r.swap_pct:.0f}%**\n" + + resources += ( + f"\n📈 Charge (1m / 5m / 15m) : " + f"`{r.load_avg[0]:.2f}` / `{r.load_avg[1]:.2f}` / `{r.load_avg[2]:.2f}`" + ) + + embed.add_field(name="📊 Ressources", value=resources, inline=False) + + # ── Services ── + running = [s for s in r.services if s.running] + stopped = [s for s in r.services if not s.running] + + if running: + lines = [] + for svc in running: + cpu_indicator = "" + if svc.cpu >= 40: + cpu_indicator = " 🔥" + elif svc.cpu >= 20: + cpu_indicator = " ⚡" + lines.append( + f"▸ {svc.name}{cpu_indicator} — " + f"CPU **{svc.cpu:.0f}%** | RAM **{svc.ram_gb:.1f} GB**" + ) + embed.add_field( + name="🎮 Services actifs", + value="\n".join(lines), + inline=False + ) + + if stopped: + stopped_names = " • ".join(s.name for s in stopped) + embed.add_field(name="💤 Services arrêtés", value=stopped_names, inline=False) + + # ── Cause ── + if r.status != STATUS_OK: + cause_text = "\n".join(r.cause_lines) + embed.add_field(name="🔍 Pourquoi ça rame ?", value=cause_text, inline=False) + + # ── ETA ── + embed.add_field(name="⏱️ Retour à la normale", value=r.eta_text, inline=False) + + return embed + + def _bar(self, value: float, max_val: float, length: int = 10) -> str: + """Génère une barre de progression ASCII.""" + filled = round((value / max_val) * length) + filled = max(0, min(length, filled)) + bar = "█" * filled + "░" * (length - filled) + return f"`{bar}`" + + +# ─── Slash commands ─────────────────────────────────────────────────────────── + +def setup_commands(bot: StatusBot): + + @bot.tree.command(name="status", description="Affiche l'état actuel du serveur") + async def cmd_status(interaction: discord.Interaction): + if not bot.engine.has_data: + await interaction.response.send_message( + "⏳ En attente des premières métriques de l'agent…", ephemeral=True + ) + return + + result = bot.engine.analyse() + embed = bot._build_embed(result) + await interaction.response.send_message(embed=embed) + + @bot.tree.command(name="setup", description="(Admin) Configure ce salon comme tableau de bord de statut") + @discord.app_commands.default_permissions(administrator=True) + async def cmd_setup(interaction: discord.Interaction): + guild_id = str(interaction.guild_id) + + if guild_id not in bot.targets: + bot.targets[guild_id] = {} + bot.cfg["guilds"] = bot.targets + + bot.targets[guild_id]["channel_id"] = interaction.channel_id + bot.targets[guild_id]["message_id"] = None # forcera la recréation + save_config(bot.cfg) + + await interaction.response.send_message( + f"✅ Ce salon sera utilisé pour le tableau de bord de statut.\n" + f"Le message épinglé apparaîtra dans les 20 secondes.", + ephemeral=True + ) + log.info(f"Setup: guilde {guild_id} → channel {interaction.channel_id}") + + @bot.tree.command(name="services", description="Détail de tous les services détectés") + async def cmd_services(interaction: discord.Interaction): + if not bot.engine.has_data: + await interaction.response.send_message("⏳ Pas encore de données.", ephemeral=True) + return + + result = bot.engine.analyse() + lines = [] + for svc in result.services: + status_icon = "🟢" if svc.running else "⚫" + if svc.running: + lines.append(f"{status_icon} **{svc.name}** — CPU {svc.cpu:.0f}% | RAM {svc.ram_gb:.2f} GB") + else: + lines.append(f"{status_icon} **{svc.name}** — arrêté") + + embed = discord.Embed( + title="🖥️ État des services", + description="\n".join(lines) or "Aucun service détecté", + color=0x3498DB, + timestamp=datetime.now(timezone.utc), + ) + await interaction.response.send_message(embed=embed, ephemeral=True) + + +# ─── Entry point ────────────────────────────────────────────────────────────── + +def main(): + if not CONFIG_PATH.exists(): + print(f"[ERREUR] Fichier {CONFIG_PATH} introuvable. Copie config.example.json → config.json et remplis-le.") + return + + cfg = load_config() + token = cfg.get("discord_token") or os.environ.get("DISCORD_TOKEN") + if not token: + print("[ERREUR] Aucun token Discord dans config.json (clé 'discord_token') ni en variable d'env DISCORD_TOKEN.") + return + + bot = StatusBot(cfg) + setup_commands(bot) + bot.run(token, log_handler=None) + + +if __name__ == "__main__": + main() diff --git a/bot_output.log b/bot_output.log new file mode 100644 index 0000000..3f97337 --- /dev/null +++ b/bot_output.log @@ -0,0 +1,28 @@ +12:37:08 [WARNING] PyNaCl is not installed, voice will NOT be supported +12:37:08 [WARNING] Privileged message content intent is missing, commands may not work as expected. +12:37:08 [INFO] logging in using static token +12:37:09 [INFO] Services de monitoring démarrés. +12:37:09 [INFO] API HTTP en écoute sur le port 8765 +12:37:09 [INFO] Slash commands synced. +12:37:09 [INFO] Shard ID None has connected to Gateway (Session ID: 4e987803b1749930ee6baa8fa100bf49). +12:37:10 [INFO] Métriques reçues de l'agent (CPU: 3.5%) +12:37:10 [INFO] 127.0.0.1 [26/Feb/2026:12:37:10 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:37:11 [INFO] Connecté en tant que Gameurpro12bot#8094 (ID 1074714308660965398) +12:37:12 [WARNING] Impossible de récupérer le salon 123456789012345678: 404 Not Found (error code: 10003): Unknown Channel +12:37:12 [INFO] Message épinglé dans #chat-gameurcorp (1110604692263796746) +12:37:16 [INFO] Métriques reçues de l'agent (CPU: 2.1%) +12:37:16 [INFO] 127.0.0.1 [26/Feb/2026:12:37:16 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:37:22 [INFO] Métriques reçues de l'agent (CPU: 1.9%) +12:37:22 [INFO] 127.0.0.1 [26/Feb/2026:12:37:22 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:37:28 [INFO] Métriques reçues de l'agent (CPU: 6.0%) +12:37:28 [INFO] 127.0.0.1 [26/Feb/2026:12:37:28 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:37:32 [WARNING] Impossible de récupérer le salon 123456789012345678: 404 Not Found (error code: 10003): Unknown Channel +12:37:34 [INFO] Métriques reçues de l'agent (CPU: 4.7%) +12:37:34 [INFO] 127.0.0.1 [26/Feb/2026:12:37:34 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:37:40 [INFO] Métriques reçues de l'agent (CPU: 5.2%) +12:37:40 [INFO] 127.0.0.1 [26/Feb/2026:12:37:40 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:37:46 [INFO] Métriques reçues de l'agent (CPU: 2.4%) +12:37:46 [INFO] 127.0.0.1 [26/Feb/2026:12:37:46 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:37:52 [WARNING] Impossible de récupérer le salon 123456789012345678: 404 Not Found (error code: 10003): Unknown Channel +12:37:53 [INFO] Métriques reçues de l'agent (CPU: 2.3%) +12:37:53 [INFO] 127.0.0.1 [26/Feb/2026:12:37:53 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" diff --git a/bot_precision.log b/bot_precision.log new file mode 100644 index 0000000..c7dad7d --- /dev/null +++ b/bot_precision.log @@ -0,0 +1,41 @@ +12:25:51 [WARNING] PyNaCl is not installed, voice will NOT be supported +12:25:51 [WARNING] Privileged message content intent is missing, commands may not work as expected. +12:25:51 [INFO] logging in using static token +12:25:52 [INFO] Slash commands synced. +12:25:52 [INFO] API HTTP en écoute sur le port 8765 +12:25:53 [INFO] Shard ID None has connected to Gateway (Session ID: 1e9cce5a480ef266caf1e55cbf7d415c). +12:25:55 [INFO] Connecté en tant que Gameurpro12bot#8094 (ID 1074714308660965398) +12:25:55 [INFO] Métriques reçues de l'agent (CPU: 12.1%) +12:25:55 [INFO] 127.0.0.1 [26/Feb/2026:12:25:55 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:26:01 [INFO] Métriques reçues de l'agent (CPU: 3.6%) +12:26:01 [INFO] 127.0.0.1 [26/Feb/2026:12:26:01 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:26:07 [INFO] Métriques reçues de l'agent (CPU: 5.3%) +12:26:07 [INFO] 127.0.0.1 [26/Feb/2026:12:26:07 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:26:13 [INFO] Métriques reçues de l'agent (CPU: 4.5%) +12:26:13 [INFO] 127.0.0.1 [26/Feb/2026:12:26:13 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:26:15 [ERROR] Unhandled exception in internal background task 'update_loop'. +Traceback (most recent call last): + File "/home/gameurpro12/Documents/Mes_projets/Discord_bot/GameurBot/.venv/lib/python3.12/site-packages/discord/ext/tasks/__init__.py", line 246, in _loop + await self.coro(*args, **kwargs) + File "/home/gameurpro12/Documents/Mes_projets/Discord_bot/GameurBot/bot.py", line 114, in update_loop + channel_id = target.get("channel_id") + ^^^^^^^^^^ +AttributeError: 'str' object has no attribute 'get' +12:26:19 [INFO] Métriques reçues de l'agent (CPU: 8.6%) +12:26:19 [INFO] 127.0.0.1 [26/Feb/2026:12:26:19 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:26:25 [INFO] Métriques reçues de l'agent (CPU: 6.5%) +12:26:25 [INFO] 127.0.0.1 [26/Feb/2026:12:26:25 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:26:31 [INFO] Métriques reçues de l'agent (CPU: 20.8%) +12:26:31 [INFO] 127.0.0.1 [26/Feb/2026:12:26:31 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:26:37 [INFO] Métriques reçues de l'agent (CPU: 8.1%) +12:26:37 [INFO] 127.0.0.1 [26/Feb/2026:12:26:37 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:26:43 [INFO] Métriques reçues de l'agent (CPU: 3.7%) +12:26:43 [INFO] 127.0.0.1 [26/Feb/2026:12:26:43 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:26:50 [INFO] Métriques reçues de l'agent (CPU: 4.2%) +12:26:50 [INFO] 127.0.0.1 [26/Feb/2026:12:26:50 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:26:56 [INFO] Métriques reçues de l'agent (CPU: 5.7%) +12:26:56 [INFO] 127.0.0.1 [26/Feb/2026:12:26:56 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:27:02 [INFO] Métriques reçues de l'agent (CPU: 15.0%) +12:27:02 [INFO] 127.0.0.1 [26/Feb/2026:12:27:02 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:27:08 [INFO] Métriques reçues de l'agent (CPU: 19.7%) +12:27:08 [INFO] 127.0.0.1 [26/Feb/2026:12:27:08 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" diff --git a/bot_verification.log b/bot_verification.log new file mode 100644 index 0000000..3539775 --- /dev/null +++ b/bot_verification.log @@ -0,0 +1,135 @@ +12:19:44 [WARNING] PyNaCl is not installed, voice will NOT be supported +12:19:44 [WARNING] Privileged message content intent is missing, commands may not work as expected. +12:19:44 [INFO] logging in using static token +12:19:45 [INFO] Slash commands synced. +12:19:45 [INFO] API HTTP en écoute sur le port 8765 +12:19:45 [INFO] Shard ID None has connected to Gateway (Session ID: 44cfa7b3e2d7bc3caa7722d82b8d5c85). +12:19:47 [INFO] Connecté en tant que Gameurpro12bot#8094 (ID 1074714308660965398) +12:19:48 [INFO] Métriques reçues de l'agent (CPU: 24.6%) +12:19:48 [INFO] 127.0.0.1 [26/Feb/2026:12:19:48 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:19:54 [INFO] Métriques reçues de l'agent (CPU: 18.3%) +12:19:54 [INFO] 127.0.0.1 [26/Feb/2026:12:19:54 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:20:00 [INFO] Métriques reçues de l'agent (CPU: 14.2%) +12:20:00 [INFO] 127.0.0.1 [26/Feb/2026:12:20:00 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:20:06 [INFO] Métriques reçues de l'agent (CPU: 14.7%) +12:20:06 [INFO] 127.0.0.1 [26/Feb/2026:12:20:06 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:20:07 [ERROR] Unhandled exception in internal background task 'update_loop'. +Traceback (most recent call last): + File "/home/gameurpro12/Documents/Mes_projets/Discord_bot/GameurBot/.venv/lib/python3.12/site-packages/discord/ext/tasks/__init__.py", line 246, in _loop + await self.coro(*args, **kwargs) + File "/home/gameurpro12/Documents/Mes_projets/Discord_bot/GameurBot/bot.py", line 114, in update_loop + channel_id = target.get("channel_id") + ^^^^^^^^^^ +AttributeError: 'str' object has no attribute 'get' +12:20:12 [INFO] Métriques reçues de l'agent (CPU: 15.4%) +12:20:12 [INFO] 127.0.0.1 [26/Feb/2026:12:20:12 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:20:18 [INFO] Métriques reçues de l'agent (CPU: 17.9%) +12:20:18 [INFO] 127.0.0.1 [26/Feb/2026:12:20:18 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:20:24 [INFO] Métriques reçues de l'agent (CPU: 16.5%) +12:20:24 [INFO] 127.0.0.1 [26/Feb/2026:12:20:24 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:20:30 [INFO] Métriques reçues de l'agent (CPU: 16.5%) +12:20:30 [INFO] 127.0.0.1 [26/Feb/2026:12:20:30 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:20:36 [INFO] Métriques reçues de l'agent (CPU: 21.5%) +12:20:36 [INFO] 127.0.0.1 [26/Feb/2026:12:20:36 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:20:42 [INFO] Métriques reçues de l'agent (CPU: 31.4%) +12:20:42 [INFO] 127.0.0.1 [26/Feb/2026:12:20:42 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:20:49 [INFO] Métriques reçues de l'agent (CPU: 38.2%) +12:20:49 [INFO] 127.0.0.1 [26/Feb/2026:12:20:49 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:20:55 [INFO] Métriques reçues de l'agent (CPU: 55.5%) +12:20:55 [INFO] 127.0.0.1 [26/Feb/2026:12:20:55 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:21:01 [INFO] Métriques reçues de l'agent (CPU: 31.1%) +12:21:01 [INFO] 127.0.0.1 [26/Feb/2026:12:21:01 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:21:07 [INFO] Métriques reçues de l'agent (CPU: 15.2%) +12:21:07 [INFO] 127.0.0.1 [26/Feb/2026:12:21:07 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:21:13 [INFO] Métriques reçues de l'agent (CPU: 16.2%) +12:21:13 [INFO] 127.0.0.1 [26/Feb/2026:12:21:13 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:21:19 [INFO] Métriques reçues de l'agent (CPU: 9.1%) +12:21:19 [INFO] 127.0.0.1 [26/Feb/2026:12:21:19 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:21:25 [INFO] Métriques reçues de l'agent (CPU: 9.8%) +12:21:25 [INFO] 127.0.0.1 [26/Feb/2026:12:21:25 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:21:31 [INFO] Métriques reçues de l'agent (CPU: 4.9%) +12:21:31 [INFO] 127.0.0.1 [26/Feb/2026:12:21:31 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:21:37 [INFO] Métriques reçues de l'agent (CPU: 5.1%) +12:21:37 [INFO] 127.0.0.1 [26/Feb/2026:12:21:37 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:21:43 [INFO] Métriques reçues de l'agent (CPU: 4.9%) +12:21:43 [INFO] 127.0.0.1 [26/Feb/2026:12:21:43 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:21:49 [INFO] Métriques reçues de l'agent (CPU: 5.6%) +12:21:49 [INFO] 127.0.0.1 [26/Feb/2026:12:21:49 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:21:55 [INFO] Métriques reçues de l'agent (CPU: 14.8%) +12:21:55 [INFO] 127.0.0.1 [26/Feb/2026:12:21:55 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:22:01 [INFO] Métriques reçues de l'agent (CPU: 13.4%) +12:22:01 [INFO] 127.0.0.1 [26/Feb/2026:12:22:01 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:22:08 [INFO] Métriques reçues de l'agent (CPU: 8.7%) +12:22:08 [INFO] 127.0.0.1 [26/Feb/2026:12:22:08 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:22:14 [INFO] Métriques reçues de l'agent (CPU: 4.3%) +12:22:14 [INFO] 127.0.0.1 [26/Feb/2026:12:22:14 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:22:20 [INFO] Métriques reçues de l'agent (CPU: 4.5%) +12:22:20 [INFO] 127.0.0.1 [26/Feb/2026:12:22:20 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:22:26 [INFO] Métriques reçues de l'agent (CPU: 8.6%) +12:22:26 [INFO] 127.0.0.1 [26/Feb/2026:12:22:26 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:22:32 [INFO] Métriques reçues de l'agent (CPU: 5.2%) +12:22:32 [INFO] 127.0.0.1 [26/Feb/2026:12:22:32 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:22:38 [INFO] Métriques reçues de l'agent (CPU: 6.3%) +12:22:38 [INFO] 127.0.0.1 [26/Feb/2026:12:22:38 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:22:44 [INFO] Métriques reçues de l'agent (CPU: 2.4%) +12:22:44 [INFO] 127.0.0.1 [26/Feb/2026:12:22:44 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:22:50 [INFO] Métriques reçues de l'agent (CPU: 3.7%) +12:22:50 [INFO] 127.0.0.1 [26/Feb/2026:12:22:50 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:22:56 [INFO] Métriques reçues de l'agent (CPU: 9.3%) +12:22:56 [INFO] 127.0.0.1 [26/Feb/2026:12:22:56 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:23:02 [INFO] Métriques reçues de l'agent (CPU: 2.6%) +12:23:02 [INFO] 127.0.0.1 [26/Feb/2026:12:23:02 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:23:08 [INFO] Métriques reçues de l'agent (CPU: 15.7%) +12:23:08 [INFO] 127.0.0.1 [26/Feb/2026:12:23:08 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:23:14 [INFO] Métriques reçues de l'agent (CPU: 7.5%) +12:23:14 [INFO] 127.0.0.1 [26/Feb/2026:12:23:14 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:23:20 [INFO] Métriques reçues de l'agent (CPU: 2.5%) +12:23:20 [INFO] 127.0.0.1 [26/Feb/2026:12:23:20 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:23:27 [INFO] Métriques reçues de l'agent (CPU: 4.6%) +12:23:27 [INFO] 127.0.0.1 [26/Feb/2026:12:23:27 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:23:33 [INFO] Métriques reçues de l'agent (CPU: 9.5%) +12:23:33 [INFO] 127.0.0.1 [26/Feb/2026:12:23:33 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:23:39 [INFO] Métriques reçues de l'agent (CPU: 7.6%) +12:23:39 [INFO] 127.0.0.1 [26/Feb/2026:12:23:39 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:23:45 [INFO] Métriques reçues de l'agent (CPU: 8.3%) +12:23:45 [INFO] 127.0.0.1 [26/Feb/2026:12:23:45 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:23:51 [INFO] Métriques reçues de l'agent (CPU: 10.3%) +12:23:51 [INFO] 127.0.0.1 [26/Feb/2026:12:23:51 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:23:57 [INFO] Métriques reçues de l'agent (CPU: 15.0%) +12:23:57 [INFO] 127.0.0.1 [26/Feb/2026:12:23:57 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:24:03 [INFO] Métriques reçues de l'agent (CPU: 11.1%) +12:24:03 [INFO] 127.0.0.1 [26/Feb/2026:12:24:03 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:24:09 [INFO] Métriques reçues de l'agent (CPU: 10.7%) +12:24:09 [INFO] 127.0.0.1 [26/Feb/2026:12:24:09 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:24:15 [INFO] Métriques reçues de l'agent (CPU: 20.2%) +12:24:15 [INFO] 127.0.0.1 [26/Feb/2026:12:24:15 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:24:21 [INFO] Métriques reçues de l'agent (CPU: 15.6%) +12:24:21 [INFO] 127.0.0.1 [26/Feb/2026:12:24:21 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:24:27 [INFO] Métriques reçues de l'agent (CPU: 9.6%) +12:24:27 [INFO] 127.0.0.1 [26/Feb/2026:12:24:27 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:24:34 [INFO] Métriques reçues de l'agent (CPU: 5.1%) +12:24:34 [INFO] 127.0.0.1 [26/Feb/2026:12:24:34 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:24:40 [INFO] Métriques reçues de l'agent (CPU: 8.1%) +12:24:40 [INFO] 127.0.0.1 [26/Feb/2026:12:24:40 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:24:46 [INFO] Métriques reçues de l'agent (CPU: 3.0%) +12:24:46 [INFO] 127.0.0.1 [26/Feb/2026:12:24:46 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:24:52 [INFO] Métriques reçues de l'agent (CPU: 2.7%) +12:24:52 [INFO] 127.0.0.1 [26/Feb/2026:12:24:52 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:24:58 [INFO] Métriques reçues de l'agent (CPU: 3.0%) +12:24:58 [INFO] 127.0.0.1 [26/Feb/2026:12:24:58 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:25:05 [INFO] Métriques reçues de l'agent (CPU: 3.4%) +12:25:05 [INFO] 127.0.0.1 [26/Feb/2026:12:25:05 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:25:11 [INFO] Métriques reçues de l'agent (CPU: 6.4%) +12:25:11 [INFO] 127.0.0.1 [26/Feb/2026:12:25:11 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:25:17 [INFO] Métriques reçues de l'agent (CPU: 7.8%) +12:25:17 [INFO] 127.0.0.1 [26/Feb/2026:12:25:17 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:25:23 [INFO] Métriques reçues de l'agent (CPU: 19.9%) +12:25:23 [INFO] 127.0.0.1 [26/Feb/2026:12:25:23 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:25:29 [INFO] Métriques reçues de l'agent (CPU: 4.0%) +12:25:29 [INFO] 127.0.0.1 [26/Feb/2026:12:25:29 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:25:35 [INFO] Métriques reçues de l'agent (CPU: 6.9%) +12:25:35 [INFO] 127.0.0.1 [26/Feb/2026:12:25:35 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:25:41 [INFO] Métriques reçues de l'agent (CPU: 19.0%) +12:25:41 [INFO] 127.0.0.1 [26/Feb/2026:12:25:41 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" +12:25:47 [INFO] Métriques reçues de l'agent (CPU: 18.7%) +12:25:47 [INFO] 127.0.0.1 [26/Feb/2026:12:25:47 +0100] "POST /metrics HTTP/1.1" 200 154 "-" "Python/3.12 aiohttp/3.13.3" diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..3673f12 --- /dev/null +++ b/config.example.json @@ -0,0 +1,13 @@ +{ + "_comment": "Copier ce fichier en config.json et remplir les valeurs", + "discord_token": "TON_TOKEN_BOT_DISCORD_ICI", + "api_secret": "Mon-cul-est-tres-doux", + "api_port": 8765, + "guilds": { + "_comment": "Sera rempli automatiquement par la commande /setup dans chaque serveur Discord", + "_example": { + "channel_id": 123456789012345678, + "message_id": null + } + } +} \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..c165945 --- /dev/null +++ b/config.json @@ -0,0 +1,17 @@ +{ + "_comment": "Copier ce fichier en config.json et remplir les valeurs", + "discord_token": "MTA3NDcxNDMwODY2MDk2NTM5OA.Gj_JQf.V-Xd4DRW0cEOG6PYOBlnWAXFU5MPWlZWeHmgsk", + "api_secret": "Mon-cul-est-tres-doux", + "api_port": 8765, + "guilds": { + "_comment": "Sera rempli automatiquement par la commande /setup dans chaque serveur Discord", + "_example": { + "channel_id": 123456789012345678, + "message_id": null + }, + "1110604692263796746": { + "channel_id": 1227583375704064050, + "message_id": 1476543621137170625 + } + } +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4f2507e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +discord.py>=2.3.0 +aiohttp>=3.9.0 +psutil>=5.9.0 diff --git a/status_engine.py b/status_engine.py new file mode 100644 index 0000000..07b5401 --- /dev/null +++ b/status_engine.py @@ -0,0 +1,279 @@ +""" +status_engine.py — Analyse les métriques reçues de l'agent. +Détermine le statut global, la cause des ralentissements et une ETA. +""" + +from collections import deque +from dataclasses import dataclass, field +from typing import Optional +import time + +# ─── Constantes ─────────────────────────────────────────────────────────────── + +STATUS_OK = "ok" +STATUS_WARNING = "warning" +STATUS_CRITICAL = "critical" + +THRESHOLDS = { + "cpu": {"warning": 70, "critical": 85}, + "ram": {"warning": 80, "critical": 90}, + "swap": {"warning": 40, "critical": 70}, +} + +# Noms affichables pour chaque service +SERVICE_DISPLAY = { + "minecraft": "⛏️ Minecraft", + "hytale": "🎮 Hytale", + "satisfactory": "🏭 Satisfactory", + "valheim": "🪓 Valheim", + "terraria": "🌳 Terraria", + "ark": "🦕 ARK", + "ollama": "🤖 Ollama (IA)", + "discord_bots": "🤖 Bots Discord", +} + +HISTORY_MAXLEN = 24 # 24 × 15s = 6 minutes de mémoire glissante +PUSH_INTERVAL = 5 # secondes (doit correspondre à l'agent) + + +# ─── Data classes ───────────────────────────────────────────────────────────── + +@dataclass +class ServiceStatus: + key: str + name: str + running: bool + cpu: float = 0.0 + ram_mb: float = 0.0 + + @property + def ram_gb(self) -> float: + return round(self.ram_mb / 1024, 2) + + +@dataclass +class AnalysisResult: + status: str # ok / warning / critical + status_emoji: str + cpu: float + ram_pct: float + ram_used_gb: float + ram_total_gb: float + swap_pct: float + services: list[ServiceStatus] # triés par impact décroissant + cause_lines: list[str] # explication humaine + eta_text: str # estimation retour à la normale + load_avg: tuple[float, float, float] + cpu_temp: Optional[float] + + +# ─── Moteur ─────────────────────────────────────────────────────────────────── + +class StatusEngine: + + def __init__(self): + self._history: deque[dict] = deque(maxlen=HISTORY_MAXLEN) + self.last_metrics: Optional[dict] = None + self.seen_services: set[str] = set() + + # ── Ingest ──────────────────────────────────────────────────────────────── + + def push(self, metrics: dict): + """Enregistre un snapshot de métriques.""" + self._history.append(metrics) + self.last_metrics = metrics + + # On mémorise quels services ont été vus "actifs" au moins une fois + raw_services = metrics.get("services", {}) + for key, data in raw_services.items(): + if data.get("running"): + self.seen_services.add(key) + + # ── Analyse ─────────────────────────────────────────────────────────────── + + def analyse(self) -> Optional[AnalysisResult]: + if not self.last_metrics: + return None + + m = self.last_metrics + sys = m.get("system", {}) + + cpu = sys.get("cpu_percent", 0.0) + ram_pct = sys.get("ram_percent", 0.0) + ram_used = sys.get("ram_used_gb", 0.0) + ram_total = sys.get("ram_total_gb", 1.0) + swap_pct = sys.get("swap_percent", 0.0) + load = (sys.get("load_avg_1m", 0.0), + sys.get("load_avg_5m", 0.0), + sys.get("load_avg_15m", 0.0)) + cpu_temp = sys.get("cpu_c", None) + + status = self._compute_status(cpu, ram_pct, swap_pct) + + services = self._parse_services(m.get("services", {})) + causes = self._build_causes(cpu, ram_pct, ram_used, ram_total, swap_pct, services) + eta = self._estimate_eta(status) + + emoji_map = { + STATUS_OK: "🟢", + STATUS_WARNING: "🟡", + STATUS_CRITICAL: "🔴", + } + + return AnalysisResult( + status=status, + status_emoji=emoji_map[status], + cpu=cpu, + ram_pct=ram_pct, + ram_used_gb=ram_used, + ram_total_gb=ram_total, + swap_pct=swap_pct, + services=services, + cause_lines=causes, + eta_text=eta, + load_avg=load, + cpu_temp=cpu_temp, + ) + + # ── Méthodes internes ───────────────────────────────────────────────────── + + def _compute_status(self, cpu: float, ram: float, swap: float) -> str: + if (cpu >= THRESHOLDS["cpu"]["critical"] + or ram >= THRESHOLDS["ram"]["critical"] + or swap >= THRESHOLDS["swap"]["critical"]): + return STATUS_CRITICAL + if (cpu >= THRESHOLDS["cpu"]["warning"] + or ram >= THRESHOLDS["ram"]["warning"] + or swap >= THRESHOLDS["swap"]["warning"]): + return STATUS_WARNING + return STATUS_OK + + def _parse_services(self, raw: dict) -> list[ServiceStatus]: + result = [] + for key, data in raw.items(): + running = data.get("running", False) + + # On ignore les services qui n'ont jamais été vus actifs sur ce serveur + if not running and key not in self.seen_services: + continue + + svc = ServiceStatus( + key=key, + name=SERVICE_DISPLAY.get(key, key.capitalize()), + running=running, + cpu=round(data.get("cpu", 0.0), 1), + ram_mb=round(data.get("ram_mb", 0.0), 1), + ) + result.append(svc) + + # Tri : services actifs en premier, puis par impact (CPU + RAM normalisée) + result.sort(key=lambda s: ( + not s.running, + -(s.cpu + s.ram_mb / 100) + )) + return result + + def _build_causes( + self, + cpu: float, ram_pct: float, ram_used: float, ram_total: float, + swap_pct: float, services: list[ServiceStatus] + ) -> list[str]: + lines = [] + running = [s for s in services if s.running] + + # ── Problème CPU ── + if cpu >= THRESHOLDS["cpu"]["warning"]: + top = max(running, key=lambda s: s.cpu, default=None) + if top and top.cpu > 5: + lines.append( + f"CPU à **{cpu:.0f}%** — principal consommateur : " + f"**{top.name}** ({top.cpu:.0f}%)" + ) + else: + lines.append(f"CPU à **{cpu:.0f}%** — charge élevée non attribuée à un service connu") + + # ── Problème RAM ── + if ram_pct >= THRESHOLDS["ram"]["warning"]: + top = max(running, key=lambda s: s.ram_mb, default=None) + if top and top.ram_mb > 200: + lines.append( + f"RAM à **{ram_pct:.0f}%** ({ram_used:.1f}/{ram_total:.1f} GB) — " + f"**{top.name}** occupe {top.ram_gb:.1f} GB" + ) + else: + lines.append( + f"RAM à **{ram_pct:.0f}%** ({ram_used:.1f}/{ram_total:.1f} GB)" + ) + + # ── Swap ── + if swap_pct >= THRESHOLDS["swap"]["warning"]: + lines.append( + f"Swap à **{swap_pct:.0f}%** — le serveur manque de RAM " + f"et utilise le disque comme mémoire d'appoint (ralentissement fort)" + ) + + # ── Combinaison Ollama + jeux ── + ollama = next((s for s in running if s.key == "ollama"), None) + games = [s for s in running if s.key not in ("ollama", "discord_bots")] + if ollama and games and (cpu >= THRESHOLDS["cpu"]["critical"] or ram_pct >= THRESHOLDS["ram"]["critical"]): + game_names = ", ".join(s.name for s in games) + lines.append( + f"💡 Conflit potentiel : **{ollama.name}** (IA) et **{game_names}** " + f"semblent saturer les ressources" + ) + + return lines if lines else ["✅ Aucune anomalie détectée"] + + def _estimate_eta(self, current_status: str) -> str: + if current_status == STATUS_OK: + return "✅ Tout est normal" + + if len(self._history) < 4: + return "⏳ Collecte en cours, ETA disponible dans quelques mesures…" + + scores = [self._pressure_score(m) for m in self._history] + + # Tendance : on compare la moyenne des 4 premières mesures vs les 4 dernières + n = min(4, len(scores) // 2) + old_avg = sum(scores[:n]) / n + new_avg = sum(scores[-n:]) / n + + delta = old_avg - new_avg # positif = amélioration + + if delta > 0.08: + # Le serveur s'améliore : on extrapole + current = scores[-1] + if current <= 1.0: + return "⏱️ Retour à la normale **imminent**" + + rate_per_step = delta / max(len(scores) - n, 1) + if rate_per_step <= 0: + return "📊 Situation stable — ETA indéterminée" + + steps = (current - 1.0) / rate_per_step + secs = int(steps * PUSH_INTERVAL) + + if secs < 60: + return "⏱️ Retour à la normale dans **moins d'1 minute**" + elif secs < 3600: + return f"⏱️ Retour à la normale dans **~{secs // 60} min**" + else: + h, m2 = divmod(secs // 60, 60) + return f"⏱️ Retour à la normale dans **~{h}h{m2:02d}**" + + elif delta < -0.08: + return "⚠️ La charge **augmente** — ETA indéterminée, surveillez l'évolution" + else: + return "📊 Charge **stable** — ETA indéterminée (activité constante)" + + def _pressure_score(self, m: dict) -> float: + """Score de pression normalisé — 1.0 = seuil critique.""" + sys = m.get("system", {}) + cpu = sys.get("cpu_percent", 0) / THRESHOLDS["cpu"]["critical"] + ram = sys.get("ram_percent", 0) / THRESHOLDS["ram"]["critical"] + swap = sys.get("swap_percent", 0) / THRESHOLDS["swap"]["critical"] + return round(max(cpu, ram, swap), 3) + + @property + def has_data(self) -> bool: + return self.last_metrics is not None