Stack Observabilité - edu-kit
Table des matières
Introduction, contexte, définitions et objectifs
La stack d’observabilité d’edu-kit permet de superviser l’ensemble de l’infrastructure : VMs, containers Docker, cluster k3s, firewalls MikroTik et hyperviseurs Proxmox.
Elle repose sur le modèle LGTM (Loki · Grafana · Tempo · Mimir/Prometheus), porté par l’écosystème Grafana :
| Composant | Rôle |
|---|---|
| Prometheus | Collecte et stockage des métriques (séries temporelles) |
| Loki | Agrégation et stockage des logs |
| Tempo | Stockage des traces distribuées (OpenTelemetry) |
| Grafana | Visualisation, dashboards, alerting |
| Grafana Alloy | Agent de collecte universel déployé sur chaque VM (métriques + logs) |
| cAdvisor | Métriques des containers Docker (CPU, RAM, réseau, I/O) |
| mktxp | Exporter Prometheus pour les firewalls MikroTik (RouterOS) |
| pve_exporter | Exporter Prometheus pour les hyperviseurs Proxmox |
Tous les services de la stack tournent en containers Docker (Compose) dans le segment réseau dédié obsnet (192.168.50.0/24), isolé du reste de l’infrastructure.
Prérequis
Environnement
- OS : Debian 12 (Bookworm) sur toutes les VMs obsnet
- Docker : version 24+ avec le plugin
docker-compose-plugin - Réseau : segment obsnet (192.168.50.0/24), accès inter-VMs autorisé
- Firewall : règles MikroTik configurées (voir section Règles firewall)
VMs obsnet
| VM | IP | Services |
|---|---|---|
| grafana | 192.168.50.5 | Grafana, cAdvisor, Alloy |
| prometheus | 192.168.50.10 | Prometheus, mktxp, pve_exporter, cAdvisor, Alloy |
| loki | 192.168.50.15 | Loki, cAdvisor, Alloy |
| tempo | 192.168.50.20 | Tempo, cAdvisor, Alloy |
| uptime-kuma | 192.168.50.25 | Uptime Kuma, Alloy |
VMs avec Grafana Alloy (agents)
Alloy est installé sur 15 VMs (toutes sauf haproxy-02, suricata, netbird, docusaurus) :
- config-base.alloy : métriques système + logs systemd (VMs sans Docker)
- config-docker.alloy : base + cAdvisor + logs Docker (VMs obsnet avec Docker)
- config-prometheus-vm.alloy : config Docker + scrape mktxp + scrape pve_exporter (VM prometheus uniquement)
Installation et configuration
Prometheus
Prometheus est déployé sur la VM prometheus (192.168.50.10) dans /opt/docker/prometheus/.
Il reçoit les métriques en remote write depuis les agents Alloy (push) et scrape également mktxp et pve_exporter (pull).
mkdir -p /opt/docker/prometheus
cd /opt/docker/prometheus
# Copier prometheus.yml et docker-compose.yml
docker compose up -d
docker-compose.yml :
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: unless-stopped
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--storage.tsdb.retention.time=30d"
- "--web.enable-lifecycle"
- "--web.enable-remote-write-receiver" # nécessaire pour que les agents Alloy puissent pousser leurs métriques
volumes:
prometheus_data:
prometheus.yml (configuration de base) :
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: prometheus
static_configs:
- targets: ["localhost:9090"]
Les autres jobs de scrape (mktxp, pve) sont gérés directement par Alloy sur cette VM via config-prometheus-vm.alloy.
Vérification :
docker compose ps # le container doit être "Up"
curl http://localhost:9090 # l'interface web doit répondre
Loki
Loki est déployé sur la VM loki (192.168.50.15) dans /opt/docker/loki/.
Il reçoit les logs en push depuis les agents Alloy.
mkdir -p /opt/docker/loki
cd /opt/docker/loki
docker compose up -d
docker-compose.yml :
services:
loki:
image: grafana/loki:latest
container_name: loki
restart: unless-stopped
ports:
- "3100:3100"
volumes:
- ./loki.yml:/etc/loki/loki.yml:ro
- loki_data:/loki
command: -config.file=/etc/loki/loki.yml
volumes:
loki_data:
loki.yml :
auth_enabled: false
server:
http_listen_port: 3100
grpc_listen_port: 9096
common:
instance_addr: 127.0.0.1
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemory
query_range:
results_cache:
cache:
embedded_cache:
enabled: true
max_size_mb: 100
schema_config:
configs:
- from: 2020-10-24
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
limits_config:
retention_period: 30d # conservation des logs sur 30 jours
compactor:
working_directory: /loki/compactor
retention_enabled: true
delete_request_store: filesystem
Vérification :
curl http://localhost:3100/ready # doit répondre "ready"
Tempo
Tempo est déployé sur la VM tempo (192.168.50.20) dans /opt/docker/tempo/.
Il reçoit les traces distribuées via le protocole OTLP (OpenTelemetry Protocol).
mkdir -p /opt/docker/tempo
cd /opt/docker/tempo
docker compose up -d
docker-compose.yml :
services:
tempo:
image: grafana/tempo:latest
container_name: tempo
restart: unless-stopped
ports:
- "3200:3200"
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
volumes:
- ./tempo.yml:/etc/tempo/tempo.yml:ro
- tempo_data:/var/tempo
command: -config.file=/etc/tempo/tempo.yml
volumes:
tempo_data:
tempo.yml :
server:
http_listen_port: 3200
distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
storage:
trace:
backend: local
local:
path: /var/tempo/blocks
wal:
path: /var/tempo/wal
Vérification :
curl http://localhost:3200/ready # doit répondre "ready"
Grafana
Grafana est déployé sur la VM grafana (192.168.50.5) dans /opt/docker/grafana/.
L’interface est exposée en HTTPS via NPM (Nginx Proxy Manager) à l’adresse https://grafana.int.edu-kit.fr.
mkdir -p /opt/docker/grafana/provisioning
cd /opt/docker/grafana
# Copier docker-compose.yml, .env, et le dossier provisioning/
docker compose up -d
docker-compose.yml :
services:
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: unless-stopped
ports:
- "3000:3000"
env_file:
- .env
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
- GF_SERVER_ROOT_URL=https://grafana.int.edu-kit.fr
- GF_SMTP_ENABLED=${GF_SMTP_ENABLED:-false}
- GF_SMTP_HOST=${GF_SMTP_HOST:-localhost:25}
- GF_SMTP_USER=${GF_SMTP_USER:-}
- GF_SMTP_PASSWORD=${GF_SMTP_PASSWORD:-}
- GF_SMTP_FROM_ADDRESS=${GF_SMTP_FROM_ADDRESS:-grafana@edu-kit.fr}
volumes:
- grafana_data:/var/lib/grafana
- ./provisioning:/etc/grafana/provisioning:ro # dashboards, datasources, alerting
volumes:
grafana_data:
Fichier .env (à créer depuis .env.example, ne jamais committer le .env réel) :
GRAFANA_ADMIN_PASSWORD=changeme
# Teams (Power Automate webhook)
TEAMS_WEBHOOK_URL=https://your-power-automate-url
# SMTP (optionnel)
GF_SMTP_ENABLED=false
GF_SMTP_HOST=smtp.example.com:587
GF_SMTP_USER=
GF_SMTP_PASSWORD=
GF_SMTP_FROM_ADDRESS=grafana@edu-kit.fr
Datasources (provisioning automatique)
Le fichier provisioning/datasources/datasources.yml déclare les trois sources de données :
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
uid: prometheus
url: http://192.168.50.10:9090
isDefault: true
jsonData:
timeInterval: 15s
- name: Loki
type: loki
uid: loki
url: http://192.168.50.15:3100
- name: Tempo
type: tempo
uid: tempo
url: http://192.168.50.20:3200
jsonData:
tracesToLogsV2:
datasourceUid: loki
filterByTraceID: true
lokiSearch:
datasourceUid: loki
serviceMap:
datasourceUid: prometheus
Dashboards importés
| Dashboard | Description |
|---|---|
| infra-overview | Vue globale CPU/RAM/Disque/Logs |
| network-traffic | Trafic réseau IN/OUT par VM |
| disk-io | Lecture/écriture disque, IOPS, latence |
| system-load | Load average, processus, uptime |
| security-logs | Erreurs auth, logs filtrés |
| logs-explorer | Exploration logs systemd + Docker |
| traces-explorer | Traces Tempo (actif quand apps instrumentées) |
| k3s-cluster | Métriques nœuds k3s |
| docker-containers | CPU/RAM/réseau par container + logs |
| mikrotik-chr (ID 13679) | Métriques firewalls MikroTik |
Vérification :
curl http://localhost:3000 # l'interface web doit répondre
Grafana Alloy
Alloy est l’agent de collecte universel installé sur chaque VM. Il remplace Prometheus Node Exporter + Promtail en un seul binaire.
Installation
Copier le fichier de config correspondant à la VM en config.alloy, puis exécuter install-alloy.sh en root :
# Depuis la machine locale, copier les fichiers nécessaires
scp config-base.alloy user@<ip-vm>:~/config.alloy
scp install-alloy.sh user@<ip-vm>:~/
# Sur la VM
sudo bash install-alloy.sh
Le script réalise les opérations suivantes :
- Ajoute le dépôt APT officiel de Grafana
- Installe le paquet
alloy - Copie
config.alloyvers/etc/alloy/config.alloy - Active et démarre le service systemd
alloy
Attention CRLF : si le script a été édité sous Windows, il peut contenir des retours chariot
\rqui provoquent une erreur à la ligne 9. Corriger avecsed -i 's/\r//' install-alloy.shavant exécution.
Sur les VMs obsnet (grafana, loki, tempo, uptime-kuma, prometheus) : ajouter l’utilisateur alloy au groupe docker pour permettre la lecture du socket Docker :
sudo usermod -aG docker alloy
sudo systemctl restart alloy
Configuration - config-base.alloy (VMs sans Docker)
Collecte les métriques système via le node exporter intégré et les logs systemd via journald.
// Métriques système -> Prometheus
prometheus.exporter.unix "node" {}
prometheus.scrape "node" {
targets = prometheus.exporter.unix.node.targets
forward_to = [prometheus.remote_write.default.receiver]
job_name = "node"
}
prometheus.remote_write "default" {
endpoint {
url = "http://192.168.50.10:9090/api/v1/write"
}
external_labels = {
host = constants.hostname,
}
}
// Logs systemd -> Loki
loki.relabel "journal" {
forward_to = []
rule {
source_labels = ["__journal__systemd_unit"]
target_label = "unit"
}
rule {
source_labels = ["__journal__priority_keyword"]
target_label = "level"
}
}
loki.source.journal "default" {
forward_to = [loki.write.default.receiver]
relabel_rules = loki.relabel.journal.rules
labels = {
job = "systemd-journal",
host = constants.hostname,
}
}
loki.write "default" {
endpoint {
url = "http://192.168.50.15:3100/loki/api/v1/push"
}
}
Configuration - config-docker.alloy (VMs avec Docker)
Étend la config de base avec le scrape cAdvisor et la collecte des logs Docker.
// ... (même config que config-base.alloy)
// Métriques containers Docker via cAdvisor
prometheus.scrape "cadvisor" {
targets = [{
__address__ = "localhost:8080",
host = constants.hostname,
}]
forward_to = [prometheus.remote_write.default.receiver]
job_name = "cadvisor"
}
// Logs Docker via socket
discovery.docker "containers" {
host = "unix:///var/run/docker.sock"
}
loki.source.docker "containers" {
host = "unix:///var/run/docker.sock"
targets = discovery.docker.containers.targets
forward_to = [loki.write.default.receiver]
labels = {
job = "docker",
host = constants.hostname,
}
}
Configuration - config-prometheus-vm.alloy (VM prometheus uniquement)
Étend config-docker.alloy avec le scrape de mktxp et pve_exporter.
// ... (même config que config-docker.alloy)
// Métriques MikroTik via mktxp
prometheus.scrape "mktxp" {
targets = [{
__address__ = "localhost:49090",
}]
forward_to = [prometheus.remote_write.default.receiver]
job_name = "mktxp"
scrape_interval = "60s"
}
// Métriques Proxmox VE via pve_exporter (multi-target)
prometheus.scrape "pve" {
targets = [
{ __address__ = "localhost:9221", __param_target = "10.10.40.1", __param_module = "default", instance = "pve1" },
{ __address__ = "localhost:9221", __param_target = "10.10.40.2", __param_module = "default", instance = "pve2" },
{ __address__ = "localhost:9221", __param_target = "10.10.40.3", __param_module = "default", instance = "pve3" },
]
forward_to = [prometheus.remote_write.default.receiver]
job_name = "pve"
metrics_path = "/pve"
scrape_interval = "60s"
}
Vérification Alloy :
systemctl status alloy # le service doit être "active (running)"
curl http://localhost:12345 # interface de debug Alloy (disponible en local)
journalctl -u alloy -f # logs en temps réel
cAdvisor
cAdvisor expose les métriques des containers Docker en cours d’exécution. Il est déployé sur les 5 VMs obsnet.
mkdir -p /opt/docker/cadvisor
cd /opt/docker/cadvisor
docker compose up -d
docker-compose.yml :
services:
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
container_name: cadvisor
restart: unless-stopped
privileged: true # nécessaire pour accéder aux métriques kernel
ports:
- "8080:8080"
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker:/var/lib/docker:ro
- /dev/disk:/dev/disk:ro
cAdvisor est ensuite scrapé par Alloy en local (localhost:8080) via config-docker.alloy.
Vérification :
curl http://localhost:8080/metrics | head # doit retourner des métriques Prometheus
mktxp - Monitoring MikroTik
mktxp est un exporter Prometheus qui interroge les firewalls MikroTik via l’API RouterOS. Il est déployé sur la VM prometheus (192.168.50.10) dans /opt/docker/mktxp/.
Prérequis côté MikroTik
Créer un utilisateur dédié prometheus avec les droits minimaux sur les deux firewalls (fw-01 et fw-02) :
/user/group/add name=prometheus policy=api,read
/user/add name=prometheus password=CHANGE_ME group=prometheus
/ip/service/set api disabled=no
Déploiement
mkdir -p /opt/docker/mktxp
cd /opt/docker/mktxp
# Copier docker-compose.yml, mktxp.conf, _mktxp.conf
docker compose up -d
docker-compose.yml :
services:
mktxp:
image: ghcr.io/akpw/mktxp:latest
container_name: mktxp
restart: unless-stopped
ports:
- "49090:49090"
volumes:
- ./_mktxp.conf:/home/mktxp/mktxp/_mktxp.conf # config globale (sans :ro - mktxp tente de la mettre à jour)
- ./mktxp.conf:/home/mktxp/mktxp/mktxp.conf # config des routeurs (sans :ro)
_mktxp.conf (paramètres globaux) :
[MKTXP]
listen = '0.0.0.0:49090' # doit inclure le port (sans port = waitress ne bind pas)
socket_timeout = 30 # augmenté car les CHR sont lents (défaut 10 insuffisant)
fetch_routers_in_parallel = False # séquentiel plus stable sur CHR
persistent_router_connection_pool = True # réutilise la connexion API entre scrapes
mktxp.conf (déclaration des routeurs) :
[fw-01]
hostname = 192.168.50.1
[fw-02]
hostname = 192.168.50.2
[default]
enabled = True
port = 8728
username = prometheus
password = CHANGE_ME
plaintext_login = True # obligatoire sur RouterOS 7.x (MD5 challenge non supporté)
# Collectors désactivés (timeout sur CHR RouterOS 7.x)
interface = False
firewall = False
connections = False
connection_stats = False
monitor = False
neighbor = False
ipv6_firewall = False
ipv6_neighbor = False
health = False # pas de capteurs physiques sur CHR
routing_stats = False # non supporté sur 7.x
# Collectors actifs
system = True
route = True
dns = True
user = True
certificate = True
pool = True
public_ip = True
installed_packages = True
Vérification :
curl http://localhost:49090/metrics | grep mktxp # doit retourner des métriques
docker logs mktxp # vérifier l'absence d'erreurs de connexion
pve_exporter - Monitoring Proxmox
pve_exporter expose les métriques des hyperviseurs Proxmox via l’API PVE. Il est déployé sur la VM prometheus dans /opt/docker/proxmox/.
Prérequis côté Proxmox
Créer un utilisateur et un token API sur le cluster Proxmox :
# Dans l'interface web Proxmox ou en CLI sur un nœud
pveum user add prometheus@pve
pveum aclmod / -user prometheus@pve -role PVEAuditor
pveum user token add prometheus@pve monitoring
Le token généré est à reporter dans pve.yml.
Réseau pvenet
Les hyperviseurs sont joignables depuis la VM prometheus via le VLAN 40 (pvenet, 10.10.40.0/24) :
- pve1 : 10.10.40.1
- pve2 : 10.10.40.2
- pve3 : 10.10.40.3
- VRRP VIP (fw-01) : 10.10.40.252
La route est configurée sur chaque nœud Proxmox dans l’interface vmbr1 :
# Dans /etc/network/interfaces sur chaque nœud pve
post-up ip route add 192.168.50.0/24 via 10.10.40.252
Déploiement
mkdir -p /opt/docker/proxmox
cd /opt/docker/proxmox
docker compose up -d
docker-compose.yml :
services:
pve-exporter:
image: prompve/prometheus-pve-exporter:latest
container_name: pve-exporter
restart: unless-stopped
ports:
- "9221:9221"
volumes:
- ./pve.yml:/etc/pve.yml:ro
command:
- "--config.file=/etc/pve.yml"
pve.yml :
default:
user: prometheus@pve
token_name: monitoring
token_value: CHANGE_ME
verify_ssl: false
Le scrape multi-target est géré par Alloy dans config-prometheus-vm.alloy avec l’endpoint /pve?target=<ip>.
Vérification :
curl "http://localhost:9221/pve?target=10.10.40.1&module=default" | head # métriques pve1
Alerting Grafana
L’alerting est géré nativement par Grafana avec un provisioning via fichiers YAML dans provisioning/alerting/.
Important : toujours utiliser
docker compose up -d --force-recreatepour recharger les variables d’environnement (notamment$TEAMS_WEBHOOK_URL). Un simplerestartne recharge pas le.env.
Contact point Teams
Les alertes sont envoyées via un webhook Power Automate vers Microsoft Teams.
provisioning/alerting/contact-points.yaml :
apiVersion: 1
contactPoints:
- orgId: 1
name: teams
receivers:
- uid: teams-webhook
type: teams
disableResolveMessage: false
settings:
url: $TEAMS_WEBHOOK_URL # variable d'environnement injectée depuis .env
provisioning/alerting/notification-policy.yaml :
apiVersion: 1
policies:
- orgId: 1
receiver: teams
group_by: [grafana_folder, alertname]
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
Règles d’alerte
Six règles provisionnées dans provisioning/alerting/rules.yaml :
| Règle | Seuil | Durée | Sévérité |
|---|---|---|---|
| CPU Warning | > 80% | 10 min | warning |
| CPU Critique | > 95% | 5 min | critical |
| Disque Warning | > 70% | 5 min | warning |
| Disque Critique | > 85% | 5 min | critical |
| RAM Warning | > 80% | 10 min | warning |
| RAM Critique | > 90% | 5 min | critical |
Règles firewall MikroTik
Règles ajoutées sur fw-01 pour permettre la communication entre les composants :
| Source | Destination | Port | Objet |
|---|---|---|---|
| alloy-agents (address-list) | prometheus (192.168.50.10) | 9090 | Remote write métriques |
| alloy-agents | loki (192.168.50.15) | 3100 | Push logs |
| prometheus | alloy-agents | 12345 | Debug interface Alloy |
| NPM (192.168.10.5) | grafana (192.168.50.5) | 3000 | Accès interface web |
| prometheus (src-list) | fw-01 / fw-02 | 8728 | API RouterOS (mktxp) |
| prometheus | pve1/2/3 | 8006, 8007 | API Proxmox (pve_exporter) |
L’address-list alloy-agents contient toutes les VMs où Alloy est installé, y compris les nœuds Proxmox (pve1/2/3).
DNS interne
Un enregistrement CNAME est configuré sur fw-01 pour l’accès à Grafana :
/ip/dns/static/add name=grafana.int.edu-kit.fr cname=npm.int.edu-kit.fr
Problèmes rencontrés
mktxp - HTTP 000 / waitress ne bind pas
Symptôme : curl http://localhost:49090 retourne une erreur de connexion refusée, les logs Docker ne montrent aucune erreur de démarrage.
Cause : la valeur listen = '0.0.0.0' dans _mktxp.conf sans le numéro de port empêche waitress (le serveur HTTP Python) de binder correctement.
Solution : spécifier explicitement le port dans le paramètre listen :
listen = '0.0.0.0:49090'
mktxp - Timeout des collectors sur CHR RouterOS 7.x
Symptôme : mktxp démarre mais Prometheus ne reçoit aucune métrique, les logs montrent des timeout récurrents.
Cause : plusieurs collectors (interface, firewall, connections, monitor, neighbor) effectuent des requêtes lourdes non supportées ou très lentes sur les CHR (Cloud Hosted Router) RouterOS 7.x.
Solution : désactiver les collectors problématiques dans mktxp.conf. Voir la section mktxp.conf pour la liste complète.
mktxp - Authentification échoue sur RouterOS 7.x
Symptôme : erreur d’authentification dans les logs mktxp malgré un user/password correct.
Cause : RouterOS 7.x ne supporte pas l’authentification MD5 challenge utilisée par défaut par mktxp.
Solution : ajouter plaintext_login = True dans la section [default] de mktxp.conf.
Grafana alerting - Variables .env non rechargées
Symptôme : $TEAMS_WEBHOOK_URL reste vide dans la configuration du contact point après modification du .env.
Cause : docker compose restart ne relit pas le fichier .env - il conserve les variables de l’environnement de création du container.
Solution : toujours utiliser --force-recreate pour forcer la relecture du .env :
docker compose up -d --force-recreate
pve_exporter - Incompatibilité PBS 4.2
Symptôme : le scrape de PBS (Proxmox Backup Server) via pbs_exporter v0.9.1 échoue sur l’endpoint /nodes.
Cause : incompatibilité de l’exporter avec l’API PBS 4.2.
Solution : monitoring PBS abandonné. La supervision de PBS devra être reprise avec une version compatible de l’exporter.
Sources
- Documentation officielle Grafana Alloy
- Documentation officielle Prometheus
- Documentation officielle Loki
- Documentation officielle Tempo
- Documentation officielle Grafana
- Projet mktxp - GitHub akpw/mktxp
- Projet prometheus-pve-exporter - GitHub prometheus-pve
- Projet cAdvisor - GitHub google/cadvisor
- Blog Stéphane Robert - Grafana Alloy et stack LGTM