Logging
Overview¶
SpeedPy uses django-structlog for structured logging. It provides consistent, machine-parseable log output with automatic request context binding.
Configuration¶
The logging setup in project/settings.py includes three formatters:
plain_console— human-readable console output (used by default)json_formatter— JSON output for log aggregation serviceskey_value— key=value format
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"json_formatter": {
"()": structlog.stdlib.ProcessorFormatter,
"processor": structlog.processors.JSONRenderer(),
},
"plain_console": {
"()": structlog.stdlib.ProcessorFormatter,
"processor": structlog.dev.ConsoleRenderer(),
},
},
"handlers": {
"console": {"class": "logging.StreamHandler", "formatter": "plain_console"}
},
"loggers": {
"": {"handlers": ["console"], "level": LOG_LEVEL},
"botocore": {"handlers": ["console"], "level": "WARNING", "propagate": False},
"boto3": {"handlers": ["console"], "level": "WARNING", "propagate": False},
"s3transfer": {"handlers": ["console"], "level": "WARNING", "propagate": False},
"urllib3": {"handlers": ["console"], "level": "WARNING", "propagate": False},
},
}
Log level¶
The root level comes from LOG_LEVEL, which defaults to DEBUG in development
and INFO in production:
Set LOG_LEVEL=DEBUG on a deployed app when you need to debug something, and put
it back afterwards.
Why boto and urllib3 are pinned¶
Those four loggers are held at WARNING independently of the root level, and
that is deliberate rather than tidiness.
At DEBUG, botocore logs the full body of every HTTP request it sends. If your
app sends mail through an AWS-backed provider — SES via Anymail, for example —
that body is the whole email. In practice it means password-reset tokens (as
base64 MIME anyone can decode), complete message bodies, and SigV4
Authorization headers written into your container log, where log aggregation and
support access spread them further.
Lowering the root level alone would fix that today and quietly undo it the next
time somebody sets LOG_LEVEL=DEBUG to investigate something unrelated — which
is exactly the moment nobody is looking at boto output. Pinning each logger with
propagate: False means raising the root level stays safe.
If you add another chatty client library that logs request bodies, pin it the same way rather than relying on the root level.
Check this if you upgraded from an older SpeedPy
Earlier versions set the root logger to DEBUG unconditionally and had no
per-library pins. If your project/settings.py still reads
"loggers": {"": {"handlers": ["console"], "level": "DEBUG"}} and you send
mail through SES, your production log very likely contains real
password-reset tokens. Apply the block above, then treat the existing log
history as sensitive.
Middleware¶
The structlog request middleware is included to automatically bind request data (user, IP, request ID) to log entries:
MIDDLEWARE = [
...
"django_structlog.middlewares.RequestMiddleware",
]
DJANGO_STRUCTLOG_CELERY_ENABLED = True
DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED = True
Usage¶
import structlog
logger = structlog.get_logger(__name__)
def my_view(request):
logger.info("processing_request", user_id=request.user.id)
try:
result = do_something()
logger.info("processing_complete", result=result)
except Exception:
logger.exception("processing_failed")
Key practices:
- Use structlog.get_logger(__name__) at module level
- Pass context as keyword arguments, not in the message string
- Use snake_case for event names
- Use logger.exception() for errors (automatically includes traceback)
Celery Integration¶
Structlog is also configured for Celery workers in project/celeryapp.py via DjangoStructLogInitStep. This ensures Celery tasks get the same structured logging format.
from django_structlog.celery.steps import DjangoStructLogInitStep
app.steps["worker"].add(DjangoStructLogInitStep)
Switching to JSON in Production¶
To output JSON logs (for services like Datadog, Elastic, etc.), change the handler formatter: