Pourquoi centraliser les logs ?
Quand on a un seul serveur avec deux ou trois services, journalctl et docker logs suffisent. Mais dès qu’on dépasse la dizaine de conteneurs répartis sur plusieurs VMs et LXCs, ça devient ingérable.
Mon homelab fait tourner plus de 20 conteneurs Docker sur une VM principale, 7 LXCs avec des services dédiés (DNS, base de données, VPN, backup…), et 2 VPS cloud. Quand un service tombe à 3h du matin, je ne vais pas SSH sur chaque machine pour chercher dans les logs.
La centralisation résout ça : une seule interface pour consulter tous les logs et toutes les métriques de toute l’infrastructure.
La stack complète : Loki + Prometheus + Grafana
Mon stack d’observabilité repose sur trois piliers :
| Composant | Rôle | Données |
|---|---|---|
| Grafana Loki | Centralisation des logs | Logs textuels (Docker, syslog, access logs) |
| Prometheus | Centralisation des métriques | Métriques numériques (CPU, RAM, requêtes/s) |
| Grafana | Visualisation + alerting | Dashboards, exploration, notifications Discord |
Plus les agents de collecte :
| Agent | Rôle | Déploiement |
|---|---|---|
| Promtail | Envoie les logs vers Loki | Sur la VM Docker (lit les logs de tous les conteneurs) |
| Node Exporter | Expose les métriques système | Sur chaque VM/LXC (7 hosts) |
Pourquoi Loki et pas Elasticsearch ?
La stack ELK (Elasticsearch, Logstash, Kibana) est la référence en entreprise, mais elle est gourmande en ressources. Elasticsearch seul peut consommer 4 Go de RAM au repos, et un cluster multi-nœuds est recommandé en production.
Grafana Loki prend le problème autrement :
- N’indexe pas le contenu des logs — seulement les labels (comme Prometheus pour les métriques)
- Stockage compact — compression des chunks en LZ4, pas de full-text index
- RAM minimale — quelques centaines de Mo suffisent
- S’intègre nativement avec Grafana et Prometheus (même éditeur, mêmes dashboards)
- LogQL — langage de requête inspiré de PromQL, familier si on connaît déjà Prometheus
Pour un homelab avec des contraintes de RAM (32 Go partagés entre tout), c’est le choix évident. Mon instance Loki consomme ~200 Mo de RAM pour 14 jours de logs de 20+ conteneurs.
Architecture
┌─────────────────────────────────────────────────────────┐
│ docker-infra │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │
│ │ Promtail │───→│ Loki │←───│ Grafana │ │
│ │ (Docker │ │ (3100) │ │ (3000, SSO OAuth) │ │
│ │ + sys) │ └──────────┘ │ │ │
│ └──────────┘ │ ┌─────────────┐ │ │
│ │←─│ Prometheus │ │ │
│ │ │ (9090) │ │ │
│ └──┴─────────────┴──┘ │
└─────────────────────────────────────────────────────────┘
↑ scrape ↑ scrape
┌────┴────────────────────────┐ ┌───┴──────────────┐
│ Node Exporters (9100) │ │ Exporters apps │
│ atlas, postgres, adguard, │ │ traefik (8080) │
│ freebox-docker, headscale,│ │ authentik (9300) │
│ hermes │ │ crowdsec (6060) │
└────────────────────────────┘ │ postgres (9187) │
│ headscale (9090) │
└───────────────────┘
Configuration de Loki
Le compose
services:
loki:
image: grafana/loki:3.3.2
command: -config.file=/etc/loki/loki.yml
volumes:
- ./config/loki.yml:/etc/loki/loki.yml:ro
- loki-data:/loki
ports:
- "3100:3100" # exposé sur le LAN pour les agents distants
healthcheck:
test: ["CMD-SHELL", "wget --spider -q http://localhost:3100/ready || exit 1"]
interval: 30s
timeout: 10s
retries: 3
Configuration clé
Les points importants de la configuration Loki :
schema_config:
configs:
- from: "2024-01-01"
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
limits_config:
retention_period: 336h # 14 jours
ingestion_rate_mb: 4 # 4 MB/s max
ingestion_burst_size_mb: 6 # burst 6 MB/s
compactor:
retention_enabled: true
compaction_interval: 10m
delete_request_cancel_period: 2h
max_compaction_parallelism: 150
chunk_store_config:
chunk_cache_config:
embedded_cache:
enabled: true
max_size_mb: 100
Quelques choix notables :
- Schema v13 avec TSDB : le format le plus récent et performant de Loki
- Rétention 14 jours : suffisant pour du débogage courant. Au-delà, les logs n’apportent plus grand-chose (sauf conformité)
- Rate limiting : 4 MB/s d’ingestion max — évite qu’un conteneur verbose ne sature Loki
- Cache 100 Mo : accélère les requêtes répétitives sans consommer trop de RAM
Configuration de Promtail
Promtail est l’agent qui collecte les logs et les envoie à Loki. Il tourne sur la VM Docker et a accès au socket Docker pour la découverte automatique des conteneurs.
Découverte automatique des conteneurs Docker
scrape_configs:
- job_name: docker
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
relabel_configs:
- source_labels: ['__meta_docker_container_name']
regex: '/(.*)'
target_label: container
- source_labels: ['__meta_docker_container_image']
target_label: image
- source_labels: ['__meta_docker_compose_project']
target_label: stack
- source_labels: ['__meta_docker_compose_service']
target_label: service
pipeline_stages:
- json:
expressions:
level: level
- labels:
level:
Chaque conteneur reçoit automatiquement les labels container, image, stack et service. Zéro configuration manuelle quand on ajoute un nouveau service — Promtail le détecte et commence à collecter ses logs.
Le pipeline stage JSON extrait automatiquement le champ level des logs structurés. La plupart des applications modernes loggent en JSON, ce qui permet de filtrer par niveau (error, warn, info, debug) dans Grafana.
Parsing dédié des access logs Traefik
Les access logs de Traefik méritent un traitement spécial car ils contiennent des informations structurées précieuses :
- job_name: traefik-access
static_configs:
- targets: [localhost]
labels:
job: traefik-access
__path__: /var/log/traefik/access.log
pipeline_stages:
- json:
expressions:
client: ClientAddr
method: RequestMethod
path: RequestPath
status: OriginStatus
duration: Duration
router: RouterName
service: ServiceName
- labels:
method:
status:
router:
Avec ce parsing, on peut faire des requêtes LogQL du type « toutes les requêtes POST qui ont retourné un 401 sur le router authentik en dernière heure ».
Logs système
En plus des logs Docker, Promtail collecte les logs système (syslog, auth.log) :
- job_name: syslog
static_configs:
- targets: [localhost]
labels:
job: syslog
host: docker-infra
__path__: /var/log/syslog
- job_name: auth
static_configs:
- targets: [localhost]
labels:
job: auth
host: docker-infra
__path__: /var/log/auth.log
Configuration de Prometheus
Prometheus collecte les métriques (données numériques) par opposition aux logs (données textuelles). Il scrape des endpoints HTTP /metrics à intervalles réguliers.
Scrape config
global:
scrape_interval: 30s
external_labels:
monitor: homelab
scrape_configs:
# Métriques système (Node Exporter sur chaque hôte)
- job_name: node
static_configs:
- targets:
- docker-infra.:9100
- 192.168.1.72:9100 # atlas (Proxmox)
- 192.168.1.55:9100 # postgres
- 192.168.1.54:9100 # adguard
- 192.168.1.8:9100 # freebox-docker
- 192.168.1.139:9100 # headscale
- 192.168.1.35:9100 # hermes
# Reverse proxy
- job_name: traefik
static_configs:
- targets: ['traefik.:8080']
# SSO
- job_name: authentik
static_configs:
- targets: ['authentik-server.:9300']
# Base de données
- job_name: postgres
static_configs:
- targets: ['192.168.1.55:9187']
# Sécurité
- job_name: crowdsec
static_configs:
- targets: ['crowdsec-internal.:6060']
Astuce DNS : les noms avec un point final (traefik.) empêchent Docker d’ajouter le search domain. C’est le même piège que pour Traefik — le search domain peut causer des résolutions vers l’IP hôte au lieu du conteneur.
Rétention
# Argument de démarrage de Prometheus
--storage.tsdb.retention.time=90d
90 jours de métriques. Avec un intervalle de scrape de 30s et 7 hosts + 10 exporters applicatifs, ça représente environ 2-3 Go de stockage — négligeable.
Alertes Prometheus
C’est là que Prometheus prend toute sa valeur. Les règles d’alerte surveillent en permanence les métriques et déclenchent des notifications :
Alertes système
groups:
- name: homelab
rules:
- alert: HostDown
expr: up{job="node"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: "Host {{ $labels.instance }} down"
- alert: DiskSpaceCritical
expr: (1 - node_filesystem_avail_bytes / node_filesystem_size_bytes) > 0.90
for: 5m
labels:
severity: critical
annotations:
summary: "Disque > 90% sur {{ $labels.instance }}"
- alert: MemoryHigh
expr: (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) > 0.85
for: 5m
labels:
severity: warning
- alert: CPUCritical
expr: (1 - avg by(instance)(rate(node_cpu_seconds_total{mode="idle"}[5m]))) > 0.90
for: 5m
labels:
severity: critical
Alertes sécurité CrowdSec
- alert: CrowdSecAlertSpike
expr: increase(cs_alerts[1h]) > 50
labels:
severity: warning
annotations:
summary: "Pic d'alertes CrowdSec : attaque coordonnée ?"
- alert: CrowdSecMassiveBan
expr: cs_active_decisions > 1000
labels:
severity: warning
annotations:
summary: "Plus de 1000 IPs bannies — situation anormale"
Alertes mises à jour
- alert: CriticalCVEDetected
expr: updates_cve_count{severity="critical"} > 0
labels:
severity: critical
annotations:
summary: "CVE critique détectée — mise à jour urgente"
- alert: SecurityUpdatesAvailable
expr: updates_apt_count{type="security"} > 0
for: 4h
labels:
severity: warning
annotations:
summary: "Mises à jour de sécurité APT en attente"
Recording rules pour le SMART
Un bonus : des recording rules qui pré-calculent des métriques SMART pour les disques :
- record: smartmon:life_remaining:hourly
expr: smartmon_attr_value{attr_name="Media_Wearout_Indicator"}
Avec les données SMART, Prometheus peut calculer le taux de dégradation sur 7 et 30 jours et prédire le nombre de jours restants avant que le SSD atteigne 0% de vie restante. Pratique pour anticiper un remplacement de disque.
Configuration de Grafana
Grafana est l’interface de visualisation. Quelques choix de configuration notables :
Authentification SSO
Grafana est connecté à Authentik (le SSO du homelab) via OIDC natif :
environment:
- GF_AUTH_GENERIC_OAUTH_ENABLED=true
- GF_AUTH_GENERIC_OAUTH_NAME=Authentik
- GF_AUTH_GENERIC_OAUTH_AUTH_URL=https://auth.mondomaine.ovh/application/o/authorize/
- GF_AUTH_GENERIC_OAUTH_TOKEN_URL=https://auth.mondomaine.ovh/application/o/token/
- GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH=contains(groups, 'authentik Admins') && 'Admin' || 'Viewer'
Le mapping de rôles est automatique : les membres du groupe authentik Admins sont Admin Grafana, tous les autres sont Viewer. Pas besoin de gérer les utilisateurs Grafana séparément.
Base de données externe
Grafana utilise PostgreSQL (LXC dédié) plutôt que SQLite :
environment:
- GF_DATABASE_TYPE=postgres
- GF_DATABASE_HOST=192.168.1.55:5432
- GF_DATABASE_NAME=grafana
Ça facilite les backups (intégrés aux backups PostgreSQL) et évite les problèmes de corruption SQLite sous charge.
Image Renderer
Un conteneur dédié pour le rendu d’images (captures d’écran des dashboards pour les alertes Discord) :
grafana-image-renderer:
image: grafana/grafana-image-renderer:latest
mem_limit: 1g
environment:
- RENDERING_CLUSTERING_MODE=context
- RENDERING_CLUSTERING_MAX_CONCURRENCY=5
Quand une alerte se déclenche, Grafana capture un screenshot du panel concerné et l’envoie dans la notification Discord. Très utile pour voir d’un coup d’œil la gravité de la situation.
Requêtes LogQL
LogQL est le langage de requête de Loki. La syntaxe est inspirée de PromQL :
# Tous les logs du conteneur traefik
{container="traefik"}
# Filtrer les erreurs
{container="traefik"} |= "error"
# Exclure les health checks
{container="traefik"} != "/ping" != "/health"
# Logs d'un stack complet
{stack="observability-stack"}
# Requêtes HTTP avec status 5xx
{job="traefik-access"} | json | status >= 500
# Compter les erreurs 4xx par minute, groupées par router
count_over_time({job="traefik-access"} | json | status >= 400 | status < 500 [1m]) by (router)
# Top 10 des IPs qui génèrent des erreurs
topk(10, sum by (client) (count_over_time({job="traefik-access"} | json | status >= 400 [1h])))
# Tentatives de login échouées sur Authentik
{container=~"authentik-.*"} |= "login" |= "failed"
La puissance de LogQL apparaît quand on combine les filtres : on peut corréler un pic de 502 avec les logs CrowdSec et les logs Authentik en quelques secondes, sans quitter Grafana.
Politique de rétention multi-niveaux
Pour ne pas remplir le disque, j’applique une rétention à trois niveaux :
| Niveau | Durée | Stockage | Raison |
|---|---|---|---|
| Local (Docker) | ~30 Mo par conteneur | json-file, 3 fichiers × 10 Mo | Fallback si Loki down |
| Loki | 14 jours | ~1-2 Go filesystem | Débogage courant |
| Prometheus | 90 jours | ~2-3 Go TSDB | Tendances long terme |
Configuration log-driver Docker
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
Ce réglage dans /etc/docker/daemon.json s’applique à tous les conteneurs par défaut. 10 Mo × 3 fichiers = 30 Mo max par conteneur. Avec 20+ conteneurs, ça fait ~600 Mo max de logs locaux — raisonnable.
Configuration journald (systèmes)
# /etc/systemd/journald.conf
[Journal]
SystemMaxUse=200M
200 Mo de journal systemd par machine. Au-delà, les plus vieux logs sont purgés automatiquement.
Ajout d’un nouveau host au monitoring
Quand j’ajoute un nouveau LXC ou une nouvelle VM, la procédure est systématique :
- Installer Node Exporter (métriques système) :
# Sur le nouveau host
apt install prometheus-node-exporter
systemctl enable --now prometheus-node-exporter
- Ajouter le target dans Prometheus :
# Dans prometheus.yml, job "node"
- targets:
- 192.168.1.XX:9100 # nouveau host
- Installer Promtail (si des logs applicatifs à collecter) :
# Télécharger et configurer Promtail
# Pointer vers Loki: http://192.168.1.52:3100/loki/api/v1/push
Configurer journald :
SystemMaxUse=200MVérifier dans Grafana que le nouveau host apparaît dans les dashboards
La procédure prend 10 minutes par host. Avec 7 hosts, c’est un investissement d’une heure qui fait gagner des dizaines d’heures de débogage.
Résultat en production
Après plusieurs mois d’utilisation, quelques métriques sur la stack elle-même :
- Volume d’ingestion Loki : ~500 Mo/jour de logs bruts
- Taille Loki sur disque : ~3 Go (14 jours, compression LZ4)
- Taille Prometheus : ~2.5 Go (90 jours, 7 hosts + 10 exporters)
- RAM totale : Loki ~200 Mo, Prometheus ~300 Mo, Grafana ~150 Mo
- Requêtes LogQL : < 100 ms pour la plupart des requêtes sur 24h
- Alertes déclenchées par mois : 5-10 (principalement disque et CPU)
Conseils pratiques
- Commencer par Docker — Promtail + Docker service discovery se met en place en 15 minutes
- Ajouter des labels pertinents —
host,container,stack,servicefacilitent le filtrage - Utiliser les annotations Grafana — marquer les déploiements sur les dashboards pour corréler incidents et changements
- Créer des dashboards par stack — un dashboard “Observability” pour la stack elle-même, un “Traefik” pour le trafic HTTP, un “CrowdSec” pour la sécurité
- Alerter sur l’essentiel, pas sur tout — trop d’alertes = alert fatigue. Commencer par HostDown, DiskCritical, et CVE critiques
- Surveiller Loki lui-même — Prometheus scrape Loki (
loki:3100/metrics) pour détecter les problèmes d’ingestion
La centralisation des logs et métriques transforme le débogage d’une chasse au trésor en une recherche structurée. C’est un investissement de quelques heures qui en fait gagner des dizaines — et qui, surtout, donne confiance dans l’état de son infrastructure.