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 :

ComposantRôleDonnées
Grafana LokiCentralisation des logsLogs textuels (Docker, syslog, access logs)
PrometheusCentralisation des métriquesMétriques numériques (CPU, RAM, requêtes/s)
GrafanaVisualisation + alertingDashboards, exploration, notifications Discord

Plus les agents de collecte :

AgentRôleDéploiement
PromtailEnvoie les logs vers LokiSur la VM Docker (lit les logs de tous les conteneurs)
Node ExporterExpose les métriques systèmeSur 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 :

NiveauDuréeStockageRaison
Local (Docker)~30 Mo par conteneurjson-file, 3 fichiers × 10 MoFallback si Loki down
Loki14 jours~1-2 Go filesystemDébogage courant
Prometheus90 jours~2-3 Go TSDBTendances 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 :

  1. Installer Node Exporter (métriques système) :
# Sur le nouveau host
apt install prometheus-node-exporter
systemctl enable --now prometheus-node-exporter
  1. Ajouter le target dans Prometheus :
# Dans prometheus.yml, job "node"
- targets:
    - 192.168.1.XX:9100  # nouveau host
  1. 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
  1. Configurer journald : SystemMaxUse=200M

  2. Vé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

  1. Commencer par Docker — Promtail + Docker service discovery se met en place en 15 minutes
  2. Ajouter des labels pertinentshost, container, stack, service facilitent le filtrage
  3. Utiliser les annotations Grafana — marquer les déploiements sur les dashboards pour corréler incidents et changements
  4. 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é
  5. Alerter sur l’essentiel, pas sur tout — trop d’alertes = alert fatigue. Commencer par HostDown, DiskCritical, et CVE critiques
  6. 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.