Logging + monitoring cheatsheet.
Basic LOGGING
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "{levelname} {asctime} {name} {message}",
"style": "{",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "verbose",
},
},
"root": {
"handlers": ["console"],
"level": "INFO",
},
"loggers": {
"django.request": {"handlers": ["console"], "level": "WARNING"},
"django.db.backends": {"handlers": ["console"], "level": "INFO"},
},
}
structlog
uv add structlog python-json-logger
import structlog
import logging
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"json": {"()": "pythonjsonlogger.json.JsonFormatter"},
},
"handlers": {"console": {"class": "logging.StreamHandler", "formatter": "json"}},
"root": {"handlers": ["console"], "level": "INFO"},
}
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.processors.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer(),
],
)
Using structlog
import structlog
log = structlog.get_logger(__name__)
def view(request):
log.info("post_created", post_id=post.id, author_id=request.user.id)
JSON output:
{"level":"info","event":"post_created","post_id":42,"author_id":7,"timestamp":"2026-01-15T12:00:00Z"}
Request ID middleware
import uuid
import structlog
class RequestIDMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
rid = request.META.get("HTTP_X_REQUEST_ID") or str(uuid.uuid4())
structlog.contextvars.bind_contextvars(request_id=rid, user_id=getattr(request.user, "id", None))
try:
response = self.get_response(request)
response["X-Request-ID"] = rid
return response
finally:
structlog.contextvars.clear_contextvars()
Now every log within the request has request_id.
Access log
class AccessLogMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
import time
t0 = time.monotonic()
response = self.get_response(request)
log.info(
"http",
method=request.method,
path=request.path,
status=response.status_code,
duration_ms=int((time.monotonic() - t0) * 1000),
)
return response
Sentry
uv add sentry-sdk
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.celery import CeleryIntegration
sentry_sdk.init(
dsn=os.environ["SENTRY_DSN"],
environment=os.environ.get("ENV", "production"),
traces_sample_rate=0.1,
profiles_sample_rate=0.1,
integrations=[DjangoIntegration(), CeleryIntegration()],
send_default_pii=False,
)
Captures unhandled exceptions automatically.
# Manual capture
sentry_sdk.capture_exception(e)
sentry_sdk.capture_message("something happened", level="warning")
# Add context
sentry_sdk.set_user({"id": user.id, "email": user.email})
sentry_sdk.set_tag("feature", "checkout")
sentry_sdk.set_extra("payload", payload)
Logging exceptions
log = structlog.get_logger()
try:
risky()
except Exception:
log.exception("operation_failed") # includes traceback
exception only inside except.
Query logging (debug)
LOGGING["loggers"]["django.db.backends"] = {
"level": "DEBUG",
"handlers": ["console"],
}
Logs every SQL query. Dev only — too verbose for prod.
Per-app loggers
LOGGING["loggers"]["blog"] = {"level": "DEBUG", "handlers": ["console"]}
LOGGING["loggers"]["accounts"] = {"level": "INFO", "handlers": ["console"]}
log = logging.getLogger(__name__) # "blog.views"
Health check + metrics
# urls.py
from django.http import JsonResponse
def health(request):
return JsonResponse({"ok": True})
def metrics(request):
return HttpResponse(generate_latest(), content_type=CONTENT_TYPE_LATEST)
uv add prometheus-client django-prometheus
INSTALLED_APPS = [..., "django_prometheus"]
MIDDLEWARE = [
"django_prometheus.middleware.PrometheusBeforeMiddleware",
...,
"django_prometheus.middleware.PrometheusAfterMiddleware",
]
Exposes /metrics for Prometheus.
Audit logging
class AuditLog(models.Model):
user = models.ForeignKey(...)
action = models.CharField(max_length=100)
target_model = models.CharField(max_length=100)
target_id = models.IntegerField()
diff = models.JSONField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
def log_action(user, action, target, diff=None):
AuditLog.objects.create(
user=user,
action=action,
target_model=target.__class__.__name__,
target_id=target.id,
diff=diff,
)
OpenTelemetry
uv add opentelemetry-distro opentelemetry-instrumentation-django
opentelemetry-bootstrap --action=install
opentelemetry-instrument python manage.py runserver
Auto-instruments Django, DB, requests, etc.
Don’t log
- Passwords / tokens / SSNs.
- Full request bodies with sensitive data.
- Email addresses for users without consent.
Redact:
def redact_pii(obj):
if isinstance(obj, dict):
return {k: ("[REDACTED]" if k.lower() in {"password", "token"} else redact_pii(v)) for k, v in obj.items()}
if isinstance(obj, list):
return [redact_pii(x) for x in obj]
return obj
Common mistakes
print()in prod — not structured, not captured.- Log levels mismatched (everything
INFO→ noise). - Logging full exception with PII to disk.
- No
disable_existing_loggers: False— django’s own logs disappear. - Logging in a tight loop — drowns out signal.
Read this next
If you want my structlog + Sentry + request-ID stack, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .