Skip to content

Tema:

Docker Logs


Introducción

Docker es una herramienta poderosa para el despliegue de aplicaciones contenerizadas, y su sistema de logging es un componente esencial para monitorear, depurar y mantener aplicaciones en contenedores. Los logs en Docker permiten capturar la salida de las aplicaciones, proporcionando visibilidad sobre su comportamiento, errores y eventos importantes durante su ejecución.

Los logs en Docker no solo facilitan la identificación de problemas, sino que también ofrecen control sobre aspectos como la retención de logs, formato, drivers de almacenamiento y centralización de logs, haciéndolo una pieza clave en la observabilidad de sistemas modernos basados en contenedores.


Objetivo

Objetivo General:

  • El objetivo de este tema es proporcionar una comprensión completa del sistema de logging en Docker, las mejores prácticas para implementarlo y cómo desarrollar aplicaciones que envíen logs correctamente a stdout y stderr.

Fundamentos de Docker Logs

¿Qué son los Docker Logs?

Docker captura automáticamente todo lo que una aplicación escribe a: - STDOUT (Standard Output): Salida estándar para mensajes informativos y de operación normal - STDERR (Standard Error): Salida de error estándar para mensajes de error y advertencias

Los logs se almacenan en el host de Docker y pueden ser consultados usando el comando docker logs.

Visualización de Logs

Ver los logs de un contenedor en ejecución:

docker logs <container_name_or_id>

Ver logs en tiempo real (seguir logs):

docker logs -f <container_name_or_id>

Ver las últimas N líneas de logs:

docker logs --tail 100 <container_name_or_id>

Ver logs con marcas de tiempo:

docker logs -t <container_name_or_id>

Ver logs desde una fecha específica:

docker logs --since 2024-12-09T10:00:00 <container_name_or_id>

Combinar opciones:

docker logs -f --tail 50 -t <container_name_or_id>


Mejores Prácticas para Docker Logs

1. Escribir Logs a STDOUT y STDERR

✅ Recomendado: Las aplicaciones deben enviar logs directamente a stdout y stderr, no a archivos.

Razones: - Docker captura automáticamente stdout/stderr - Facilita la agregación centralizada de logs - Simplifica la gestión de logs en entornos distribuidos - Evita problemas de espacio en disco dentro del contenedor - Sigue el principio de "Twelve-Factor App"

❌ Evitar: Escribir logs a archivos dentro del contenedor - Consume espacio en la capa de escritura del contenedor - Dificulta el acceso a los logs - Los logs se pierden cuando el contenedor se elimina

2. Utilizar Niveles de Log Apropiados

Implementar niveles de log estándar: - ERROR: Errores que requieren atención inmediata - WARN: Advertencias sobre situaciones anómalas pero no críticas - INFO: Información general sobre operaciones normales - DEBUG: Información detallada para depuración

3. Incluir Contexto en los Logs

Los logs deben incluir: - Marca de tiempo (timestamp) - Nivel de severidad - Identificador de solicitud o transacción - Información contextual relevante (usuario, recurso, acción)

Ejemplo de formato estructurado (JSON):

{"timestamp":"2024-12-09T10:30:45Z","level":"ERROR","service":"api","message":"Database connection failed","error":"connection timeout"}

4. Configurar Rotación de Logs

Evitar que los logs consuman todo el espacio en disco:

docker run -d \
  --log-opt max-size=10m \
  --log-opt max-file=3 \
  nginx

5. Usar Logging Drivers Apropiados

Docker soporta múltiples drivers de logging según las necesidades:

Driver Descripción Uso Recomendado
json-file Driver por defecto, almacena logs en JSON Desarrollo y testing
syslog Envía logs a syslog Integración con sistemas Unix/Linux
journald Envía logs a journald Sistemas con systemd
gelf Envía logs a endpoints GELF (Graylog) Centralización con Graylog
fluentd Envía logs a Fluentd Agregación avanzada de logs
awslogs Envía logs a AWS CloudWatch Aplicaciones en AWS
splunk Envía logs a Splunk Ambientes enterprise con Splunk

Desarrollando Aplicaciones que Envían Logs a STDOUT/STDERR

Python

Ejemplo básico:

import sys
import logging

# Configurar logging para enviar a stdout
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.StreamHandler(sys.stdout)
    ]
)

logger = logging.getLogger(__name__)

# Logs informativos a stdout
logger.info("Aplicación iniciada correctamente")
logger.debug("Procesando solicitud del usuario")

# Logs de error a stderr
try:
    result = 10 / 0
except Exception as e:
    logger.error(f"Error en operación: {e}", exc_info=True)

Ejemplo con JSON (estructurado):

import json
import sys
from datetime import datetime

