Module 6: Debugging with Claude Code

Log Analysis with Claude Code

Log Analysis with Claude Code

Capsule overview

If you had to choose a single debugging skill with Claude Code, it should be this one: log analysis. 70% of the bugs you'll encounter day to day can be diagnosed by analyzing logs. And Claude Code is extraordinarily good at this — it can process 20, 50, or 200 lines of logs and find the needle in the haystack much faster than you reading line by line.

But there's a catch: if you paste it only the error line, you'll get a generic diagnosis. If you paste it 200 lines with no context, you'll get a scattered diagnosis. The key is what you give it and how you ask it to analyze it. In this capsule you'll learn to prepare logs for Claude Code, to give the right context, and most importantly: to evaluate its diagnosis before acting.


What Claude Code Can and Can't See

Before passing any log to Claude Code, understand what it's seeing:

What Claude Code can analyze

  • ✅ Log text that you provide (copy/paste)
  • ✅ Patterns of timestamps, severity levels, error messages
  • ✅ Complete stack traces included in the logs
  • ✅ The sequence of events (what happened before the error)
  • ✅ The correlation between messages (request → processing → error)

What Claude Code CANNOT do

  • ❌ Access your application's logs in real time
  • ❌ Run your application to reproduce the error
  • ❌ See the state of variables at the moment of the error
  • ❌ Access your log files directly (you need to copy the content)
  • ❌ Monitor changes in the logs while you make changes

The implication is clear: you're the one who captures the logs and passes them to it. The quality of the diagnosis depends directly on the quality of what you give it.


Anatomy of a Good Log for Diagnosis

Not all logs are equally useful. Compare these two scenarios:

Bad scenario: Only the error

You pass Claude Code:
────────────────────────────
KeyError: 'user_id'
────────────────────────────

Claude Code can tell you there's a dictionary where the key user_id is looked up and doesn't exist. But you already knew that. It doesn't have enough context to tell you where or why.

Good scenario: The error with context

You pass 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
────────────────────────────

Now Claude Code can see:

  1. The request was POST /api/tasks
  2. Authentication passed correctly
  3. The user_data contains email and role but not user_id
  4. The code in task_service.py:45 expects user_id but the token doesn't include it
  5. The likely fix: include user_id in the token or use email as the identifier

The difference between a generic diagnosis and an actionable one is in the context you provide.


The Context Rule: 5 Lines Above, 5 Lines Below

As a general rule, when you capture logs to pass to Claude Code, include at least 5 lines before the error and 5 lines after (if any). This captures:

  • Before: What operations ran correctly, what data was available
  • The error: The error message and the stack trace
  • After: How the system responded to the error, whether there was a cascade of errors

When you need more context

Sometimes 5 lines aren't enough. Include more context when:

  • ✅ The error seems to be a consequence of something that happened earlier (a data error, an incorrect state)
  • ✅ There are multiple requests in the logs and you need to isolate which one failed
  • ✅ The log shows a sequence of operations (start → processing → error)
  • ✅ The error is intermittent and you need to compare a successful execution with a failed one

When less is more

Reduce the context when:

  • ✅ The logs have a lot of noise (health checks, metrics, etc.)
  • ✅ There's sensitive information you shouldn't share (tokens, passwords, PII)
  • ✅ The error is clear and the stack trace is enough

How to Pass Logs to Claude Code: Prompt Templates

The way you ask Claude Code to analyze logs affects the quality of the response. Here are proven templates:

Template 1: General diagnosis

Analyze these logs from my FastAPI application. The POST /api/tasks endpoint 
is returning a 500 error. Identify the root cause and suggest a fix.

Logs:
"""
[paste logs here]
"""

Additional context:
- The application uses JWT for authentication
- The Task model has fields: id, title, priority, owner_id, created_at
- This endpoint was working before the last deploy

Template 2: Successful vs failed comparison

These are the logs of two requests to the same endpoint. The first works 
correctly, the second fails. Compare both and identify what's different 
that causes the error.

Successful request:
"""
[paste logs of the request that works]
"""

Failed request:
"""
[paste logs of the request that fails]
"""

Template 3: Intermittent error

This error appears in approximately 1 out of 10 requests to the 
GET /api/users/{id} endpoint. Analyze these logs that show 3 successful 
requests and 1 failed. What pattern do you see that could explain why it fails 
intermittently?

