Logging Guide¶
This guide covers how to add structured logging to your code.
Getting a Logger¶
Always pass __name__ to get a logger named after your module. This appears in the logger field of log output.
Logging Events¶
Basic Logging¶
# Simple event
log.info("user_registered")
# Event with context
log.info("user_registered", user_id=42, username="alice")
# With dynamic values
log.info("order_placed", order_id=order.id, total=order.total)
Event Naming¶
Use snake_case event names that describe what happened:
# Good
log.info("tournament_created", tournament_id=5)
log.info("draft_started", draft_id=12, participant_count=8)
log.info("match_completed", match_id=99, winner_id=3)
# Avoid
log.info("Created a tournament") # Human-readable strings are harder to filter
log.info("TournamentCreated") # PascalCase inconsistent with conventions
Log Levels¶
| Level | Method | When to Use |
|---|---|---|
| DEBUG | log.debug() |
Detailed debugging info, rarely needed in production |
| INFO | log.info() |
Normal operations, key events |
| WARNING | log.warning() |
Unexpected but handled situations |
| ERROR | log.error() |
Failures that need attention |
Examples¶
# DEBUG - Detailed tracing
log.debug("cache_lookup", key="tournament:5", hit=True)
# INFO - Normal operations
log.info("draft_pick_made", draft_id=12, hero="Invoker", pick_num=5)
# WARNING - Unexpected but handled
log.warning("rate_limit_approached", user_id=42, requests=95, limit=100)
# ERROR - Failures
log.error("payment_failed", order_id=99, error="Card declined")
Logging Exceptions¶
try:
process_payment(order)
except PaymentError as e:
log.error("payment_failed", order_id=order.id, error=str(e), exc_info=True)
raise
The exc_info=True parameter includes the full stack trace.
Context Fields¶
Context fields are automatically added to all logs within scope:
HTTP Request Context¶
These fields are automatically added by TelemetryMiddleware:
| Field | Description |
|---|---|
request.id |
Unique request identifier |
user.id |
Authenticated user ID |
labels |
Resource types in URL |
{resource}.id |
Resource IDs (e.g., tournament.id) |
WebSocket Context¶
Added by TelemetryConsumerMixin:
| Field | Description |
|---|---|
ws.conn_id |
Unique connection identifier |
user.id |
Authenticated user ID |
labels |
Resource types in path |
{resource}.id |
Resource IDs |
Celery Task Context¶
Added by TelemetryTask:
| Field | Description |
|---|---|
task.id |
Celery task ID |
task.name |
Task function name |
request.id |
Propagated or generated request ID |
user.id |
Propagated user ID |
Binding Additional Context¶
You can bind additional context for a scope:
import structlog
# Bind context for remaining scope
structlog.contextvars.bind_contextvars(
organization_id=org.id,
subscription_tier="premium"
)
# All subsequent logs include this context
log.info("feature_enabled", feature="bulk_import")
# Output includes: organization_id=5, subscription_tier="premium"
Context Cleanup
Context is automatically cleared at the end of request/task scope. If binding context in long-running code, clear it manually:
Output Formats¶
Development (Pretty)¶
In development (NODE_ENV=dev), logs use colored console output:
2024-01-15 10:23:45 [info ] tournament_created [app.views.tournament] tournament_id=5 request.id=abc-123
Production (JSON)¶
In production, logs are JSON for easy parsing:
{
"timestamp": "2024-01-15T10:23:45.123456Z",
"level": "info",
"logger": "app.views.tournament",
"event": "tournament_created",
"tournament_id": 5,
"request.id": "abc-123"
}
Best Practices¶
1. Log at Decision Points¶
def process_draft_pick(draft_id, hero):
draft = get_draft(draft_id)
if draft.is_complete:
log.warning("draft_pick_rejected", reason="draft_complete")
return None
pick = draft.add_pick(hero)
log.info("draft_pick_added", hero=hero, pick_number=pick.number)
return pick
2. Include Relevant IDs¶
# Good - includes all relevant identifiers
log.info("match_started",
match_id=match.id,
tournament_id=match.tournament_id,
team_a_id=match.team_a_id,
team_b_id=match.team_b_id
)
# Less useful - missing context
log.info("match_started")
3. Avoid Logging Sensitive Data¶
# Bad - logs password
log.info("login_attempt", username=user, password=pwd)
# Good - only log what's needed
log.info("login_attempt", username=user)
log.info("login_success", user_id=authenticated_user.id)
4. Use Appropriate Levels¶
# Don't use ERROR for expected failures
if not user.has_permission:
log.info("access_denied", user_id=user.id, resource="admin_panel")
# Not log.error() - this is expected behavior
# Use ERROR for unexpected failures
try:
send_email(user.email)
except SMTPError as e:
log.error("email_failed", user_id=user.id, error=str(e), exc_info=True)