def log_json(level, message, **kwargs):
    log_entry = {
        "timestamp": datetime.utcnow().isoformat(),
        "level": level,
        "message": message,
        **kwargs
    }
    print(json.dumps(log_entry), file=sys.stdout)

log_json("INFO", "Usuario autenticado", user_id=12345, ip="192.168.1.1")
log_json("ERROR", "Conexión a BD fallida", error="timeout", retry_count=3)

Node.js

Ejemplo básico:

// Logs a stdout
console.log('INFO: Servidor iniciado en puerto 3000');
console.info('INFO: Conexión a base de datos exitosa');

// Logs a stderr
console.error('ERROR: No se pudo conectar a Redis');
console.warn('WARN: Límite de rate limiting alcanzado');

// Logs estructurados en JSON
const logInfo = (message, metadata = {}) => {
    const logEntry = {
        timestamp: new Date().toISOString(),
        level: 'INFO',
        message,
        ...metadata
    };
    console.log(JSON.stringify(logEntry));
};

const logError = (message, error, metadata = {}) => {
    const logEntry = {
        timestamp: new Date().toISOString(),
        level: 'ERROR',
        message,
        error: error.message,
        stack: error.stack,
        ...metadata
    };
    console.error(JSON.stringify(logEntry));
};

logInfo('Usuario creado', { userId: 123, username: 'john_doe' });

Ejemplo con Winston (librería popular):

const winston = require('winston');

const logger = winston.createLogger({
    level: 'info',
    format: winston.format.json(),
    transports: [
        new winston.transports.Console({
            format: winston.format.combine(
                winston.format.timestamp(),
                winston.format.json()
            )
        })
    ]
});

logger.info('Servidor iniciado', { port: 3000, env: 'production' });
logger.error('Error en base de datos', { error: 'connection timeout', db: 'postgres' });

Java (Spring Boot)

Configuración en application.properties:

# Enviar logs a stdout
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n
logging.level.root=INFO
logging.level.com.myapp=DEBUG

Ejemplo de código:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    private static final Logger logger = LoggerFactory.getLogger(Application.class);

    public static void main(String[] args) {
        logger.info("Iniciando aplicación Spring Boot");

        try {
            SpringApplication.run(Application.class, args);
            logger.info("Aplicación iniciada exitosamente");
        } catch (Exception e) {
            logger.error("Error al iniciar aplicación", e);
            System.exit(1);
        }
    }
}

Go

Ejemplo básico:

package main

import (
    "encoding/json"
    "log"
    "os"
    "time"
)

type LogEntry struct {
    Timestamp string `json:"timestamp"`
    Level     string `json:"level"`
    Message   string `json:"message"`
}

func logJSON(level, message string) {
    entry := LogEntry{
        Timestamp: time.Now().UTC().Format(time.RFC3339),
        Level:     level,
        Message:   message,
    }

    jsonData, _ := json.Marshal(entry)

    if level == "ERROR" {
        os.Stderr.Write(jsonData)
        os.Stderr.WriteString("\n")
    } else {
        os.Stdout.Write(jsonData)
        os.Stdout.WriteString("\n")
    }
}

func main() {
    // Configurar logger estándar para stdout
    log.SetOutput(os.Stdout)
    log.Println("INFO: Aplicación iniciada")

    // Logs estructurados
    logJSON("INFO", "Servidor HTTP iniciado en :8080")
    logJSON("ERROR", "No se pudo conectar a la base de datos")
}


Laboratorio 1: Explorando Docker Logs

Objetivo

Familiarizarse con los comandos de visualización de logs y entender cómo Docker captura stdout y stderr.

  1. Crear un contenedor que genere logs continuamente:

    docker run -d --name log-generator alpine sh -c \
      "while true; do echo 'INFO: Operación normal'; sleep 2; echo 'ERROR: Algo salió mal' >&2; sleep 3; done"
    

  2. Ver todos los logs del contenedor:

    docker logs log-generator
    

  3. Seguir los logs en tiempo real:

    docker logs -f log-generator
    

    📝 Presione Ctrl+C para detener el seguimiento de logs.

  4. Ver solo las últimas 10 líneas:

    docker logs --tail 10 log-generator
    

  5. Ver logs con marcas de tiempo:

    docker logs -t log-generator
    

  6. Ver logs desde hace 1 minuto:

    docker logs --since 1m log-generator
    

  7. Inspeccionar la ubicación de los logs en el host:

    docker inspect log-generator | grep LogPath
    

  8. Ver el contenido del archivo de logs directamente:

    sudo cat $(docker inspect log-generator | grep LogPath | awk '{print $2}' | tr -d '",')
    

Limpieza de ambiente

docker stop log-generator
docker rm log-generator

Laboratorio 2: Aplicación Python con Logs Estructurados

Objetivo

Crear una aplicación Python que envíe logs estructurados en formato JSON a stdout y stderr.

  1. Crear un directorio para el proyecto:

    mkdir ~/docker-logs-lab && cd ~/docker-logs-lab
    

  2. Crear el archivo de la aplicación Python (app.py):

    import json
    import sys
    import time
    from datetime import datetime
    import random
    
    def log_json(level, message, **kwargs):
        log_entry = {
            "timestamp": datetime.utcnow().isoformat() + "Z",
            "level": level,
            "service": "api-demo",
            "message": message,
            **kwargs
        }
    
        output = sys.stdout if level in ["INFO", "DEBUG"] else sys.stderr
        print(json.dumps(log_entry), file=output, flush=True)
    
    def main():
        log_json("INFO", "Aplicación iniciada", version="1.0.0")
    
        counter = 0
        while True:
            counter += 1
    
            # Simular diferentes tipos de logs
            if counter % 5 == 0:
                log_json("ERROR", "Error simulado en operación", 
                        request_id=f"req-{counter}", 
                        error_code=500)
            elif counter % 3 == 0:
                log_json("WARN", "Latencia alta detectada",
                        request_id=f"req-{counter}",
                        latency_ms=random.randint(1000, 3000))
            else:
                log_json("INFO", "Solicitud procesada exitosamente",
                        request_id=f"req-{counter}",
                        duration_ms=random.randint(10, 200))
    
            time.sleep(2)
    
    if __name__ == "__main__":
        try:
            main()
        except KeyboardInterrupt:
            log_json("INFO", "Aplicación detenida por usuario")
            sys.exit(0)
        except Exception as e:
            log_json("ERROR", "Error fatal en aplicación", error=str(e))
            sys.exit(1)
    

  3. Crear el Dockerfile:

    FROM python:3.11-alpine
    
    WORKDIR /app
    
    COPY app.py .
    
    CMD ["python", "-u", "app.py"]
    

  4. Construir la imagen:

    docker build -t python-logs-demo .
    

  5. Ejecutar el contenedor sin limitación de logs (⚠️ no recomendado en producción):

    docker run -d --name logs-unlimited python-logs-demo
    

  6. Ver los logs estructurados:

    docker logs -f --tail 20 logs-unlimited
    

  7. Filtrar solo logs de ERROR usando jq:

    docker logs logs-unlimited 2>&1 | grep ERROR | jq .
    

  8. Detener el contenedor sin limitación:

    docker stop logs-unlimited
    docker rm logs-unlimited
    

  9. Ejecutar el contenedor con rotación de logs (✅ recomendado):

    docker run -d --name logs-limited \
      --log-opt max-size=5m \
      --log-opt max-file=3 \
      python-logs-demo
    

  10. Verificar la configuración de logs:

    docker inspect logs-limited | jq '.[0].HostConfig.LogConfig'
    

  11. Seguir los logs y observar el formato JSON:

    docker logs -f logs-limited
    

📝 Los logs están estructurados en JSON, lo que facilita su procesamiento por sistemas de agregación como ELK Stack, Splunk, o Graylog.

Limpieza de ambiente

docker stop logs-limited
docker rm logs-limited
docker rmi python-logs-demo

Laboratorio 3: Configuración de Logging Drivers

Objetivo

Explorar diferentes drivers de logging y entender cómo redirigir logs a diferentes destinos.

  1. Ejecutar contenedor con driver json-file (por defecto):

    docker run -d --name nginx-json \
      --log-driver json-file \
      --log-opt max-size=1m \
      --log-opt max-file=2 \
      nginx
    

  2. Verificar el driver de logging:

    docker inspect nginx-json | jq '.[0].HostConfig.LogConfig'
    

  3. Generar tráfico para producir logs:

    docker exec nginx-json curl -s http://localhost > /dev/null
    

  4. Ver los logs:

    docker logs nginx-json
    

  5. Ejecutar contenedor con driver syslog (si está disponible):

    docker run -d --name nginx-syslog \
      --log-driver syslog \
      --log-opt syslog-address=udp://127.0.0.1:514 \
      --log-opt tag="nginx-container" \
      nginx
    

📝 Nota: Este contenedor enviará logs a syslog. Si no tienes un servidor syslog configurado, el contenedor podría no iniciar correctamente.

  1. Configurar logging a nivel de daemon de Docker (opcional, requiere permisos root).

Editar /etc/docker/daemon.json:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3",
    "labels": "production_status",
    "env": "os,customer"
  }
}

Reiniciar Docker:

sudo systemctl restart docker

Limpieza de ambiente

docker stop nginx-json nginx-syslog 2>/dev/null
docker rm nginx-json nginx-syslog 2>/dev/null