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.