Logs (marked with [OK] and [FAIL]):
"""
[paste logs]
"""

Template 4: Sequence of errors

My application started failing 20 minutes ago. These are the first 
errors that appeared. Identify which was the ORIGINAL error (root cause) 
and which are a consequence of that first error.

Logs (in chronological order):
"""
[paste logs]
"""

Complete Example: From Log to Fix

Let's look at a complete realistic scenario. Your FastAPI task-management application has an endpoint that fails.

Step 1: Capture the logs

Your application prints these logs when it fails:

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

Step 2: Prepare the prompt for Claude Code

Analyze these logs from my FastAPI application. The GET /api/tasks/stats endpoint 
returns 500. Identify the root cause.

Logs:
"""
[the logs above]
"""

Context:
- total_active should be total - cancelled
- This endpoint works normally, but failed today for the first time

Step 3: Claude Code's diagnosis (typical)

Claude Code will probably say something like:

The error is a ZeroDivisionError on line 89 of task_service.py. The variable total_active is 0, which causes the division by zero.

Looking at the logs, the total number of tasks is 47, with the distribution:

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

If total_active = total - cancelled = 47 - 2 = 45, it shouldn't be zero. This suggests that the calculation of total_active has a bug: it's probably calculating something different from total - cancelled.

Possible cause: total_active is calculated by filtering by a specific status that has no tasks (for example, a new status that was added but has no records), or completed_count and total_active are using different queries that aren't consistent.

Step 4: Verify the diagnosis

Now you don't apply the suggested fix blindly. You verify:

  1. You open task_service.py line 89 and read the real code:
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' isn't a valid status

        completion_rate = completed_count / total_active * 100  # line 89

        return {
            "total": total,
            "by_status": by_status,
            "completion_rate": completion_rate
        }
  1. You confirm the diagnosis: total_active looks up the status 'active' which doesn't exist in TaskStatus. It should be total - by_status.get('cancelled', 0).

  2. Claude Code was right about the direction but not the exact detail. Without seeing the code, it couldn't know the bug was looking up a nonexistent status.

Step 5: Apply and verify the 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

You verify by running the endpoint again and confirming that it returns correct data.


The 5 Most Common Errors When Passing Logs to Claude Code

Error 1: Passing only the last line of the error

# Bad
"ZeroDivisionError: division by zero"

# Good
[20+ lines with context: which request, what data, what previous operations]

Error 2: Not cleaning up sensitive information

# Bad - includes real tokens
"Auth: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM..."

# Good - sanitized
"Auth: Bearer [TOKEN_REDACTED]"

Error 3: Not giving context about the application

Without context, Claude Code assumes a generic application. Tell it which framework you use, which database, which data model.

Error 4: Accepting the first diagnosis without verifying

Claude Code gives hypotheses. Always verify by opening the file and the line mentioned. The diagnosis can be 80% correct but the wrong 20% changes the fix.

Error 5: Not including logs of successful requests

If you have an intermittent error, comparing a successful request with a failed one is the most powerful technique. Claude Code can find the difference.


Evaluating Claude Code's Diagnosis

Not everything Claude Code says about your logs is correct. Here's a framework for evaluating:

High confidence level

Trust the diagnosis more when:

  • ✅ Claude Code points to a specific line and explains why it fails
  • ✅ The diagnosis is consistent with the data in the logs
  • ✅ The explanation includes cause and effect (A happened, which caused B, which resulted in C)
  • ✅ The error is a common type (KeyError, TypeError, ValueError) with a clear cause

Low confidence level

Distrust the diagnosis when:

  • ⚠️ Claude Code says "probably" or "possibly" without evidence in the logs
  • ⚠️ The diagnosis doesn't explain why the error is intermittent
  • ⚠️ The fix suggestion is generic ("add a try/except")
  • ⚠️ Claude Code suggests the problem is in an external library without evidence

Signs that Claude Code is guessing

  • ❌ "This could be caused by..." followed by 5 possible causes without prioritizing
  • ❌ Suggestions that don't relate to the data in the logs
  • ❌ "The error is probably in the configuration" without pointing to which configuration
  • ❌ Diagnoses that contradict the information in the logs

Advanced Technique: Structured Logs

If you configure your application with structured logging, Claude Code can do much more effective analyses.

Basic vs structured logging

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))

Usage example in an 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}

