Exploitation

Cette page rassemble les leviers de supervision et de maintenance courante : services, sondes de santé, métriques, diagnostic, file d'envoi, journaux, sauvegarde et tâches périodiques.

Services

Les trois services runtime (difuzio-web, difuzio-lmtp, difuzio-worker@N) sont rattachés à la cible difuzio.target : toute la pile se pilote en une seule commande.

systemctl restart difuzio.target   # redémarre web + lmtp + tous les workers
systemctl stop difuzio.target      # arrête toute la pile
systemctl start difuzio.target     # démarre tous les services activés

difuzio-migrate.service (oneshot) n'est pas rattaché à la cible, mais chaque service le tire par Requires= : les migrations en attente sont appliquées automatiquement avant chaque démarrage, y compris sur un simple systemctl restart difuzio-web. Quand il n'y a rien à appliquer, l'unité n'écrit rien et se termine aussitôt. Un service reste pilotable individuellement.

Conséquence à connaître après une mise à jour de binaire : le premier redémarrage porte la migration, donc il peut durer le temps de celle-ci. La sauvegarde se fait avant ce redémarrage, pas après (voir Installation, section 10).

Sondes de santé

serve-web expose deux sondes HTTP (sur le port web.bind) :

  • GET /healthz : vivacité. Renvoie toujours 200 ok si le processus répond.
  • GET /readyz : disponibilité. Vérifie que la base est joignable et que le schéma est celui qu'attend le binaire en cours d'exécution. Renvoie 200 avec un détail JSON si tout va bien, 503 sinon (avec une ligne de journal).

Exemple :

curl -fsS http://127.0.0.1:8080/readyz

Trois états rendent le service indisponible :

  • base injoignable ;
  • schéma dirty : une migration a échoué en cours de route (voir Référence CLI, migrate force) ;
  • schéma en retard sur le binaire : la mise à jour a démarré le nouveau code sans jouer difuzio migrate up. Le détail nomme les deux versions (version 32, binary expects 33 (1 migration(s) pending)). Les services refusent d'ailleurs de démarrer dans cet état, voir Installation, section 10.

Un schéma plus récent que le binaire ne rend pas le service indisponible : c'est l'état normal juste après un retour arrière de version, et les migrations sont additives. Le détail le signale (newer than this binary).

À brancher sur les health checks du reverse proxy et de l'orchestrateur.

Métriques Prometheus

Quand metrics.bind est renseigné, le worker expose /metrics sur ce port séparé. Attention en multi-workers : toutes les instances difuzio-worker@N partagent le même config.yaml, donc un port statique ne peut être pris que par la première (les suivantes loguent metrics server failed ... bind: address already in use et tournent sans /metrics). Le template systemd livré règle le port par instance via une surcharge d'environnement :

Environment=DIFUZIO_METRICS__BIND=127.0.0.1:909%i

difuzio-worker@1 expose donc /metrics sur 9091, @2 sur 9092, etc. (nommer les instances avec un chiffre) ; déclarer chaque port comme cible de scrape Prometheus. Le metrics.bind du YAML ne sert alors que de repli pour un worker lancé hors systemd.

