---
title: "Exploitation"
weight: 90
description: "Pilotage des services, sondes de santé, métriques Prometheus, file d'envoi, journaux, sauvegarde et tâches périodiques."
---

# 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](/difuzio/admin/installation)).

## 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](/difuzio/admin/cli-reference), `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](/difuzio/admin/installation).

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](/difuzio/admin/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](/difuzio/admin/base-de-donnees).
- **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.
