5. Dual-audience logging
One error, two readers. The user wants plain-language guidance they can act on. The developer wants a stack trace, a timestamp, and enough request context to reproduce the problem locally. The pages you've built so far handle the user side; this one connects a proper logger so the developer side exists too, without ever leaking through to the screen.
| Audience | What They Need | Example |
|---|---|---|
| End Users | Plain language guidance, actionable steps, concrete examples | "We couldn't find weather data for 'Lndon'. Please check the spelling or try a nearby city. Examples: London, Dublin, Tokyo" |
| Developers | Timestamps, exception types, stack traces, request context, user input | [ERROR] 2024-03-15 14:23:15 | Category: not_found | Exception: KeyError('results') | Input: 'Lndon' | Stack: File "dashboard.py", line 34 |
The same error event generates two outputs: a friendly three-part message shown to users, and technical details logged for developers. Users get guidance to recover. Developers get everything needed to debug. This dual approach supports both excellent UX and effective troubleshooting.
Setting up structured logging
Python's logging module provides everything needed for dual-audience error handling. Configure it once at application startup:
import logging
from datetime import datetime
# Configure logging at application startup
logging.basicConfig(
level=logging.INFO,
format='[%(levelname)s] %(asctime)s | %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
handlers=[
# Log to file for developers
logging.FileHandler('weather_dashboard.log'),
# Also show in console during development
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
This configuration writes logs to both a file (for production) and the console (for development). The format includes timestamp, log level, and message - everything developers need to understand what happened and when.
What to log for each error
When an error occurs, log all technical details that help developers debug the issue:
def log_error(category, error_type, exception, context=None):
"""Log technical details for developers."""
log_entry = {
"timestamp": datetime.now().isoformat(),
"category": category,
"error_type": error_type,
"exception_type": type(exception).__name__,
"exception_message": str(exception),
"context": context or {}
}
if category == "user_input":
logger.info(f"User input rejected: {log_entry}")
elif category == "transient":
logger.warning(f"Transient failure: {log_entry}")
elif category == "not_found":
logger.info(f"Resource not found: {log_entry}")
else: # unknown
logger.error(f"Unknown error: {log_entry}", exc_info=True)
# Usage
try:
result = find_location("Lndon")
except KeyError as e:
category, error_type, context = categorize_error(e)
# Log for developers
log_error(category, error_type, e, {"user_input": "Lndon"})
# Show message to users
user_message = compose_error_message(category, error_type,
city_name="Lndon")
print(user_message)
The log includes everything developers need: timestamp (when it happened), category and error type (how it was classified), exception details (what broke), and context (what the user was trying to do). For unknown errors, exc_info=True captures the full stack trace.
Log levels signal urgency
Use different log levels to indicate severity and filter noise:
| Level | When to Use | Examples |
|---|---|---|
| INFO | Expected behavior, user input rejections | User typed invalid city name, resource not found |
| WARNING | Recoverable issues, retry attempts | Network timeout (retrying), rate limit hit |
| ERROR | Unexpected failures, unknown errors | Unhandled exceptions, parsing failures |
| CRITICAL | Application-level failures | Can't connect to database, config file missing |
Proper log levels let you filter logs by severity. In production, you might only review WARNING and above. During debugging, you might view all INFO logs. The levels communicate urgency without requiring manual triage.
Example: the complete error-handling flow
Here's how user messages and developer logs work together in a complete error handling flow:
def get_weather_with_logging(city_name):
"""Get weather with dual-audience error handling."""
try:
# Validate input first
validated_name = validate_city_name(city_name)
# Make API call
location_data = find_location(validated_name)
weather_data = get_weather(location_data)
return weather_data, None
except Exception as e:
# Categorize the error
category, error_type, context = categorize_error(e)
context["city_name"] = city_name
# Log technical details for developers
log_entry = {
"category": category,
"error_type": error_type,
"exception": str(e),
"context": context
}
if category == "user_input":
logger.info(f"Input validation failed: {log_entry}")
elif category == "transient":
logger.warning(f"Transient error occurred: {log_entry}")
elif category == "not_found":
logger.info(f"Resource not found: {log_entry}")
else:
logger.error(f"Unknown error: {log_entry}", exc_info=True)
# Compose friendly message for users
user_message = compose_error_message(category, error_type, **context)
return None, user_message
# Usage
weather, error = get_weather_with_logging("Lndon")
if error:
# User sees this:
print(error)
# "We couldn't find weather data for 'Lndon'."
# "Please check the spelling or try a nearby city."
# "Examples: London, Dublin, Manchester"
else:
print(f"Temperature: {weather['temp']}°C")
# Developer sees this in logs:
# [INFO] 2024-03-15 14:23:15 | Resource not found: {
# 'category': 'not_found',
# 'error_type': 'city',
# 'exception': "KeyError: 'results'",
# 'context': {'city_name': 'Lndon'}
# }
The user sees a friendly, actionable message. The developer sees technical details including the exact exception, how it was categorized, and what the user was trying to do. Both audiences get what they need from the same error event.
With logging in place, every handled failure now produces both outputs the application needs: a user-facing message and a developer-facing record. Next, we'll test those paths directly so the reliability code keeps working even when you refactor the dashboard around it.