Fix Django Logs Not Showing in Production
Fix Django logging that shows nothing in production: why logger.info disappears with DEBUG off, configure LOGGING with console handlers, find Gunicorn and systemd output in journalctl, and capture errors from Celery.
Problem statement
You added logger.info("payment received") and print() calls, deployed, and nothing appears anywhere. A user reports a 500 error, and there is no traceback in Gunicorn's output either. Locally with runserver the messages showed up in the terminal.
With DEBUG = False and no LOGGING setting, Django's defaults send almost nothing to the console: only messages at WARNING and above from some loggers, and request errors go to email handlers that are often not configured. Your own logger.info calls go to loggers with no handler, and Python discards them. Then Gunicorn, systemd, or Docker decide where whatever is printed ends up.
- Define
LOGGINGwith a console handler and a root logger atINFO. - Use named loggers:
logger = logging.getLogger(__name__). - Run Gunicorn with
--access-logfile -and--error-logfile -so everything goes to stdout and stderr. - Read logs where the process manager puts them:
journalctl -u gunicorn,docker compose logs web, or the platform's log viewer. - Add Sentry or another error tracker for tracebacks of unhandled exceptions.
Step-by-step solution
1. Configure LOGGING explicitly
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"plain": {"format": "{asctime} {levelname} {name} {message}", "style": "{"},
},
"handlers": {
"console": {"class": "logging.StreamHandler", "formatter": "plain"},
},
"root": {"handlers": ["console"], "level": "INFO"},
"loggers": {
"django": {"handlers": ["console"], "level": "INFO", "propagate": False},
"django.request": {"handlers": ["console"], "level": "ERROR", "propagate": False},
},
}
disable_existing_loggers: False keeps loggers created by libraries working. The django.request logger records unhandled exceptions in views with full tracebacks, which is what you want for 500 errors.
2. Log through named loggers
import logging
logger = logging.getLogger(__name__)
def checkout(request):
logger.info("checkout started user=%s", request.user.pk)
print() works only if stdout is captured and unbuffered; set PYTHONUNBUFFERED=1 in the service environment or the output may appear late or not at all when the process is killed.
3. Make Gunicorn log to stdout and stderr
gunicorn config.wsgi:application \
--access-logfile - \
--error-logfile - \
--capture-output \
--log-level info
- means stdout/stderr. --capture-output redirects your app's stdout and stderr into the error log, so print() output appears too. Avoid writing to files inside containers; the process manager handles rotation and shipping.
4. Read logs where they actually go
- systemd:
journalctl -u gunicorn -fandjournalctl -u celery-worker --since "-1h". - Docker:
docker compose logs -f web. - Kubernetes:
kubectl logs deploy/web -f. - PaaS: the platform's log tab or CLI (
heroku logs --tail,fly logs,railway logs).
If you configured a FileHandler, check that the directory exists and is writable by the service user; a permission error there can stop Django from starting or silently drop records.
5. Do not forget Celery and management commands
Celery hijacks the root logger by default and applies its own format. Keep your configuration with:
CELERY_WORKER_HIJACK_ROOT_LOGGER = False
Management commands run via cron or systemd timers need their output captured too; see Fix a Django Management Command That Fails in Cron.
6. Add error tracking
Logs show what happened; an error tracker groups exceptions, shows request data, and alerts you. Sentry integrates in a few lines and captures unhandled exceptions from views and Celery tasks; see Set Up Sentry for Django in Production.
Verification check:
python manage.py shell -c "import logging; logging.getLogger('probe').info('logging works')"
sudo journalctl -u gunicorn --since "-5min" | tail
Trigger a deliberate error on a staging URL and confirm the traceback appears in the logs and in your error tracker.
Explanation
Python's logging only emits a record if the logger's effective level allows it and some handler in the chain accepts it. Django's default configuration, used when LOGGING is not set, is designed for development: with DEBUG = False, it filters console output and routes request errors to mail_admins. In production, the twelve-factor convention is to write logs to stdout and stderr and let the process supervisor collect them, which requires both an explicit console handler in Django and a server configured to pass output through.
Edge cases / notes
- Log level per environment. Read the level from an environment variable so you can raise verbosity temporarily without a code change.
- Sensitive data. Do not log passwords, tokens, or full request bodies. Django's error reports filter some settings, but your own log lines are your responsibility.
- Structured logs. JSON-formatted logs make searching much easier in Loki, CloudWatch, or Datadog.
- Volume. Access logs at high traffic can be large; rotate journald or Docker logs.
Next steps
- Build a complete configuration with Django Logging Setup for Production.
- Keep disk usage bounded with Django Log Rotation on Linux.
FAQ
Why does logger.info not show up in production?
Without a LOGGING setting, there is no handler for your logger at INFO level, so the message is dropped.
Where do Gunicorn logs go under systemd?
To the journal. Read them with journalctl -u <service-name>.
Why is print() output missing?
It may be buffered or not captured. Set PYTHONUNBUFFERED=1 and use Gunicorn's --capture-output, or use logging instead.
How do I see 500 error tracebacks?
Send the django.request logger to the console handler, and add an error tracker like Sentry.