Why structured logs are better for Claude Code

When you pass it structured logs, Claude Code can:

  • ✅ Parse specific fields (event, data, error)
  • ✅ Correlate events by timestamp
  • ✅ Identify what data was available at each step
  • ✅ Detect unexpected values in specific fields

Effective Logging for Debugging with AI

To maximize Claude Code's usefulness in debugging, configure your logging like this:

What to log at each level

import logging

logger = logging.getLogger("app")

# DEBUG: internal data useful for debugging
logger.debug(f"Query result: {result}")
logger.debug(f"Cache hit: key={cache_key}")

# INFO: normal business operations
logger.info(f"Task created: id={task.id}")
logger.info(f"User logged in: email={user.email}")

# WARNING: anomalous situations that aren't errors
logger.warning(f"Slow query: {elapsed_ms}ms for {query_name}")
logger.warning(f"Rate limit approaching: {current}/{limit}")

# ERROR: errors that affect the operation
logger.error(f"Failed to create task: {str(e)}")
logger.error(f"Database connection failed: {str(e)}")

# CRITICAL: errors that compromise the application
logger.critical(f"All database connections exhausted")

Recommended configuration for development

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)

This configuration:

  • ✅ Shows DEBUG and above (maximum detail)
  • ✅ Includes readable timestamps
  • ✅ Reduces noise from uvicorn and SQLAlchemy
  • ✅ Output to stdout (easy to copy)

Practical Case: Debugging an Authentication Error

Scenario: your protected endpoint returns 401 for a user who should have access.

The 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 for Claude Code

This user (maria@company.com) should have access to the 
POST /api/admin/reports endpoint. She's a senior editor and has admin 
permissions according to our role system. But she gets 401.

Logs:
"""
[the logs above]
"""

