Django Logging: LOGGING Setting, JSON, and Request IDs
Last verified on Nov 8, 2026.
Django configures logging through one setting, LOGGING, which it hands to Python's dictConfig during startup. The defaults are tuned for development: console output when DEBUG is on, and almost nothing useful when it is off. This guide sets up JSON on stdout, per-logger levels controlled by environment variables, and a request ID on every line, then covers what django.request does and does not log.
What Django Logs by Default
The Django reference spells out the defaults. When DEBUG is True, the django logger sends INFO and above to the console. When DEBUG is False, it sends only ERROR and CRITICAL to AdminEmailHandler, which emails the addresses in ADMINS. The django.server logger, used by runserver, always writes INFO and above to the console.
The loggers you will meet most:
Notice what is missing: a production access log. django.request covers 4XX and 5XX only, so successful requests need a log line from your WSGI or ASGI server or from your own middleware.
A Production LOGGING Setting
Set disable_existing_loggers to False. Django's docs warn that when the key is missing it defaults to True, which disables Django's own default loggers so they silently discard everything. They suggest either redefining the defaults you care about or setting LOGGING_CONFIG = None to take over entirely.
# settings.py
import os
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"filters": {"request_id": {"()": "myproject.logging_utils.RequestIdFilter"}},
"formatters": {"json": {"()": "myproject.logging_utils.JsonFormatter"}},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "json",
"filters": ["request_id"],
},
},
"root": {"handlers": ["console"], "level": "WARNING"},
"loggers": {
"django": {
"handlers": ["console"],
"level": os.getenv("DJANGO_LOG_LEVEL", "INFO"),
"propagate": False,
},
"myapp": {
"handlers": ["console"],
"level": os.getenv("APP_LOG_LEVEL", "INFO"),
"propagate": False,
},
},
}The "()" key is dictConfig's way to build a custom class from a dotted path. StreamHandler with no stream writes to stderr, which container platforms capture the same way as stdout. Pick whichever your platform treats as a normal log. Name your own loggers with logging.getLogger(__name__) in each app so myapp.orders inherits from myapp. See log levels for how to choose between INFO and WARNING.
The Formatter and Filter
The formatter turns each record into one JSON object. It skips the request attribute that django.request attaches, because dumping a request object is noisy and can include headers you did not mean to log:
# myproject/logging_utils.py
import contextvars
import json
import logging
from datetime import datetime, timezone
request_id_var = contextvars.ContextVar("request_id", default=None)
_STANDARD = set(logging.LogRecord("", 0, "", 0, "", (), None).__dict__) | {
"message", "asctime", "taskName",
}
_SKIP = {"request"}
class RequestIdFilter(logging.Filter):
def filter(self, record):
record.request_id = request_id_var.get()
return True
class JsonFormatter(logging.Formatter):
def format(self, record):
payload = {
"timestamp": datetime.fromtimestamp(record.created, timezone.utc).isoformat(),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
}
for key, value in record.__dict__.items():
if key not in _STANDARD and key not in _SKIP and value is not None:
payload[key] = value
if record.exc_info:
payload["exception"] = self.formatException(record.exc_info)
return json.dumps(payload, default=str)Anything passed through extra shows up as a field, along with status_code from the request loggers. The Python logging guide explains this formatter in more detail, and the JSON logging guide covers field naming. If you would rather keep keyword-style logging, the structlog guide shows how to route Django's records through structlog.
Add a Request ID With Middleware
Set the ID when the request enters, and the filter attaches it to every record logged afterward, including from libraries:
import uuid
from .logging_utils import request_id_var
class RequestIdMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
rid = request.headers.get("X-Request-ID") or str(uuid.uuid4())
request_id_var.set(rid)
response = self.get_response(request)
response["X-Request-ID"] = rid
return responseAdd it first in MIDDLEWARE. Output looks like this:
{"timestamp": "2026-11-08T06:48:14.894559+00:00", "level": "INFO", "logger": "myapp.orders", "message": "order placed", "order_id": 4812, "request_id": "req-8f3a"}
{"timestamp": "2026-11-08T06:48:14.895387+00:00", "level": "WARNING", "logger": "django.request", "message": "Not Found: /missing/", "status_code": 404, "request_id": "req-9b21"}Returning the ID in a response header lets a user or a front-end team quote it in a bug report, and it is the same value you search for as a correlation ID.
Do not reset the context variable in a finally block after the response. Django logs some django.request records, such as a 4XX response a view returns, after the middleware chain returns, and those records would come out with no request_id. Leaving the variable set until the next request overwrites it avoids that. On ASGI each request runs in its own context, so there is nothing to leak.
Common Mistakes
- Missing
disable_existing_loggers: False. Django's own loggers go quiet, and a 500 leaves no trace. - Using
printor a barelogging.basicConfigin settings.LOGGINGis merged with Django's defaults, so a second configuration fights it. - Expecting SQL logs in production. The
django.db.backendslogger only emits whenDEBUG = True, regardless of level or handlers. - Leaving
DEBUG = Trueon a deployed host. It also switches on SQL query logging, which Django limits toDEBUGfor performance reasons. - Logging whole request or user objects. Pass IDs, not objects.
- Relying on
AdminEmailHandleras monitoring. Email on every ERROR does not scale past a few errors an hour.
Production Tips
- Keep
djangoat INFO or WARNING and set it with an environment variable so you can raise it for one deploy without a code change. - Send to stdout or stderr and let the platform forward it. Do not manage log files inside the container.
- Log the user ID or tenant ID on each request in the same middleware, next to
request_id. - Read logging best practices for what to put in a log line and what to keep out.
Sending Django Logs to Sazabi
Sazabi, an AI-native observability and security platform, accepts logs and traces over OTLP/HTTP. Its OpenTelemetry source docs describe starting a Python app under opentelemetry-instrument with OTEL_EXPORTER_OTLP_ENDPOINT set. The OpenTelemetry Python configuration reference lists OTEL_PYTHON_DJANGO_INSTRUMENT, which is enabled by default, so request spans appear without code changes, and OTEL_PYTHON_LOG_CORRELATION for putting trace context into log records. Your JSON console handler can stay as it is. See the OpenTelemetry Python guide, the OpenTelemetry integration, and client errors for watching the 4XX stream that django.request produces.
Frequently Asked Questions
Where do I configure logging in Django?
In the LOGGING dictionary in settings.py. Django passes it to Python's dictConfig during setup and merges it with Django's default configuration.
Why does Django not log successful requests?
The django.request logger records 4XX and 5XX responses. Access logging for 2XX responses comes from your WSGI or ASGI server, or from middleware you write.
How do I get JSON logs from Django?
Define a logging.Formatter subclass that returns json.dumps of the record, reference it with the () key in LOGGING, and attach it to a console handler.
Why did my Django loggers stop working after I added LOGGING?
If disable_existing_loggers is True, which is the dictConfig default when the key is missing, Django's default loggers are disabled. Set it to False.
Sources
- Django uses dictConfig via the LOGGING setting, merges it with its defaults, and disables default loggers when disable_existing_loggers is True; LOGGING_CONFIG = None hands configuration to you (accessed Nov 8, 2026)
- django.request logs 5XX as ERROR and 4XX as WARNING with status_code and request context; django.server covers runserver; django.db.backends SQL logging needs DEBUG True; default console and mail_admins behavior (accessed Nov 8, 2026)
- The dictConfig schema and the () key for custom formatter and filter classes (accessed Nov 8, 2026)
- StreamHandler writes to sys.stderr when no stream is given (accessed Nov 8, 2026)
- Filters and contextvars attach contextual information such as a request ID to records (accessed Nov 8, 2026)
- OTEL_PYTHON_DJANGO_INSTRUMENT controls the Django instrumentation, enabled by default; OTEL_PYTHON_LOG_CORRELATION injects trace context into logs (accessed Nov 8, 2026)
- Sazabi receives logs and traces over OTLP/HTTP; Python apps start under opentelemetry-instrument with the OTLP endpoint set (accessed Nov 8, 2026)