Troubleshooting
Monitoring & logging

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.

Where log lines disappearlogger.info()in your codeNo handler / leveldropped at WARNINGGunicorn stdoutcaptured or notjournal / dockerwhere it landsConfigure LOGGING to the console, then read it where your process manager stores stdout.
  1. Define LOGGING with a console handler and a root logger at INFO.
  2. Use named loggers: logger = logging.getLogger(__name__).
  3. Run Gunicorn with --access-logfile - and --error-logfile - so everything goes to stdout and stderr.
  4. Read logs where the process manager puts them: journalctl -u gunicorn, docker compose logs web, or the platform's log viewer.
  5. 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.

from logger call to log viewer1logger.info() in your codenamed logger2Level checklogger and handler must allowINFO3Console handlerwrites to stderr4Gunicorn passes it through--error-logfile -5journald, Docker, or platformwhere you read itA message is lost at the first step that does not pass it ondjango-deployment.com
Check each step in order when a message goes missing.

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 -f and journalctl -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.

what is missing and whyMissingCause and fixyour logger.info linesno handler or levelWARNING; root at INFO500 tracebacksdjango.request goes tomail_admins; add consoleprint() outputbuffered or not captured;PYTHONUNBUFFERED, capture-outputCelery task logsroot logger hijacked;set hijack to Falseeverythingreading the wrong place;journalctl -u or compose logsMost missing logs are a configuration default, not a broken loggerdjango-deployment.com
Start with LOGGING and Gunicorn flags; most cases end there.

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

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.

Keep going