How does Django apply your LOGGING setting on top of DEFAULT_LOGGING, and why is disable_existing_loggers usually set to False?
answer
- two configuration passes, not one
- a disabled logger does not propagate
- django.request carries the 500s
- missing key means True
basics
~10 sDjango runs dictConfig(DEFAULT_LOGGING), then your LOGGING as a second pass. With disable_existing_loggers True, the dictConfig default, loggers you don't name, including django.request, are disabled and silently drop 500-error records and admin emails.
solid answer
~40 sIn `django.setup()`, `configure_logging()` applies `DEFAULT_LOGGING` with `dictConfig` and then calls `LOGGING_CONFIG`, `dictConfig` by default, with your `LOGGING` if it is non-empty. Loggers you name are fully reconfigured; loggers you don't name keep Django's defaults only if `disable_existing_loggers` is `False`. If it is `True`, which is what dictConfig assumes when the key is missing, every existing logger you didn't configure is disabled: it discards records and doesn't propagate. That includes `django.request`, where unhandled exceptions are logged as `ERROR`, so 500s disappear from your handlers and the admin emails stop, without any error. I always set it to `False` and redefine the loggers I care about, re-adding an `AdminEmailHandler` if I replace the `django` logger's handlers.
code
python · 18 linesLOGGING = {
"version": 1,
"disable_existing_loggers": False,
"filters": {
"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"},
},
"handlers": {
"console": {"class": "logging.StreamHandler"},
"mail_admins": {
"level": "ERROR",
"filters": ["require_debug_false"],
"class": "django.utils.log.AdminEmailHandler",
},
},
"loggers": {
"django": {"handlers": ["console", "mail_admins"], "level": "INFO"},
},
}go deeper
Recall that Django applies its default logging first and your LOGGING afterwards, and that disable_existing_loggers should normally be False.
Explain the two dictConfig passes, what happens to named and unnamed loggers, and why a disabled django.request silently drops 500 records.
Audit a LOGGING dict for lost admin emails and disabled Django loggers, and verify in a shell or staging run that errors reach every destination.
Decide whether a project keeps Django's defaults as a base or owns its whole logging configuration, and how that is shared across services.
## How Django applies LOGGING During `django.setup()`, Django calls `django.utils.log.configure_logging(LOGGING_CONFIG, LOGGING)`: 1. It imports the function named by **`LOGGING_CONFIG`** (default `"logging.config.dictConfig"`). 2. It calls `logging.config.dictConfig(DEFAULT_LOGGING)`, installing Django's defaults. 3. If **`LOGGING`** is non-empty, it calls the configured function with it, a **second** dictConfig pass on top of the first. So "merged" does not mean a dictionary merge. The defaults are live first; your configuration is then applied over them. What survives depends on what your dictionary names: | In your `LOGGING` | Result | |---|---| | a logger you configure, such as `django` | its level, handlers and `propagate` are replaced by yours | | a Django logger you do not mention, with `disable_existing_loggers: False` | keeps Django's defaults | | a Django logger you do not mention, with `disable_existing_loggers: True` or the key missing | **disabled** | | `LOGGING_CONFIG = None` | neither pass runs; nothing is configured by Django | ## What disable_existing_loggers does `disable_existing_loggers` is a dictConfig key. When it is `True`, and that is the dictConfig default if the key is missing, every logger that already exists and is not named in your configuration is **disabled**. Django's docs are precise about what that means: a disabled logger still exists, but silently discards anything logged to it, **not even propagating** to its parent. At that moment the existing loggers include the ones Django just configured or created, among them `django`, `django.server` and `django.request`. The damage: - `django.request` is where Django logs every 5XX response with its exception. Disabled, those records vanish before reaching any handler. - The `mail_admins` handler never sees them, so **500 error emails stop**. - Your new file or console handler on `django` never sees them either, because a disabled child does not propagate. The failure is silent: nothing errors, and the logs simply lack the one category you most need. ## The safe pattern ```python LOGGING = { "version": 1, "disable_existing_loggers": False, "handlers": { "console": {"class": "logging.StreamHandler"}, }, "root": {"handlers": ["console"], "level": "WARNING"}, "loggers": { "django": {"handlers": ["console"], "level": "INFO", "propagate": False}, }, } ``` - `disable_existing_loggers: False` keeps every Django logger alive. - Loggers you name are fully reconfigured, so the `django` entry above **replaces** the default `console` and `mail_admins` handlers. If you still want error emails, list a handler of class `django.utils.log.AdminEmailHandler` there as well. - Filters from the defaults are not inherited by name into your configuration: to gate a handler on `DEBUG`, declare `django.utils.log.RequireDebugTrue` or `RequireDebugFalse` in your own `filters` section. ## When to replace the defaults entirely Setting `LOGGING_CONFIG = None` disables Django's configuration step, not logging itself. You then call `logging.config.dictConfig()` yourself, for example at the end of `settings.py`. Two caveats from the docs: - Nothing from `DEFAULT_LOGGING` exists unless you configure it, including the admin emails. - Your call runs while settings are still loading, so the configuration must come after any setting it depends on. This is useful when a shared logging setup is imported from a package; for most projects, `disable_existing_loggers: False` plus explicit loggers is simpler. ## A worked failure A team copies a logging snippet from an old blog post into a project whose `LOGGING` previously did not exist: 1. The snippet defines a `file` handler and a logger for their own `orders` app, and has no `disable_existing_loggers` key. 2. Deployment succeeds; `orders` records appear in the file, so the change looks fine. 3. A week later a view raises in production. The user sees the 500 page, but no email arrives and nothing is in the file. 4. The cause: the missing key defaulted to `True`, disabling `django.request`, so the exception record was discarded at its source. Adding `"disable_existing_loggers": False` restores the emails without any other change, because the default `django` logger and its `mail_admins` handler were never removed, only cut off. ## Checking the result - In `manage.py shell`, `logging.getLogger("django.request").disabled` should be `False` and the logger's effective level should be what you expect. - Trigger a test 500 in a staging environment and confirm the record reaches every intended destination before going live.
- Your LOGGING redefines the django logger with only a console handler; what happens to admin error emails?They stop. A logger named in `LOGGING` is reconfigured with exactly the handlers you list, so the default `mail_admins` handler is removed from `django`. Records from `django.request` still propagate there, but only reach your console handler. Add an `AdminEmailHandler` handler to the list to keep the emails.
- What does LOGGING_CONFIG = None do?It skips Django's configuration step entirely: neither `DEFAULT_LOGGING` nor `LOGGING` is applied. Logging still works, but you must call `logging.config.dictConfig()` yourself, typically at the end of `settings.py`, and you lose the default admin emails unless you configure them.
Django hangs its own set of pipes first, then lets you add plumbing. disable_existing_loggers set to True is like capping every pipe you didn't personally install: the building looks fine until you notice the one carrying alarms is sealed.
saying these in an interview costs you the question
- Django deep-merges LOGGING into DEFAULT_LOGGING key by key
- Omitting disable_existing_loggers keeps Django's loggers enabled
- A disabled logger still passes records to its parent
- Naming the django logger in LOGGING keeps its default handlers too
- LOGGING_CONFIG = None turns logging off completely