Módulo 6: Debugging con Claude Code

Log Analysis con Claude Code

Log Analysis con Claude Code

Descripción de la cápsula

Si tuvieras que elegir una sola habilidad de debugging con Claude Code, debería ser esta: log analysis. El 70% de los bugs que encontrarás en tu día a día se pueden diagnosticar analizando logs. Y Claude Code es extraordinariamente bueno en esto — puede procesar 20, 50, o 200 líneas de logs y encontrar la aguja en el pajar mucho más rápido que tú leyendo línea por línea.

Pero hay una trampa: si le pegas solo la línea del error, obtendrás un diagnóstico genérico. Si le pegas 200 líneas sin contexto, obtendrás un diagnóstico disperso. La clave está en qué le das y cómo le pides que lo analice. En esta cápsula aprenderás a preparar logs para Claude Code, a dar el contexto correcto, y lo más importante: a evaluar su diagnóstico antes de actuar.


Qué Puede y Qué No Puede Ver Claude Code

Antes de pasar cualquier log a Claude Code, entiende qué está viendo:

Lo que Claude Code puede analizar

  • ✅ Texto de logs que tú le proporcionas (copy/paste)
  • ✅ Patrones de timestamps, niveles de severidad, mensajes de error
  • ✅ Stack traces completos incluidos en los logs
  • ✅ Secuencia de eventos (qué pasó antes del error)
  • ✅ Correlación entre mensajes (request → processing → error)

Lo que Claude Code NO puede hacer

  • ❌ Acceder a logs en tiempo real de tu aplicación
  • ❌ Ejecutar tu aplicación para reproducir el error
  • ❌ Ver el estado de variables en el momento del error
  • ❌ Acceder a tus archivos de log directamente (necesitas copiar el contenido)
  • ❌ Monitorear cambios en los logs mientras haces cambios

La implicación es clara: tú eres quien captura los logs y se los pasa. La calidad del diagnóstico depende directamente de la calidad de lo que le des.


Anatomía de un Buen Log para Diagnosis

No todos los logs son igualmente útiles. Compara estos dos escenarios:

Escenario malo: Solo el error

Tú le pasas a Claude Code:
────────────────────────────
KeyError: 'user_id'
────────────────────────────

Claude Code puede decirte que hay un diccionario donde se busca la clave user_id y no existe. Pero eso ya lo sabías. No tiene suficiente contexto para decirte dónde ni por qué.

Escenario bueno: Error con contexto

Tú le pasas a Claude Code:
────────────────────────────
2026-03-13 14:22:01 INFO     Starting request: POST /api/tasks
2026-03-13 14:22:01 INFO     Auth middleware: token validated
2026-03-13 14:22:01 INFO     Auth middleware: user extracted from token
2026-03-13 14:22:01 DEBUG    Request body: {"title": "New task", "priority": "high"}
2026-03-13 14:22:01 INFO     TaskService.create_task called
2026-03-13 14:22:01 DEBUG    Looking up user: user_data={'email': 'test@example.com', 'role': 'admin'}
2026-03-13 14:22:01 ERROR    Unhandled exception in create_task
Traceback (most recent call last):
  File "/app/services/task_service.py", line 45, in create_task
    owner_id = user_data['user_id']
KeyError: 'user_id'
2026-03-13 14:22:01 ERROR    Response: 500 Internal Server Error
────────────────────────────

Ahora Claude Code puede ver:

  1. El request era POST /api/tasks
  2. La autenticación pasó correctamente
  3. El user_data contiene email y role pero no user_id
  4. El código en task_service.py:45 espera user_id pero el token no lo incluye
  5. El fix probable: incluir user_id en el token o usar email como identificador

La diferencia entre un diagnóstico genérico y uno accionable está en el contexto que proporcionas.


La Regla del Contexto: 5 Líneas Arriba, 5 Líneas Abajo

Como regla general, cuando captures logs para pasarle a Claude Code, incluye al menos 5 líneas antes del error y 5 líneas después (si las hay). Esto captura:

  • Antes: Qué operaciones se ejecutaron correctamente, qué datos estaban disponibles
  • El error: El mensaje de error y el stack trace
  • Después: Cómo respondió el sistema al error, si hubo cascada de errores

Cuándo necesitas más contexto

A veces 5 líneas no son suficientes. Incluye más contexto cuando:

  • ✅ El error parece una consecuencia de algo que pasó antes (error de datos, estado incorrecto)
  • ✅ Hay múltiples requests en los logs y necesitas aislar cuál falló
  • ✅ El log muestra una secuencia de operaciones (inicio → procesamiento → error)
  • ✅ El error es intermitente y necesitas comparar una ejecución exitosa con una fallida

Cuándo menos es más

