Módulo 2: Local & Container Deployment

6. Debugging Deployment Local

Descripción

En esta cápsula vas a dominar el flujo de debugging cuando algo falla en tu Docker Compose. Saber levantar servicios es la mitad; saber diagnosticar cuando no arrancan o fallan es la otra mitad. Al terminar, tendrás un proceso sistemático para resolver los problemas más comunes en deployment local de apps AI.

Contexto: El estudiante que no sabe debuggear Docker Compose se atasca al primer error y busca en StackOverflow 30 minutos. El que tiene un flujo sistemático resuelve en 5 minutos. Esta cápsula te da ese flujo.


El Flujo de Debugging Sistemático

Los 5 pasos

1. ¿Qué servicio falló?       → docker compose ps
2. ¿Qué dicen los logs?       → docker compose logs <service>
3. ¿Puedo entrar al container? → docker compose exec <service> sh
4. ¿La red funciona?           → ping/curl desde dentro
5. ¿La config es correcta?    → docker compose config

Paso 1: Estado de los servicios

docker compose ps
# NAME           SERVICE   STATUS          PORTS
# mod02-api-1    api       Up (healthy)    0.0.0.0:8000->8000/tcp
# mod02-cache-1  cache     Up (healthy)    6379/tcp
# mod02-worker-1 worker    Exited (1)

# El worker se cayó. Siguiente: ver logs.

Presta atención a la columna STATUS. Los estados clave:

EstadoSignificadoAcción
Up (healthy)Container corriendo y health check pasandoNo action
Up (unhealthy)Container corriendo pero health check fallaRevisar health check endpoint
UpContainer corriendo, sin health checkVerificar si debería tener uno
Exited (0)Container terminó normalmenteRevisar si era esperado
Exited (1)Container terminó con errorIr a Paso 2 (logs)
Exited (137)Container killed (OOM o signal)Revisar memoria con docker stats
RestartingContainer en restart loopDetener y revisar logs

Paso 2: Logs

# Logs de un servicio específico
docker compose logs worker
# [ERROR] redis.ConnectionError: Connection refused

# Logs con follow (en tiempo real)
docker compose logs -f api

# Últimas 50 líneas
docker compose logs --tail 50 api

# Logs de todos los servicios
docker compose logs

# Logs con timestamps (útil para correlacionar entre servicios)
docker compose logs -t api cache

# Logs desde un momento específico
docker compose logs --since 5m api
# Últimos 5 minutos

# Logs entre dos momentos
docker compose logs --since 2024-01-15T10:00:00 --until 2024-01-15T10:05:00 api

Paso 3: Exec (entrar al container)

# Shell interactivo
docker compose exec api sh
# o bash si está disponible:
docker compose exec api bash

# Ejecutar un comando puntual
docker compose exec api python -c "import redis; r = redis.Redis.from_url('redis://cache:6379'); print(r.ping())"

# Verificar variables de entorno
docker compose exec api env | grep REDIS

# Verificar que los archivos están donde esperas
docker compose exec api ls -la /app/

# Verificar la versión de Python y paquetes instalados
docker compose exec api python --version
docker compose exec api pip list

# Ejecutar un script de debug rápido
docker compose exec api python -c "
from config import settings
print(f'Environment: {settings.environment}')
print(f'Redis URL: {settings.redis_url}')
print(f'Model: {settings.model_name}')
"

Paso 4: Networking

# Desde dentro del container api, verificar conectividad
docker compose exec api ping -c 3 cache
docker compose exec api curl -s http://localhost:8000/health

# Ver puertos abiertos
docker compose exec api netstat -tlnp 2>/dev/null || ss -tlnp

# Verificar resolución DNS dentro de la red de Compose
docker compose exec api nslookup cache 2>/dev/null || \
  docker compose exec api getent hosts cache

# Verificar que Redis responde desde el container de API
docker compose exec api redis-cli -h cache -p 6379 ping

# Listar las redes de Compose
docker network ls | grep module-02

# Inspeccionar qué containers están en la red
docker network inspect module-02_default

Paso 5: Verificar config

# Ver la config mergeada (con variables sustituidas)
docker compose config

# Ver solo un servicio
docker compose config --services

# Verificar que las variables de entorno se resolvieron correctamente
docker compose config | grep -A 5 "environment:"

# Validar que el compose file no tiene errores de sintaxis
docker compose config --quiet && echo "✅ Config válida" || echo "❌ Config inválida"

# Ver la config con un override específico
docker compose -f docker-compose.yml -f docker-compose.prod.yml config

Comparación: logs vs inspect vs exec

Tienes tres herramientas principales para obtener información de un container. Cada una tiene un caso de uso distinto:

HerramientaQué haceCuándo usarla
docker compose logsVe stdout/stderr del proceso principalPrimer paso siempre: errores de app, stack traces, warnings
docker inspectVe config interna del container (env vars, mounts, redes, estado)Cuando necesitas ver config de runtime: IPs, variables, restart count
docker compose execEjecuta comandos dentro del container vivoCuando necesitas interactuar: probar conectividad, verificar archivos, correr scripts

Cuándo usar cada una — ejemplos prácticos

# ESCENARIO: "La API no arranca"
# Paso 1: ¿Hay error en los logs?
docker compose logs --tail 30 api
# → Si ves "ModuleNotFoundError" → falta dependencia
# → Si ves "Connection refused" → dependencia no lista

# ESCENARIO: "La API arranca pero no conecta a Redis"
# Paso 1: ¿Redis está corriendo?
docker compose ps cache
# Paso 2: ¿La URL es correcta?
docker inspect module-02-api-1 --format='{{range .Config.Env}}{{println .}}{{end}}' | grep REDIS
# → REDIS_URL=redis://cache:6379
# Paso 3: ¿Puedo conectar desde dentro?
docker compose exec api redis-cli -h cache ping

# ESCENARIO: "El container se reinicia sin parar"
# Paso 1: ¿Cuántos restarts lleva?
docker inspect module-02-api-1 --format='{{.RestartCount}}'
# → 15 (muchos restarts)
# Paso 2: ¿Qué dice el log antes de morir?
docker compose logs --tail 5 api
# Paso 3: Detener restart para investigar
docker compose stop api

docker inspect — los campos más útiles

# Estado del container (running, exited, restarting)
docker inspect --format='{{.State.Status}}' module-02-api-1

# Exit code del último crash
docker inspect --format='{{.State.ExitCode}}' module-02-api-1

# Cuántos restarts ha tenido
docker inspect --format='{{.RestartCount}}' module-02-api-1

# IP del container en la red de Compose
docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' module-02-api-1

# Mounts (volúmenes montados)
docker inspect --format='{{json .Mounts}}' module-02-api-1 | python -m json.tool

# Timestamp del último start
docker inspect --format='{{.State.StartedAt}}' module-02-api-1

Monitoreo de Recursos con docker stats

En apps AI, el monitoreo de recursos es crítico. Un modelo que carga embeddings consume gigabytes. Un request largo a OpenAI puede mantener conexiones abiertas. docker stats es tu dashboard de recursos en tiempo real.

Uso básico

# Monitoreo en tiempo real (actualiza cada segundo)
docker stats
# CONTAINER       CPU %   MEM USAGE / LIMIT     MEM %   NET I/O         BLOCK I/O
# mod02-api-1     2.5%    145MiB / 512MiB        28%     1.2kB / 800B    0B / 0B
# mod02-cache-1   0.1%    12MiB / 128MiB         9%      500B / 300B     0B / 4kB

# Snapshot (sin actualización en vivo)
docker stats --no-stream

# Formato personalizado
docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}"

# Solo un container
docker stats module-02-api-1

Qué vigilar en apps AI

MÉTRICA          NORMAL (AI app)     ALERTA              CRÍTICO
CPU %            1-20% (idle)        >50% sostenido      >90%
                 30-80% (request)
MEM USAGE        100-300MiB          >80% del límite     Cerca del límite
NET I/O          Variable            Crecimiento sin     Saturación
                                     requests

Script de monitoreo continuo

#!/bin/bash
# monitor.sh — Registra stats cada 5 segundos

LOG_FILE="docker-stats-$(date +%Y%m%d-%H%M%S).csv"
echo "timestamp,container,cpu,mem_usage,mem_limit,mem_pct" > "$LOG_FILE"

while true; do
  docker stats --no-stream --format "$(date +%s),{{.Name}},{{.CPUPerc}},{{.MemUsage}},{{.MemPerc}}" \
    >> "$LOG_FILE"
  sleep 5
done

Establecer límites de memoria en Compose

services:
  api:
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "1.0"
        reservations:
          memory: 256M
          cpus: "0.5"

Cuando un container excede el límite de memoria, Docker lo mata con signal 9 (exit code 137). En apps AI esto es común cuando:

  1. Cargas un modelo grande en memoria — Si tu app carga embeddings o un modelo local, necesita más RAM
  2. Acumulas responses sin liberar — Si guardas todas las responses en una lista en memoria
  3. FastAPI con muchos workers — Cada worker es un proceso separado con su propia memoria

Problemas Comunes en AI Apps

Problema 1: ImportError en la API

api-1  | ImportError: No module named 'openai'
# Causa: requirements.txt no incluye la dependencia
# o el Dockerfile no corre pip install

# Solución: Verificar requirements.txt y Dockerfile
docker compose exec api pip list | grep openai

# Si falta, agregar a requirements.txt y rebuild:
docker compose build api
docker compose up -d

Problema 2: Redis ConnectionError

api-1  | redis.exceptions.ConnectionError: Error while connecting to redis://cache:6379
# Verificar que Redis está running
docker compose ps cache
# Si está "Exited", ver logs:
docker compose logs cache

# Verificar que la URL es correcta
docker compose exec api env | grep REDIS
# Debe ser: REDIS_URL=redis://cache:6379

# Verificar conectividad
docker compose exec api redis-cli -h cache ping
# PONG = OK

Problema 3: Out of Memory (OOM)

api-1  | Killed
# o
api-1 exited with code 137  # 137 = killed by OOM
# Verificar uso de memoria
docker stats
# CONTAINER   CPU %   MEM USAGE / LIMIT
# api-1       2.5%    450MiB / 512MiB    ← Cerca del límite

# Soluciones:
# 1. Aumentar el límite de memoria en compose
# 2. Reducir el uso de memoria de la app
# 3. No cargar modelos grandes en memoria

Problema 4: Timeout de OpenAI API

api-1  | openai.APITimeoutError: Request timed out
# Aumentar timeout en el client
client = OpenAI(
    api_key=os.environ["OPENAI_API_KEY"],
    timeout=60.0,  # Default es 10s, aumentar para prompts largos
    max_retries=3,
)

Problema 5: Container se reinicia en loop

docker compose ps
# api-1  Restarting (1) 5 seconds ago

# Ver los últimos logs antes del crash
docker compose logs --tail 20 api
# Buscar el error que causa el crash

# Detener los restarts para investigar
docker compose stop api
docker compose logs api

AI-Specific Debugging: Problemas que Solo Pasan en Apps AI

Las apps AI tienen problemas que no existen en apps web tradicionales. Esta sección cubre los más comunes.

OOM Kills por Carga de Modelos

Cuando tu app carga un modelo local (embeddings, sentence-transformers, etc.), el consumo de memoria salta al arrancar:

# Patrón típico en logs
api-1  | Loading model sentence-transformers/all-MiniLM-L6-v2...
api-1  | Killed

# El container muere antes de terminar de cargar el modelo
docker inspect --format='{{.State.ExitCode}}' module-02-api-1
# 137 = OOM kill

Diagnóstico:

# Ver cuánta memoria necesita tu app al arrancar
docker stats --no-stream
# Antes de cargar modelo: 80MiB
# Después: 450MiB
# Límite: 512MiB → no deja espacio para requests

# Solución 1: Aumentar el límite
# En docker-compose.yml:
# deploy.resources.limits.memory: 1G

# Solución 2: Usar modelos más pequeños
# all-MiniLM-L6-v2 (~80MB) vs all-mpnet-base-v2 (~420MB)

# Solución 3: Lazy loading (cargar en primer request, no al arrancar)
# Lazy loading pattern
class ModelManager:
    def __init__(self):
        self._model = None

    @property
    def model(self):
        if self._model is None:
            from sentence_transformers import SentenceTransformer
            self._model = SentenceTransformer("all-MiniLM-L6-v2")
        return self._model

model_manager = ModelManager()

LLM API Timeouts y Retries

Los LLMs son lentos comparados con APIs tradicionales. Un request a GPT-4 puede tomar 10-30 segundos:

import time
import logging
from openai import OpenAI, APITimeoutError, RateLimitError

logger = logging.getLogger(__name__)

client = OpenAI(
    api_key=os.environ["OPENAI_API_KEY"],
    timeout=60.0,
    max_retries=3,
)

def ask_llm_with_retry(prompt: str, max_retries: int = 3) -> str:
    for attempt in range(max_retries):
        try:
            start = time.time()
            response = client.chat.completions.create(
                model="gpt-4o-mini",
                messages=[{"role": "user", "content": prompt}],
                max_tokens=500,
            )
            elapsed = time.time() - start
            logger.info(f"LLM response in {elapsed:.1f}s (attempt {attempt + 1})")
            return response.choices[0].message.content

        except APITimeoutError:
            logger.warning(f"Timeout on attempt {attempt + 1}/{max_retries}")
            if attempt == max_retries - 1:
                raise
            time.sleep(2 ** attempt)

        except RateLimitError:
            wait = 2 ** (attempt + 2)
            logger.warning(f"Rate limited, waiting {wait}s")
            time.sleep(wait)

Slow Cold Starts

La primera request después de docker compose up tarda mucho más que las siguientes:

# Medir cold start
time curl -s -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"prompt":"test"}'
# real    0m8.234s ← primera request (cold)

time curl -s -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"prompt":"test"}'
# real    0m0.045s ← segunda request (cached)

Causas y soluciones:

CausaTiempoSolución
Uvicorn startup + módulos2-5sPreload modules, --preload flag
Primera conexión a Redis0.5-1sstart_period en health check
Primera request a OpenAI3-10sWarm-up request al arrancar
Carga de modelo local5-30sLazy loading, modelos más pequeños
# Warm-up pattern: ejecutar al arrancar
from contextlib import asynccontextmanager
from fastapi import FastAPI

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Warm up connections
    try:
        cache.ping()
        logger.info("Redis connection warm")
    except Exception:
        logger.warning("Redis not available at startup")
    yield

app = FastAPI(lifespan=lifespan)

Herramientas de Debugging

docker compose exec vs docker compose run

# exec: entra a un container que YA está corriendo
docker compose exec api sh

# run: crea un nuevo container temporal
docker compose run --rm api python -c "print('test')"

# Diferencia clave:
# exec → mismo container, mismo estado, mismas conexiones
# run → container nuevo, limpio, sin ports mapeados

Inspeccionar un container

# Ver toda la config del container
docker inspect module-02-api-1

# Ver solo las variables de entorno
docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' module-02-api-1

# Ver los mounts
docker inspect --format='{{json .Mounts}}' module-02-api-1 | python -m json.tool

Rebuild limpio

# Si sospechas que el build está corrupto:
docker compose down
docker compose build --no-cache
docker compose up -d

docker compose events — ver eventos en tiempo real

# Monitorear eventos de todos los containers
docker compose events

# Eventos típicos que verás:
# container start, container die, container health_status
# network connect, volume mount

# Útil para debug de restart loops:
docker compose events --filter event=die
# Te muestra cada vez que un container muere

Logs estructurados con JSON

Si configuras tu app para loguear en JSON, puedes filtrar más fácilmente:

import logging
import json

class JSONFormatter(logging.Formatter):
    def format(self, record):
        log_entry = {
            "timestamp": self.formatTime(record),
            "level": record.levelname,
            "message": record.getMessage(),
            "module": record.module,
        }
        if record.exc_info:
            log_entry["exception"] = self.formatException(record.exc_info)
        return json.dumps(log_entry)

handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
logger = logging.getLogger("ai-api")
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)
# Filtrar logs JSON por nivel de error
docker compose logs api 2>&1 | python -c "
import sys, json
for line in sys.stdin:
    try:
        entry = json.loads(line.split('| ', 1)[-1])
        if entry.get('level') == 'ERROR':
            print(json.dumps(entry, indent=2))
    except (json.JSONDecodeError, IndexError):
        pass
"

Ejercicios Prácticos

Ejercicio 1: Diagnostica un fallo simulado

Detén Redis mientras la API está corriendo. ¿Qué pasa? ¿Cómo lo diagnosticas?

Ver solución
# Detener Redis
docker compose stop cache

# Verificar estado
docker compose ps
# cache: Exited, api: Up (pero ¿healthy?)

# Hacer un request
curl http://localhost:8000/ask -X POST \
  -H "Content-Type: application/json" \
  -d '{"prompt":"test"}'
# Si implementaste graceful degradation: funciona sin cache
# Si no: retorna error de conexión

# Verificar health
curl http://localhost:8000/health
# {"status":"degraded","services":{"api":"up","redis":"down"}}

# Restaurar
docker compose start cache

Ejercicio 2: Debug de memory leak

Usa docker stats para monitorear el uso de memoria de tu API durante 10 requests. ¿Crece la memoria?

Ver solución
# Terminal 1: monitorear stats
docker stats module-02-api-1

# Terminal 2: enviar requests
for i in $(seq 1 10); do
  curl -s -X POST http://localhost:8000/ask \
    -H "Content-Type: application/json" \
    -d '{"prompt":"Test request '$i'"}' > /dev/null
  echo "Request $i sent"
done

# Observar en Terminal 1 si MEM USAGE crece significativamente
# Si crece y no baja: posible memory leak
# Si se estabiliza: normal (caching, object pools)

Ejercicio 3: Script de diagnóstico

Crea un script debug.sh que ejecute los 5 pasos de debugging automáticamente.

Ver solución
#!/bin/bash
echo "=== 1. Service Status ==="
docker compose ps

echo -e "\n=== 2. Recent Logs (last 10 lines per service) ==="
for service in $(docker compose config --services); do
  echo "--- $service ---"
  docker compose logs --tail 10 "$service" 2>&1
done

echo -e "\n=== 3. Health Checks ==="
curl -s http://localhost:8000/health | python -m json.tool 2>/dev/null || echo "API not responding"

echo -e "\n=== 4. Resource Usage ==="
docker stats --no-stream

echo -e "\n=== 5. Config Validation ==="
docker compose config --quiet && echo "Config OK" || echo "Config ERROR"

Ejercicio 4: Diagnostica un exit code 137

Configura un límite de memoria muy bajo (64MB) para el servicio api y haz un request que cargue mucho en memoria. Diagnostica el crash y corrígelo.

Ver solución
# docker-compose.debug.yml — override con memoria muy baja
services:
  api:
    deploy:
      resources:
        limits:
          memory: 64M
# Levantar con el override restrictivo
docker compose -f docker-compose.yml -f docker-compose.debug.yml up -d

# Verificar que arrancó
docker compose ps
# api   Up (healthy) — puede que sí arranque con 64MB

# Hacer un request pesado
curl -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"prompt":"Escribe un ensayo extenso sobre la historia de la inteligencia artificial desde los años 50 hasta hoy", "max_tokens": 2000}'

# Verificar si murió
docker compose ps
# Si STATUS es "Exited (137)" → OOM kill

# Confirmar con inspect
docker inspect --format='{{.State.ExitCode}}' module-02-api-1
# 137

# Verificar cuánta memoria estaba usando antes del kill
docker compose logs --tail 5 api
# Puede que no haya log (killed abruptamente)

# Solución: aumentar el límite
# Cambiar 64M → 512M en el override
# O quitar el override: docker compose up -d (usa config base)

# Verificar la corrección
docker compose up -d
docker stats --no-stream
# MEM USAGE debería tener margen suficiente

Ejercicio 5: Correlación de logs entre servicios

Tu API reporta "Redis connection error" pero Redis dice que está UP. Usa logs con timestamps para diagnosticar el problema real.

Ver solución
# Ver logs con timestamps de ambos servicios
docker compose logs -t api cache 2>&1 | sort -t 'Z' -k1

