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 .