Reduce el contexto cuando:

  • ✅ Los logs tienen mucho ruido (health checks, metrics, etc.)
  • ✅ Hay información sensible que no debes compartir (tokens, passwords, PII)
  • ✅ El error es claro y el stack trace es suficiente

Cómo Pasar Logs a Claude Code: Prompt Templates

La forma en que le pides a Claude Code que analice logs afecta la calidad de la respuesta. Aquí hay templates probados:

Template 1: Diagnóstico general

Analiza estos logs de mi aplicación FastAPI. El endpoint POST /api/tasks 
está devolviendo un error 500. Identifica la causa raíz y sugiere un fix.

Logs:
"""
[pegar logs aquí]
"""

Contexto adicional:
- La aplicación usa JWT para autenticación
- El modelo Task tiene campos: id, title, priority, owner_id, created_at
- Este endpoint funcionaba antes del último deploy

Template 2: Comparación exitoso vs fallido

Estos son los logs de dos requests al mismo endpoint. El primero funciona 
correctamente, el segundo falla. Compara ambos e identifica qué es diferente 
que causa el error.

Request exitoso:
"""
[pegar logs del request que funciona]
"""

Request fallido:
"""
[pegar logs del request que falla]
"""

Template 3: Error intermitente

Este error aparece aproximadamente 1 de cada 10 requests al endpoint 
GET /api/users/{id}. Analiza estos logs que muestran 3 requests exitosos 
y 1 fallido. ¿Qué patrón ves que podría explicar por qué falla 
intermitentemente?

Logs (marcados con [OK] y [FAIL]):
"""
[pegar logs]
"""

Template 4: Secuencia de errores

Mi aplicación empezó a fallar hace 20 minutos. Estos son los primeros 
errores que aparecieron. Identifica cuál fue el error ORIGINAL (root cause) 
y cuáles son consecuencia de ese primer error.

Logs (ordenados cronológicamente):
"""
[pegar logs]
"""

Ejemplo Completo: Del Log al Fix

Veamos un escenario realista completo. Tu aplicación FastAPI de gestión de tareas tiene un endpoint que falla.

Paso 1: Capturar los logs

Tu aplicación imprime estos logs cuando falla:

2026-03-13 10:15:30 INFO     uvicorn.access: "GET /api/tasks?status=completed HTTP/1.1" 200
2026-03-13 10:15:45 INFO     uvicorn.access: "GET /api/tasks?status=pending HTTP/1.1" 200
2026-03-13 10:16:02 INFO     Starting request: GET /api/tasks/stats
2026-03-13 10:16:02 INFO     Auth: user admin@company.com authenticated
2026-03-13 10:16:02 DEBUG    TaskService.get_stats called
2026-03-13 10:16:02 DEBUG    Querying tasks for stats...
2026-03-13 10:16:02 DEBUG    Total tasks: 47
2026-03-13 10:16:02 DEBUG    Tasks by status: {'pending': 12, 'in_progress': 8, 'completed': 25, 'cancelled': 2}
2026-03-13 10:16:02 DEBUG    Calculating completion rate...
2026-03-13 10:16:02 ERROR    Unhandled exception in get_stats
Traceback (most recent call last):
  File "/app/services/task_service.py", line 89, in get_stats
    completion_rate = completed_count / total_active * 100
ZeroDivisionError: division by zero
2026-03-13 10:16:02 ERROR    Response: 500 Internal Server Error

Paso 2: Preparar el prompt para Claude Code

Analiza estos logs de mi aplicación FastAPI. El endpoint GET /api/tasks/stats 
devuelve 500. Identifica la causa raíz.

Logs:
"""
[los logs de arriba]
"""

Contexto:
- total_active debería ser total - cancelled
- Este endpoint funciona normalmente, pero falló hoy por primera vez

Paso 3: Diagnóstico de Claude Code (típico)

Claude Code probablemente dirá algo como:

El error es un ZeroDivisionError en la línea 89 de task_service.py. La variable total_active es 0, lo que causa la división por cero.

Mirando los logs, el total de tasks es 47, con la distribución:

  • pending: 12, in_progress: 8, completed: 25, cancelled: 2

Si total_active = total - cancelled = 47 - 2 = 45, no debería ser cero. Esto sugiere que el cálculo de total_active tiene un bug: probablemente está calculando algo diferente a total - cancelled.

Posible causa: total_active se calcula filtrando por un status específico que no tiene tasks (por ejemplo, un status nuevo que se agregó pero no tiene registros), o completed_count y total_active están usando queries diferentes que no son consistentes.

Paso 4: Verificar el diagnóstico

Ahora no aplicas el fix sugerido ciegamente. Verificas:

  1. Abres task_service.py línea 89 y lees el código real:
