Aller au contenu

Note d'ingénierie

Observabilité auto-hébergée : GlitchTip, Loki et Grafana sur un VPS

Par Alexandre Kaczor7 min de lecture
  • DevOps
  • Ops
  • Docker
  • RGPD
Illustration de l'article « Observabilité auto-hébergée : GlitchTip, Loki et Grafana sur un VPS »

Erreurs, logs, métriques et alertes sans SaaS ni port public : GlitchTip, Alloy, Loki, Prometheus et Grafana derrière un tunnel SSH, avec les pièges silencieux et les angles morts assumés.

Sans observabilité, on apprend qu'un produit est tombé par un utilisateur, puis on cherche la cause dans des docker logs à la main. Les offres SaaS d'observabilité règlent ce problème, mais elles ajoutent une facture par produit et un sous-traitant de plus qui reçoit des données de vos utilisateurs.

Pour mes produits, j'ai fait l'autre choix : tout est auto-hébergé sur le même VPS que les applications. GlitchTip collecte les erreurs, Loki et Grafana Alloy centralisent les logs, Prometheus garde les métriques, et Grafana affiche le tout et envoie les alertes. Le compose de monitoring d'Atelier du Tuning le résume : « 100 % self-hosted, RGPD, zéro SaaS ».

Rien n'est exposé, tout passe par un tunnel SSH

La première règle est que aucune interface de monitoring n'écoute sur Internet. Tous les ports sont liés à la boucle locale du serveur :

grafana:
  ports:
    # 127.0.0.1 uniquement — accès par tunnel SSH
    - "127.0.0.1:8101:3000"
  environment:
    GF_USERS_ALLOW_SIGN_UP: "false"
    GF_ANALYTICS_REPORTING_ENABLED: "false"
    # ...

Depuis mon poste, une entrée ~/.ssh/config cumule plusieurs LocalForward (GlitchTip, Grafana, Uptime Kuma). Un seul ssh -N ouvre tout le monitoring. J'y ajoute IdentitiesOnly yes pour une raison concrète : sans cette option, l'agent SSH propose une à une toutes les clés du poste, et chaque refus compte comme un échec pour fail2ban. On se rapproche ainsi de son propre bannissement.

Il y a une exception, et elle est voulue. Pour que les erreurs JavaScript des visiteurs atteignent GlitchTip, il faut un point d'entrée public. Le vhost Caddy n'expose alors que l'ingestion, et tout le reste répond 404 :

