Los incidentes por disco lleno rara vez se anuncian educadamente. Aparecen como “no se puede crear archivo”, “base de datos en solo lectura” o “nodo no listo”, y al final descubres que el culpable es un contenedor que trató stdout como un diario.
El logging en Docker es simple por diseño: escribe en stdout/stderr y el runtime se encarga del resto. El runtime también escribe cada byte con diligencia —ya sea útil, redundante o un único error impreso diez mil veces por minuto. Si quieres ahorrar discos (y tu cordura on-call), las mayores mejoras suceden en el origen: dentro de la aplicación, en la línea exacta donde se emite el log.
Por qué “arreglarlo en Docker” no basta
Sí, deberías configurar la rotación de logs de Docker. Es lo básico. Pero también es control de daños. Si tu app registra como un subastador en pánico, la rotación solo convierte un gran problema en muchos problemas más pequeños que aún llenan el disco, consumen CPU, saturan I/O, ahogan la señal con ruido y siguen generando costes de ingestión en el logging centralizado.
Las plataformas de contenedores fomentan un pecado particular: “simplemente imprime todo en stdout.” Es el camino de menor resistencia y el camino de máximo arrepentimiento. Un runtime de contenedores no conoce tu intención. No puede distinguir entre “checkout de usuario falló” y “debug: iteración de bucle 892341.” Simplemente escribe bytes.
Limitar en la fuente significa: se generan menos eventos de log desde el principio, y los que se generan son más comprimibles, más buscables y más accionables. Aquí es donde ingenieros de aplicación y SREs se encuentran en el pasillo y se ponen de acuerdo: menos logs, mejores logs, vencen a más logs peores.
La verdad operativa es esta: el volumen de logs es una característica de rendimiento. Trátalo como la latencia. Mídelo. Presupuéstalo. Las regresiones deben fallar las compilaciones.
Una cita que debería estar en cada revisión de PR de logging:
Idea parafraseada — Werner Vogels: lo construyes, lo operas; la propiedad incluye lo que tu software hace en producción, incluido su ruido.
Hechos y contexto que explican el desorden actual de logs
Esto no es trivia. Son las razones por las que tus discos acaban llenos de tonterías preservadas perfectamente.
- Unix trató los logs como archivos primero y streams segundo. Syslog y los archivos de texto vinieron antes de “todo a stdout.” El logging en contenedores cambió el transporte por defecto a streams.
- El driver de logging por defecto original de Docker (
json-file) escribe un objeto JSON por línea. Es algo amigable para humanos, fácil de ingerir por máquinas y peligrosamente fácil de crecer sin límites. - “12-factor” popularizó el logging en stdout. Genial para portabilidad. Pero no vino con disciplina incorporada para controlar volumen; esa parte depende de ti.
- Los proveedores de agregación de logs cobran por volumen de ingestión. Tu CFO ahora puede verse afectado por un solo
logger.debugen un bucle caliente. - La cultura temprana de microservicios normalizó “loguea todo; busca después.” Funcionó cuando el tráfico era pequeño y los sistemas pocos. A escala, es como guardar cada pulsación “por si acaso.”
- El logging estructurado resurgió porque grep dejó de escalar. Los logs JSON son geniales —hasta que emites 20 campos por cada petición y triplicas los bytes.
- Los contenedores volvieron ambigua la persistencia de logs. En VMs rotabas archivos. En contenedores, a menudo no tienes un filesystem escribible de confianza, así que la gente vuelca a stdout y espera.
- Las etiquetas y campos de alta cardinalidad se volvieron un impuesto silencioso. Los IDs de trazas son buenos; añadir entrada única del usuario como campo en cada línea es cómo construyes un data lake por accidente.
Playbook de diagnóstico rápido
Si estás on-call y el nodo está gritando, no tienes tiempo para filosofía. Necesitas encontrar el cuello de botella rápido y decidir si es un problema de logging, del runtime o del almacenamiento.
Primero: confirma el síntoma y el radio de blast (disco vs I/O vs CPU)
- Presión de disco:
dfmuestra el sistema de archivos cerca del 100%. - Presión de I/O: tiempos de espera elevados, IOPS de escritura altos, respuestas lentas de la aplicación.
- Presión de CPU: la serialización de logs y el formateo JSON pueden consumir CPU, especialmente con stack traces y objetos grandes.
Segundo: identifica al mayor emisor (contenedor, proceso o agente del host)
- Busca los archivos de log de Docker más grandes y los contenedores asociados.
- Revisa si un shipper de logs (Fluent Bit, Filebeat, etc.) está amplificando el problema con reintentos/bucle de backpressure.
- Confirma si la aplicación repite el mismo mensaje; si es así, limita la tasa o deduplica en la capa de la app.
Tercero: decide la mitigación más rápida y segura
- Mitigación de emergencia: detener/reiniciar al peor culpable, aplicar rotación si falta, reducir el nivel de logs vía flag de configuración o muestrear logs temporalmente.
- Solución post-incidente: cambiar patrones de logging para que el incidente no pueda repetirse desde un único camino de código.
Broma #1: Si tu disco se llena con logs, felicitaciones —has inventado una base de datos muy cara y muy lenta sin índices.
Tareas prácticas: comandos, salidas, decisiones
Estos son los tipos de comprobaciones que haces en un host real a las 02:13. Cada tarea incluye el comando, qué significa su salida y la decisión que tomas a partir de ella.
Tarea 1: Confirmar la presión de disco y qué sistema de archivos está afectado
cr0x@server:~$ df -h
Filesystem Size Used Avail Use% Mounted on
/dev/nvme0n1p2 220G 214G 2.9G 99% /
tmpfs 32G 0 32G 0% /dev/shm
Significado: El filesystem raíz está esencialmente lleno. Los contenedores y sus logs a menudo viven bajo /var/lib/docker en /.
Decisión: No ejecutes “scripts de limpieza” a ciegas. Identifica primero qué consume espacio; evita borrar estado en ejecución a menos que aceptes downtime.
Tarea 2: Encontrar los directorios más grandes bajo el almacenamiento de Docker
cr0x@server:~$ sudo du -xhd1 /var/lib/docker | sort -h
1.2G /var/lib/docker/containers
8.4G /var/lib/docker/image
12G /var/lib/docker/overlay2
22G /var/lib/docker
Significado: containers es lo suficientemente grande como para sospechar crecimiento de logs. overlay2 también puede ser grande debido a capas escribibles.
Decisión: Profundiza en /var/lib/docker/containers para encontrar archivos de log grandes y mapearlos a contenedores.
Tarea 3: Localizar los archivos de log de contenedores más grandes
cr0x@server:~$ sudo find /var/lib/docker/containers -name "*-json.log" -printf "%s %p\n" | sort -n | tail -5
2147483648 /var/lib/docker/containers/2c1c3e.../2c1c3e...-json.log
3221225472 /var/lib/docker/containers/7a8b9c.../7a8b9c...-json.log
4294967296 /var/lib/docker/containers/aa0bb1.../aa0bb1...-json.log
Significado: Tienes archivos JSON de varios gigabytes. Eso no son “algunos debug logs”, es una manguera de logs.
Decisión: Identifica los contenedores detrás de esos IDs e inspecciona lo que están emitiendo.
Tarea 4: Mapear un ID de contenedor a un nombre e imagen
cr0x@server:~$ docker ps --no-trunc --format "table {{.ID}}\t{{.Names}}\t{{.Image}}" | grep aa0bb1
aa0bb1d3f0e9c1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4 payments-api registry.local/payments-api:3.14.2
Significado: El contenedor payments-api está generando el archivo de log masivo.
Decisión: Inspecciona logs recientes y busca patrones de repetición (misma línea, mismo stack trace, misma ruta de petición).
Tarea 5: Muestrear los últimos logs sin volcar el archivo completo
cr0x@server:~$ docker logs --tail 50 payments-api
{"level":"error","msg":"db timeout","tenant":"blue","path":"/charge","retry":1}
{"level":"error","msg":"db timeout","tenant":"blue","path":"/charge","retry":1}
{"level":"error","msg":"db timeout","tenant":"blue","path":"/charge","retry":1}
Significado: Errores idénticos repetidos. Probablemente un bucle de reintentos que registra cada intento.
Decisión: Mitigar ahora reduciendo el nivel de logs o limitando la tasa de ese mensaje específico. Luego arreglar el código: registrar una vez por ventana de fallo, no por cada reintento.
Tarea 6: Comprobar el driver de logging del contenedor y sus opciones
cr0x@server:~$ docker inspect -f '{{.HostConfig.LogConfig.Type}} {{json .HostConfig.LogConfig.Config}}' payments-api
json-file {"max-file":"1","max-size":"0"}
Significado: max-size es 0 (efectivamente ilimitado) mientras max-file es irrelevante.
Decisión: Arregla la configuración del runtime (compose/systemd/daemon.json) pero no te quedes ahí; la app sigue emitiendo demasiado.
Tarea 7: Verificar los valores por defecto del daemon
cr0x@server:~$ sudo cat /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
Significado: Si esto está presente, los nuevos contenedores deberían rotar. Los contenedores existentes pueden haberse creado antes de estos valores por defecto, o tener anulación por contenedor.
Decisión: Estandariza las rutas de creación. Asegura que los stacks de compose o las definiciones del orquestador no anulen los límites.
Tarea 8: Identificar escritura alta de I/O causada por logging
cr0x@server:~$ iostat -xz 1 3
avg-cpu: %user %nice %system %iowait %steal %idle
12.31 0.00 6.44 38.27 0.00 42.98
Device r/s rkB/s rrqm/s %rrqm r_await w/s wkB/s w_await aqu-sz %util
nvme0n1 2.1 86.3 0.0 0.0 3.21 912.4 18432.0 42.10 39.2 98.7
Significado: Utilización de escritura extremadamente alta y alto w_await. El logging a disco puede dominar el tiempo del dispositivo.
Decisión: Reduce el volumen de logs ahora. Si sigues escribiendo a esta tasa, el disco se convierte en el cuello de botella para todo.
Tarea 9: Confirmar qué procesos están escribiendo mucho
cr0x@server:~$ sudo pidstat -d 1 3
Linux 6.5.0 (server) 01/03/2026 _x86_64_ (16 CPU)
01:12:11 UID PID kB_rd/s kB_wr/s kB_ccwr/s Command
01:12:12 0 2471 0.00 18240.00 0.00 dockerd
01:12:12 0 19382 0.00 3100.00 0.00 fluent-bit
Significado: El daemon de Docker está escribiendo enormes volúmenes (logs de contenedores). El shipper también está escribiendo/manejando mucho.
Decisión: Ataca primero el contenedor fuente; luego afina el buffering/reintentos del shipper para evitar bucles de realimentación.
Tarea 10: Comprobar si la aplicación registra stack traces repetidamente
cr0x@server:~$ docker logs --tail 200 payments-api | grep -c "Traceback\|Exception\|stack"
147
Significado: Los stack traces frecuentes son costosos en bytes y CPU. A menudo es la misma excepción repitiéndose.
Decisión: Registra un stack trace por error único por ventana de tiempo; emite contadores/métricas para el resto.
Tarea 11: Identificar la tasa de eventos de log (líneas por segundo) desde el archivo bruto
cr0x@server:~$ cid=$(docker inspect -f '{{.Id}}' payments-api); sudo sh -c "tail -n 20000 /var/lib/docker/containers/$cid/${cid}-json.log | wc -l"
20000
Significado: Son 20k líneas en el segmento tail reciente. Si ese tail corresponde a “unos segundos”, estás inundando. Si es “minutos”, sigue siendo demasiado parlanchín.
Decisión: Establece un presupuesto: por ejemplo, estado estable < 50 líneas/s por instancia; solo permitir ráfagas con muestreo y límites.
Tarea 12: Medir qué tan rápido crece el archivo de log
cr0x@server:~$ cid=$(docker inspect -f '{{.Id}}' payments-api); sudo sh -c "stat -c '%s %y' /var/lib/docker/containers/$cid/${cid}-json.log; sleep 5; stat -c '%s %y' /var/lib/docker/containers/$cid/${cid}-json.log"
4294967296 2026-01-03 01:12:41.000000000 +0000
4311744512 2026-01-03 01:12:46.000000000 +0000
Significado: ~16 MB en 5 segundos (~3.2 MB/s). Eso llenará discos rápido y estrangulará el I/O.
Decisión: Mitigación inmediata: reducir nivel, deshabilitar componente ruidoso, reiniciar con flag de entorno. A largo plazo: implementar throttling/dedupe.
Tarea 13: Comprobar si el contenedor se reinicia por crash-loop y emite logs de inicio verbosos
cr0x@server:~$ docker inspect -f '{{.State.Status}} {{.RestartCount}}' payments-api
running 47
Significado: Muchos reinicios. Cada reinicio puede volver a emitir banners voluminosos/dumps de configuración, multiplicando el ruido.
Decisión: Arregla la causa raíz del crash y suprime los dumps de inicio verbosos; haz que la “información de inicio única” sea realmente única.
Tarea 14: Verificar si los shippers de logs están con backpressure y reintentando (amplificando ruido)
cr0x@server:~$ docker logs --tail 50 fluent-bit
[warn] [output:es:es.0] HTTP status=429 URI=/_bulk
[warn] [engine] failed to flush chunk '1-173587...' retry in 8 seconds
Significado: El downstream está limitando. Tus logs no solo llenan disco; también causan una tormenta de reintentos y buffering en memoria/disco.
Decisión: Reduce volumen en la app, luego afina el buffering del shipper y considera descartar logs de bajo valor bajo presión.
Tarea 15: Inspeccionar la frecuencia de un mensaje sospechoso (líneas repetidas superiores)
cr0x@server:~$ docker logs --tail 5000 payments-api | jq -r '.msg' | sort | uniq -c | sort -nr | head
4821 db timeout
112 cache miss
45 payment authorized
Significado: Un mensaje domina. Ese es un objetivo perfecto para deduplicación y limitación de tasa.
Decisión: Sustituye logs por evento con: (a) un resumen periódico, (b) contador métrico, (c) un ejemplar muestreado con contexto.
Patrones de logging en la app que realmente reducen el volumen
Este es el meollo: patrones de codificación y contratos operativos que previenen el spam de logs. Puedes implementarlos en cualquier lenguaje; los principios no dependen de tu framework.
1) Deja de loguear por cada intento de reintento; registra por ventana de resultado
Los reintentos son normales. Registrar cada reintento no lo es. Si una dependencia cae, un bucle de reintentos puede crear un amplificador perfecto de logs: fallos causan reintentos, reintentos causan logs, logs causan presión de I/O, la presión de I/O provoca más timeouts, los timeouts provocan más fallos.
Haz esto: registra la primera falla con contexto; luego limita las siguientes; emite un resumen cada N segundos: “db timeout continuo; suprimidos 4.821 errores similares”.
Un buen patrón:
- Un error “ejemplar” con stack trace y metadatos de la petición (pero véanse las notas de privacidad abajo).
- Métrica contador por cada evento de fallo.
- Resumen periódico por dependencia, por instancia.
2) Elige un presupuesto de logs en estado estable y hazlo cumplir
La mayoría de equipos discuten sobre formatos de logs. Mejor discusión: presupuestos de tasa de logs. Por ejemplo:
- Por instancia de servicio en estado estable: < 1 KB/s de throughput de logs en promedio.
- Ráfaga permitida: hasta 50 KB/s por 60 segundos durante incidentes.
- Por encima de la ráfaga: muestreo al 1%, conservar ejemplares de error, descartar debug/info.
Esto le da a SREs un umbral estilo SLO y a los equipos de aplicación un objetivo que pueden probar. Añade una comprobación en CI que ejecute una carga sintética y falle si los logs exceden el presupuesto.
3) Predetermina logs estructurados, pero sin sobre-estructurar
Los logs JSON son el estándar en el mundo de contenedores. También son fáciles de abusar. Cada campo extra cuesta bytes. Algunos campos también cuestan dinero de indexación.
Mantén: timestamp, level, message, service name, instance ID, request ID/trace ID, latency, status code, dependency name, error class.
Evita: bodies completos de petición, arrays sin límite, strings SQL crudos y etiquetas de alta cardinalidad copiadas en cada línea.
4) No registres dentro de bucles cerrados a menos que hagas throttling
Los bucles aparecen en todas partes: polling, consumidores de colas, escaneo de directorios, reintentos de locks, comprobaciones de salud de conexión. Si registras en un bucle, has creado un incidente futuro. No una posibilidad; una cita programada.
Regla: cualquier sentencia de log que pueda ejecutarse más de una vez por segundo en estado estable debe estar detrás de un limitador de tasa, una puerta de cambio de estado o ambas.
5) Registra cambios de estado, no confirmaciones de estado
“Seguimos conectados” cada 5 segundos es un desperdicio. “Conexión restablecida tras 42 segundos, suprimidos 500 fallos” es útil. Los humanos necesitan transiciones. Las máquinas necesitan contadores.
Implementa una máquina de estados simple para la salud de dependencias (UP → DEGRADED → DOWN) y emite logs solo en transiciones y resúmenes periódicos.
6) Usa “claves de dedupe” para errores repetidos
Los errores repetidos suelen compartir una firma: mismo tipo de excepción, misma dependencia, mismo endpoint. Calcula una clave de dedupe como:
dedupe_key = hash(error_class + dependency + path + error_code)
Luego mantén un pequeño mapa en memoria por proceso: timestamp de última vez, contador de suprimidos y un payload ejemplar. Emite:
- Primera ocurrencia: registra normalmente.
- Dentro de la ventana: incrementa contador de suprimidos, quizá emite debug muestreado.
- Al final de la ventana: registra resumen con contador de suprimidos y un ID de ejemplar.
7) Muestra muestreos en logs informativos; nunca muestres métricas
El muestreo es un bisturí. Úsalo para eventos de alto volumen y bajo valor: access logs por petición, “cache miss”, “job started”. Mantén los errores mayormente no muestreados, pero puedes muestrear errores idénticos repetidos después del primer ejemplar.
Las métricas sirven para contar. Un contador es barato y preciso. No sustituyas métricas por logs; es como reemplazar un termómetro por una danza interpretativa.
8) Haz que el “modo debug” sea un disyuntor, no un nivel
Los debug logs en producción deben ser temporales, dirigidos y reversibles sin redeploy. El enfoque más seguro:
- Los debug logs existen, pero están apagados por defecto.
- Habilita debug para un request ID específico, user ID (hashed) o tenant por tiempo limitado.
- Auto-desactiva tras un TTL.
Esto evita el error clásico: “habilitamos debug para investigar, lo olvidamos y lo pagamos durante una semana.”
9) Deja de registrar “errores esperados” como ERROR
Si un cliente cancela una petición, eso no es un error; es un martes. Si un usuario pone una contraseña errónea, no es un fallo de servidor; es realidad de producto. Si registras estos como error, enseñas a on-call a ignorar ERROR. Así es como te pierdes la verdadera interrupción.
Patrón:
- Usa
infoowarnpara fallos impulsados por el cliente. - Usa
errorpara fallos del lado servidor que requieren atención. - Usa
fatalraramente, y solo cuando el proceso va a salir.
10) Elimina payloads; registra punteros
Registrar cuerpos completos de petición/respuesta es un comeladrillo de disco y una trampa de privacidad. En su lugar:
- Registra el tamaño del payload (bytes).
- Registra un hash del contenido (para correlacionar repeticiones sin almacenar contenido).
- Registra un ID de objeto que pueda recuperarse desde un almacén seguro si es necesario.
11) Haz los stack traces opt-in y acotados
Los stack traces pueden ser valiosos. También pueden ser 200 líneas de ruido repetidas 10.000 veces. Acótalos:
- Incluye stack traces en la primera ocurrencia de una clave de dedupe por ventana.
- Trunca la profundidad del stack cuando sea posible.
- Prefiere tipo de excepción + mensaje + frames superiores para repeticiones.
12) Usa un logger “una vez” para la configuración de inicio
Los logs de inicio a menudo imprimen configuración completa, entorno, feature flags y listas de dependencias. Está bien una vez. Es caos cuando el proceso está en crash-loop y lo imprime 50 veces.
Patrón: registra un resumen compacto de inicio y un hash de configuración. Almacena la configuración detallada en otro lugar (o expónla vía un endpoint protegido), no en los logs.
13) Trata el logging como una dependencia con backpressure
La mayoría de bibliotecas de logging fingen que las escrituras son gratis. No lo son. Cuando la salida bloquea (disco lento, pipe stdout bloqueado, presión del driver de logs), tu app puede quedarse parada.
Haz esto:
- Prefiere logging asíncrono con colas acotadas.
- Cuando la cola esté llena, descarta primero logs de baja prioridad.
- Expón métricas: logs descartados, profundidad de cola, tiempo de logging.
14) Haz los logs más fáciles de comprimir
Si no puedes reducir suficiente el volumen, al menos haz que se compriman bien. La repetición se comprime. La aleatoriedad no. Un buen logging:
- Usa plantillas de mensaje estables:
"db timeout"no"db timeout after 123ms on host a1b2"embebido en la cadena del mensaje. - Coloca datos variables en campos, no en el mensaje.
- Evita imprimir UUIDs aleatorios en cada línea a menos que sean necesarios para correlación.
15) Añade un “fusible de logs” para emergencias
A veces necesitas un interruptor de parada: “Si los logs exceden X líneas/s durante Y segundos, automáticamente aumenta el muestreo y suprime INFO/WARN repetitivos.” No es bonito, pero evita un outage por disco lleno.
Implementa esto con un contador local y una ventana móvil. Cuando salta, emite un solo log fuerte: “fusible de logs activado; muestreo ahora 1%; suprimidos N líneas.”
Broma #2: El logging es como el café —pequeñas cantidades mejoran el rendimiento, pero demasiado convierte tu sistema en un desorden nervioso que no para de hablar.
Tres mini-historias corporativas (anonimizadas, plausibles y técnicamente precisas)
Mini-historia 1: El incidente causado por una suposición equivocada
Asumieron que Docker rotaba logs por defecto. El equipo había pasado de una configuración basada en VM donde logrotate estaba en todas partes, y trataron el runtime de contenedores como un reemplazo moderno con valores por defecto sensatos.
Durante una prueba de integración con un partner, un servicio empezó a fallar la autenticación. El servicio tenía una política de reintentos bastante normal: backoff exponencial con jitter. Pero el desarrollador había añadido un error dentro del bucle de reintentos para “hacerlo visible.” Era visible. Y además implacable.
La primera señal de problema no fue una alerta sobre uso de disco. Fue un nodo de base de datos quejándose de consultas lentas. El host que ejecutaba el contenedor parlanchín tenía su disco raíz casi lleno y la latencia de I/O se había ido por las nubes. El driver de logging escribía líneas JSON como un metrónomo.
El on-call hizo lo que hace la gente: reinició el servicio. Eso redujo temporalmente el volumen de logs porque compró unos segundos antes de que los reintentos volvieran a subir. Lo reiniciaron otra vez. Mismo resultado. Mientras tanto, el shipper de logs reintentaba la ingestión porque el downstream estaba limitando, lo que añadió otra capa de churn de escritura.
La solución fue embarazosa en su simplicidad: añadir rotación de logs a nivel de Docker y cambiar la app para registrar solo la primera falla por ventana y luego resumir. La lección fue más aguda: “suposiciones por defecto” no son una estrategia de confiabilidad.
Mini-historia 2: La optimización que se volvió contra ellos
Otra organización quería “observabilidad perfecta.” Añadieron logging estructurado en todas partes, lo cual es bueno. Luego decidieron que cada línea de log debería incluir el contexto completo de la petición para facilitar el debug: headers, query params y un trozo del body.
Funcionó de maravilla en staging. En producción se convirtió en una hoguera de costes de ingestión. Peor aún, se volvió un problema de rendimiento: la serialización JSON de objetos grandes por cada petición consumía CPU, y el runtime del contenedor escribía líneas más grandes. La latencia aumentó, lo que creó más timeouts, que generaron más errores, que generaron stack traces aún mayores. Un bucle de realimentación clásico.
El síntoma on-call parecía de capacidad: “Necesitamos nodos más grandes.” Pero el verdadero cuello de botella fue autoinfligido: presión de I/O y CPU causada por el logging. Cuando redujeron el logging de payloads y pasaron a “punteros de logs” (request ID, hash del payload, tamaño), el sistema se estabilizó sin cambiar el tamaño de las instancias.
La optimización fue “reducir tiempo de debugging registrando todo.” El revés fue “aumentar la tasa de incidentes y el coste registrando todo.” El final feliz fue que mantuvieron logs estructurados —solo que no las partes que pertenecían a un almacén de trazas seguro.
Mini-historia 3: La práctica aburrida pero correcta que salvó el día
Un servicio financiero manejaba jobs batch periódicos. Nada excitante. El equipo tenía una práctica casi anticuada: cada servicio tenía un presupuesto de logs escrito y una prueba que medía throughput de logs bajo carga. Si un cambio aumentaba los logs más allá del presupuesto, el build fallaba a menos que el ingeniero lo justificara.
Un viernes, una dependencia empezó a devolver 500 intermitentes. El servicio reintentó, pero los patrones de logging ya estaban limitados por tasa y deduplicados. Emitieron un error ejemplar con un trace ID, luego un resumen cada 30 segundos: “errores de dependencia continuos; suprimidos N.” Los contadores métricos subieron, las alertas sonaron, pero los discos se mantuvieron tranquilos.
Mientras otros equipos peleaban con la presión del disco y se ahogaban en stack traces repetidos, este servicio permaneció legible. On-call pudo ver qué cambió (comportamiento de la dependencia), cuantificarlo (métricas) y correlacionarlo (trace IDs). El incidente fue molesto, pero no se convirtió en un outage a nivel de nodo.
Después, nadie escribió un gran post interno sobre “las heroicidades.” Fue aburrido. Ese es el punto. Las prácticas de confiabilidad aburridas envejecen bien.
Errores comunes: síntoma → causa raíz → solución
1) Síntoma: los logs de Docker crecen sin control
Causa raíz: driver json-file sin max-size/max-file, o contenedores creados antes de establecer valores por defecto.
Solución: Establece valores por defecto del daemon y aplica configuración por servicio. Recrea contenedores para recoger límites. Aún así, arregla la app para que no genere basura.
2) Síntoma: disco lleno tras una caída de una dependencia
Causa raíz: bucle de reintentos que registra cada intento (a menudo con stack traces).
Solución: registrar primera falla + resumen; contar reintentos como métricas; limitar tasa por clave de dedupe; añadir circuit breakers.
3) Síntoma: on-call ignora ERROR porque siempre es ruidoso
Causa raíz: eventos esperados del cliente registrados como error (timeouts por cancels, respuestas 4xx, fallos de validación).
Solución: corregir mapeo de severidad y reglas de alertas; reservar ERROR para fallos accionables del lado servidor.
4) Síntoma: CPU alta sin aumento obvio de carga de negocio
Causa raíz: formateo de logs costoso (interpolación de strings, serialización JSON de objetos grandes) en caminos calientes.
Solución: logging lazy (formatear solo si está habilitado), evitar serializar objetos completos, precompilar plantillas de mensaje, muestrear logs de bajo valor.
5) Síntoma: el shipper de logs muestra reintentos, crecimiento de memoria o chunks descartados
Causa raíz: throttling del downstream más alto volumen upstream; el buffering del shipper amplifica el uso de disco.
Solución: reducir volumen de logs en la app; configurar backpressure y políticas de descarte en el shipper; priorizar ejemplares de error y resúmenes.
6) Síntoma: “No encontramos las líneas relevantes” durante incidentes
Causa raíz: campos de contexto faltantes (request ID, versión del servicio, nombre de dependencia) y demasiado ruido repetitivo.
Solución: añadir campos de contexto esenciales; deduplicar logs repetitivos; registrar transiciones de estado; mantener mensajes consistentes.
7) Síntoma: aparecen datos sensibles en los logs
Causa raíz: logging de payloads request/response, volcados de headers o mensajes de excepción con secretos.
Solución: redactar en la fuente, dejar de registrar payloads, añadir listas blancas de campos, auditar logs automáticamente, tratar los logs como datos de producción.
8) Síntoma: la “solución” fue aumentar el tamaño del disco, pero el problema vuelve
Causa raíz: parche de capacidad; sin cambio en los patrones de emisión.
Solución: implementar presupuestos de logs, aplicar limitación de tasa y añadir pruebas de regresión para el volumen de logs.
Listas de verificación / plan paso a paso
Paso a paso: detener la hemorragia durante un incidente activo
- Confirma el uso de disco:
df -h. Si la raíz está > 95%, trata como urgente. - Encuentra los archivos de log principales:
find /var/lib/docker/containers -name "*-json.log"ordenados por tamaño. - Mapea archivo → contenedor:
docker ps --no-truncydocker inspect. - Identifica repetición: muestrea logs recientes; comprueba los mensajes repetidos principales.
- Mitiga rápido: reduce temporalmente el nivel de logs, habilita muestreo o deshabilita el componente ruidoso. Si hace falta, reinicia el contenedor con configuraciones más seguras.
- Recupera espacio: una vez que la emisión pare, elimina o trunca solo el archivo de log peor si aceptas perder logs. Prefiere rotación y reinicios controlados sobre borrados manuales.
- Confirma recuperación de I/O:
iostaty latencias de servicio deberían normalizarse.
Paso a paso: prevenir recurrencia (qué hacer después del incidente)
- Configura valores por defecto de Docker:
max-sizeymax-fileen/etc/docker/daemon.json. - Audita overrides por servicio: archivos de compose, unidades systemd, especificaciones del orquestador.
- Instrumenta volumen de logs: monitoriza líneas/s y bytes/s por instancia de servicio.
- Implementa dedupe + límites de tasa: por firma de error, por dependencia.
- Reemplaza spam con resúmenes: rollups periódicos, más ejemplares.
- Mueve contexto voluminoso a trazas: mantén los logs ligeros; usa request IDs para pivotar.
- Añade una barrera en CI: prueba de carga y fallo por regresiones en presupuesto de logs.
- Haz una revisión de privacidad: redacta, usa listas blancas y verifica que los secretos no puedan filtrarse.
Checklist operacional: cómo se ve “bien”
- Los logs ERROR son raros, accionables y no están dominados por una línea repetida.
- Los logs Info están muestreados o limitados en caminos calientes (peticiones, consumidores de colas).
- Cada servicio tiene un presupuesto de logs y una tasa conocida en estado estable.
- Cada error repetido tiene una clave de dedupe, una ventana de supresión y una línea resumen.
- Los logs contienen el contexto necesario para correlacionar (trace/request ID, versión), no los datos que no deberías almacenar.
- Cuando la ingestión está limitada, el sistema degrada con gracia (descarta primero logs de bajo valor).
Preguntas frecuentes
1) ¿Debería simplemente cambiar el driver de logging de Docker para arreglar esto?
No. Cambiar drivers puede ayudar con rotación, envío o características de rendimiento, pero no arregla una aplicación que emite basura. Arregla la emisión primero; luego elige el driver según necesidades operativas.
2) ¿Es siempre correcto loguear en stdout en contenedores?
Es el enfoque estándar, no automáticamente el correcto. Stdout está bien si lo tratas como un canal limitado con presupuestos, muestreo y límites de tasa. Si necesitas logs locales durables, usa un volumen y gestiona la rotación —pero eso es una decisión deliberada, no un accidente.
3) ¿Qué nivel de logs debería usar en producción?
Típicamente info o warn, con toggles debug dirigidos. Si necesitas debug constantemente para operar, probablemente te faltan métricas, trazas o contexto estructurado.
4) ¿Cómo convencemos a los equipos de dejar de loguear cuerpos de petición?
Diles la verdad: es un riesgo de confiabilidad y seguridad. Ofrece una alternativa: loguear request IDs, tamaños de payload, hashes y almacenar payloads detallados en un sistema seguro y con control de acceso si realmente se necesitan.
5) ¿Cuál es el enfoque más simple de limitación de tasa en una app?
Una ventana temporal por mensaje (o por clave de dedupe): registra la primera ocurrencia, luego suprime durante N segundos mientras cuentas las supresiones, y luego emite un resumen.
6) ¿El muestreo hará que depurar sea más difícil?
El muestreo hace que depurar sea posible cuando la alternativa es ahogarse. Mantén ejemplares (primera ocurrencia, firmas únicas) y conserva contadores métricos para completitud. No puedes depurar lo que no puedes leer.
7) ¿Cómo detecto el spam de logs antes de que tumbe un nodo?
Alerta por la tasa de crecimiento de logs (bytes/s) y por cambios súbitos en los mensajes repetidos principales. Si solo alertas en “disco > 90%”, lo sabrás demasiado tarde.
8) ¿Por qué los stack traces repetidos hacen tanto daño?
Son grandes, lentos de formatear y a menudo idénticos. Desperdician CPU y disco, y arruinan la señal de búsqueda. Mantén un ejemplar por ventana; cuenta el resto.
9) ¿Puedo eliminar de forma segura un gran archivo *-json.log para recuperar espacio?
A veces, pero es una herramienta afilada. Eliminar un archivo que un proceso aún tiene abierto puede no recuperar espacio hasta que se cierre el handle. Prefiere rotación, reinicio del contenedor o truncado controlado durante un incidente —y luego arregla la emisión subyacente.
10) ¿Cómo mantengo los logs útiles reduciendo el volumen?
Haz que los logs sean significativos: transiciones de estado, resúmenes, ejemplares. Mueve el detalle de alto volumen a métricas (contadores) y trazas (contexto por petición). Los logs deben explicar incidentes, no recrearlos.
Próximos pasos que puedes hacer esta semana
Si solo haces una cosa, haz esto: elimina el logging desde los bucles de reintentos y reemplázalo por ejemplares deduplicados más resúmenes periódicos. Ese único patrón previene toda una clase de incidentes por disco lleno y thrash de I/O.
Luego:
- Configura valores de rotación de logs de Docker y verifica que cada contenedor realmente los herede.
- Define un presupuesto de logs por servicio y mide líneas/s y bytes/s bajo carga.
- Implementa limitación de tasa y claves de dedupe para errores repetidos y logs en caminos calientes.
- Deja de loguear payloads; registra punteros y hashes en su lugar.
- Añade un “fusible de logs” para que un mal deploy no pueda tumbar un nodo hablando demasiado.
No ganas confiabilidad escribiendo más logs. La ganas haciendo que los logs que conservas valgan los bytes que ocupan.