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:
- The request was
POST /api/tasks - Authentication passed correctly
- The
user_datacontainsemailandrolebut notuser_id - The code in
task_service.py:45expectsuser_idbut the token doesn't include it - The likely fix: include
user_idin the token or useemailas 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
ZeroDivisionErroron line 89 oftask_service.py. The variabletotal_activeis 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 oftotal_activehas a bug: it's probably calculating something different fromtotal - cancelled.Possible cause:
total_activeis calculated by filtering by a specific status that has no tasks (for example, a new status that was added but has no records), orcompleted_countandtotal_activeare using different queries that aren't consistent.
Step 4: Verify the diagnosis
Now you don't apply the suggested fix blindly. You verify:
- You open
task_service.pyline 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
}
-
You confirm the diagnosis:
total_activelooks up the status'active'which doesn't exist inTaskStatus. It should betotal - by_status.get('cancelled', 0). -
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:
- The user's token contains
role: 'editor', notrole: 'admin' - The middleware does an exact match:
role == 'admin' - 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
preferencesisNone— a key clue) - They show the exact error (
NoneTypeis not iterable — someone doesinonpreferenceswhich isNone) - 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:
- Open the batch processor code and confirm that an error does break the loop
- Check whether there's already retry logic (perhaps the "max retries exceeded" indicates there are retries but insufficient ones)
- 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
- Write the prompt you'd give Claude Code using the comparison template
- What do you think the diagnosis would be?
- 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:
- Reads the task from the DB (gets the object)
- Runs the UPDATE in SQL
- 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:
- Open
TaskService.update_task()and check whether it doesdb.refresh(task)after the update - Check whether the method returns the original object or does a new query
- 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
- 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
- Python Logging Cookbook - Advanced logging recipes in Python
- Structured Logging with structlog - A structured logging library for Python
- FastAPI — Handling Errors - Handling errors and exceptions in FastAPI
- 12-Factor App — Logs - Logging principles in modern applications
- Anthropic — Claude Code Documentation - Official Claude Code best practices
- 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