sentry.{$DOMAIN} {
    @ingest path /api/*/envelope/ /api/*/store/ /api/*/security/
    handle @ingest {
        reverse_proxy glitchtip:8080
    }
    handle {
        respond 404
    }
}

Côté API, le DSN pointe sur le nom du conteneur, à travers le réseau Docker interne. Les erreurs du backend ne quittent jamais la machine.

GlitchTip : des erreurs, pas du bruit

GlitchTip est compatible avec les SDK Sentry : on garde @sentry/node et on change seulement le DSN. Sur Accueil IA, l'initialisation est importée en tout premier par main.ts :

const dsn = process.env.SENTRY_DSN ?? '';
if (dsn) {
  Sentry.init({
    dsn,
    environment: process.env.NODE_ENV ?? 'development',
    // Erreurs uniquement : le tracing de performance gonflerait le volume
    // pour un intérêt faible sur une API de cette taille.
    tracesSampleRate: 0,
    // Jamais de corps de requête ni de cookies : ils contiendraient des
    // numéros d'appelants et des jetons (invariant RGPD n° 5).
    sendDefaultPii: false,
  });
}

Un DSN vide rend le SDK inerte en développement et en test. Surtout, seules les 500 non gérées partent au suivi d'erreurs, depuis le filtre d'exceptions global, avec le requestId en étiquette. Le commentaire du filtre résume la règle : « une 4xx est un comportement, pas un incident ».

Sur Atelier du Tuning, une fonction pure et testée passe sur chaque événement avant l'envoi. Elle supprime cookies, corps et query string, filtre les en-têtes sensibles (authorization, x-forwarded-for…), masque les e-mails et ne garde de l'utilisateur que son identifiant interne. Les événements sont purgés au bout de 90 jours et l'inscription est fermée.

Deux pièges d'installation : le port interne du conteneur dépend de la version (8000 sur une première installation, 8080 pour l'image v5.1), et l'image ne contient ni curl ni wget, si bien que le healthcheck passe par Python.

Les logs : Alloy, Loki et une rétention bornée

Promtail est en fin de vie, et son successeur officiel est Grafana Alloy. Il branche trois sources :

  • Tous les conteneurs Docker, découverts par le socket monté en lecture seule. Le nom du conteneur devient un label, et aucune application n'a besoin d'être modifiée pour être collectée.
  • Les logs d'accès du proxy, en JSON pour Caddy, ce qui permet de filtrer sur le statut HTTP dans LogQL.
  • Le journal systemd : sshd, fail2ban, noyau et OOM killer.

La troisième source cache le piège le plus sournois de la pile :

volumes:
  - /var/run/docker.sock:/var/run/docker.sock:ro
  # Journal systemd : machine-id indispensable, sinon lecture silencieusement vide
  - /var/log/journal:/var/log/journal:ro
  - /etc/machine-id:/etc/machine-id:ro

Sans /etc/machine-id, Alloy démarre sans erreur et ne lit rien.

Côté Loki, la rétention n'est pas un détail : sans elle, les logs finissent par remplir le disque. Il faut les deux réglages ensemble :

limits_config:
  retention_period: 720h

compactor:
  working_directory: /loki/compactor
  retention_enabled: true
  delete_request_store: filesystem

Trente jours, c'est aussi un choix lié au RGPD : les logs d'accès contiennent les IP des visiteurs. Prometheus garde ses métriques 15 jours, alimenté par node_exporter pour l'hôte et par cAdvisor pour les conteneurs. cAdvisor consomme beaucoup de ressources, alors je le lance avec --docker_only=true --housekeeping_interval=30s. Au total, la documentation d'Atelier du Tuning estime la pile à environ 1 Go de RAM au repos (GlitchTip environ 600 Mo, Grafana et Loki environ 300 Mo), avec un mem_limit sur chaque service. Indispensable sur une machine partagée (voir cinq SaaS sur un seul VPS).

Des alertes qui partent même si personne ne regarde

Les alertes sont provisionnées par fichier, versionnées dans le dépôt de chaque produit et déposées dans le dossier d'alerting du Grafana partagé. Grafana fusionne les groupes de tous les fichiers du dossier : chaque produit ajoute le sien sans toucher à celui du voisin, et hérite du point de contact SMTP déjà configuré.

Pour ZeroStake, la règle la plus utile n'est pas celle qu'on attend :

- uid: zerostake-api-silent
  title: API ZeroStake silencieuse (conteneur down ?)
  data:
    - refId: A
      model:
        expr: sum(count_over_time({job="docker", container="zerostake-api"} [10m]))
  # ... seuil : moins d'une ligne
  # noData = pas de série du tout = vraiment muette → on ALERTE
  noDataState: Alerting
  for: 5m

Une API vivante écrit des logs en continu, ne serait-ce que pour ses health checks. Aucune ligne pendant dix minutes est donc le signal le plus fiable d'un conteneur mort ou figé, et ce signal ne dépend d'aucune sonde externe. Le réglage clé est noDataState : il vaut Alerting ici, alors qu'il vaut OK sur la règle sœur qui compte les erreurs (plus de 15 lignes contenant error, exception ou fatal en 5 minutes). Là, l'absence de données est saine. Sur Atelier du Tuning, une alerte se déclenche au-delà de 20 réponses 5xx en 5 minutes dans les logs de Caddy.

Ce que la pile ne voit pas

Les angles morts sont écrits noir sur blanc :

  • Une sonde sur le serveur ne voit pas les pannes du serveur. Uptime Kuma tourne sur la même machine : si le réseau du VPS tombe, il ne le verra pas. La documentation prévoit en plus un ping externe gratuit.
  • Les erreurs du navigateur. Sur Accueil IA, la documentation le dit sans détour : les erreurs du front ne sont vues par « RIEN à ce jour », et c'est un écart assumé.
  • Les données du monitoring ne sont pas sauvegardées. La base GlitchTip n'entre pas dans le pg_dump applicatif. C'est une perte acceptable, mais il faut l'avoir décidé.
  • La pile peut s'effacer elle-même. Quand le monitoring est fusionné avec le compose de production, un up --remove-orphans lancé sans le fichier de monitoring supprime ses conteneurs. Sur un projet plus récent, j'en ai fait un projet compose distinct (name: dédié) pour supprimer le risque.

Ce que j'en retiens

  • Zéro port public par défaut. Le tunnel SSH suffit pour les interfaces, et seule l'ingestion des erreurs du navigateur mérite une exception filtrée.
  • Le silence est une alerte. Une API qui n'écrit plus rien est plus inquiétante qu'une API qui écrit des erreurs.
  • Le RGPD se règle dans la configuration : sendDefaultPii: false, nettoyage des événements, 30 jours de logs et 90 jours d'erreurs.
  • Les pannes silencieuses de la pile elle-même (un machine-id absent ou une rétention oubliée) se vérifient à l'installation, pas le jour où l'on cherche un log.

Vos applications tournent sans que vous sachiez ce qui s'y passe ? Parlons-en : ce type de pile se pose sur un serveur existant, sans rien exposer.