from fastapi import APIRouter, Depends, HTTPException
from typing import Dict, Any
from app.models import Task, TaskStatus
from app.database import get_db
from sqlalchemy.orm import Session

router = APIRouter()

class TaskService:
    def __init__(self, db: Session):
        self.db = db

    def get_stats(self) -> Dict[str, Any]:
        all_tasks = self.db.query(Task).all()
        total = len(all_tasks)

        by_status = {}
        for task in all_tasks:
            status = task.status.value
            by_status[status] = by_status.get(status, 0) + 1

        completed_count = by_status.get('completed', 0)
        total_active = by_status.get('active', 0)  # BUG: 'active' no es un status válido

        completion_rate = completed_count / total_active * 100  # línea 89

        return {
            "total": total,
            "by_status": by_status,
            "completion_rate": completion_rate
        }
  1. Confirmas el diagnóstico: total_active busca el status 'active' que no existe en TaskStatus. Debería ser total - by_status.get('cancelled', 0).

  2. Claude Code acertó en la dirección pero no en el detalle exacto. Sin ver el código, no podía saber que el bug era buscar un status inexistente.

Paso 5: Aplicar y verificar el fix

completed_count = by_status.get('completed', 0)
cancelled_count = by_status.get('cancelled', 0)
total_active = total - cancelled_count

if total_active == 0:
    completion_rate = 0.0
else:
    completion_rate = completed_count / total_active * 100

Verificas ejecutando el endpoint de nuevo y confirmando que devuelve datos correctos.


Los 5 Errores Más Comunes al Pasar Logs a Claude Code

Error 1: Pasar solo la última línea del error

# Malo
"ZeroDivisionError: division by zero"

# Bueno
[20+ líneas con contexto: qué request, qué datos, qué operaciones previas]

Error 2: No limpiar información sensible

# Malo - incluye tokens reales
"Auth: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM..."

# Bueno - sanitizado
"Auth: Bearer [TOKEN_REDACTED]"

Error 3: No dar contexto sobre la aplicación

Sin contexto, Claude Code asume una aplicación genérica. Dile qué framework usas, qué base de datos, qué modelo de datos.

Error 4: Aceptar el primer diagnóstico sin verificar

Claude Code da hipótesis. Siempre verifica abriendo el archivo y la línea mencionada. El diagnóstico puede ser 80% correcto pero el 20% incorrecto cambia el fix.

Error 5: No incluir logs de requests exitosos

Si tienes un error intermitente, comparar un request exitoso con uno fallido es la técnica más poderosa. Claude Code puede encontrar la diferencia.


Evaluando el Diagnóstico de Claude Code

No todo lo que Claude Code dice sobre tus logs es correcto. Aquí hay un framework para evaluar:

Nivel de confianza alto

Confía más en el diagnóstico cuando:

  • ✅ Claude Code señala una línea específica y explica por qué falla
  • ✅ El diagnóstico es consistente con los datos en los logs
  • ✅ La explicación incluye causa y efecto (A pasó, lo que causó B, que resultó en C)
  • ✅ El error es un tipo común (KeyError, TypeError, ValueError) con causa clara

Nivel de confianza bajo

Desconfía del diagnóstico cuando:

  • ⚠️ Claude Code dice "probablemente" o "posiblemente" sin evidencia en los logs
  • ⚠️ El diagnóstico no explica por qué el error es intermitente
  • ⚠️ La sugerencia de fix es genérica ("agrega un try/except")
  • ⚠️ Claude Code sugiere que el problema está en una librería externa sin evidencia

Señales de que Claude Code está adivinando

  • ❌ "Esto podría ser causado por..." seguido de 5 posibles causas sin priorizar
  • ❌ Sugerencias que no se relacionan con los datos en los logs
  • ❌ "El error probablemente está en la configuración" sin señalar qué configuración
  • ❌ Diagnósticos que contradicen la información en los logs

Técnica Avanzada: Logs Estructurados

Si configuras tu aplicación con logging estructurado, Claude Code puede hacer análisis mucho más efectivos.

Logging básico vs estructurado

import logging
import json
from datetime import datetime
from typing import Any, Optional

logger = logging.getLogger("app")

def log_basic(message: str):
    logger.info(message)

def log_structured(
    event: str,
    data: Optional[dict[str, Any]] = None,
    error: Optional[str] = None
):
    log_entry = {
        "timestamp": datetime.utcnow().isoformat(),
        "event": event,
        "data": data or {},
    }
    if error:
        log_entry["error"] = error
    logger.info(json.dumps(log_entry))

Ejemplo de uso en un endpoint

from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel
from typing import Optional
import logging
import json
from datetime import datetime

app = FastAPI()
logger = logging.getLogger("app")

class TaskCreate(BaseModel):
    title: str
    priority: str
    assignee_id: Optional[int] = None

