Demostramos que cada alerta podía dispararse. La mayoría no podía.
La monitorización tiene una propiedad que la vuelve peligrosa como ninguna otra: es el código que solo se ejecuta durante un desastre. Cualquier otra parte de tu sistema se ejecuta constantemente, ante los usuarios, lanzando errores que notas. Tu sistema de alertas se ejecuta el día en que la base de datos se llena, y no antes, y si está roto te enteras justo en el momento en que estabas confiando en él, y precisamente de la manera que garantiza que nadie estaba mirando. Una alerta silenciosa es peor que ninguna alerta, porque de una alerta ausente sabes que no la tienes, mientras que de una silenciosa crees que sí.
Así que dejamos de confiar en que nuestras alertas funcionaban solo porque las habíamos escrito, y construimos un banco de pruebas para demostrarlo. Para cada regla de alerta que desplegamos, el banco de pruebas envía una señal real que debería dispararla, y luego hace lo que de verdad importa: espera a que la notificación llegue físicamente a un relé de captura, un pequeño servicio que hace las veces del teléfono que, de otro modo, vibraría. Que se dispare en la propia interfaz de la herramienta de monitorización no cuenta. Lo que cuenta es la entrega. El camino completo desde la señal hasta un mensaje en la mano, o es un fracaso.
La primera ejecución real hizo fracasar la mayoría de las reglas. Esto es lo que se escondía tras unos paneles en verde.
La alerta insignia estaba muerta
La regla más importante que tenemos es la que se dispara cuando la tasa de errores sube: demasiados HTTP 500, avisa a alguien. El banco de pruebas lanzó cuatrocientos cincuenta errores de servidor reales contra ella, verificó que al menos el noventa por ciento devolvía de verdad un 500, y esperó. No llegó nada. La única alerta en la que más habríamos querido confiar no se había disparado ni una sola vez, y no se habría disparado en un incidente real.
La causa era un nombre. Nuestras métricas salen de OpenTelemetry, donde los nombres llevan puntos, como http.server.request.duration. En algún punto entre leer un tutorial y escribir la consulta, habíamos supuesto que el backend de monitorización normalizaría esos puntos a guiones bajos, como hace buena parte del utillaje de Prometheus, y escribimos la consulta de la alerta contra la forma con guiones bajos. El backend no normaliza. Conserva los nombres con puntos exactamente como se envían. Así que la alerta consultaba una métrica que no existía, no coincidía con nada, calculaba una tasa de errores de cero para siempre, y se quedaba ahí, tranquila y en verde, mientras la métrica real, bajo su verdadero nombre con puntos, registraba cada uno de esos quinientos. La consulta no estaba mal de una forma que produzca un error. Estaba mal de una forma que devuelve en silencio el conjunto vacío, que la aritmética convierte luego en un cero perfectamente plausible.
La que se llevó por delante a todas las alertas de registros
Esa era la mala. La siguiente era peor, y el banco de pruebas solo la atrapó porque insiste en la entrega y no en que la regla parezca correcta.
Toda una categoría de nuestras alertas se basa en registros: encuentra una línea en los registros, dispárate. Cada una de ellas se evaluaba sobre nada, y la razón no tenía nada que ver con las reglas de alerta. Era un ajuste de retención.
Reconciliamos la retención de nuestros registros a través de la API de configuración del backend. Esa API había pasado a una nueva versión con una nueva forma de solicitud, y nosotros seguíamos enviando la forma antigua. El cuerpo antiguo no fue rechazado. El decodificador del otro lado ignora en silencio los campos que no reconoce, así que leyó nuestros campos ahora desconocidos como ausentes, tomó el valor por defecto de la retención que no encontraba, y ese valor por defecto era cero. La llamada devolvía un alegre 200. Y una retención de cero días significa expiración instantánea: los datos de registro recién escritos quedaban marcados para expirar de inmediato, de modo que el almacén que consultaban las alertas de registros estaba, en la práctica, siempre vacío. La ingesta parecía completamente sana todo el tiempo, porque los registros llegaban. Solo que envejecían y desaparecían en el mismo instante en que aterrizaban.
Dos sistemas hicieron, cada uno, algo defendible. La API de configuración aceptó una solicitud que solo entendía en parte en lugar de fallar, lo cual es una decisión común y a menudo razonable. Nuestro despliegue envió un cuerpo que se había quedado obsoleto frente a un cambio de versión. Ninguno de los dos alertó de nada. El resultado fue que el número más trascendental de todo nuestro flujo de registros, cuánto vive un registro, había quedado puesto en silencio a cero, y el único síntoma visible era que cada alerta de registros permanecía callada de forma permanente, con aspecto saludable. Si hubiéramos comprobado que las reglas tenían buen aspecto, tenían un aspecto perfecto. Solo comprobar que un evento real registrado produce un aviso entregado podía sacarlo a la luz, porque solo esa prueba lee de verdad de vuelta del almacén que había quedado vaciado.
Las alertas que se disparaban para siempre, que es lo mismo que nunca
Una tercera falla iba en la dirección contraria, y es la sutil. Un puñado de alertas de auditoría se disparaban sin parar. Eso suena a lo opuesto de una alerta silenciosa, pero tiene el efecto idéntico, y entender por qué merece el rodeo.
Cuando una de estas alertas se disparaba, el backend de monitorización escribía una línea de registro sobre la evaluación, una miga de pan, y esa línea contenía la propia expresión de filtro de la alerta como texto. Esa miga se ingería a su vez como un registro, igual que cualquier otra cosa. Así que la siguiente vez que la regla se evaluaba, encontraba una línea coincidente: su propia huella de la vez anterior. La regla se había vuelto autosostenida. Coincidía con su propia sombra, en cada evaluación, para siempre.
Una alerta que se dispara siempre nunca pasa de no disparada a disparada, y esa transición es lo que envía una notificación. Así que la capa de alertas, al ver una condición que ya era verdadera y seguía siéndolo, suprimía las notificaciones siguientes, exactamente como está diseñada, para ahorrarte un aviso cada minuto sobre algo de lo que ya te han informado. La consecuencia es que un evento real, una auténtica acción de auditoría digna de un aviso, llegaba a una regla que ya estaba encallada en activa y por tanto no decía nada. Una alerta clavada de forma permanente en activa es tan muda como una clavada de forma permanente en inactiva. El arreglo fue acotar esas reglas a las líneas de registro emitidas por nuestros propios servicios, para que dejen de coincidir con la cháchara del backend sobre sí mismo, con una excepción deliberada cuya señal real proviene de verdad de fuera de nuestros servicios.
La regla que tomaba el primer aliento de un pod como algo normal
La última es una lección pequeña y afilada sobre los cálculos de tasa. Una alerta construida sobre la tasa de cambio de una métrica no puede calcular una tasa a partir de la primerísima muestra de una serie, porque no hay nada antes con lo que compararla. Así que cuando un pod recién nacido emitió su primera ráfaga de errores, esa ráfaga se convirtió en la línea de base en lugar de en un pico, y la regla que debería haberlo atrapado solo vio un punto de partida. Lo arreglamos en el banco de pruebas enviando esas señales en dos oleadas, una pequeña para establecer la serie y una real después, lo cual también es una descripción justa de lo que el tráfico de producción hace por sí solo y de lo que una prueba sintética tiene que imitar de forma deliberada.
Cada fallo se escondía en una costura que solo encuentras al ejercitarla
Cada uno de estos era invisible a la inspección. Las consultas se leían bien. Las reglas estaban desplegadas. Los paneles estaban en verde. La ingesta estaba sana. Cada fallo vivía en una costura entre dos cosas que, por separado, estaban bien: un nombre de métrica y una suposición sobre la normalización, un cuerpo de solicitud y un cambio de versión, una alerta y el registro que produce su propio disparo, una función de tasa y una serie que acaba de empezar. No puedes leer una costura. Tienes que ejercitarla.
Así que trata la monitorización por lo que es, que es código, y en concreto código cuya única ejecución en producción es la emergencia. El código que solo se ejecuta en la emergencia necesita que sus pruebas se ejecuten todos los demás días, porque no hay un primer fallo suave que te avise. Envía una señal real de extremo a extremo, y verifica el artefacto justo al final de la cadena, la notificación entregada, no la regla con aspecto saludable en el medio. Cualquier cosa por debajo de eso es comprobar que escribiste una alerta, lo cual ya sabías, en vez de comprobar que se dispara, que es lo único que en realidad querías.
Si prefieres que tu proveedor de identidad ya se estuviera vigilando a sí mismo de esta manera, Authagonal hace pasar su propio sistema de alertas por exactamente este banco de pruebas, para que el día que algo se rompa, el aviso que debería dispararse lo haga de verdad.