V1.0 de GameurBot (Metrics surveillance des perfs)

This commit is contained in:
Mathis 2026-02-26 12:43:43 +01:00
commit cd99ed12eb
14 changed files with 1248 additions and 0 deletions

192
README.md Normal file
View file

@ -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
```

Binary file not shown.

138
agent.py Normal file
View file

@ -0,0 +1,138 @@
"""
agent.py Moniteur système à déployer sur le serveur de jeux.
Collecte CPU, RAM, swap + usage par service, et push vers le bot Discord.
Lancement : python agent.py
"""
import asyncio
import time
import aiohttp
import psutil
from datetime import datetime
# ─── Configuration ────────────────────────────────────────────────────────────
BOT_API_URL = "http://localhost:8765/metrics" # URL du bot (même machine ou réseau local)
API_SECRET = "Mon-cul-est-tres-doux" # Doit correspondre à config.json
PUSH_INTERVAL = 5 # secondes entre chaque envoi
# Mots-clés pour identifier chaque service dans la liste des processus
# Clé = identifiant interne, valeurs = sous-chaînes à chercher dans le nom / cmdline
SERVICE_KEYWORDS: dict[str, list[str]] = {
"minecraft": ["minecraft", "paper", "spigot", "purpur", "fabric", "forge"],
"hytale": ["hytale"],
"satisfactory": ["factoryserver", "satisfactory"],
"valheim": ["valheim_server", "valheim"],
"terraria": ["terraria"],
"ark": ["shootergame", "ark"],
"ollama": ["ollama"],
"discord_bots": ["discord_bot", "bot.py", "main.py"], # adapter si besoin
}
# ─── Collecte ─────────────────────────────────────────────────────────────────
def scan_services() -> 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())

0
agent_output.log Normal file
View file

14
agent_precision.log Normal file
View file

@ -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% → ✓

61
agent_verification.log Normal file
View file

@ -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% → ✓

327
bot.py Normal file
View file

@ -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()

28
bot_output.log Normal file
View file

@ -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"

41
bot_precision.log Normal file
View file

@ -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"

135
bot_verification.log Normal file
View file

@ -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"

13
config.example.json Normal file
View file

@ -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
}
}
}

17
config.json Normal file
View file

@ -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
}
}
}

3
requirements.txt Normal file
View file

@ -0,0 +1,3 @@
discord.py>=2.3.0
aiohttp>=3.9.0
psutil>=5.9.0

279
status_engine.py Normal file
View file

@ -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