Plain-text logs are hard to search, filter, and alert on at scale. Structured
logging (JSON) makes every log line a queryable record — essential for
production observability.
| Feature | Plain Text | Structured (JSON) |
|---|---|---|
| Human-readable | Yes | With tooling |
| Machine-parseable | Regex needed | Native |
| Searchable | grep | Field queries |
| Alerting | Pattern match | Exact field match |
| Context (request_id) | Manual | Automatic |
Use environment-specific YAML configs:
configs/
├── logging_dev.yaml # Colorized console, DEBUG level
├── logging_prod.yaml # JSON to stdout + file, INFO level
└── logging_test.yaml # WARNING only, minimal output
Load with one call:
from src.setup import configure_logging
configure_logging("prod")Every log line should identify which request it belongs to. This toolkit
uses contextvars (works with asyncio, threads, and sync code):
from src.context import set_context, generate_request_id
# In middleware (automatic):
set_context(request_id=generate_request_id())
# In your code:
set_context(user_id="usr-42", tenant="acme")
# Every log line now includes these fields automaticallyfrom src.middleware import ASGILoggingMiddleware
app.add_middleware(ASGILoggingMiddleware)from src.middleware import WSGILoggingMiddleware
app.wsgi_app = WSGILoggingMiddleware(app.wsgi_app)Both middleware:
1. Generate or extract request_id from headers
2. Set context variables
3. Log request start/finish with duration
4. Clean up context after the request
from src.filters import SuppressLoggerFilter
handler.addFilter(SuppressLoggerFilter(["urllib3", "botocore", "asyncio"]))from src.filters import RateLimitFilter
handler.addFilter(RateLimitFilter(period_seconds=60))1. Use QueueHandler in production — create_async_handler() offloads
I/O to a background thread so logging never blocks your request.
2. Set appropriate levels — Don't log DEBUG in production. Every log
line has a cost (CPU, I/O, storage).
3. Use lazy formatting — logger.info("User %s", user_id) is faster
than logger.info(f"User {user_id}") because formatting is skipped
if the level is disabled.
4. Rotate logs — Use RotatingFileHandler or TimedRotatingFileHandler
to prevent disk exhaustion.
try:
process_order(order_id)
except Exception:
logger.exception("Failed to process order", extra={"order_id": order_id})logger.info(
"Payment processed",
extra={"amount": 49.99, "currency": "USD", "order_id": "ORD-123"},
)if logger.isEnabledFor(logging.DEBUG):
# Expensive serialization only when DEBUG is active
logger.debug("Full payload: %s", json.dumps(payload, indent=2))1. print() instead of logging — No levels, no formatting, no routing.
2. Logging sensitive data — Never log passwords, tokens, or PII.
3. Catching and logging without re-raising — Swallows the error.
4. String concatenation in log calls — Use %s placeholders instead.
5. One logger for everything — Use logging.getLogger(__name__) per module.
Get the full Python Logging & Config and unlock everything.
Get the complete guide with every chapter unlocked, including code samples, diagrams, and best practices.
Access all interactive tools with complete data, all workload profiles, and the full scenario library.
Downloadable source code, configuration files, and working examples from every chapter.
Free updates for life. Every new chapter, tool, and improvement included.