# Ejemplo de output:
# 2024-01-15T10:00:01Z cache-1 | Ready to accept connections
# 2024-01-15T10:00:01Z api-1   | Starting uvicorn...
# 2024-01-15T10:00:02Z api-1   | redis.ConnectionError: Connection refused
# 2024-01-15T10:00:05Z cache-1 | Ready to accept connections on port 6379

# El problema: api intentó conectar ANTES de que Redis estuviera listo
# Aunque Redis "arrancó", no estaba aceptando conexiones aún

# Solución: depends_on con condition: service_healthy
# (Ya implementado en nuestro compose, pero si no lo tuvieras)

# Verificar que el health check de Redis funciona
docker compose exec cache redis-cli ping
# PONG

# Verificar que depends_on está bien configurado
docker compose config | grep -A 5 "depends_on"
#   depends_on:
#     cache:
#       condition: service_healthy

# Si el timing sigue siendo un problema, agregar retry en la app:
import time
import redis

def get_redis_connection(url: str, max_retries: int = 5) -> redis.Redis:
    for attempt in range(max_retries):
        try:
            r = redis.Redis.from_url(url, decode_responses=True)
            r.ping()
            return r
        except redis.ConnectionError:
            if attempt < max_retries - 1:
                wait = 2 ** attempt
                print(f"Redis not ready, retrying in {wait}s...")
                time.sleep(wait)
            else:
                raise

Troubleshooting

"docker compose logs no muestra nada"

Si un container muere inmediatamente, puede que no haya logs. Usa docker compose run para ver el error:

docker compose run --rm api python -c "from config import settings; print('OK')"

Si el error es en el import de settings (como una variable faltante), lo verás aquí.

"Container Exited (2) — syntax error"

Exit code 2 suele ser error de bash/shell. Verifica el CMD/ENTRYPOINT en tu Dockerfile:

docker compose logs api
# /bin/sh: uvicorn: not found

# Solución: pip install no se ejecutó, rebuild:
docker compose build --no-cache api

"Port already in use"

# Error: bind: address already in use
# Alguien más está usando el puerto 8000

# Encontrar qué proceso usa el puerto
lsof -i :8000
# o
netstat -tlnp | grep 8000

# Solución: cambiar el puerto en .env o matar el proceso
# En .env: API_PORT=8001

"Container no puede resolver hostname de otro servicio"

# Error: Could not resolve host: cache
# Los containers no están en la misma red

docker network ls | grep module-02
# Si no hay red → los containers no están conectados

# Solución: verificar que ambos servicios están en el mismo compose file
docker compose config --services
# Debe listar: api, cache

"Health check siempre falla pero la app funciona"

# El health check usa curl, pero curl no está instalado en la imagen
docker compose exec api curl --version
# sh: curl: not found

# Solución: instalar curl en el Dockerfile
# RUN apt-get update && apt-get install -y curl

# O usar un health check que no requiera curl:
# test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]

Resumen

  • Debugging sistemático en 5 pasos: ps → logs → exec → networking → config.
  • Los problemas más comunes en AI apps: ImportError, ConnectionError, OOM, timeout de LLM APIs, restart loops.
  • docker compose logs -f <service> es tu herramienta principal.
  • docker compose exec <service> sh te da acceso directo al container.
  • docker stats monitorea CPU y memoria en tiempo real — crítico para apps AI con modelos pesados.
  • docker inspect revela config interna: env vars, IPs, restart count, exit codes.
  • OOM kills (exit code 137) son el problema #1 en apps AI — monitorea memoria siempre.
  • LLM timeouts necesitan retry con backoff exponencial — los LLMs son lentos.
  • Rebuild limpio (build --no-cache) cuando sospechas corrupción del build.

Recursos Adicionales

  1. Docker Compose CLI Reference — Todos los comandos de Compose
  2. Docker Logs — Configuración de logging
  3. Docker Stats — Monitoreo de recursos
  4. Debugging Docker Containers — Métricas y debugging
  5. FastAPI Debugging — Tips de debugging en FastAPI
  6. Python Logging Best Practices — Configurar logging en Python
  7. Docker Events — Monitoreo de eventos en tiempo real