← blog

Alerting agéntico: que el que decide si te despierta no sea un umbral

Armé un vigía que junta señales de la infra, le pasa el diff a un agente y deja que él decida si esto amerita interrumpirme y por qué canal: texto, botones ✅/❌ o una llamada telefónica. La arquitectura, cómo se prueba algo así sin esperar a que se rompa de verdad, y los gotchas que no salen en ningún blog post.

·
  • alerting
  • observability
  • ai
  • agents
  • grafana
  • kubernetes
  • homelab
  • devops
  • telegram

clau-vigía — el agente que mira los tableros y decide si vale la pena interrumpirte.

TL;DR

Hice un vigía para mi infra que no tiene reglas de “si pasa X, avisá”. Tiene un loop que junta señales, calcula qué cambió desde el tick anterior, y le pasa ese diff a un agente con una sola pregunta: ¿esto amerita interrumpir a alguien, y por qué medio?

El agente contesta con un canal y un mensaje. Según lo que elija, el aviso sale como texto, como mensaje con botones ✅/❌ para que alguien se haga cargo, o como una llamada telefónica que te habla y te pregunta si lo tomás.

Suena a exceso hasta que contás los avisos que ignoraste esta semana.

El problema no es detectar

Todos los que operamos infra tenemos el mismo cementerio: un canal de alertas que nadie mira. No porque las alertas estén mal configuradas — están bien configuradas, disparan cuando el umbral se cruza, que es exactamente lo que les pedimos. El problema es que el umbral no sabe nada del contexto.

disk > 85% es crítico un martes a las 3 AM y es “ya sé, lo estoy migrando” un sábado a las 4 de la tarde. El pod que reinicia una vez es ruido; el mismo pod reiniciando la tercera vez en veinte minutos es otra cosa. Un servicio caído que ya me avisaste hace diez minutos no necesita que me lo repitas: necesita que me digas si sigue caído o si volvió.

Esa capa —la de “¿esto amerita?”— siempre la puso una persona leyendo el canal. La idea del lab fue simple: poner ahí un modelo.

La forma: colecta → diff → decide → delega

El diseño terminó siendo cuatro piezas bien separadas, y la separación es lo que lo hace mantenible:

collectors (pull) ─┐
                   ├─→ diff (estado previo) ─→ decide (agente) ─→ dispatch ─→ efectores
webhook (push) ────┘

Collectors. Cada uno mira una cosa y devuelve señales con una key estable: salud de los servicios core, uso de VRAM en la GPU, pods del cluster en mal estado, el calendario y los mails importantes sin leer. Agregar un collector nuevo es una clase y un alta en un registro — es la parte aburrida a propósito.

Webhook. La entrada push para lo que ya tiene su propio sistema de reglas: Grafana, principalmente. Las alertas externas entran al mismo loop de decisión que las señales propias. Esto importa más de lo que parece: significa que Grafana sigue haciendo lo que hace bien (evaluar series de tiempo) y el agente hace lo que Grafana no puede (decidir si vale la pena molestarte con eso).

Diff. El estado anterior se persiste. Cada tick compara: qué apareció, qué cambió de contenido, qué desapareció. La decisión nunca se toma sobre “el estado actual” sino sobre el cambio — y con el registro de qué ya se avisó, con su TTL, para no repetir.

Decide. Se arma un prompt con la situación, los cambios y lo ya avisado, y el agente devuelve JSON: por cada aviso, un canal y el mensaje. Nada más. No manda nada.

Dispatch. Rutea el aviso al efector que corresponde según el canal. Si el efector elegido no responde, degrada a texto en vez de perder el aviso. El vigía nunca manda un mensaje por sí mismo — ni siquiera tiene el token del bot. Detecta, decide, delega.

Esa última línea es la decisión de diseño de la que estoy más conforme. El vigía no sabe mandar mensajes, y por eso no puede spamear: para llegar a vos tiene que convencer a otro proceso.

Los canales, o el escalamiento como conversación

Acá está lo que un Alertmanager no te da. El agente no elige severidad, elige cómo te llega:

CanalCuándoQué pasa
textDefault. Informativo.Mensaje y listo.
imageCuando una imagen aporta algo.Genera el gráfico y lo manda con caption.
actionableCuando hace falta que alguien se haga cargo.Mensaje con botones ✅/❌. El ack vuelve al vigía y frena el escalamiento.
voiceCuando arde.Te llama por teléfono. Atendés y un agente de voz te cuenta qué pasó.

La llamada es la parte que hace levantar las cejas en las demos, y no por la voz: por lo que pasa después. Durante la llamada podés decir “lo tomo” y el agente tiene una herramienta para marcar el incidente como tomado. No hay que abrir la notebook, ni el dashboard, ni buscar el botón. Contestás el teléfono medio dormido y decís que te estás haciendo cargo, y el sistema deja de escalar.

El criterio de canal lo pone el agente en la decisión. El dispatcher solo ejecuta.

Cómo probás algo que solo se ve cuando se rompe

Este fue el problema real de ingeniería, y me sorprendió que fuera el que más tiempo se llevó.

Un sistema de alertas es imposible de demostrar —y difícil de calibrar— porque su comportamiento interesante ocurre justo cuando algo anda mal. No podés sentarte con alguien a mostrarle el escalamiento y esperar a que se caiga un servicio de verdad.

Así que armé simuladores: procesos que escriben métricas sintéticas en una base cada pocos segundos, con un baseline realista, y escenarios de falla que son rampas graduales en vez de saltos. Una temperatura que baja de a poco hasta cruzar el warning. Una humedad que sube hasta el crítico. Un botón que dispara el escenario y otro que devuelve todo al baseline.

La rampa importa: un salto instantáneo no te deja ver el escalamiento, que es lo que estás tratando de mostrar. Con la rampa, el warning entra por texto a los 25 segundos, y si el escenario sigue, el crítico entra por llamada a los 30. La demo dura un minuto y se ve el sistema pensando.

De paso, el simulador es el banco de pruebas: cada vez que toco el prompt de decisión, puedo correr los mismos cinco escenarios y ver si el criterio se movió.

Los gotchas que me costaron noches

El falso “se recuperó”. Las señales push llegan una vez y se van. Al tick siguiente ya no están, y el diff las lee como desaparecidas — es decir, como resueltas. Te avisa que se recuperó algo que sigue roto: el peor bug posible en un sistema de alertas, porque miente para el lado tranquilizador. Se arregla persistiendo las señales push con un TTL y re-emitiéndolas en cada tick; la recuperación pasa a ser explícita (el resolved de Grafana, o una llamada al endpoint diciéndolo). Además sobrevive a reinicios, que era el mismo bug por otra puerta.

El agente filtraba las demos. La primera vez que mostré esto en vivo no pasó nada. Cero avisos. El modelo estaba haciendo exactamente lo que le pedí —evaluar si la señal amerita— y decidió, con buen criterio, que una alerta que se llama DEMO ... no es un incidente real. Terminó siendo una regla explícita: las señales marcadas como demo se entregan siempre, con urgencia alta y canal por severidad. Es la lección general de trabajar con agentes: cuando el modelo “falla”, muchas veces está siguiendo tu instrucción mejor de lo que vos la pensaste.

El ack sobre una señal que ya no está. Si alguien aprieta ✅ sobre un aviso viejo cuya señal ya se resolvió, el ack quedaba pegado y silenciaba el próximo disparo de esa misma key. O sea: te silenciaba el incidente siguiente, el de verdad. Ahora un ack sobre señal no activa se ignora. Queda un caso abierto —la señal que expira por TTL con el ack puesto deja el ack huérfano— y lo tengo anotado como deuda, mitigado con intervalos de repetición cortos.

Dedup con hash de contenido, no solo por key. La misma key con distinto contenido es un cambio y hay que avisarlo. La misma key con el mismo contenido, no. Suena obvio hasta que te llegan cuatro veces los mismos tres pods.

Lo que deliberadamente no hace

No ejecuta nada. Versión cero: observa y avisa, cero acciones.

Tengo escrito el roadmap de autonomía —proponer una acción y ejecutarla con OK, y después un set acotado de acciones de confianza que hace solo y avisa después— pero no lo apuré. Un agente que decide cuándo hablarte con criterio propio ya es bastante superficie nueva; darle además la mano para tocar el cluster es otro nivel de confianza, y esa confianza se gana con meses de decisiones que envejezcan bien.

Mientras tanto, la métrica que miro no es cuántas alertas detectó. Es cuántos avisos me llegaron que no sirvieron para nada. Ese número bajó muchísimo, y es la única razón por la que sigo mirando el canal.

Si te vas a armar uno

Tres cosas que me hubiera gustado saber antes:

  1. Separá quien decide de quien notifica. El que decide no debería tener el token del bot. Es una restricción arquitectónica que te regala varias garantías gratis.
  2. La recuperación es explícita o no es. “Dejé de ver la señal” nunca puede significar “se arregló”.
  3. Armate el simulador antes que el sistema. Vas a iterar el criterio de decisión veinte veces, y cada iteración necesita un incidente reproducible. Esperar a que se rompa algo de verdad no es un plan de testing.

Y una cuarta, más general: el valor no estuvo en meterle un LLM al monitoreo. Estuvo en admitir que la pregunta “¿esto amerita interrumpir a una persona?” siempre fue una pregunta de criterio, y que veníamos respondiéndola con números porque era lo único que teníamos.