@app.post("/api/tasks")
async def create_task(task: TaskCreate, request: Request):
    logger.info(json.dumps({
        "event": "create_task.start",
        "data": {
            "title": task.title,
            "priority": task.priority,
            "assignee_id": task.assignee_id,
            "client_ip": request.client.host
        }
    }))

    if task.priority not in ["low", "medium", "high", "critical"]:
        logger.warning(json.dumps({
            "event": "create_task.invalid_priority",
            "data": {"priority": task.priority}
        }))
        raise HTTPException(status_code=400, detail="Invalid priority")

    logger.info(json.dumps({
        "event": "create_task.success",
        "data": {"title": task.title}
    }))

    return {"status": "created", "title": task.title}

Por qué los logs estructurados son mejores para Claude Code

Cuando le pasas logs estructurados, Claude Code puede:

  • ✅ Parsear campos específicos (event, data, error)
  • ✅ Correlacionar eventos por timestamp
  • ✅ Identificar qué datos estaban disponibles en cada paso
  • ✅ Detectar valores inesperados en campos específicos

Logging Efectivo para Debugging con AI

Para maximizar la utilidad de Claude Code en debugging, configura tu logging así:

Qué loggear en cada nivel

import logging

logger = logging.getLogger("app")

# DEBUG: datos internos útiles para debugging
logger.debug(f"Query result: {result}")
logger.debug(f"Cache hit: key={cache_key}")

# INFO: operaciones de negocio normales
logger.info(f"Task created: id={task.id}")
logger.info(f"User logged in: email={user.email}")

# WARNING: situaciones anómalas que no son errores
logger.warning(f"Slow query: {elapsed_ms}ms for {query_name}")
logger.warning(f"Rate limit approaching: {current}/{limit}")

# ERROR: errores que afectan la operación
logger.error(f"Failed to create task: {str(e)}")
logger.error(f"Database connection failed: {str(e)}")

# CRITICAL: errores que comprometen la aplicación
logger.critical(f"All database connections exhausted")

Configuración recomendada para desarrollo

import logging
import sys

def setup_dev_logging():
    logging.basicConfig(
        level=logging.DEBUG,
        format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S",
        handlers=[logging.StreamHandler(sys.stdout)]
    )

    logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
    logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)

Esta configuración:

  • ✅ Muestra DEBUG y superiores (máximo detalle)
  • ✅ Incluye timestamps legibles
  • ✅ Reduce ruido de uvicorn y SQLAlchemy
  • ✅ Output a stdout (fácil de copiar)

Caso Práctico: Debugging de un Error de Autenticación

Escenario: tu endpoint protegido devuelve 401 para un usuario que debería tener acceso.

Los logs

