Nous avons prouvé que chaque alerte pouvait se déclencher. La plupart ne le pouvaient pas.
La supervision possède une propriété qui la rend singulièrement dangereuse: c'est le code qui ne s'exécute que pendant une catastrophe. Toute autre partie de votre système s'exécute en permanence, devant les utilisateurs, en émettant des erreurs que vous remarquez. Votre alerting s'exécute le jour où la base de données se remplit, et pas avant, et s'il est cassé, vous l'apprenez à l'instant précis où vous comptiez dessus, et précisément de la manière qui garantit que personne ne regardait. Une alerte silencieuse est pire que pas d'alerte du tout, car une alerte absente, vous savez que vous ne l'avez pas, tandis qu'une alerte silencieuse, vous croyez l'avoir.
Nous avons donc cessé de croire que nos alertes fonctionnaient simplement parce que nous les avions écrites, et nous avons bâti un harnais pour le prouver. Pour chaque règle d'alerte que nous déployons, le harnais envoie un vrai signal censé la déclencher, puis il fait ce qui compte vraiment: il attend que la notification arrive physiquement dans un relais de capture, un petit service qui tient lieu du téléphone qui, autrement, vibrerait. Un déclenchement dans l'interface de l'outil de supervision ne compte pas. La livraison compte. Le chemin complet du signal jusqu'à un message en main, sinon c'est un échec.
Le premier vrai passage a fait échouer la plupart des règles. Voici ce qui se cachait derrière des tableaux de bord verts.
L'alerte phare était morte
La règle la plus importante que nous possédions est celle qui se déclenche quand le taux d'erreurs grimpe: trop de HTTP 500, alerte quelqu'un. Le harnais a lancé quatre cent cinquante vraies erreurs serveur contre elle, a vérifié qu'au moins quatre-vingt-dix pour cent d'entre elles renvoyaient réellement 500, et a attendu. Rien n'est arrivé. La seule alerte à laquelle nous voudrions le plus nous fier ne s'était jamais déclenchée, et ne se serait pas déclenchée lors d'un incident réel.
La cause était un nom. Nos métriques sortent d'OpenTelemetry, où les noms sont pointés, comme http.server.request.duration. Quelque part entre la lecture d'un tutoriel et l'écriture de la requête, nous avions supposé que le backend de supervision normaliserait ces points en tirets bas, comme le fait une grande partie de l'outillage Prometheus, et nous avions écrit la requête d'alerte contre la forme à tirets bas. Le backend ne normalise pas. Il conserve les noms pointés exactement tels qu'ils sont envoyés. L'alerte interrogeait donc une métrique qui n'existait pas, ne correspondait à rien, calculait un taux d'erreurs de zéro pour toujours, et restait là, calme et verte, pendant que la vraie métrique, sous son vrai nom pointé, enregistrait chacune de ces erreurs 500. La requête n'était pas fausse d'une manière qui provoque une erreur. Elle était fausse d'une manière qui renvoie silencieusement l'ensemble vide, que l'arithmétique transforme ensuite en un zéro parfaitement plausible.
Celle qui a entraîné toutes les alertes de journaux dans sa chute
Ça, c'était la mauvaise. La suivante était pire, et le harnais ne l'a attrapée que parce qu'il exige la livraison plutôt que l'apparence correcte de la règle.
Toute une catégorie de nos alertes est fondée sur les journaux: repérer une ligne dans les journaux, se déclencher. Chacune d'entre elles évaluait contre rien, et la raison n'avait rien à voir avec les règles d'alerte. C'était un réglage de rétention.
Nous réconcilions notre rétention de journaux via l'API de configuration du backend. Cette API était passée à une nouvelle version avec une nouvelle forme de requête, et nous envoyions toujours l'ancienne forme. L'ancien corps n'a pas été rejeté. Le décodeur de l'autre côté ignore silencieusement les champs qu'il ne reconnaît pas, il a donc lu nos champs désormais inconnus comme absents, a pris la valeur par défaut pour la rétention qu'il ne trouvait pas, et cette valeur par défaut était zéro. L'appel a renvoyé un joyeux 200. Et une rétention de zéro jour signifie une expiration instantanée: les données de journaux nouvellement écrites étaient marquées pour expirer immédiatement, si bien que le magasin interrogé par les alertes de journaux était, en pratique, toujours vide. L'ingestion paraissait parfaitement saine pendant tout ce temps, car les journaux arrivaient. Ils vieillissaient simplement jusqu'à disparaître à l'instant même où ils atterrissaient.
Deux systèmes ont chacun fait quelque chose de défendable. L'API de configuration a accepté une requête qu'elle ne comprenait qu'en partie plutôt que d'échouer, ce qui est un choix courant et souvent raisonnable. Notre déploiement a envoyé un corps devenu obsolète face à un changement de version. Ni l'un ni l'autre n'a signalé quoi que ce soit. Le résultat, c'est que le nombre le plus lourd de conséquences de tout notre pipeline de journaux, la durée de vie d'un journal, avait été discrètement mis à zéro, et le seul symptôme visible était que chaque alerte de journaux était en permanence, et sainement en apparence, silencieuse. Si nous avions testé que les règles avaient l'air correctes, elles avaient l'air parfaites. Seul un test vérifiant qu'un vrai événement journalisé produit une notification livrée pouvait le révéler, car seul ce test relit réellement le contenu du magasin qui avait été vidé.
Les alertes qui se déclenchaient sans fin, ce qui revient à jamais
Une troisième défaillance allait dans le sens inverse, et c'est la subtile. Une poignée d'alertes d'audit se déclenchaient en permanence. Cela semble être le contraire d'une alerte silencieuse, mais l'effet est identique, et comprendre pourquoi vaut le détour.
Quand l'une de ces alertes se déclenchait, le backend de supervision écrivait une ligne de journal à propos de l'évaluation, une miette, et cette ligne contenait sous forme de texte la propre expression de filtre de l'alerte. Cette miette était elle-même ingérée comme un journal, comme n'importe quoi d'autre. Ainsi, à l'évaluation suivante, la règle trouvait une ligne correspondante: sa propre empreinte de la fois précédente. La règle était devenue autoalimentée. Elle correspondait à sa propre ombre, à chaque évaluation, pour toujours.
Une alerte qui se déclenche toujours ne passe jamais de non-déclenchée à déclenchée, et c'est cette transition qui envoie une notification. La couche d'alerting, voyant une condition déjà vraie qui le restait, a donc supprimé les notifications suivantes, exactement comme prévu, pour vous épargner une alerte chaque minute à propos d'une chose dont on vous a déjà informé. La conséquence, c'est qu'un vrai événement, une véritable action d'audit méritant une alerte, arrivait dans une règle déjà bloquée en position active et ne disait donc rien. Une alerte bloquée en permanence sur active est aussi muette qu'une bloquée en permanence sur inactive. Le correctif a été de restreindre ces règles aux lignes de journal émises par nos propres services, afin qu'elles cessent de correspondre au bavardage du backend à propos de lui-même, avec une exception délibérée dont le vrai signal vient réellement de l'extérieur de nos services.
La règle qui prenait le premier souffle d'un pod pour la normale
La dernière est une petite leçon tranchante sur les calculs de taux. Une alerte fondée sur le taux de variation d'une métrique ne peut pas calculer un taux à partir du tout premier échantillon d'une série, car il n'y a rien avant lui à quoi le comparer. Ainsi, quand un pod frais a émis sa première salve d'erreurs, cette salve est devenue la référence plutôt qu'un pic, et la règle qui aurait dû l'attraper n'a vu qu'un point de départ. Nous l'avons corrigé dans le harnais en envoyant de tels signaux en deux vagues, une petite pour établir la série et une vraie ensuite, ce qui décrit d'ailleurs assez justement ce que le trafic de production fait de lui-même et ce qu'un test synthétique doit imiter délibérément.
Chaque défaillance se cachait dans une jointure que l'on ne trouve qu'en la sollicitant
Chacune d'elles était invisible à l'inspection. Les requêtes se lisaient correctement. Les règles étaient déployées. Les tableaux de bord étaient verts. L'ingestion était saine. Chaque défaillance vivait dans une jointure entre deux choses individuellement irréprochables: un nom de métrique et une hypothèse sur la normalisation, un corps de requête et un changement de version, une alerte et le journal que produit son propre déclenchement, une fonction de taux et une série qui vient tout juste de commencer. On ne peut pas lire une jointure. Il faut la solliciter.
Traitez donc la supervision pour ce qu'elle est, à savoir du code, et précisément du code dont la seule exécution en production est l'urgence. Le code qui ne s'exécute que lors de l'urgence a besoin que ses tests s'exécutent tous les autres jours, car il n'y a pas de première défaillance clémente pour vous avertir. Envoyez un vrai signal de bout en bout, et faites votre assertion sur l'artefact tout au bout de la chaîne, la notification livrée, et non sur la règle qui a l'air saine au milieu. Tout ce qui est en deçà revient à tester que vous avez écrit une alerte, ce que vous saviez déjà, plutôt qu'à tester qu'elle se déclenche, ce qui est la seule chose que vous vouliez vraiment.
Si vous préféreriez que votre fournisseur d'identité se surveille déjà lui-même de cette façon, Authagonal fait passer son propre alerting par exactement ce harnais, de sorte que le jour où quelque chose casse, l'alerte qui doit se déclencher le fait vraiment.