A las 02:13, tu pager (o tu cliente) te avisa de que algo está roto. Haces SSH, ejecutas journalctl -xe y te recibe una cascada de mensajes que todos parecen igualmente culpables. La mitad son “normales”. La otra mitad son “no normales, pero no la causa”. En algún punto está la línea que importa: el primer error que hizo que todo lo demás empezara a gritar.
Esto trata de encontrar esa línea rápido, probar que es causal (no solo ruido) y cablear el resultado a un correo aburrido y fiable para ti mismo. No es un argumento de “plataforma de análisis de logs”. Es un hábito de producción.
Un modelo mental práctico: síntomas, disparadores y el primer evento malo
Los registros de eventos no son una narrativa. Son una multitud. Tu trabajo es encontrar a la persona que dio el primer puñetazo.
Tres categorías que debes separar
- Síntomas: reintentos, timeouts, “connection reset”, “failed to send”, “context deadline exceeded”, “upstream unavailable.” Son los servicios descendentes expresando dolor.
- Disparadores: un proceso que se cae, expiración de un certificado, un sistema de archivos remontado en modo solo lectura, una oscilación de red, un kill por OOM del kernel. Normalmente están cerca de la causa raíz.
- Ruido: chequeos periódicos de salud, cron jobs, probes de liveness, reinicios esperados, logs de depuración activados en producción, advertencias deprecation. El ruido no es “sin importancia”; simplemente no es útil ahora mismo.
Cuando la gente dice “los logs son demasiado ruidosos”, por lo general quieren decir: nunca construyeron un método para convertir una secuencia de logs en una cadena causal. El ruido es la información que no sabes indexar.
La regla del “primer evento malo”
La mayoría de los incidentes siguen una de estas formas:
- Un fallo duro: p. ej., error de I/O del disco → sistema de archivos remonta en solo lectura → base de datos entra en pánico → errores en la API.
- Un fallo lento: p. ej., pico de latencia → cola de peticiones crece → pool de hilos se satura → timeouts por todas partes.
- Una mala configuración: p. ej., rotación de certificados fallida → fallan los handshakes TLS → fallan los health checks → el autoscaler entra en inestabilidad.
- La mentira de “nada cambió”: una dependencia cambió fuera de tu control (hora, DNS, despliegue upstream, plano de control cloud).
Tu movimiento de mayor impacto es identificar la marca temporal más temprana donde la realidad se desvió de lo normal, luego caminar hacia adelante y ver cómo se expande el radio de impacto. Camina hacia atrás también, pero hacia atrás sirve sobre todo para eliminar pistas falsas.
Los logs solo tienen sentido cuando alineas los relojes
Si los relojes de tu flota derivan, la correlación es ficción. Arregla NTP/chrony antes de ponerte creativo con el parsing. El tiempo es la clave primaria de las operaciones.
Una idea parafraseada que vale la pena clavar en tu monitor (y verás por qué en el playbook): paraphrased idea
— John Allspaw, sobre el valor de la respuesta a incidentes basada en evidencia y aprender del comportamiento real del sistema.
Hechos e historia interesantes que vuelven raros los logs modernos
- Syslog precede a la mayor parte de tu stack. Fue diseñado en los años 80 para mensajes de texto simples, no para payloads JSON ni trace IDs.
- Los timestamps de RFC 3164 syslog carecen del año. Por eso algunos parsers adivinan y a veces se equivocan alrededor de Año Nuevo.
- El journal de systemd almacena campos estructurados. Puedes filtrar por
_SYSTEMD_UNIT,PID,_COMMy más sin regexear texto libre. - El ring buffer del kernel no es un archivo fiable.
dmesges un buffer circular; bajo estrés, los mensajes más importantes pueden sobrescribirse primero. - Los niveles de log son construcciones sociales. Muchas aplicaciones tratan “ERROR” como “quiero atención” y “WARN” como “también quiero atención pero con menos vergüenza”.
- rsyslog y syslog-ng se diseñaron para throughput. Pueden descartar mensajes bajo carga si están mal configurados o si el I/O de disco se detiene; “logs faltantes” pueden ser un síntoma.
- Las alertas por correo existen desde antes que los webhooks. SMTP es aburrido, ubicuo y sigue siendo uno de los mecanismos de notificación de último tramo más fiables cuando las APIs están en llamas.
- El rate limiting de journald es una espada de doble filo. Puede salvar tu disco durante una falla con spam, pero también puede ocultar la frecuencia de un patrón de error.
- Las fallas de almacenamiento a menudo susurran primero. Errores UDMA CRC, resets de enlace o errores de checksum en ZFS pueden aparecer mucho antes de un “I/O error” contundente.
Y sí, tus logs contendrán al menos una línea que es técnicamente correcta pero emocionalmente inútil.
Chiste 1: Si tu plan de respuesta a incidentes es “tailear los logs y sentir emociones”, felicitaciones—has inventado la depuración artesanal.
Plan de diagnóstico rápido: primeras / segundas / terceras comprobaciones
Esta es la secuencia que uso cuando no sé qué está mal aún. Está optimizada para “encontrar el cuello de botella rápido”, no para construir una narrativa perfecta. La narrativa perfecta la puedes hacer después, en el postmortem, cuando el sistema esté estable y tu café sea legal.
Primero: establece la ventana del incidente y el radio de impacto
- Elige una ventana temporal. Empieza con “cuando los usuarios lo notaron” y expande 10–30 minutos hacia atrás.
- Identifica los host(s) y unit(s) afectados. ¿Es un nodo, una AZ, un servicio o todo?
- Busca el primer error duro. Kernel, almacenamiento, OOM, caída de servicio, fallo TLS.
Segundo: revisa las “tres grandes” restricciones de recursos
- ¿Saturación de CPU o throttling? Busca picos de load, latencia del scheduler, throttling de cgroup.
- ¿Presión de memoria? OOM kills, swapping, tormentas de reclaim.
- ¿I/O y almacenamiento? Latencia de disco, errores de sistema de archivos, resets de controlador, eventos ZFS, profundidad de cola.
Tercero: confirma causalidad con correlación
- Correlaciona timestamps. ¿El error de la app sigue al evento de kernel/almacenamiento?
- Encuentra la “primera aparición”. El mensaje más temprano del patrón suele ser el disparador.
- Valida con una segunda señal. Métricas si las tienes; si no, usa otros logs (auth, kernel, almacenamiento).
Cuando estás atascado
- Cambia de grepear a filtrar por origen: unit, PID, ejecutable, container ID.
- Reduce el alcance: un host, un minuto, una unit.
- Mira las capas de kernel y almacenamiento aunque “sepas” que es un bug de la app. No lo sabes.
Tareas prácticas (comandos, salidas, decisiones)
Estas son tareas reales que puedes ejecutar en una caja Linux con systemd. Cada una incluye: comando, qué significa la salida y qué decisión tomar a continuación. El objetivo es convertir “sopa de logs” en una secuencia de decisiones.
Tarea 1: Confirma la sincronía de relojes (porque la correlación depende de ello)
cr0x@server:~$ timedatectl
Local time: Tue 2026-02-05 02:21:18 UTC
Universal time: Tue 2026-02-05 02:21:18 UTC
RTC time: Tue 2026-02-05 02:21:18
Time zone: Etc/UTC (UTC, +0000)
System clock synchronized: yes
NTP service: active
RTC in local TZ: no
Significado: Quieres “System clock synchronized: yes” y NTP activo.
Decisión: Si los relojes no están sincronizados, arregla la hora primero (chrony/systemd-timesyncd) o tu correlación de logs te mentirá.
Tarea 2: Acota la ventana del incidente rápidamente
cr0x@server:~$ journalctl --since "2026-02-05 01:45:00" --until "2026-02-05 02:15:00" -p err..alert --no-pager
Feb 05 01:52:11 server kernel: blk_update_request: I/O error, dev sdb, sector 1946152832 op 0x0:(READ) flags 0x0 phys_seg 1 prio class 0
Feb 05 01:52:11 server kernel: Buffer I/O error on dev sdb1, logical block 243269104, async page read
Feb 05 01:52:12 server systemd[1]: postgresql.service: Main process exited, code=killed, status=9/KILL
Feb 05 01:52:12 server systemd[1]: postgresql.service: Failed with result 'signal'.
Significado: Esto muestra solo eventos de alta severidad. El error de I/O del kernel aparece antes de que PostgreSQL muera. Ese orden importa.
Decisión: Trata el almacenamiento como sospechoso de inmediato; no pierdas 45 minutos “afinando Postgres” mientras tu disco devuelve errores de I/O.
Tarea 3: Identificar la primera ocurrencia de un patrón
cr0x@server:~$ journalctl --since "2026-02-05 01:00:00" | grep -m1 -E "I/O error, dev sdb|Buffer I/O error"
Feb 05 01:52:11 server kernel: blk_update_request: I/O error, dev sdb, sector 1946152832 op 0x0:(READ) flags 0x0 phys_seg 1 prio class 0
Significado: -m1 se detiene en la primera coincidencia, dándote la aparición más temprana conocida en ese flujo.
Decisión: Usa esa marca temporal como punto pivot. Todo lo que venga después es potencialmente consecuencia.
Tarea 4: Filtrar logs por unit de systemd en lugar de grepear todo
cr0x@server:~$ journalctl -u postgresql.service --since "2026-02-05 01:45:00" --no-pager | tail -n 12
Feb 05 01:52:10 server postgres[21455]: LOG: could not read block 243269104 in file "base/16384/2619": read only 0 of 8192 bytes
Feb 05 01:52:10 server postgres[21455]: LOG: unexpected pageaddr 0/0 in log segment 0000000100000000000000A3, offset 0
Feb 05 01:52:11 server postgres[21455]: PANIC: could not read from log segment 0000000100000000000000A3 at offset 0: read only 0 of 8192 bytes
Feb 05 01:52:12 server systemd[1]: postgresql.service: Main process exited, code=killed, status=9/KILL
Feb 05 01:52:12 server systemd[1]: postgresql.service: Failed with result 'signal'.
Significado: La base de datos reporta lecturas cortas. Eso no es un problema de configuración; es o almacenamiento o kernel.
Decisión: Deja de asumir “bug de la base de datos.” Escala la investigación a almacenamiento; considera hacer failover.
Tarea 5: Buscar OOM kills (el asesino silencioso)
cr0x@server:~$ journalctl -k --since "2026-02-05 01:45:00" | grep -E "Out of memory|oom-kill|Killed process" | tail -n 5
Feb 05 01:47:03 server kernel: oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0,global_oom,task_memcg=/system.slice/api.service,task=java,pid=18802,uid=1001
Feb 05 01:47:03 server kernel: Killed process 18802 (java) total-vm:7421860kB, anon-rss:4823112kB, file-rss:0kB, shmem-rss:0kB, UID:1001 pgtables:12544kB oom_score_adj:0
Significado: El kernel mató un proceso por presión de memoria, y nombra el cgroup (/system.slice/api.service).
Decisión: Si el OOM coincide con el inicio del incidente, trata la presión de memoria como el disparador. Si ocurre más tarde, puede ser consecuencia de reintentos y crecimiento de colas.
Tarea 6: Verificar el rate limiting de journald (porque los logs faltantes son un problema)
cr0x@server:~$ journalctl -u systemd-journald --since "2026-02-05 01:45:00" --no-pager | tail -n 8
Feb 05 01:51:58 server systemd-journald[412]: Suppressed 1529 messages from postgresql.service
Feb 05 01:52:01 server systemd-journald[412]: Suppressed 941 messages from kernel
Significado: Estás perdiendo detalle durante la parte más caliente del incidente. El patrón sigue importando, pero conteos y secuencias pueden estar incompletos.
Decisión: Si la supresión es significativa, recoge otras evidencias (contadores del kernel, herramientas de almacenamiento). Considera ajustar límites de tasa después del incidente, no durante.
Tarea 7: Detectar problemas de almacenamiento/enlace del kernel en dmesg/journal
cr0x@server:~$ dmesg -T | egrep -i "ata[0-9]|nvme|reset|link is down|I/O error|blk_update_request" | tail -n 12
[Tue Feb 5 01:52:10 2026] ata2.00: exception Emask 0x0 SAct 0x0 SErr 0x0 action 0x6 frozen
[Tue Feb 5 01:52:10 2026] ata2.00: failed command: READ FPDMA QUEUED
[Tue Feb 5 01:52:11 2026] blk_update_request: I/O error, dev sdb, sector 1946152832 op 0x0:(READ) flags 0x0 phys_seg 1 prio class 0
[Tue Feb 5 01:52:11 2026] ata2: hard resetting link
[Tue Feb 5 01:52:12 2026] ata2: SATA link up 6.0 Gbps (SStatus 133 SControl 300)
Significado: Resets de enlace + lecturas fallidas son territorio clásico de “drive, cable, controlador o backplane”.
Decisión: Planifica reemplazo de hardware o migración. Si es una VM con discos virtuales, trátalo como inestabilidad del almacenamiento subyacente e involucra a tu proveedor/equipo de plataforma.
Tarea 8: Confirmar señales de salud del sistema de archivos
cr0x@server:~$ journalctl -k --since "2026-02-05 01:45:00" | egrep -i "EXT4-fs error|XFS.*corruption|Remounting filesystem read-only|I/O error" | tail -n 10
Feb 05 01:52:11 server kernel: EXT4-fs error (device sdb1): ext4_find_entry:1456: inode #131104: comm postgres: reading directory lblock 0
Feb 05 01:52:11 server kernel: Aborting journal on device sdb1-8.
Feb 05 01:52:11 server kernel: EXT4-fs (sdb1): Remounting filesystem read-only
Significado: El sistema de archivos se remontó en solo lectura. Las apps fallarán de maneras extrañas a partir de aquí.
Decisión: Deja de “reiniciar servicios.” Necesitas hacer failover, remontar o reparar. Los reinicios solo agregan ruido y pueden empeorar la corrupción.
Tarea 9: Si usas ZFS, revisa eventos y errores del pool
cr0x@server:~$ sudo zpool status -x
pool: tank
state: DEGRADED
status: One or more devices has experienced an error resulting in data corruption.
action: Replace the device using 'zpool replace'.
scan: scrub repaired 0B in 0 days 00:12:44 with 3 errors on Tue Feb 5 01:55:21 2026
config:
NAME STATE READ WRITE CKSUM
tank DEGRADED 0 0 0
mirror-0 DEGRADED 0 0 0
sdb FAULTED 5 0 3 too many errors
sdc ONLINE 0 0 0
errors: Permanent errors have been detected in the following files:
/tank/pg/wal/0000000100000000000000A3
Significado: Esto no es “quizá.” ZFS te dice que un dispositivo está fallado y puede nombrar archivos afectados.
Decisión: Reemplaza el disco fallado, restaura datos afectados desde réplica/backup y trata los errores de aplicación como síntomas descendentes hasta probar lo contrario.
Tarea 10: Ver qué servicios hicieron flap (las tormentas de reinicios crean falsas causas raíz)
cr0x@server:~$ systemctl list-units --type=service --state=failed
UNIT LOAD ACTIVE SUB DESCRIPTION
postgresql.service loaded failed failed PostgreSQL RDBMS
api.service loaded failed failed Example API
LOAD = Reflects whether the unit definition was properly loaded.
ACTIVE = The high-level unit activation state.
SUB = The low-level unit activation state.
Significado: Esto es un inventario rápido de “qué está visiblemente roto”. No te dice por qué.
Decisión: Úsalo para acotar el radio de impacto, luego inspecciona los logs de cada unit alrededor de la falla más temprana.
Tarea 11: Extraer campos estructurados de journald (deja de usar regex cuando puedas filtrar)
cr0x@server:~$ journalctl -u api.service --since "2026-02-05 01:45:00" -o json | head -n 3
{"_SYSTEMD_UNIT":"api.service","PRIORITY":"3","MESSAGE":"DB connection failed: timeout","_PID":"18890","_COMM":"java","__REALTIME_TIMESTAMP":"1738720565000000"}
{"_SYSTEMD_UNIT":"api.service","PRIORITY":"3","MESSAGE":"DB connection failed: timeout","_PID":"18890","_COMM":"java","__REALTIME_TIMESTAMP":"1738720566000000"}
{"_SYSTEMD_UNIT":"api.service","PRIORITY":"4","MESSAGE":"Retrying in 500ms","_PID":"18890","_COMM":"java","__REALTIME_TIMESTAMP":"1738720566000000"}
Significado: Puedes parsear programáticamente campos como unit, PID, priority, timestamp. Así es como construyes alertas fiables.
Decisión: Si escribes automatización, prefiere salida JSON. El texto es para humanos; los campos estructurados son para máquinas.
Tarea 12: Contar ráfagas de errores para ver si tratas con una tormenta
cr0x@server:~$ journalctl -u api.service --since "2026-02-05 01:45:00" --until "2026-02-05 02:00:00" -p err --no-pager | wc -l
842
Significado: 842 mensajes de nivel error en 15 minutos no es “unos pocos fallos”. Es un problema sistémico o un bucle de reintentos agresivo.
Decisión: Si el conteo es enorme, céntrate en identificar la dependencia upstream que está fallando y considera amortiguar reintentos o habilitar circuit breakers.
Tarea 13: Correlación cross-host rápida y tosca (cuando no tienes logging centralizado)
cr0x@server:~$ for h in app01 app02 db01; do echo "== $h =="; ssh $h "journalctl --since '2026-02-05 01:50:00' --until '2026-02-05 01:55:00' -p err --no-pager | head -n 3"; done
== app01 ==
Feb 05 01:52:13 app01 api[18890]: DB connection failed: timeout
Feb 05 01:52:14 app01 api[18890]: DB connection failed: timeout
Feb 05 01:52:15 app01 api[18890]: DB connection failed: timeout
== app02 ==
Feb 05 01:52:13 app02 api[19011]: DB connection failed: timeout
Feb 05 01:52:14 app02 api[19011]: DB connection failed: timeout
Feb 05 01:52:15 app02 api[19011]: DB connection failed: timeout
== db01 ==
Feb 05 01:52:11 db01 kernel: blk_update_request: I/O error, dev sdb, sector 1946152832 op 0x0:(READ) flags 0x0 phys_seg 1 prio class 0
Feb 05 01:52:11 db01 kernel: EXT4-fs (sdb1): Remounting filesystem read-only
Feb 05 01:52:12 db01 systemd[1]: postgresql.service: Main process exited, code=killed, status=9/KILL
Significado: Los nodos de app muestran timeouts; el nodo de DB muestra errores de almacenamiento. Esa es tu cadena, con timestamps.
Decisión: Deja de solucionar apps. Inicia failover/recuperación de DB, luego arregla el almacenamiento subyacente.
Tarea 14: Extraer la línea del “error real” con contexto (las 10 líneas alrededor)
cr0x@server:~$ journalctl --since "2026-02-05 01:45:00" --no-pager | grep -n -E "Remounting filesystem read-only|blk_update_request: I/O error" | head -n 1
1823:Feb 05 01:52:11 server kernel: EXT4-fs (sdb1): Remounting filesystem read-only
cr0x@server:~$ journalctl --since "2026-02-05 01:45:00" --no-pager | sed -n '1813,1833p'
Feb 05 01:52:10 server kernel: ata2.00: failed command: READ FPDMA QUEUED
Feb 05 01:52:11 server kernel: blk_update_request: I/O error, dev sdb, sector 1946152832 op 0x0:(READ) flags 0x0 phys_seg 1 prio class 0
Feb 05 01:52:11 server kernel: Buffer I/O error on dev sdb1, logical block 243269104, async page read
Feb 05 01:52:11 server kernel: EXT4-fs error (device sdb1): ext4_find_entry:1456: inode #131104: comm postgres: reading directory lblock 0
Feb 05 01:52:11 server kernel: Aborting journal on device sdb1-8.
Feb 05 01:52:11 server kernel: EXT4-fs (sdb1): Remounting filesystem read-only
Feb 05 01:52:12 server systemd[1]: postgresql.service: Main process exited, code=killed, status=9/KILL
Feb 05 01:52:13 server api[18890]: DB connection failed: timeout
Significado: El contexto convierte una sola línea aterradora en una secuencia causal: fallo de lectura ATA → error de I/O de bloque → aborto del journal del sistema de archivos → remount en solo lectura → la base de datos muere → timeouts en la app.
Decisión: Ese es tu ancla de la línea temporal del incidente. Úsalo para impulsar acción (failover) y luego escribir un postmortem limpio.
Tarea 15: Cuando el “error real” es TLS/certificados, encuéntralo una vez, no 5.000 veces
cr0x@server:~$ journalctl -u nginx.service --since "2026-02-05 00:00:00" --no-pager | grep -m1 -E "certificate|SSL_do_handshake|PEM_read_bio"
Feb 05 01:03:44 server nginx[901]: [emerg] SSL_CTX_use_PrivateKey_file("/etc/nginx/tls/site.key") failed (SSL: error:0B080074:x509 certificate routines:X509_check_private_key:key values mismatch)
Significado: Una línea explica la caída: key/cert mismatch. Todo lo demás son clientes fallando al conectar.
Decisión: Arregla el par cert/key, recarga y luego añade un paso de validación pre-despliegue para que esto no vuelva a ocurrir.
Tarea 16: Detectar fallos por “optimización” como rotación agresiva de logs o llenado de tmpfs
cr0x@server:~$ journalctl --since "2026-02-05 01:00:00" -p warning --no-pager | grep -E "No space left on device|failed to write|ENOSPC" | head -n 5
Feb 05 01:11:02 server systemd-journald[412]: Failed to write entry (24 items, 882 bytes), ignoring: No space left on device
Feb 05 01:11:03 server rsyslogd[777]: action 'action-0-builtin:omfile' suspended (module 'builtin:omfile'), retry 0. There is no space left on device.
Significado: Tu sistema de logging no puede escribir. Eso significa que estás perdiendo evidencia durante un incidente.
Decisión: Libera espacio inmediatamente; luego reevalúa dónde se almacenan los logs, la política de rotación y si los cambios para “ahorrar disco” están saboteando la observabilidad.
Tres microhistorias corporativas desde las trincheras de logs
Microhistoria 1: El incidente causado por una suposición equivocada
La compañía estaba en mitad de una migración: nuevo cluster Kubernetes, nuevo service mesh, todo nuevo. Una API orientada al cliente empezó a devolver 502s de forma intermitente. El ingeniero on-call hizo lo que la mayoría hacemos bajo presión: buscó en los logs “error” y vio una tormenta de mensajes “upstream reset” en el proxy.
La suposición se formó al instante: “bug del service mesh.” Se montó una war room. La gente ajustó timeouts del proxy, reinició sidecars, cambió feature flags. La tasa de error se movió, lo que se sintió como progreso, pero en realidad era varianza aleatoria confundida con control.
Eventualmente, alguien revisó los logs del kernel en un nodo. No los logs de la app, no los del proxy. El nodo reportaba flaps del NIC y retransmisiones TCP. El proxy era inocente; solo fue el primer componente lo bastante honesto como para quejarse. Cuando el equipo correlacionó timestamps, cada reset upstream se alineaba con un ciclo link down/up.
La causa raíz fue mundana: un puerto del switch top-of-rack configurado con velocidad/duplex incorrectos tras mantenimiento rutinario. El “incidente de mesh” era un incidente de red con disfraz de proxy.
La lección no fue “siempre culpar a la red.” Fue: no dejes que el primer mensaje de error plausible se convierta en tu causa raíz. Los logs te dicen lo que un componente observó, no lo que causó que la realidad se rompiera.
Microhistoria 2: La optimización que salió mal
Un equipo de plataforma intentaba reducir uso de disco en una flota de nodos de base de datos. Alguien notó que journald consumía una cantidad sorprendente de espacio. La solución pareció limpia: limitar agresivamente el tamaño del journal y habilitar rate limiting más estricto. Menos disco, menos escrituras, SSDs más felices. A todos les encantó la victoria.
Dos semanas después, un nodo experimentó picos intermitentes de latencia de almacenamiento. PostgreSQL empezó a registrar fsyncs lentos y ocasionales bloqueos de escritura WAL. La capa de aplicación vio timeouts. El on-call intentó reconstruir la línea temporal, pero los mensajes del kernel alrededor del pico faltaban. Journald había suprimido mucho, y el journal capado había rotado las advertencias tempranas.
Sin la evidencia temprana, el equipo se centró en la configuración de la base de datos. Ajustaron checkpoints, movieron settings de WAL, ajustaron ratios de dirty del kernel. Algunos cambios mejoraron el rendimiento en estado estable y dieron la sensación reconfortante de “hacer ingeniería”. Pero los picos persistieron.
Más tarde, durante una ventana de mantenimiento, alguien ejecutó un scrub en el pool de almacenamiento y encontró errores de checksum en aumento. Un disco se estaba degradando lentamente, y el sistema había estado avisando. Esas advertencias fueron las primeras en rotarse porque ocurrieron “antes del incidente”.
La optimización no causó la falla hardware. Causó que el equipo perdiera la única señal barata y temprana que podría haber acortado el incidente. Ahorrar disco estaba bien; ahorrar disco sacrificando retención forense no lo estaba.
Microhistoria 3: La práctica aburrida pero correcta que salvó el día
Un servicio de pagos tenía un hábito que nadie celebraba: cada despliegue requería una pequeña “lista de verificación de sanidad de logs”. No una gran auditoría de seguridad. Solo comprobaciones aburridas—sincronía de tiempo, espacio en disco, salud de journald y una simple transacción end-to-end en un entorno canario.
Una mañana, el canario falló. Los logs mostraron errores de handshake TLS. El ingeniero de turno no hizo grep a tontas y a locas; filtró por unit y buscó el primer error relacionado con certificados. Fue inmediato: el servicio había cargado una nueva cadena de certificados, pero faltaba un certificado intermedio. El servicio seguía iniciando. Simplemente rechazaba clientes que requerían la cadena completa.
La corrección fue una línea: instalar el intermedio faltante y recargar. No hubo incidente, ni impacto al cliente. La checklist lo atrapó antes de que se escapara. El equipo siguió, ligeramente molesto porque “no pasó nada”, que es la respuesta emocional correcta a un proceso bien diseñado.
Esa práctica no se sentía innovadora. Lo fue. La mayoría de los outages se previenen con cosas que parecen papeleo hasta que las necesitas.
Envíate un correo: una canalización de alertas de producción desde logs
El logging centralizado es genial. A veces no lo tienes. A veces es lo que está caído. El correo, con todos sus defectos, es resistente, de baja dependencia y accesible casi desde cualquier lugar. “Envíate un correo” no es primitivo; es pragmático.
El truco es evitar dos modos de fallo:
- Spam de alertas: lo ignorarás y luego perderás el mensaje que importaba.
- Silencio de alertas: el script falla, SMTP falla, DNS falla o journald suprime eventos y nunca lo sabes.
Principios de diseño para alertas de logs a correo
- Alerta sobre disparadores, no sobre síntomas. “EXT4 remounted read-only” supera “API timeout” por mucho.
- Deduplica agresivamente. El correo no es una serie temporal de métricas.
- Incluye contexto. Envía la primera línea de error más una pequeña ventana de líneas circundantes.
- Hazlo testeable. Debes poder ejecutar el script y ver una salida única y determinista.
- Falla ruidosamente. Si la canalización de alertas no puede enviar, registra esa falla en algún lugar que veas.
Una implementación simple y robusta: timer de systemd + cursor de journalctl
Usaremos el cursor de journald para recordar hasta dónde llegamos. Esto evita “reenviar el mundo” y es más fiable que adivinar ventanas temporales.
1) Crea un script de alerta
Este script busca eventos de alta severidad y un puñado de patrones de disparador específicos (almacenamiento, OOM, remount read-only, desajuste de certificado). Envía un correo por ejecución, deduplicado, con el hallazgo “top”.
cr0x@server:~$ sudo tee /usr/local/sbin/journal-alert-email.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
STATE_DIR="/var/lib/journal-alert"
CURSOR_FILE="$STATE_DIR/cursor"
HOST="$(hostname -f 2>/dev/null || hostname)"
TO_ADDR="oncall@example.com"
FROM_ADDR="journal-alert@$HOST"
SUBJECT_PREFIX="[journal-alert]"
TMP="$(mktemp)"
trap 'rm -f "$TMP"' EXIT
mkdir -p "$STATE_DIR"
chmod 700 "$STATE_DIR"
# Pull new high-priority logs since last cursor.
# We intentionally include kernel + system units; customize for your environment.
if [[ -f "$CURSOR_FILE" ]]; then
journalctl --after-cursor "$(cat "$CURSOR_FILE")" -p err..alert -o short-iso --no-pager > "$TMP" || true
else
journalctl --since "10 minutes ago" -p err..alert -o short-iso --no-pager > "$TMP" || true
fi
# Update cursor regardless; prevents loops if one bad message repeats.
journalctl -n 1 -o json --no-pager | sed -n 's/.*"__CURSOR":"\([^"]*\)".*/\1/p' > "$CURSOR_FILE" || true
chmod 600 "$CURSOR_FILE"
# If no new events, exit quietly.
if [[ ! -s "$TMP" ]]; then
exit 0
fi
# Prefer trigger patterns; fall back to first error line.
TRIGGER_LINE="$(grep -m1 -E \
'Remounting filesystem read-only|blk_update_request: I/O error|Buffer I/O error|oom-kill|Killed process|X509_check_private_key|SSL_CTX_use_PrivateKey_file|No space left on device|Permanent errors have been detected' \
"$TMP" || true)"
if [[ -z "$TRIGGER_LINE" ]]; then
TRIGGER_LINE="$(head -n 1 "$TMP")"
fi
# Add some context around the trigger if present in TMP.
CONTEXT="$(awk -v needle="$TRIGGER_LINE" '
BEGIN{found=0}
$0==needle{found=1; start=NR-5; end=NR+10}
{lines[NR]=$0}
END{
if(found){
for(i=(start<1?1:start); i<=end; i++) if(i in lines) print lines[i]
} else {
for(i=1;i<=20;i++) if(i in lines) print lines[i]
}
}' "$TMP")"
SUBJECT="$SUBJECT_PREFIX $HOST $(echo "$TRIGGER_LINE" | cut -c1-120)"
BODY=$(cat <<EOT
Host: $HOST
Time: $(date -Is)
Trigger:
$TRIGGER_LINE
Context:
$CONTEXT
EOT
)
# Send email. Requires a local MTA or msmtp configured.
printf "%s\n" "$BODY" | /usr/bin/mail -s "$SUBJECT" -r "$FROM_ADDR" "$TO_ADDR"
EOF
sudo chmod 750 /usr/local/sbin/journal-alert-email.sh
Qué significa: Esto usa journald como fuente de verdad y un cursor para el estado. Envía un mensaje por ejecución, lo que lo hace naturalmente rate-limited.
Decisión: Si no puedes garantizar un MTA local funcionando, usa msmtp o un relay. No dependas del “laptop de alguien” para la entrega de alertas.
2) Configura un remitente de correo mínimo (ejemplo: msmtp)
En muchos sistemas mail puede funcionar con un MTA local. Si no tienes uno, msmtp es una opción común. Este es un ejemplo de config; adáptalo a tu entorno.
cr0x@server:~$ sudo tee /etc/msmtprc >/dev/null <<'EOF'
defaults
auth on
tls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile /var/log/msmtp.log
account relay
host smtp.relay.local
port 587
from journal-alert@server.example
user journal-alert@server.example
passwordeval "cat /etc/msmtp-password"
account default : relay
EOF
sudo chmod 600 /etc/msmtprc
Qué significa: Las credenciales se mantienen fuera del script. El mailer registra en un archivo que puedes auditar.
Decisión: Si no puedes asegurar credenciales apropiadamente, no las envíes. Usa un relay local con auth por IP, o un servicio SMTP interno.
3) Crea un servicio + timer de systemd
cr0x@server:~$ sudo tee /etc/systemd/system/journal-alert-email.service >/dev/null <<'EOF'
[Unit]
Description=Send email alerts for high-severity journal events
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/journal-alert-email.sh
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
EOF
cr0x@server:~$ sudo tee /etc/systemd/system/journal-alert-email.timer >/dev/null <<'EOF'
[Unit]
Description=Run journal email alert script every minute
[Timer]
OnBootSec=2min
OnUnitActiveSec=1min
AccuracySec=10s
Persistent=true
[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now journal-alert-email.timer
Qué significa: Cada minuto, el timer ejecuta el script una vez. Si el host estuvo caído, Persistent=true ejecuta los intervalos perdidos al arrancar (útil para capturar fallos tempranos en el arranque).
Decisión: Si te preocupa el volumen de correos, aumenta el intervalo a 2–5 minutos y deduplica dentro del script. No lo ejecutes cada 5 segundos. Acabarás odiando tu propia bandeja de entrada.
4) Prueba la canalización de extremo a extremo
cr0x@server:~$ sudo systemctl start journal-alert-email.service
cr0x@server:~$ sudo systemctl status journal-alert-email.service --no-pager
● journal-alert-email.service - Send email alerts for high-severity journal events
Loaded: loaded (/etc/systemd/system/journal-alert-email.service; static)
Active: inactive (dead) since Tue 2026-02-05 02:23:20 UTC; 2s ago
Process: 22901 ExecStart=/usr/local/sbin/journal-alert-email.sh (code=exited, status=0/SUCCESS)
Qué significa: status=0/SUCCESS significa que el script corrió. Puede que no haya enviado nada si no hubo nuevos errores.
Decisión: Si necesitas un correo de prueba, temporalmente baja el umbral o genera una línea de error controlada en una unit/entorno seguro.
Chiste 2: Las alertas por correo son como detectores de humo: solo las notas cuando son molestas, y justo entonces están haciendo su trabajo.
Listas de verificación / plan paso a paso
Lista: encontrar el error real en 10 minutos
- Fija la ventana: define
sinceyuntil; expande hacia atrás si es necesario. - Extrae alta severidad primero:
journalctl -p err..alertpara la ventana. - Escanea por disparadores: I/O del kernel, sistema de archivos en solo lectura, OOM, caída de servicio, errores TLS/cert, ENOSPC.
- Encuentra la línea del disparador más temprana: usa
grep -m1o escaneos basados en cursor. - Añade contexto: captura 10 líneas antes/después o filtra por unit.
- Correlaciona entre capas: logs de unit de app + systemd + kernel.
- Decide acción: failover, rollback, reinicio (rara vez), o escalar a hardware/red/plataforma.
- Preserva evidencia: copia fragmentos relevantes del journal y logs del kernel antes de que el sistema reinicie o rote.
Lista: alertas por correo seguras para producción
- Elige patrones de disparador por los que realmente quieras despertarte.
- Implementa estado por cursor para evitar duplicados y problemas de deriva temporal.
- Deduplica dentro de una ejecución (envía un correo con el disparador principal + contexto).
- Usa una ruta de correo fiable (relay local o SMTP autenticado).
- Ejecuta desde timer de systemd y monitoriza fallos del propio servicio de alertas.
- Prueba trimestralmente (sí, hazlo): las canalizaciones de alertas se degradan silenciosamente.
Lista: triaje de logs enfocado en almacenamiento (porque el almacenamiento miente al ir lento)
- Busca en logs del kernel resets de enlace, errores I/O, remounts.
- Revisa eventos RAID/ZFS/MD si aplica.
- Confirma que el “error de app” empezó después de las advertencias de almacenamiento.
- Haz failover o reduce carga antes de intentar reparaciones.
- Tras la estabilidad, ejecuta scrubs/checks y reemplaza componentes marginales.
Errores comunes: síntoma → causa raíz → reparación
1) “Todo está timing out”
Síntoma: logs de app llenos de timeouts; reverse proxy muestra errores upstream.
Causa raíz: nodo de base de datos con sistema de archivos remount en solo lectura tras errores de I/O; la app es inocente.
Reparación: revisa logs del kernel + sistema de archivos, haz failover de la base de datos, reemplaza disco/cable/controlador fallante; deja de reiniciar servicios.
2) “El servicio sigue reiniciándose, debe ser un crash loop bug”
Síntoma: systemd muestra reinicios repetidos; logs de la app terminan abruptamente.
Causa raíz: kernel OOM killer que termina el proceso (a menudo por fuga de memoria, picos de tráfico o límites de cgroup).
Reparación: confirma OOM en logs del kernel, aumenta memoria o corrige la fuga, ajusta límites de cgroup y añade backpressure/circuit breakers.
3) “No hay errores en los logs, así que no es el host”
Síntoma: no ves los mensajes de kernel/almacenamiento esperados durante el incidente.
Causa raíz: supresión de journald, rotación agresiva o disco lleno impidió escribir logs.
Reparación: revisa logs de journald por supresión y ENOSPC, aumenta retención, asegura que particiones de logs tengan margen y alerta sobre “pipeline de logging no saludable”.
4) “Lo arreglamos aumentando timeouts”
Síntoma: subir timeouts hace que la tasa de error baje temporalmente.
Causa raíz: dependencia lenta (latencia de almacenamiento, problemas DNS, DB sobrecargada) fue el disparador; los timeouts solo movieron el dolor.
Reparación: identifica el disparador de latencia/IO más temprano, reduce carga o haz failover; ajusta timeouts solo tras la estabilidad para evitar tormentas de reintentos.
5) “La rotación de certificados es automática, así que TLS no puede ser el problema”
Síntoma: fallos de handshake tras un despliegue; clientes reportan errores en la cadena del certificado.
Causa raíz: parche key/cert desajustado, intermedio faltante, permisos de archivo incorrectos o un proceso que no recargó.
Reparación: busca la primera línea de error TLS/key, valida par cert/key en CI, fuerza comprobaciones de recarga y alerta sobre expiración próxima.
6) “Es un problema de red” (cada vez)
Síntoma: resets intermitentes, retransmisiones, fallos aleatorios.
Causa raíz: a veces red, sí. Otras veces: CPU agotada causando ACKs retrasados, stalls de disco que congelan procesos o agotamiento de conntrack.
Reparación: corrobora con logs del kernel y señales de presión de recursos del host. No busques chivos expiatorios. Prueba.
7) “Simplemente haremos grep de ERROR”
Síntoma: coincidencias infinitas, ninguna claridad, enfoque erróneo.
Causa raíz: mal uso de severidad en logs y falta de filtrado estructurado (unit/PID/campos).
Reparación: filtra por unit y prioridad, luego busca patrones de disparador; usa campos JSON para automatización.
FAQ
1) ¿Qué es el “error real” en un registro de eventos?
Es el evento de alta señal más temprano que explica fallos downstream: el primer error de I/O, el primer kill por OOM, el primer desajuste de clave TLS, el primer “remount en solo lectura”. No es el timeout número 50.
2) ¿Por qué no enviar todo a una plataforma de logs y buscar allí?
Deberías hacerlo cuando puedas. Pero la respuesta a incidentes no puede depender de un sistema que podría degradarse por el mismo incidente. Las herramientas locales primero son tu balsa de salvamento.
3) ¿Por qué journalctl -xe se siente inútil durante incidentes?
Porque mezcla severidad, alcance y tiempo de una forma que es genial para “qué acaba de pasar” pero mala para “qué causó el incidente”. Usa ventanas acotadas, prioridades y filtros por unit.
4) ¿Cómo evito la fatiga de alertas al enviarme correos?
Alerta sobre disparadores, deduplica, envía un correo por ejecución y mantiene el asunto corto pero específico. Si no tomarías una acción por el correo, no lo envíes.
5) ¿Debería parsear logs con regex o con campos estructurados?
Para fuentes journald, prefiere campos estructurados cuando sea posible. Usa regex para coincidir contenido de mensajes, pero no construyas una canalización frágil que dependa únicamente del texto exacto.
6) ¿Y si mi sistema usa archivos syslog clásicos en vez de journald?
El método sigue siendo el mismo: acota la ventana, prioriza alta severidad, encuentra el primer disparador, añade contexto y correlaciona entre capas. Tus comandos cambian (p. ej., grep, awk, zgrep en archivos rotados), pero las decisiones no.
7) ¿Cómo sé si un error de app es causa o efecto?
Revisa el orden y la independencia. Si eventos de kernel/almacenamiento/oom preceden a los errores de la app, la app probablemente es downstream. Si los errores de la app aparecen solos con logs del kernel estables, la app puede ser el disparador.
8) ¿Cuál es el conjunto mínimo de patrones de disparador que vale la pena alertar?
Empieza con: remount filesystem read-only, errores de I/O del kernel, OOM kills, disco lleno/ENOSPC, caídas repetidas de servicios y fallos de carga TLS/cert. Expande sólo cuando hayas probado que una alerta es accionable.
9) ¿Y los contenedores—journald sigue ayudando?
Sí, si los logs de contenedor se enrutan a journald o si systemd gestiona el runtime de contenedores. Si usas otro driver de logging, recoge señales equivalentes del runtime y aun así correlaciona con logs del kernel.
10) ¿Cuánto tiempo debo retener logs en un servidor?
Suficiente para incluir “advertencias tempranas” que preceden incidentes: días a semanas, según presupuesto de disco. Si no puedes permitir retención, tampoco puedes permitir depurar el próximo fallo de crecimiento lento.
Conclusión: pasos siguientes que puedes hacer antes del almuerzo
Si no haces nada más, haz estas tres cosas:
- Adopta la regla del “primer evento malo”. Encuentra la línea disparadora más temprana y construye la cadena causal hacia adelante.
- Usa el filtrado de journald como un adulto. Prioridades, filtros por unit, campos estructurados. Regex es una herramienta, no un estilo de vida.
- Conecta una pequeña alerta por correo para los disparadores que importan. Basada en cursor, deduplicada, con contexto. Te lo agradecerás a las 02:13.
Entonces, cuando llegue el próximo incidente, no leerás los logs como hojas de té. Extraerás evidencia, tomarás una decisión y seguirás adelante. Ese es el trabajo.