2026-03-13 09:30:15 INFO     POST /api/admin/reports
2026-03-13 09:30:15 DEBUG    Auth middleware: extracting token
2026-03-13 09:30:15 DEBUG    Token payload: {'sub': 'maria@company.com', 'role': 'editor', 'exp': 1741859415}
2026-03-13 09:30:15 DEBUG    Required role for /api/admin/*: admin
2026-03-13 09:30:15 WARNING  Access denied: user role 'editor' does not match required role 'admin'
2026-03-13 09:30:15 INFO     Response: 401 Unauthorized

Prompt para Claude Code

Este usuario (maria@company.com) debería tener acceso al endpoint 
POST /api/admin/reports. Es editora senior y tiene permisos de admin 
según nuestro sistema de roles. Pero recibe 401.

Logs:
"""
[los logs de arriba]
"""

Contexto:
- Nuestro sistema de roles tiene: viewer, editor, admin
- Los editores senior deberían tener acceso a /api/admin/* endpoints
- El endpoint usa un middleware que verifica roles

Diagnóstico esperado de Claude Code

Claude Code debería identificar que:

  1. El token del usuario contiene role: 'editor', no role: 'admin'
  2. El middleware hace un match exacto: role == 'admin'
  3. Hay un disconnect entre "editores senior tienen acceso admin" (regla de negocio) y "el middleware solo acepta role=admin" (implementación)

El fix depende de la decisión de negocio:

  • Opción A: Cambiar el token para que editores senior tengan role: 'admin'
  • Opción B: Cambiar el middleware para aceptar una lista de roles: ['admin', 'editor']
  • Opción C: Implementar un sistema de permisos más granular

Tu verificación

Abres el middleware y confirmas:

from fastapi import Request, HTTPException
from typing import Callable

async def require_admin(request: Request, call_next: Callable):
    user = request.state.user
    if user.get("role") != "admin":
        raise HTTPException(status_code=401, detail="Unauthorized")
    response = await call_next(request)
    return response

Efectivamente, hace match exacto. El fix correcto depende del contexto de negocio — algo que Claude Code no puede decidir por ti.


Conexión con Proyecto

Cómo aplica al proyecto integrador (Módulo 8)

En el proyecto integrador, la aplicación FastAPI tiene logging configurado. Parte de tu trabajo será ejecutar endpoints, capturar logs cuando fallen, y usar Claude Code para diagnosticar los problemas. Las técnicas de esta cápsula — cuánto contexto dar, qué prompt usar, cómo evaluar el diagnóstico — son exactamente las que usarás.

La diferencia es que en el proyecto integrador, algunos bugs se manifiestan solo cuando interactúan con otros componentes. Necesitarás capturar logs más amplios y correlacionar eventos entre diferentes endpoints.


Troubleshooting

Problema 1: "Claude Code da diagnósticos genéricos"

Causa: Probablemente le estás pasando muy poco contexto — solo el error sin las líneas anteriores. Solución: Usa la regla de 5+5: al menos 5 líneas antes y 5 después del error. Incluye el tipo de request, los datos que se estaban procesando, y cualquier log de debug disponible.

Problema 2: "Los logs de mi aplicación no tienen suficiente detalle"

Causa: Tu logging está configurado en nivel INFO o superior, sin DEBUG. Solución: Configura logging.basicConfig(level=logging.DEBUG) en desarrollo. Agrega logger.debug() en los puntos clave: entrada de funciones, datos recibidos, resultados de queries.

Problema 3: "Claude Code sugiere un fix pero no funciona"

Causa: El diagnóstico era parcialmente correcto pero el fix no consideraba todo el contexto. Solución: Antes de aplicar un fix sugerido por Claude Code, siempre abre el archivo y lee las líneas alrededor de donde sugiere el cambio. Verifica que el fix es compatible con el resto del código.

Problema 4: "Mis logs tienen información sensible"

Causa: Logging de tokens, passwords, o datos personales. Solución: Antes de pasarle logs a Claude Code, sanitiza: reemplaza tokens con [TOKEN], passwords con [REDACTED], emails reales con user@example.com. El diagnóstico no necesita datos reales.

Problema 5: "Claude Code da 5 posibles causas sin priorizar"

Causa: Le diste información ambigua que permite múltiples interpretaciones. Solución: Incluye contexto de negocio: "Este endpoint funcionaba ayer", "El error solo aparece con usuarios nuevos", "Empezó después del último deploy". Cuanto más específico el contexto, más específico el diagnóstico.


Ejercicios

Ejercicio 1: Identificar qué logs pasar (Fácil)

Tienes estos logs. Tu endpoint GET /api/users/42 devuelve 500. ¿Cuáles líneas le pasarías a Claude Code para diagnóstico?

2026-03-13 08:00:01 INFO     Health check: OK
2026-03-13 08:00:05 INFO     GET /api/tasks 200
2026-03-13 08:00:12 INFO     GET /api/users 200
2026-03-13 08:01:30 INFO     Health check: OK
2026-03-13 08:02:15 INFO     GET /api/users/42
2026-03-13 08:02:15 DEBUG    UserService.get_user(id=42)
2026-03-13 08:02:15 DEBUG    DB query: SELECT * FROM users WHERE id = 42
2026-03-13 08:02:15 DEBUG    Query result: {'id': 42, 'name': 'Ana', 'email': 'ana@test.com', 'preferences': None}
2026-03-13 08:02:15 ERROR    TypeError: argument of type 'NoneType' is not iterable
2026-03-13 08:02:15 ERROR    Response: 500 Internal Server Error
2026-03-13 08:02:45 INFO     GET /api/tasks 200
2026-03-13 08:03:00 INFO     Health check: OK
Ver solución

Líneas a incluir (6-10):

2026-03-13 08:02:15 INFO     GET /api/users/42
2026-03-13 08:02:15 DEBUG    UserService.get_user(id=42)
2026-03-13 08:02:15 DEBUG    DB query: SELECT * FROM users WHERE id = 42
2026-03-13 08:02:15 DEBUG    Query result: {'id': 42, 'name': 'Ana', 'email': 'ana@test.com', 'preferences': None}
2026-03-13 08:02:15 ERROR    TypeError: argument of type 'NoneType' is not iterable
2026-03-13 08:02:15 ERROR    Response: 500 Internal Server Error

Por qué estas líneas:

  • Muestran el request específico que falló
  • Muestran los datos del query (donde preferences es None — pista clave)
  • Muestran el error exacto (NoneType is not iterable — alguien hace in sobre preferences que es None)
  • El contexto antes y después (health checks, otros requests) no aporta información útil

Qué NO incluir:

  • Health checks (ruido)
  • Requests a otros endpoints que funcionaron (irrelevantes)
  • Logs posteriores al error que no se relacionan

Ejercicio 2: Escribir el prompt correcto (Medio)

Tienes estos logs de un error en tu sistema de notificaciones. Escribe el prompt que le darías a Claude Code para obtener un buen diagnóstico.

2026-03-13 11:00:00 INFO     NotificationService: processing batch of 15 notifications
2026-03-13 11:00:00 DEBUG    Notification 1/15: type=email, user=user1@test.com, sent OK
2026-03-13 11:00:01 DEBUG    Notification 2/15: type=email, user=user2@test.com, sent OK
2026-03-13 11:00:01 DEBUG    Notification 3/15: type=sms, user=user3@test.com, sent OK
2026-03-13 11:00:02 DEBUG    Notification 4/15: type=push, user=user4@test.com
2026-03-13 11:00:02 WARNING  Push notification service returned 429 Too Many Requests
2026-03-13 11:00:02 DEBUG    Notification 5/15: type=push, user=user5@test.com
2026-03-13 11:00:02 WARNING  Push notification service returned 429 Too Many Requests
2026-03-13 11:00:02 ERROR    Batch processing failed: max retries exceeded for push service
2026-03-13 11:00:02 ERROR    Notifications 4-15 not sent
2026-03-13 11:00:02 ERROR    Response: 500 Internal Server Error
Ver solución

Un buen prompt:

Mi sistema de notificaciones en FastAPI falla al procesar un batch. 
Las primeras 3 notificaciones (email y SMS) se envían correctamente, 
pero cuando llega a las notificaciones push (tipo=push), el servicio 
externo de push devuelve 429 (rate limit) y todo el batch falla.

El problema: cuando una notificación push falla, las notificaciones 
restantes (4-15) no se envían — incluso las que son de tipo email o SMS.

Logs:
"""
[los logs de arriba]
"""

Contexto:
- Usamos un servicio externo para push notifications
- El batch se procesa secuencialmente
- Las notificaciones son de 3 tipos: email, sms, push
- El error empezó hoy — probablemente estamos enviando más 
  push notifications de lo normal

Preguntas específicas:
1. ¿Por qué las notificaciones 6-15 (que podrían ser email/sms) 
   no se envían cuando push falla?
2. ¿Cómo debería manejar el rate limiting del servicio de push 
   sin bloquear el resto del batch?

Por qué este prompt es efectivo:

  • ✅ Da contexto de negocio (3 tipos de notificaciones, servicio externo)
  • ✅ Describe el comportamiento esperado vs el actual
  • ✅ Incluye los logs completos
  • ✅ Hace preguntas específicas (no solo "¿qué pasa?")
  • ✅ Menciona que es un cambio reciente ("empezó hoy")

Ejercicio 3: Evaluar un diagnóstico (Medio)

Claude Code te da este diagnóstico basado en los logs del ejercicio 2. Evalúa si es correcto, parcialmente correcto, o incorrecto.

Diagnóstico de Claude Code:

El problema es que el batch processing no tiene manejo de errores por tipo de notificación. Cuando el servicio de push devuelve 429, el código lanza una excepción que detiene todo el loop de procesamiento.

Fix sugerido: Agregar un try/except dentro del loop para cada notificación individual, y acumular los errores en lugar de detener el batch. Para las notificaciones push específicamente, implementar un retry con exponential backoff.

Ver solución

Evaluación: Parcialmente correcto.

Lo que está bien:

  • ✅ Identifica correctamente que una excepción en push detiene todo el batch
  • ✅ La sugerencia de try/except individual es razonable
  • ✅ Exponential backoff para rate limiting es una buena práctica

Lo que falta o podría ser incorrecto:

  • ⚠️ No menciona que las notificaciones push podrían procesarse en un queue separado (mejor arquitectura)
  • ⚠️ "Retry con exponential backoff" podría empeorar el rate limiting si el servicio de push ya está sobrecargado
  • ⚠️ No sugiere separar el batch por tipo de notificación (procesar email/sms primero, push después)
  • ⚠️ No menciona la necesidad de un circuit breaker para el servicio de push

Cómo verificarías:

  1. Abre el código del batch processor y confirma que efectivamente un error rompe el loop
  2. Verifica si ya hay retry logic (quizás el "max retries exceeded" indica que sí hay retries pero insuficientes)
  3. Revisa la documentación del servicio de push para conocer sus rate limits exactos

Acción correcta: Usar las partes buenas del diagnóstico (try/except individual) pero investigar más antes de implementar retry con backoff (podría empeorar el problema).

Ejercicio 4: Pasar logs con contexto correcto (Difícil)

Tu aplicación tiene un bug: el endpoint PATCH /api/tasks/{id} a veces actualiza la tarea correctamente y a veces devuelve la tarea sin los cambios aplicados. No hay error en los logs — siempre devuelve 200.

Estos son los logs de un request donde el bug se manifiesta:

2026-03-13 14:00:01 INFO     PATCH /api/tasks/123
2026-03-13 14:00:01 DEBUG    Request body: {"status": "completed", "priority": "high"}
2026-03-13 14:00:01 DEBUG    TaskService.update_task(id=123)
2026-03-13 14:00:01 DEBUG    Current task: id=123, status=pending, priority=medium
2026-03-13 14:00:01 DEBUG    Updating fields: status=completed, priority=high
2026-03-13 14:00:01 DEBUG    DB update executed
2026-03-13 14:00:01 DEBUG    Returning task: id=123, status=pending, priority=medium
2026-03-13 14:00:01 INFO     Response: 200 OK

Y estos son los logs de un request donde funciona correctamente:

2026-03-13 14:05:22 INFO     PATCH /api/tasks/456
2026-03-13 14:05:22 DEBUG    Request body: {"status": "in_progress"}
2026-03-13 14:05:22 DEBUG    TaskService.update_task(id=456)
2026-03-13 14:05:22 DEBUG    Current task: id=456, status=pending, priority=low
2026-03-13 14:05:22 DEBUG    Updating fields: status=in_progress
2026-03-13 14:05:22 DEBUG    DB update executed
2026-03-13 14:05:22 DEBUG    Returning task: id=456, status=in_progress, priority=low
2026-03-13 14:05:22 INFO     Response: 200 OK
  1. Escribe el prompt que le darías a Claude Code usando el template de comparación
  2. ¿Cuál crees que sería el diagnóstico?
  3. ¿Cómo lo verificarías?
Ver solución

1. Prompt:

Mi endpoint PATCH /api/tasks/{id} a veces actualiza correctamente 
y a veces devuelve la tarea SIN los cambios aplicados (devuelve 200, 
no hay error). Compara estos dos requests — uno donde falla 
silenciosamente y otro donde funciona.

Request con bug (task 123 - no aplica cambios):
"""
2026-03-13 14:00:01 INFO     PATCH /api/tasks/123
2026-03-13 14:00:01 DEBUG    Request body: {"status": "completed", "priority": "high"}
2026-03-13 14:00:01 DEBUG    TaskService.update_task(id=123)
2026-03-13 14:00:01 DEBUG    Current task: id=123, status=pending, priority=medium
2026-03-13 14:00:01 DEBUG    Updating fields: status=completed, priority=high
2026-03-13 14:00:01 DEBUG    DB update executed
2026-03-13 14:00:01 DEBUG    Returning task: id=123, status=pending, priority=medium
2026-03-13 14:00:01 INFO     Response: 200 OK
"""

Request exitoso (task 456 - aplica cambios):
"""
2026-03-13 14:05:22 INFO     PATCH /api/tasks/456
2026-03-13 14:05:22 DEBUG    Request body: {"status": "in_progress"}
2026-03-13 14:05:22 DEBUG    TaskService.update_task(id=456)
2026-03-13 14:05:22 DEBUG    Current task: id=456, status=pending, priority=low
2026-03-13 14:05:22 DEBUG    Updating fields: status=in_progress
2026-03-13 14:05:22 DEBUG    DB update executed
2026-03-13 14:05:22 DEBUG    Returning task: id=456, status=in_progress, priority=low
2026-03-13 14:05:22 INFO     Response: 200 OK
"""

Contexto:
- La DB update se ejecuta en ambos casos
- La diferencia: task 123 actualiza 2 campos, task 456 actualiza 1 campo
- El "Returning task" muestra los valores ANTES del update en el caso buggy

2. Diagnóstico probable de Claude Code:

El problema es un issue de timing/caching: el "DB update executed" confirma que la actualización se escribió en la base de datos, pero "Returning task" muestra los valores anteriores. Esto sugiere que el código lee el objeto Task ANTES de hacer el update y lo devuelve sin refrescarlo.

La diferencia entre 1 campo y 2 campos podría ser coincidencia, pero probablemente el código:

  1. Lee el task de la DB (obtiene el objeto)
  2. Ejecuta el UPDATE en SQL
  3. Devuelve el objeto leído en paso 1 (que tiene los valores viejos)

El request exitoso podría ser coincidencia (los valores son los mismos antes y después) o podría haber un path diferente de código.

3. Cómo verificarías:

  1. Abrir TaskService.update_task() y verificar si hace db.refresh(task) después del update
  2. Verificar si el método devuelve el objeto original o hace un nuevo query
  3. Probar: hacer PATCH y luego GET — si GET muestra los valores actualizados, confirma que la DB se actualizó pero el response no se refrescó
  4. Revisar si hay alguna diferencia en el path de código entre actualizar 1 campo vs 2 campos

Bug real probable:

def update_task(self, task_id: int, updates: dict):
    task = self.db.query(Task).get(task_id)  # lee el objeto
    self.db.execute(
        update(Task).where(Task.id == task_id).values(**updates)
    )
    self.db.commit()
    return task  # devuelve el objeto VIEJO, sin refresh

El fix: agregar self.db.refresh(task) antes del return, o usar el ORM para hacer el update directamente en el objeto.

Ejercicio 5: Sanitizar logs antes de compartir (Fácil)

Estos logs tienen información sensible. Sanitízalos antes de pasarlos a Claude Code, manteniendo la información necesaria para el diagnóstico.

2026-03-13 15:00:00 INFO     POST /api/auth/login
2026-03-13 15:00:00 DEBUG    Login attempt: email=maria.gonzalez@empresa-real.com, password=MyS3cur3P@ss!
2026-03-13 15:00:00 DEBUG    DB query: SELECT * FROM users WHERE email='maria.gonzalez@empresa-real.com'
2026-03-13 15:00:00 DEBUG    User found: id=42, name=María González, ssn=123-45-6789
2026-03-13 15:00:00 DEBUG    Token generated: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiJ9.abc123
2026-03-13 15:00:00 DEBUG    DB connection string: postgresql://admin:db_password_123@prod-db.empresa.com:5432/users
2026-03-13 15:00:00 ERROR    Failed to save session: connection refused
Ver solución
2026-03-13 15:00:00 INFO     POST /api/auth/login
2026-03-13 15:00:00 DEBUG    Login attempt: email=user@example.com, password=[REDACTED]
2026-03-13 15:00:00 DEBUG    DB query: SELECT * FROM users WHERE email='user@example.com'
2026-03-13 15:00:00 DEBUG    User found: id=42, name=[REDACTED], ssn=[REDACTED]
2026-03-13 15:00:00 DEBUG    Token generated: [JWT_TOKEN_REDACTED]
2026-03-13 15:00:00 DEBUG    DB connection string: postgresql://[CREDENTIALS]@[HOST]:5432/users
2026-03-13 15:00:00 ERROR    Failed to save session: connection refused

Qué se sanitizó:

  • ✅ Email real → user@example.com (mantiene el formato para diagnóstico)
  • ✅ Password → [REDACTED] (nunca debería estar en logs, pero si está, sanitiza)
  • ✅ Nombre real → [REDACTED]
  • ✅ SSN → [REDACTED] (dato altamente sensible)
  • ✅ JWT token → [JWT_TOKEN_REDACTED]
  • ✅ DB connection string → credenciales y host redactados

Qué se mantuvo:

  • ✅ El endpoint y método HTTP
  • ✅ La estructura del query (para diagnosticar si hay SQL issues)
  • ✅ El ID del usuario (no es PII en sí mismo)
  • ✅ El puerto de la DB (5432 = PostgreSQL, útil para diagnóstico)
  • ✅ El nombre de la base de datos (users)
  • ✅ El error exacto (connection refused)

Nota importante: El hecho de que el password aparezca en los logs en texto plano es un bug de seguridad que deberías corregir, además de resolver el connection refused.


Resumen

En esta cápsula aprendiste:

  • Log analysis es la habilidad #1 de debugging con Claude Code — resuelve el 70% de bugs más rápido
  • La calidad del diagnóstico depende directamente de la calidad de los logs que proporcionas
  • La regla de 5+5: incluye al menos 5 líneas antes y 5 después del error
  • Usa prompt templates específicos para cada tipo de problema (general, comparación, intermitente, secuencia)
  • Siempre evalúa el diagnóstico de Claude Code: verifica contra el código real antes de aplicar un fix
  • Sanitiza logs antes de compartirlos: reemplaza tokens, passwords, PII con placeholders
  • Logs estructurados dan diagnósticos más precisos que logs en texto libre

Próxima cápsula: Runtime Errors y Stack Traces — interpretar errores de Python con ayuda de Claude Code.


Recursos Adicionales

  1. Python Logging Cookbook - Recetas avanzadas de logging en Python
  2. Structured Logging with structlog - Librería de logging estructurado para Python
  3. FastAPI — Handling Errors - Manejo de errores y excepciones en FastAPI
  4. 12-Factor App — Logs - Principios de logging en aplicaciones modernas
  5. Anthropic — Claude Code Documentation - Mejores prácticas oficiales de Claude Code
  6. OWASP Logging Cheat Sheet - Qué loggear y qué nunca loggear (seguridad)

Debugging & Code Review with Claude Code — Módulo 6, Cápsula 02 Claude Code Agentic Development Path — Guía #6 de 11