Context:
- Our role system has: viewer, editor, admin
- Senior editors should have access to /api/admin/* endpoints
- The endpoint uses a middleware that checks roles

Expected diagnosis from Claude Code

Claude Code should identify that:

  1. The user's token contains role: 'editor', not role: 'admin'
  2. The middleware does an exact match: role == 'admin'
  3. There's a disconnect between "senior editors have admin access" (a business rule) and "the middleware only accepts role=admin" (the implementation)

The fix depends on the business decision:

  • Option A: Change the token so that senior editors have role: 'admin'
  • Option B: Change the middleware to accept a list of roles: ['admin', 'editor']
  • Option C: Implement a more granular permission system

Your verification

You open the middleware and confirm:

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

Indeed, it does an exact match. The correct fix depends on the business context — something Claude Code can't decide for you.


Connection to the Project

How it applies to the capstone project (Module 8)

In the capstone project, the FastAPI application has logging configured. Part of your job will be running endpoints, capturing logs when they fail, and using Claude Code to diagnose the problems. The techniques from this capsule — how much context to give, which prompt to use, how to evaluate the diagnosis — are exactly the ones you'll use.

The difference is that in the capstone project, some bugs only show up when they interact with other components. You'll need to capture broader logs and correlate events across different endpoints.


Troubleshooting

Problem 1: "Claude Code gives generic diagnoses"

Cause: You're probably giving it too little context — just the error without the previous lines. Solution: Use the 5+5 rule: at least 5 lines before and 5 after the error. Include the type of request, the data being processed, and any available debug log.

Problem 2: "My application's logs don't have enough detail"

Cause: Your logging is configured at INFO level or above, without DEBUG. Solution: Configure logging.basicConfig(level=logging.DEBUG) in development. Add logger.debug() at the key points: function entry, received data, query results.

Problem 3: "Claude Code suggests a fix but it doesn't work"

Cause: The diagnosis was partially correct but the fix didn't consider all the context. Solution: Before applying a fix suggested by Claude Code, always open the file and read the lines around where it suggests the change. Verify that the fix is compatible with the rest of the code.

Problem 4: "My logs have sensitive information"

Cause: Logging of tokens, passwords, or personal data. Solution: Before passing logs to Claude Code, sanitize: replace tokens with [TOKEN], passwords with [REDACTED], real emails with user@example.com. The diagnosis doesn't need real data.

Problem 5: "Claude Code gives 5 possible causes without prioritizing"

Cause: You gave it ambiguous information that allows multiple interpretations. Solution: Include business context: "This endpoint was working yesterday", "The error only appears with new users", "It started after the last deploy". The more specific the context, the more specific the diagnosis.


Exercises

Exercise 1: Identify which logs to pass (Easy)

You have these logs. Your GET /api/users/42 endpoint returns 500. Which lines would you pass to Claude Code for a diagnosis?

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
See solution

Lines to include (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

Why these lines:

  • They show the specific request that failed
  • They show the query data (where preferences is None — a key clue)
  • They show the exact error (NoneType is not iterable — someone does in on preferences which is None)
  • The context before and after (health checks, other requests) doesn't add useful information

What NOT to include:

  • Health checks (noise)
  • Requests to other endpoints that worked (irrelevant)
  • Logs after the error that don't relate

Exercise 2: Write the correct prompt (Medium)

You have these logs of an error in your notification system. Write the prompt you'd give Claude Code to get a good diagnosis.

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
See solution

A good prompt:

My notification system in FastAPI fails when processing a batch. 
The first 3 notifications (email and SMS) are sent correctly, 
but when it reaches the push notifications (type=push), the external 
push service returns 429 (rate limit) and the entire batch fails.

The problem: when a push notification fails, the remaining 
notifications (4-15) aren't sent — even the ones that are email or SMS.

Logs:
"""
[the logs above]
"""

Context:
- We use an external service for push notifications
- The batch is processed sequentially
- The notifications are of 3 types: email, sms, push
- The error started today — we're probably sending more 
  push notifications than usual

Specific questions:
1. Why are notifications 6-15 (which could be email/sms) 
   not sent when push fails?
2. How should I handle the push service's rate limiting 
   without blocking the rest of the batch?

Why this prompt is effective:

  • ✅ It gives business context (3 types of notifications, external service)
  • ✅ It describes the expected behavior vs the actual one
  • ✅ It includes the complete logs
  • ✅ It asks specific questions (not just "what's happening?")
  • ✅ It mentions that it's a recent change ("started today")

Exercise 3: Evaluate a diagnosis (Medium)

Claude Code gives you this diagnosis based on the logs from exercise 2. Evaluate whether it's correct, partially correct, or incorrect.

Claude Code's diagnosis:

The problem is that the batch processing doesn't have error handling by notification type. When the push service returns 429, the code raises an exception that stops the entire processing loop.

Suggested fix: Add a try/except inside the loop for each individual notification, and accumulate the errors instead of stopping the batch. For the push notifications specifically, implement a retry with exponential backoff.

See solution

Evaluation: Partially correct.

What's right:

  • ✅ It correctly identifies that an exception in push stops the entire batch
  • ✅ The suggestion of an individual try/except is reasonable
  • ✅ Exponential backoff for rate limiting is a good practice

What's missing or could be incorrect:

  • ⚠️ It doesn't mention that the push notifications could be processed in a separate queue (a better architecture)
  • ⚠️ "Retry with exponential backoff" could worsen the rate limiting if the push service is already overloaded
  • ⚠️ It doesn't suggest separating the batch by notification type (process email/sms first, push later)
  • ⚠️ It doesn't mention the need for a circuit breaker for the push service

How you'd verify:

  1. Open the batch processor code and confirm that an error does break the loop
  2. Check whether there's already retry logic (perhaps the "max retries exceeded" indicates there are retries but insufficient ones)
  3. Review the push service's documentation to know its exact rate limits

Correct action: Use the good parts of the diagnosis (individual try/except) but investigate more before implementing retry with backoff (it could worsen the problem).

Exercise 4: Pass logs with the correct context (Hard)

Your application has a bug: the PATCH /api/tasks/{id} endpoint sometimes updates the task correctly and sometimes returns the task without the changes applied. There's no error in the logs — it always returns 200.

These are the logs of a request where the bug shows up:

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

And these are the logs of a request where it works correctly:

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. Write the prompt you'd give Claude Code using the comparison template
  2. What do you think the diagnosis would be?
  3. How would you verify it?
See solution

1. Prompt:

My PATCH /api/tasks/{id} endpoint sometimes updates correctly 
and sometimes returns the task WITHOUT the changes applied (returns 200, 
no error). Compare these two requests — one where it fails 
silently and one where it works.

Request with the bug (task 123 - doesn't apply changes):
"""
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
"""

Successful request (task 456 - applies changes):
"""
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
"""

Context:
- The DB update runs in both cases
- The difference: task 123 updates 2 fields, task 456 updates 1 field
- The "Returning task" shows the values BEFORE the update in the buggy case

2. Probable diagnosis from Claude Code:

The problem is a timing/caching issue: the "DB update executed" confirms that the update was written to the database, but "Returning task" shows the previous values. This suggests that the code reads the Task object BEFORE doing the update and returns it without refreshing it.

The difference between 1 field and 2 fields could be a coincidence, but the code probably:

  1. Reads the task from the DB (gets the object)
  2. Runs the UPDATE in SQL
  3. Returns the object read in step 1 (which has the old values)

The successful request could be a coincidence (the values are the same before and after) or there could be a different code path.

3. How you'd verify:

  1. Open TaskService.update_task() and check whether it does db.refresh(task) after the update
  2. Check whether the method returns the original object or does a new query
  3. Test: do a PATCH and then a GET — if GET shows the updated values, it confirms that the DB was updated but the response wasn't refreshed
  4. Check whether there's any difference in the code path between updating 1 field vs 2

The probable real bug:

def update_task(self, task_id: int, updates: dict):
    task = self.db.query(Task).get(task_id)  # reads the object
    self.db.execute(
        update(Task).where(Task.id == task_id).values(**updates)
    )
    self.db.commit()
    return task  # returns the OLD object, with no refresh

The fix: add self.db.refresh(task) before the return, or use the ORM to do the update directly on the object.

Exercise 5: Sanitize logs before sharing (Easy)

These logs have sensitive information. Sanitize them before passing them to Claude Code, keeping the information needed for the diagnosis.

2026-03-13 15:00:00 INFO     POST /api/auth/login
2026-03-13 15:00:00 DEBUG    Login attempt: email=maria.gonzalez@real-company.com, password=MyS3cur3P@ss!
2026-03-13 15:00:00 DEBUG    DB query: SELECT * FROM users WHERE email='maria.gonzalez@real-company.com'
2026-03-13 15:00:00 DEBUG    User found: id=42, name=Maria Gonzalez, 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.company.com:5432/users
2026-03-13 15:00:00 ERROR    Failed to save session: connection refused
See solution
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

What was sanitized:

  • ✅ Real email → user@example.com (keeps the format for the diagnosis)
  • ✅ Password → [REDACTED] (it should never be in logs, but if it is, sanitize it)
  • ✅ Real name → [REDACTED]
  • ✅ SSN → [REDACTED] (highly sensitive data)
  • ✅ JWT token → [JWT_TOKEN_REDACTED]
  • ✅ DB connection string → credentials and host redacted

What was kept:

  • ✅ The endpoint and HTTP method
  • ✅ The query structure (to diagnose whether there are SQL issues)
  • ✅ The user ID (it's not PII in itself)
  • ✅ The DB port (5432 = PostgreSQL, useful for the diagnosis)
  • ✅ The database name (users)
  • ✅ The exact error (connection refused)

Important note: The fact that the password appears in the logs in plaintext is a security bug that you should fix, in addition to resolving the connection refused.


Summary

In this capsule you learned:

  • Log analysis is the #1 skill of debugging with Claude Code — it resolves 70% of bugs faster
  • The quality of the diagnosis depends directly on the quality of the logs you provide
  • The 5+5 rule: include at least 5 lines before and 5 after the error
  • Use prompt templates specific to each type of problem (general, comparison, intermittent, sequence)
  • Always evaluate Claude Code's diagnosis: verify against the real code before applying a fix
  • Sanitize logs before sharing them: replace tokens, passwords, PII with placeholders
  • Structured logs give more precise diagnoses than free-text logs

Next capsule: Runtime Errors and Stack Traces — interpreting Python errors with help from Claude Code.


Additional resources

  1. Python Logging Cookbook - Advanced logging recipes in Python
  2. Structured Logging with structlog - A structured logging library for Python
  3. FastAPI — Handling Errors - Handling errors and exceptions in FastAPI
  4. 12-Factor App — Logs - Logging principles in modern applications
  5. Anthropic — Claude Code Documentation - Official Claude Code best practices
  6. OWASP Logging Cheat Sheet - What to log and what to never log (security)

Debugging & Code Review with Claude Code — Module 6, Capsule 02 Claude Code Agentic Development Path — Guide #6 of 11