V1.0 de GameurBot (Metrics surveillance des perfs)
This commit is contained in:
commit
cd99ed12eb
14 changed files with 1248 additions and 0 deletions
192
README.md
Normal file
192
README.md
Normal 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
|
||||||
|
```
|
||||||
BIN
__pycache__/status_engine.cpython-312.pyc
Normal file
BIN
__pycache__/status_engine.cpython-312.pyc
Normal file
Binary file not shown.
138
agent.py
Normal file
138
agent.py
Normal 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
0
agent_output.log
Normal file
14
agent_precision.log
Normal file
14
agent_precision.log
Normal 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
61
agent_verification.log
Normal 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
327
bot.py
Normal 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
28
bot_output.log
Normal 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
41
bot_precision.log
Normal 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
135
bot_verification.log
Normal 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
13
config.example.json
Normal 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
17
config.json
Normal 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
3
requirements.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
discord.py>=2.3.0
|
||||||
|
aiohttp>=3.9.0
|
||||||
|
psutil>=5.9.0
|
||||||
279
status_engine.py
Normal file
279
status_engine.py
Normal 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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue