Los logs que me faltaban: Loki, el tercer pilar de la observabilidad

Los logs que me faltaban: Loki, el tercer pilar de la observabilidad
ContenidoContents

Este es el tercer post de una serie que no había planeado como serie. Primero le puse OpenTelemetry a mi Moodle y mandé las trazas a un Jaeger en casa. Luego Grafana acabó siendo el panel único de toda mi infraestructura. En ninguno de los dos hablé de logs —y resulta que era la parte con más historia detrás—. Si te autoalojas cosas, creo que este te va a servir; y si quieres más detalle de cualquier parte, escríbeme sin problema.

El error en rojo que lo empezó todo

Un día, mirando el registro de errores de mi Moodle, me encontré esto repetido hasta el infinito:

OpenTelemetry: [error] Export failure [exception] Not Found

No una vez. En cada evento. Cada login, cada página vista, cada acción disparaba ese error. La instrumentación intentaba exportar algo y recibía un 404 a cambio, una y otra vez.

Tirando del hilo, el mensaje me llevó a una pieza inesperada: no era un fallo de las trazas (esas iban perfectas a Jaeger), era un exportador de logs. El nuevo paquete de instrumentación de Moodle, además de trazar, emitía un log por cada evento y lo mandaba a un endpoint (/v1/logs) que en mi collector no existía. De ahí el 404 en bucle.

Podía haberlo silenciado en dos líneas. Pero el error tenía razón: me faltaba una capa entera de observabilidad.

El tercer pilar

La observabilidad moderna se apoya en tres tipos de señal, y cada una responde a una pregunta distinta:

  • Trazas¿por dónde pasó una petición y dónde tardó? (Jaeger, del primer post).
  • Métricas¿cuánto está pasando, en agregado? (Prometheus + Grafana, del segundo).
  • Logs¿qué pasó, exactamente, con nombres y apellidos?

Yo tenía dos de tres. El error en rojo era, literalmente, mi Moodle intentando entregarme la tercera y yo sin nadie preparado para recogerla. Así que en vez de callar el aviso, monté al destinatario: Loki.

Montar Loki (con desagüe desde el día uno)

Loki es a los logs lo que Prometheus a las métricas: los almacena y los hace consultables desde Grafana, con su propio lenguaje (LogQL). Se monta en un contenedor y listo.

Solo que esta vez venía con una lección aprendida a base de susto. En el post anterior conté cómo Jaeger, con su almacenamiento por defecto, era un depósito con el grifo abierto y sin desagüe: la memoria solo subía. No pensaba repetirlo. Así que Loki nació con retención desde la primera línea de config:

1limits_config:
2  retention_period: 168h        # 7 días — el desagüe, desde el día 1
3compactor:
4  retention_enabled: true

Y en el collector, el pipeline de logs que me faltaba: recibe los logs por OTLP y los reenvía a Loki. Con eso, ese /v1/logs que antes daba 404 empezó a responder 200, y el error en rojo de mi Moodle desapareció solo. No lo silencié: le di un sitio a donde ir.

1service:
2  pipelines:
3    logs:
4      receivers: [otlp]
5      exporters: [otlphttp/loki]

El susto: lo que parecía un label y no lo era

Con los logs ya llegando a Loki, quise lo obvio: filtrarlos por tipo de evento. Cada log de Moodle traía un montón de campos —event_data_eventname, event_data_action, event_data_target…— y yo, feliz, escribí la consulta más natural del mundo:

{service_name="moodlelms", event_data_action="failed"}

Cero resultados. Pero los logs estaban ahí; los veía con un filtro más simple. Media hora de desconcierto hasta caer en la distinción que Loki hace y que no salta a la vista: hay labels indexados (pocos, para seleccionar streams) y structured metadata (todo lo demás, que llega por OTLP). Aquellos event_data_* no eran labels, eran metadata. Y al selector de stream, entre llaves, solo le valen los labels indexados.

La consulta buena no selecciona por esos campos, los filtra en el pipeline:

{service_name="moodlelms"} | event_data_eventname=~".*user_login_failed.*"

Lección: en la respuesta de Loki los dos tipos de campo aparecen mezclados, así que parecen lo mismo. No lo son. Uno selecciona, el otro filtra.

Alertas que nacen de los logs

Aquí es donde los logs dejan de ser un archivo bonito y se vuelven útiles. Ese último filtro —user_login_failed— es, literalmente, un intento de login fallido. Y si cuento cuántos hay en una ventana corta, tengo un detector de fuerza bruta casi gratis:

sum(count_over_time({service_name="moodlelms"}
    | event_data_eventname=~".*user_login_failed.*" [5m]))

Una regla en Grafana que salte cuando eso supere, digamos, 15 en cinco minutos, y la ruta al mismo bot de Telegram que ya me avisaba del disco y los certificados. Un usuario que se equivoca de contraseña genera dos o tres; quince en cinco minutos es alguien probando el diccionario. Lo probé a lo bestia —dieciocho logins fallidos seguidos— y el aviso llegó al móvil como un reloj.

Con la misma técnica monté otra para actividad administrativa sensible: que me avise si se borra un usuario o se cambia un rol. Si fui yo, lo sé; si no fui yo, quiero enterarme.

La lección: lo que fluye solo y lo que hay que ir a buscar

Y aquí llega lo que de verdad me llevo de todo esto, más allá de Loki.

Montar la alerta de fuerza bruta fue fácil. ¿Por qué? Porque un login fallido es un evento de Moodle, y los eventos ya viajaban solos hasta Loki gracias a la instrumentación. El dato estaba ahí; yo solo escribí la pregunta.

Poco después quise una alerta para tareas de cron que fallan. Y me di de bruces con lo contrario. Cuando una tarea de Moodle falla, no genera ningún evento que llegue a Loki, y tampoco marca su traza como error (Moodle captura la excepción por dentro y no la deja salir). El único sitio donde queda constancia de que “esta tarea está fallando” es un campo en la base de datos. Para esa alerta no me valió ni Loki ni las trazas: tuve que escribir un pequeño script que consulta la BD y publica el dato como métrica, y alertar sobre eso.

Dos alertas, dos mundos:

  • Fuerza bruta → el dato fluía solo. Trabajo: una consulta.
  • Tareas fallidas → el dato había que ir a fabricarlo. Trabajo: un puente entero.

La regla general, la que me llevo grabada: la observabilidad solo ve lo que la aplicación decide emitir. Las trazas, las métricas y los logs te dan una foto riquísima… de lo que la app publica. Lo que se queda dentro —como el estado de fallo de una tarea— no aparece por arte de magia en ningún panel: hay que ir a sacarlo tú. Saber distinguir un caso del otro es la mitad del oficio.

El trío completo

Ahora, en un mismo Grafana, tengo las tres señales de mi Moodle y de toda mi infraestructura: las trazas para ver por dónde tardó una petición, las métricas para las tendencias, y los logs para saber qué pasó exactamente, con nombres. Y encima, alertas que nacen de las tres.

Del árbol (una traza) al bosque (las métricas) al detalle (los logs). El trío que le faltaba a la serie, y que —cómo no— empezó por un error en rojo que casi silencio. Menos mal que no lo hice.

CompartirShare