skip to content

How do Django's AdminEmailHandler, RequireDebugFalse and CallbackFilter combine in LOGGING to email only actionable production errors?

level: middleimportance: should knowfreq 36%

answer

  1. one handler, three filters
  2. the ADMINS list gates everything
  3. quiet in development
  4. a callback drops the noise

basics

~10 s

AdminEmailHandler emails each ERROR record to ADMINS and sends nothing if ADMINS is empty. RequireDebugFalse keeps it quiet during development, and a CallbackFilter drops records such as UnreadablePostError whose callback returns False.

solid answer

~30 s

`AdminEmailHandler` sends one email per record to the addresses in `ADMINS`, using the request on `django.request` records to add details, and it returns without sending when `ADMINS` is empty, the default. In `LOGGING` I declare it as `mail_admins` at `ERROR` with a `RequireDebugFalse` filter, so it only fires when `DEBUG` is off, and a `CallbackFilter` whose callback returns `False` for non-bugs like `UnreadablePostError` from aborted uploads. I attach it once, to the `django` logger, since `django.request` propagates there; attaching it to both doubles every email. In Django 6.1 the handler's `using` argument selects a `MAILERS` alias and `email_backend` is deprecated.

code

python · 23 lines
python
ADMINS = ["[email protected]"]

LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "filters": {
        "require_debug_false": {"()": "django.utils.log.RequireDebugFalse"},
        "skip_unreadable_post": {
            "()": "django.utils.log.CallbackFilter",
            "callback": skip_unreadable_post,
        },
    },
    "handlers": {
        "mail_admins": {
            "level": "ERROR",
            "filters": ["require_debug_false", "skip_unreadable_post"],
            "class": "django.utils.log.AdminEmailHandler",
        },
    },
    "loggers": {
        "django": {"handlers": ["mail_admins"], "level": "INFO"},
    },
}

go deeper

for a junior

Recall that AdminEmailHandler emails errors to ADMINS and that RequireDebugFalse keeps those emails off during development.

for a middle

Explain the empty-ADMINS early return, how CallbackFilter decides, and why the handler belongs on one logger only.

for a senior

Tune alerting: silence DisallowedHost floods, filter non-bugs, and never let email be the only channel for production errors.

for a principal

Decide whether per-error emails remain an alerting channel at all as traffic grows, versus aggregated error tracking and paging.

## The three pieces Django ships one logging handler and three filters of its own in `django.utils.log`: | Class | Kind | What it does | |---|---|---| | `AdminEmailHandler` | handler | emails each record it receives to the addresses in `ADMINS` | | `RequireDebugFalse` | filter | passes records only when `settings.DEBUG` is `False` | | `RequireDebugTrue` | filter | passes records only when `settings.DEBUG` is `True` | | `CallbackFilter` | filter | passes records for which your callback returns a truthy value | Together they turn "email me when production breaks" into a few lines of `LOGGING`. ## AdminEmailHandler - Sends one email per record, using `mail_admins()`, to every address in `ADMINS`. Since Django 6.0, `ADMINS` is a list of address strings; the old `(name, address)` tuples are deprecated. - **Returns early without sending when `ADMINS` is empty**, which is the default, so an unconfigured project silently drops the alert. - Builds the subject from the level and message and, when the record has a `request` attribute (as `django.request` records do), marks it "internal" or "EXTERNAL" IP depending on `INTERNAL_IPS`. - Options, all set as keys on the handler in `LOGGING`: - `include_html=True` attaches the full technical error page as HTML, with the security implications the error-reporting topic covers; - `reporter_class` names an `ExceptionReporter` subclass that builds the body; - `using` (new in 6.1) picks a `MAILERS` alias; the older `email_backend` argument is deprecated. What the report contains and how sensitive values are scrubbed belong to the error-reporting topic; here the point is routing. ## RequireDebugFalse and RequireDebugTrue The default configuration puts `RequireDebugFalse` on `mail_admins`, so developers are not emailed for errors they already see in the browser, and `RequireDebugTrue` on `console`, so production output stays quiet. In your own `LOGGING` you declare them under `filters` with the `"()"` factory key and list them on handlers: ```python "filters": { "require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}, }, ``` They read `settings.DEBUG` on every record, so the same configuration file behaves correctly in both environments. ## CallbackFilter: dropping what is not actionable Some errors are real exceptions but not bugs. The documented example is `UnreadablePostError`, raised when a client disconnects during an upload: ```python from django.http import UnreadablePostError def skip_unreadable_post(record): if record.exc_info: exc_type, exc_value = record.exc_info[:2] if isinstance(exc_value, UnreadablePostError): return False return True ``` Wired in as `{"()": "django.utils.log.CallbackFilter", "callback": skip_unreadable_post}` on the `mail_admins` handler, it keeps these out of the inbox while everything else still arrives. ## Putting it together 1. Set `ADMINS` to real addresses. 2. Declare `require_debug_false` and your callback filter. 3. Define `mail_admins` as an `AdminEmailHandler` at `ERROR` with both filters. 4. Attach it to the `django` logger once; `django.request` records reach it by propagation. 5. Silence `django.security.DisallowedHost` with a `NullHandler` and `propagate: False` if scanners probe your hosts. ## Testing the setup - `manage.py sendtestemail --admins` sends a message to everyone in `ADMINS` through the configured mail setup, proving delivery without waiting for a real error. - In a staging environment with `DEBUG = False`, a deliberately failing view confirms the whole path: `django.request` record, filters, handler, inbox. - In the test suite, Django's test runner swaps in the in-memory email backend, so a test that triggers a logged error can assert on `mail.outbox` when the handler is attached; note that `RequireDebugFalse` passes there, because tests run with `DEBUG = False` by default. ## Common mistakes - Attaching `mail_admins` to both `django` and `django.request`, which sends each 500 twice. - Forgetting `RequireDebugFalse` in a custom configuration, so local development fires real emails. - Relying on email as the only error channel: one broken mail setup and every alert is lost. Pair it with a console or file handler. - A callback that raises: an exception inside a filter propagates instead of being treated as "drop", so a buggy callback can break the very logging it was meant to tune. Read attributes with `getattr` and a default.

  • What happens to AdminEmailHandler records when ADMINS is empty?
    `emit()` returns immediately without building or sending anything, unless you subclassed the handler and overrode `send_mail()`. Since `ADMINS` defaults to an empty list, a project that relies on the default configuration gets no error emails until the setting is filled in.
  • Why might a Django site suddenly send hundreds of error emails overnight with no code change?
    Scanners sending requests with forged `Host` headers trigger `DisallowedHost`, logged as an error to `django.security.DisallowedHost`, which propagates to `django` and reaches `mail_admins`. A `NullHandler` on that sub-logger with `propagate: False` silences it without hiding real errors.

saying these in an interview costs you the question

  • AdminEmailHandler falls back to SERVER_EMAIL when ADMINS is empty
  • CallbackFilter drops a record when its callback returns True
  • RequireDebugFalse checks the record's level, not the DEBUG setting
  • Attach mail_admins to django.request and django for full coverage
  • In Django 6.1 email_backend is the preferred way to pick a mailer