Logging Levels: A Practical Guide to What Goes Where
When to use DEBUG, INFO, WARN, ERROR, and how to make your logs actually useful.
March 1, 2026 · 4 min · 836 words · Rob Washington
Table of Contents
Logging seems simple until you’re debugging production at 2 AM, scrolling through millions of lines trying to find the one that matters. Good logging practices make that experience less painful. Here’s how to think about log levels.
Use for: Normal operations that someone might care about later.
1
2
3
4
logger.info(f"User {user_id} logged in from {ip_address}")logger.info(f"Order {order_id} placed: ${total:.2f}")logger.info(f"Background job {job_name} completed in {duration:.2f}s")logger.info("Application started on port 8080")
Guidelines:
One log per significant business event
Include enough context to understand what happened
Should tell a story when read in sequence
Not too noisy—if everything is INFO, nothing is
Good test: Would someone reviewing an incident want to see this?
Use for: Unexpected situations that the system recovered from.
1
2
3
4
logger.warning(f"Cache miss for user {user_id}, falling back to database")logger.warning(f"Retry {attempt}/3 for external API call to {endpoint}")logger.warning(f"Configuration {key} not set, using default: {default}")logger.warning(f"Request took {duration:.2f}s, exceeding threshold of {threshold}s")
Guidelines:
The system is still working, but something unexpected happened
Someone should eventually look at this
Often indicates potential problems before they become errors
Don’t overuse—if every request logs warnings, they become noise
Good test: Would you want an alert if this happened 100 times in an hour?
Use for: Failures that affected a specific operation but didn’t crash the system.
1
2
3
logger.error(f"Failed to send email to {email}: {error}")logger.error(f"Payment processing failed for order {order_id}: {error}")logger.error(f"Database query failed: {error}",exc_info=True)
Guidelines:
A user-facing operation failed
Include the error/exception details
Include context to reproduce (IDs, inputs)
Don’t log and raise—pick one or the other
Good test: Should someone be woken up if this happens?
Use for: Unrecoverable errors that require immediate intervention.
1
2
3
logger.fatal("Database connection pool exhausted, cannot serve requests")logger.fatal("Out of disk space, shutting down")logger.fatal(f"Required service {service_name} unreachable after all retries")
Guidelines:
The application cannot continue normally
Someone needs to act NOW
Should be rare—if you see these often, you have bigger problems
# BAD: Too much noisedefprocess_item(item):logger.info(f"Starting to process item {item.id}")logger.info(f"Validating item {item.id}")logger.info(f"Item {item.id} validated successfully")logger.info(f"Saving item {item.id}")logger.info(f"Item {item.id} saved successfully")logger.info(f"Finished processing item {item.id}")# GOOD: One meaningful logdefprocess_item(item):logger.debug(f"Processing item {item.id}")# ... do work ...logger.info(f"Processed item {item.id} in {duration:.2f}s")
# BAD: This isn't debug, it's infologger.debug(f"User {user_id} completed checkout")# BAD: This isn't an error, it's expectedlogger.error(f"User {user_id} entered wrong password")# Use INFO# BAD: This isn't a warning, it's an errorlogger.warning(f"Database connection failed: {error}")# Use ERROR
# BAD: Useless without contextlogger.error("Failed to process request")# GOOD: Actionablelogger.error(f"Failed to process request: path={request.path} "f"user={user_id} error={error}",exc_info=True)
Plain text logs are hard to parse. Use structured logging:
1
2
3
4
5
6
7
8
9
10
11
12
importstructloglogger=structlog.get_logger()# Instead of string interpolationlogger.info("order_placed",order_id=order.id,user_id=user.id,total=order.total,items=len(order.items))