Création d'un bot Discord de metrics
Cet article décrit la façon dont j'ai créé un bot discord de suivi de l'état de mes serveurs avec une alerte si le serveur ou un conteneur docker rencontre un problème. On va faire le tout en python et tout le code est disponible sur mon github repo.
J'ai également créé un one-liner pour lancer directement le bot (installer docker au préalable sur Windows) :
- Linux/macOS :
curl -fsSL <https://raw.githubusercontent.com/HellKaiser45/Soracord/main/install.sh> | bash - Windows :
iex (iwr -useb <https://raw.githubusercontent.com/HellKaiser45/Soracord/main/install.ps1>)
Vous pourrez suivre ici mon cheminement et comment je l'ai codé avec des explications sur mes choix et le code.
Sommaire
- L'idée de base et la structure qui en découle
- Le code
- Déploiement - Docker
- One liner - Linux/MacOS et Windows
- Conclusion
L'idée de base et la structure qui en découle
La première chose c'est de comprendre ce qui me pousse à faire ce projet. En effet, j'ai une liste de projets à faire mais j'ai choisi de commencer par celui-ci. Premièrement, j'héberge mes sites et mes projets sur des serveurs VPS, sur Hetzner. De plus, j'utilise des conteneurs (docker) pour la grande majorité de mes applications. Ensuite, il est nécessaire au fur et à mesure que mes projets évoluent, de garder un certain contrôle et une supervision sur mes serveurs. Cependant, les solutions existantes comme Grafana et Prometheus bien que très bien, ne sont tout simplement pas adaptées à mon utilisation simple. Je veux simplement des métriques simples et accessibles pour mes serveurs. Pas besoin d'outils lourds et complexes dans mon cas. Je mets en priorité l'accent sur la simplicité d'accès et de lecture des relevés.
C'est pourquoi j'ai décidé de faire mon propre outil de monitoring pour mes serveurs avec en interface un chat Discord car c'est une application accessible que j'utilise quotidiennement et qui est donc tout le temps accessible sur PC ou smartphone.
Maintenant, qu'est-ce qui m'intéresse de récupérer comme mesures ? Des mesures du support physique hardware du serveur comme l'utilisation du CPU, mémoire, disque, réseau, etc. Ces mesures peuvent me donner une idée initiale afin de savoir s'il y a besoin d'augmenter la puissance de mon serveur (scaling vertical) Ensuite, comme mes applications sont conteneurisées, il faut que je sache si mes conteneurs fonctionnent et pouvoir récupérer les métriques associées à chaque conteneur pour un potentiel scaling horizontal de conteneurs spécifiques. De plus, il est aussi intéressant de pouvoir avoir accès aux logs des conteneurs pour potentiellement comprendre rapidement une erreur sur un conteneur.
Et je pense que pour un MVP c'est suffisant. En tout cas c'est suffisant pour mon utilisation. Mais on va voir en fin d'article qu'il y a des possibilités d'amélioration plutôt impressionnantes.
Donc on va chercher à récupérer :
- l'état du serveur
- Une vue d'ensemble des conteneurs avec leur état
- Un détail par conteneur de la consommation en ressources
- les derniers logs des conteneurs
Le code
Pour le langage choisi, j'ai choisi Python mais dans ce cas c'est équivalent à du JavaScript/TypeScript en termes de simplicité et de performances.
hardware - Psutil
Dans un premier temps il faut faire le coeur du projet, c'est-à-dire récupérer les métriques de mon serveur. Pour cela, la bibliothèque psutil est très utile et permet de récupérer les métriques de mon serveur simplement. La première chose à faire c'est donc de parcourir la documentation de psutil pour voir ce qu'on peut récupérer et comment le faire. C'est très simple et vraiment puissant donc on peut vraiment récupérer ce qu'on veut. Une fois que l'on est confiant sur les possibilités de la bibliothèque, ce que j'aime faire c'est définir clairement les métriques que je veux récupérer. Pour cela, je les définis dans des dataclasses avec les métriques à récupérer. On va donc "remplir" les dataclasses grâce aux fonctions et classes de psutil.
@dataclass
class CPUMetrics:
usage_percent: float
per_core_usage: list[float]
load_avg_15min: float
@dataclass
class RAMMetrics:
total: float
used: float
free: float
@dataclass
class DiskMetrics:
total: float
used: float
free: float
Voici les dataclasses que j'ai créées pour les mesures de mon serveur. Alors bien sûr, on peut avoir beaucoup plus de métriques mais j'ai sélectionné cela car c'est ce qui m'intéresse le plus et il ne faut pas trop encombrer le discord et de toute façon, comme on va le voir, l'espace est limité en nombre de caractères. On ne va pas voir le code en détail, il est disponible sur mon GitHub si vous voulez voir le code complet. Mais globalement, pour cette partie j'ai fait une fonction par métrique que je vais récupérer. Avec le recul, je devrai tout mettre dans une classe afin de limiter les imports et par la suite potentiellement augmenter le nombre de méthodes.
Docker - docker SDK
Docker en fonctionnement sur votre système déploie une API REST - Docker Engine API. Cette API permet globalement de tout faire ce que Docker est capable via le CLI Docker. Donc récupérer des métriques, des logs, démarrer un conteneur, stopper un conteneur, etc... Et pour faciliter la création d'applications interagissant avec cette API, ils ont créé des SDK dans différents langages. Cette SDK enveloppe les communications avec l'API Docker et nous propose des classes et fonctions simples pour interagir indirectement avec l'API Docker. Donc c'est naturellement ce que j'ai choisi d'utiliser pour cette partie.
Comme je veux garder un code propre et clair je décide de séparer les fichiers afin de n'avoir qu'un fichier par utilisation de bibliothèque majeure. Donc j'avais un fichier pour la partie hardware avec psutil et je fais un autre fichier pour la partie docker avec Docker SDK Python.
Je suis simplement la documentation pour démarrer et commence à tester plusieurs fonctions de la SDK pour voir les possibilités et le type de retour. Comme précédemment, je vais utiliser les dataclasses pour définir les informations que je souhaite récupérer. Et après plusieurs tests et itérations j'arrive à deux dataclasses, une pour une vue d'ensemble des conteneurs et une pour un détail des métriques d'un conteneur.
@dataclass
class ContainerStatus:
name: str
status: str
health: str
@dataclass(frozen=True)
class IOUsage:
net_rx_gb: float
net_tx_gb: float
disk_read_mb: float
disk_write_mb: float
@dataclass(frozen=True)
class ContainerMetrics:
name: str
cpu_pct: float
mem_mb: float
mem_pct: float
io: IOUsage
Alors normalement en Python quand on cherche à communiquer avec une API (pas qu'en Python d'ailleurs), on valide la réponse de l'API avec un type de retour attendu. Pour cela on se sert de la documentation pour voir le ou les types de retour attendu. Et on fait des classes Pydantic pour valider les réponses contre les types attendus. Donc ce serait la version propre à faire, mais c'est aussi assez lourd. Et dans le cadre d'un petit projet comme celui-ci, ça n'avait pas vraiment de sens. C'est pourquoi je n'ai pas validé les réponses de l'API et simplement utilisé la forme suivante pour extraire les informations désirées des réponses.
container = self._client.containers.get(name)
raw = container.stats(stream=False)
assert isinstance(raw, dict), "stats() returned non-dict"
cpu_stats = raw["cpu_stats"]
precpu = raw["precpu_stats"]
cpu_delta = (
cpu_stats["cpu_usage"]["total_usage"] - precpu["cpu_usage"]["total_usage"]
)
try:
system_delta = cpu_stats["system_cpu_usage"] - precpu["system_cpu_usage"]
except Exception:
system_delta = 0.0
C'est un extrait simple de l'extraction des métriques. Ce qui va éviter une validation lourde et plus compliquée au détriment d'un code potentiellement moins robuste. Une fois que l'on a les fonctions qui permettent de remplir les dataclasses et qu'on a aussi une fonction pour récupérer les logs, on peut tout regrouper dans une classe, puis il sera temps de passer à la partie communication avec Discord.
Discord bot - discord.py
Comme pour Docker, Discord dispose d'une API HTTPS/REST qui permet aux développeurs de créer des applications / bots autour de Discord. Par contre Discord ne dispose pas d'un wrapper officiel comme pour Docker. Cependant, un wrapper non officiel fait par la communauté existe et permet une certaine abstraction de complexité. Ce wrapper est la bibliothèque discord.py. Cette bibliothèque dispose en plus d'extensions qui permettent encore plus simplement d'utiliser et gérer les fonctionnalités les plus courantes de Discord.
Mais avant de commencer à coder, il faut obtenir un token d'API Discord, créer le bot et l'ajouter à notre serveur Discord. Pour cela, je vous conseille de suivre le tutoriel https://discordpy.readthedocs.io/en/stable/discord.html. Mais attention, contrairement au tutoriel il ne faut pas mettre le bot public puisque vous ne voulez pas que quelqu'un d'autre ait accès aux métriques de vos serveurs. Continuez le guide et pour les permissions il faut activer Message Content Intent
Voici deux captures d'écran pour vous donner les accès à accorder au bot.
![]() | ![]() |
|---|
Une fois le token obtenu, il faut le référencer dans le code. Mais il ne faut pas l'écrire en dur dans le code. C'est pourquoi j'ai fait un fichier .env qui contient toutes les variables d'environnement nécessaires pour le bot incluant le token Discord.
Puis dans le code je le récupère proprement avec :
load_dotenv()
Discord_Token = os.getenv("BOT_TOKEN")
On va quand même avoir besoin d'autres variables qui ne sont pas nécessairement dans des variables d'environnement mais plutôt dans un fichier de configuration.
Mais par souci de simplicité je vais utiliser le fichier .env aussi comme fichier de configuration.
Donc nous allons utiliser l'extension discord.py pour les commandes qui va nous permettre de créer des commandes simplement avec des décorateurs Python. Par exemple :
@bot.command()
async def health(
ctx,
server: str | None = commands.parameter(
default=None, description=": The name of the server to check health"
),
):
"""General VPS health overview."""
c, r, d = cpu(), ram(), disk()
embed = discord.Embed(
title=f"🖥️ Server {Server_Name}: Health Report",
color=discord.Color.blue(),
timestamp=discord.utils.utcnow(),
)
embed.add_field(
name="CPU Load", value=create_progress_bar(c.usage_percent), inline=False
)
embed.add_field(
name="RAM Usage", value=f"💾 `{r.used:.1f} / {r.total:.1f} GB`", inline=True
)
embed.add_field(
name="Disk Space", value=f"📂 `{d.used:.1f} / {d.total:.1f} GB`", inline=True
)
if not server or server == Server_Name:
await ctx.send(embed=embed)
else:
pass
Ici, nous indiquons que la fonction sera utilisée comme une commande health. cpu(), ram() et disk() sont les fonctions importées depuis notre fichier psutil et sont respectivement les fonctions qui retournent les dataclasses sur les métriques du CPU, de la RAM et du disque.
Puis on fait un embed qui sera une sorte de petite carte mais voici à quoi cette commande ressemblera.

De la même façon on peut faire les commandes pour afficher les conteneurs et les logs.
Commande docker | Commande container | Commande logs |
|---|---|---|
![]() | ![]() | ![]() |
Ok, maintenant on a un MVP fonctionnel mais j'aimerais en plus ajouter une tâche de fond pour surveiller en permanence les conteneurs et me faire une alerte avec un rapport en cas de problème.
Pour cela j'ai créé une classe qui prend la logique qui vérifie s'il y a besoin de pousser l'alerte et qui construit l'embed de l'alerte.
Voici un extrait :
@dataclass
class Alert:
channel_id: int = int(os.getenv("CHANNEL_ID") or 0)
async def send_alert(self, embed: discord.Embed):
channel = bot.get_channel(self.channel_id)
await channel.send(embed=embed)
async def monitor(self, monitor: DockerMonitor = DockerMonitor()):
...
Maintenant il faut lancer la fonction monitor au lancement du bot et choisir la fréquence de scan.
C'est très simple, on va simplement utiliser le système d'événement avec le décorateur afin de déclencher une boucle au lancement du bot.
async def monitoring_loop(AlertManager: Alert = Alert(), check_frequency: int = 3600):
await bot.wait_until_ready()
while not bot.is_closed():
await AlertManager.monitor()
await asyncio.sleep(check_frequency)
@bot.event
async def on_ready():
print(f"🚀 {bot.user} is online and monitoring Docker.")
bot.loop.create_task(monitoring_loop())
Dans le cas où un problème survient, le bot va envoyer un message sur le channel défini dans le fichier .env.

Ce que j'ai fait ici c'est de créer une boucle et simplement de la lancer en arrière-plan au lancement du bot. Mais il y a plein de façons de faire cela. Je vous conseille de regarder la documentation de discord.py pour plus d'informations. Avec le recul, je n'ai pas choisi le moyen le plus simple et propre. En effet, après coup, je me suis rendu compte que je pouvais de la même façon que le décorateur commande, je pouvais aussi utiliser le décorateur task pour probablement tout faire en une seule fonction. De plus, il faudrait que je regroupe le tout dans une seule classe. Pour l'instant ça n'a pas d'importance mais par la suite je peux envisager plusieurs bots comme sur WhatsApp ou autres. Donc tout inclure dans une classe nous permettra d'adopter un Gateway Pattern qui permettra de basculer de bot ou d'activer différents bots.
Déploiement - Docker
Avant cette étape, le bot est déjà prêt à être utilisé et installé sur un serveur avec les commandes suivantes :
- Cloner le dépôt :
git clone https://github.com/HellKaiser45/Soracord.git - Créer un environnement Python :
python -m venv venv - Installer les dépendances Python :
pip install -r requirements.txt - Lancer le bot :
python bot_server.py
Dans ce cas le bot fonctionnera bien. Et c'est une façon de faire. Mais je veux bien sûr aussi laisser la possibilité à l'utilisateur de déployer le bot dans un conteneur Docker. Pour cela il va nous falloir au minimum un Dockerfile. Rien de plus simple il faut simplement répliquer les étapes précédentes dans le Dockerfile.
FROM python:3.13.11-alpine3.23
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
CMD ["python", "bot_server.py"]
Donc en seulement cinq lignes c'est fait pour ça mais il y a un problème. Comme c'est un conteneur, il n'a pas directement accès à l'API Docker de notre serveur. On doit monter la socket Docker sur le conteneur via un volume.
On peut faire cela directement dans le Dockerfile mais je préfère ajouter un fichier Compose qui est plus adapté à la tâche et pourra nous servir par la suite si on veut faire évoluer le bot.
services:
soracord:
build: .
container_name: soracord-bot
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
On voit ici que ce volume est monté sur le conteneur en lecture seule (:ro), car on n'a pas de commande qui vont écrire sur la socket Docker. Donc on limite les accès du bot au minimum dans ce cas à la lecture.
Par la suite on pourra ajouter des commandes peut-être de contrôle des conteneurs et il faudra au moins laisser un plus gros accès. Ou utiliser un conteneur de proxy pour gérer finement les accès.
Maintenant on a donc deux solutions pour déployer le bot, la première déjà présentée et la seconde avec Docker Compose.
- Cloner le dépôt :
git clone https://github.com/HellKaiser45/Soracord.git - Dans le répertoire du bot faire
docker compose up -d --build(Requiert Docker Compose)
One liner - Linux/MacOS et Windows
J'ai également créé un one-liner pour lancer directement depuis le terminal un petit script de configuration qui permettra de s'assurer que Docker est installé, cloner le dépôt et lancer le bot via Docker Compose.
Le one-liner pour Linux/macOS :
curl -fsSL https://raw.githubusercontent.com/HellKaiser45/Soracord/main/install.sh | bash
Le one-liner pour Windows :
iex (iwr -useb https://raw.githubusercontent.com/HellKaiser45/Soracord/main/install.ps1)
Conclusion
Ce projet est assez simple mais super utile pour moi qui veut garder les choses propres et simples. Pour l'instant c'est un MVP pour moi qui me convient parfaitement. Cependant, avec ce projet j'ai l'impression que les possibilités sont sans limite. Par exemple on peut imaginer une sélection de support pour les alertes/commandes comme ajouter un choix par exemple WhatsApp, Telegram, Slack ou autres. De plus, on peut ajouter des commandes ou encore un log des diagnostics persistant. On peut même envisager d'ajouter un moyen de plug-in un agent IA pour analyser les serveurs et logs et faire les rapports. Ou encore laisser la possibilité de faire des commandes plus ouvertes ou même de gérer les conteneurs directement depuis le bot. Ces possibilités me donneront sûrement envie de travailler et de faire évoluer ce projet de temps en temps sur le long terme ce qui est une preuve, à mon avis, que c'est un très bon projet.