Les compteurs Go et process standards sont présents, plus trois jauges calculées en base à chaque scrape (identiques d'un worker à l'autre, elles reflètent la base et non l'instance) :

  • difuzio_queue_depth{state} : nombre de copies sortantes par état (queued, sending, sent, deferred, failed, cancelled).
  • difuzio_oldest_queued_seconds : âge de la plus ancienne copie en attente.
  • difuzio_lists_suspended : nombre de listes dont l'envoi est suspendu (kill-switch).
  • difuzio_lists_verp_disabled : nombre de listes avec l'attribution de bounce désactivée (verp_enabled=0). C'est l'alerte persistante BNC-04 : un envoi sans VERP casse l'imputation des rebonds, donc cette jauge doit rester à 0.

Alertes utiles : difuzio_oldest_queued_seconds qui grimpe (file qui ne s'écoule plus), difuzio_queue_depth{state="deferred"} élevé (problème de remise), difuzio_lists_suspended non nul (réputation dégradée), et difuzio_lists_verp_disabled non nul (attribution de bounce dégradée).

Savoir quelle version est réellement en service

Les deux moitiés d'un déploiement se livrent séparément : le binaire d'un côté, la console (fichiers statiques servis par le proxy frontal) de l'autre. Elles peuvent donc diverger, et un cache de navigateur peut faire croire à un déploiement qui n'a pas eu lieu.

Trois endroits répondent à la question, tous alimentés par la même source :

  • La console, en pied de barre latérale : deux lignes, Console et Serveur, chacune avec sa version et sa révision courte. Les deux moitiés sortent du même dépôt, donc les deux révisions doivent être identiques ; si elles diffèrent, l'une des deux n'a pas été redéployée (ou le navigateur sert un ancien paquet, ce qu'un rechargement forcé tranche). L'infobulle de chaque ligne donne le détail : date de compilation, date du commit, backend et version de Go.
  • L'écran 'À propos' de la console, atteint depuis ce pied de barre ou depuis le menu du compte. Il reprend les mêmes faits en entier - révisions complètes, instants en UTC, backend de base lié au binaire, version de Go et plateforme - et tranche lui-même la comparaison des deux moitiés : en phase, divergentes, ou non comparables (une révision manque, ou l'un des artefacts a été compilé depuis un arbre de travail modifié). Le bouton 'Copier le rapport' en produit une version texte, à joindre à une demande d'assistance. La page rappelle aussi la licence et renvoie vers la documentation et le contrat OpenAPI de l'instance.
  • La ligne de commande : difuzio version affiche la version, la révision, la marque dirty si le binaire a été compilé depuis un arbre de travail modifié, la version de Go et le backend lié. difuzio doctor la rappelle en première ligne.
  • L'API : GET /api/v1/system/version (authentifiée), le point d'entrée que la console interroge.

La révision et sa date viennent de l'empreinte que Go pose lui-même à la compilation depuis un dépôt git : un simple go build suffit à identifier un binaire. Une compilation faite hors dépôt (archive, couche de conteneur) peut injecter les mêmes informations à l'édition de liens :

go build -ldflags "-X difuzio/internal/buildinfo.Version=1.2.3 \
  -X difuzio/internal/buildinfo.Commit=<sha> \
  -X difuzio/internal/buildinfo.BuiltAt=2026-08-11T10:16:38Z" ./cmd/difuzio

Le Makefile le fait pour make bin et make bin-sqlite (variables VERSION et BUILT_AT). Sans aucune de ces traces, la version annoncée est celle du code source et la révision est vide : c'est une réponse honnête, pas une panne.

Diagnostic

CREDENTIALS_DIRECTORY=/etc/difuzio/secrets difuzio doctor --config /etc/difuzio/config.yaml

Vérifie la joignabilité de la base et l'état du schéma (version, propre ou "dirty"). Si le fichier de config est fourni, vérifie aussi le chargement des keyrings et les invariants. La première ligne nomme le binaire en service (version, révision, backend). Code de sortie non nul en cas de problème.

File d'envoi

CREDENTIALS_DIRECTORY=/etc/difuzio/secrets difuzio queue stats --config /etc/difuzio/config.yaml

Affiche la profondeur de la file sortante (outgoing_queue) par état. Une copie peut être queued, sending, sent, deferred, failed ou cancelled.

Rappel : un 250 du relais submission signifie "remis au relais", pas "livré au destinataire". Compléter par la surveillance de la file Postfix (mailq).

Journaux

Les journaux sont structurés (slog), au format json ou text selon log.format. Chaque chemin d'erreur écrit une ligne avant de retourner : il n'y a pas d'échec silencieux. Les journaux peuvent contenir des données personnelles (adresses email) ; aligner la rétention (journald MaxRetentionSec / SystemMaxUse, ou logrotate) sur la politique RGPD (90 jours).

Chaque réponse HTTP porte un X-Request-Id corrélable aux lignes de journal.

Tâches périodiques (worker)

Le worker exécute, en plus de l'envoi des messages, du pipeline entrant et de la livraison des webhooks, des tâches périodiques singleton (protégées par verrou applicatif pour ne tourner qu'une fois même avec plusieurs workers) :

  • reaper : ré-enfile les copies sortantes bloquées en sending au-delà du délai de reprise (worker.reclaim_seconds) ;
  • purge des copies sortantes terminales au-delà de worker.outq_retention_days ;
  • expiration des éléments de modération et des demandes d'abonnement trop anciens ;
  • purge des tokens expirés ou consommés ;
  • kill-switch : évaluation de la réputation et suspension automatique des listes hors seuils ;
  • digest : compilation et envoi des digests (au plus une fois par heure et par liste, voir Listes) ;
  • send_events : émission des webhooks send.completed / send.failed une fois qu'un envoi est entièrement traité ;
  • purge d'idempotence : suppression des clés d'idempotence de plus de 24 h.

Le worker de livraison des webhooks réenfile aussi les livraisons orphelines (bloquées en sending après la mort d'un worker). Les requêtes de claim de file (FOR UPDATE ... SKIP LOCKED sous MariaDB) réessaient automatiquement en cas de deadlock transitoire (1213/1205 sous MariaDB, SQLITE_BUSY/SQLITE_LOCKED sous SQLite).

Sauvegarde et restauration

  • Base MariaDB : mariadb-dump --single-transaction nocturne (compressé), plus binlogs pour le point-in-time recovery.
  • Base SQLite : sauvegarde en ligne avec sqlite3 <fichier> ".backup <cible>", jamais un cp à chaud (le journal WAL est actif, la copie du seul .db serait incohérente). Voir Base de données.
  • Corps des messages : l'arborescence storage.blob_dir (/var/lib/difuzio/messages par défaut) contient le brut de tous les messages archivés. La base seule ne suffit plus : elle ne garde que les métadonnées et le chemin de chaque objet. Sauvegarder les deux, et de préférence la base en dernier - une ligne sans son objet est une perte, un objet sans sa ligne est seulement de l'espace récupéré au balayage suivant. Un rsync incrémental convient : les objets sont écrits une fois et jamais modifiés.
  • Keyrings : sauvegardés à part, chiffrés. Sans eux, une restauration de base ne permet pas de revérifier les tokens VERP/one-click en circulation ni de déchiffrer les sessions.

Tester régulièrement la restauration sur un environnement à part.