ZetCode

Django logging

last modified November 13, 2026 Python 3.15

When something goes wrong in production, the log is often the only witness. Django uses the logging module of the Python standard library and configures it from one setting, LOGGING. In this article we write log messages in views, configure handlers and formatters, tag every message with the identifier of the request, write JSON lines to a rotating file, look at the loggers that Django itself writes to, including the SQL log and the security log, send errors to the administrators by email, and test the output. The examples use Django 6.1 and Python 3.15.

Loggers, handlers, filters, and formatters

The logging module has four kinds of objects. A logger has a dotted name and a level; code calls its methods debug(), info(), warning(), error(), and critical(). A record that passes the level of the logger is given to the handlers of the logger, which send it somewhere: a stream, a file, an email. Each handler has its own level, a formatter that turns the record into text, and optional filters that can reject a record or add data to it. The names form a hierarchy: the logger orders.views is a child of orders, which is a child of the root logger. A logger without a level uses the level of its nearest ancestor, and after its own handlers a record goes to the handlers of the parents, unless propagate is false. The convention is to create a logger in every module with logging.getLogger(__name__), so that the hierarchy follows the packages.

The example project

The project is created with uv; the steps are explained in Django introduction. The commands below create a project directory, install Django, generate the project with the configuration package config, start the app orders, and create the folder demos for demo scripts.

$ mkdir logging
$ cd logging
$ uv init --bare
$ uv add django
$ uv run django-admin startproject config .
$ uv run python manage.py startapp orders
$ mkdir demos
$ mkdir logs

The trailing dot in startproject config . creates manage.py and the config package in the current directory instead of one level deeper. A new app is ignored by Django until it is listed in INSTALLED_APPS, so we add the app to the list in config/settings.py; the other entries are generated by startproject.

config/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'orders',
]

The listings in this article are of two kinds. A listing with a solid blue border and the label Project file is a file of the application: models, views, templates, tests, and settings. A listing with a dashed amber border and the label Demo script is a small helper in the demos folder; it sets up Django, runs a few lines of code, and prints the results, so that we can watch the behaviour. Demo scripts are not part of the application; we run one from the project directory with uv run python -m demos.name.

The last command creates the folder logs for the log file; the file handler of the configuration below does not create missing directories. The app orders has a model with a customer and a total.

orders/models.py
from django.db import models


class Order(models.Model):
    customer = models.CharField(max_length=60)
    total = models.DecimalField(max_digits=8, decimal_places=2)

    def __str__(self):
        return f'Order {self.pk} for {self.customer}'

We create and apply the migration, and the script seed.py adds two orders.

$ uv run python manage.py makemigrations orders
No changes detected in app 'orders'
$ uv run python manage.py migrate | tail -n 2
Running migrations:
  No migrations to apply.
demos/seed.py
import os

import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()

from orders.models import Order

Order.objects.all().delete()
Order.objects.create(customer='Alice', total='59.90')
Order.objects.create(customer='Bob', total='12.00')
print('orders:', Order.objects.count())

The script deletes the existing orders first, so it can be run again.

$ uv run python -m demos.seed
orders: 2

Writing log messages

The module orders/views.py creates its logger with __name__, which is orders.views. The messages use the lazy form logger.debug('Looking for order %s', pk): the arguments are formatted only when a handler really emits the record, which is cheaper than an f-string for the messages that are filtered out. The view detail logs a warning when the order is missing and then raises Http404. In the view pay the call logger.exception(), which can be used only in an except block, logs at the level ERROR and appends the traceback; the argument extra attaches additional attributes to the record. The view crash does not catch anything, so Django logs its exception.

orders/views.py
import logging

from django.http import Http404, HttpResponse, JsonResponse

from .models import Order

logger = logging.getLogger(__name__)


class PaymentError(Exception):
    pass


def charge(order):
    raise PaymentError(f'the card of {order.customer} was declined')


def index(request):
    logger.info('The index page was requested')
    return HttpResponse('orders')


def detail(request, pk):
    logger.debug('Looking for order %s', pk)
    try:
        order = Order.objects.get(pk=pk)
    except Order.DoesNotExist:
        logger.warning('Order %s does not exist', pk)
        raise Http404('no such order')
    logger.info('Order %s has the total %s', order.pk, order.total)
    return JsonResponse({'id': order.pk, 'customer': order.customer})


def pay(request, pk):
    order = Order.objects.get(pk=pk)
    try:
        charge(order)
    except PaymentError:
        logger.exception('Payment failed', extra={'order_id': order.pk})
        return HttpResponse('payment failed', status=502)
    return HttpResponse('paid')


def count(request):
    return JsonResponse({'orders': Order.objects.count()})


def crash(request):
    raise RuntimeError('something unexpected')

The URL patterns connect the views with paths, and the project includes them under /orders/.

orders/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('', views.index),
    path('count/', views.count),
    path('crash/', views.crash),
    path('<int:pk>/', views.detail),
    path('<int:pk>/pay/', views.pay),
]

The root URL configuration includes the URLs of the app.

config/urls.py
from django.urls import include, path

urlpatterns = [
    path('orders/', include('orders.urls')),
]

The paths and the names of the views are all that the demo scripts need.

Configuring LOGGING

The file orders/logs.py contains the helpers that the configuration uses. A context variable holds the identifier of the current request, which the middleware RequestIdMiddleware sets from the header X-Request-ID or generates, and removes after the request; it also stores it on the request object. The filter RequestIdFilter adds the identifier to every record as the attribute request_id, so the format string of a formatter can use it. It looks at the request first: Django logs the 4xx and 5xx responses of the django.request logger after the middleware chain has finished, when the context variable is already reset, but it passes the request in the record. The class JsonFormatter writes a record as one line of JSON, including the extra attributes and the last line of the traceback. Finally, StdoutHandler is a stream handler that looks up sys.stdout each time it writes, so that the test runner can capture its output.

orders/logs.py
import json
import logging
import sys
import uuid
from contextvars import ContextVar

request_id_var = ContextVar('request_id', default='-')


class StdoutHandler(logging.StreamHandler):
    # Always writes to the current sys.stdout, which the test runner may replace.
    def __init__(self):
        super().__init__()

    @property
    def stream(self):
        return sys.stdout

    @stream.setter
    def stream(self, value):
        pass


class RequestIdMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        request_id = request.headers.get('X-Request-ID') or uuid.uuid4().hex[:8]
        request.request_id = request_id
        token = request_id_var.set(request_id)
        try:
            response = self.get_response(request)
        finally:
            request_id_var.reset(token)
        response['X-Request-ID'] = request_id
        return response


class RequestIdFilter(logging.Filter):
    def filter(self, record):
        request = getattr(record, 'request', None)
        record.request_id = getattr(request, 'request_id', None) or request_id_var.get()
        return True


class JsonFormatter(logging.Formatter):
    extra_fields = ('order_id', 'status_code')

    def format(self, record):
        data = {
            'time': self.formatTime(record, '%H:%M:%S'),
            'level': record.levelname,
            'logger': record.name,
            'request_id': getattr(record, 'request_id', '-'),
            'message': record.getMessage(),
        }
        for field in self.extra_fields:
            if hasattr(record, field):
                data[field] = getattr(record, field)
        if record.exc_info:
            data['exception'] = self.formatException(record.exc_info).splitlines()[-1]
        return json.dumps(data)

The setting LOGGING is a dictionary in the format of logging.config.dictConfig. Its version is always 1. With disable_existing_loggers set to false the loggers that Django and the libraries have already created stay active; the default true would silence them. The dictionary has the sections filters, formatters, handlers, and loggers, which refer to each other by name, and the root logger. The key '()' names a class or a factory to create the object. The middleware is the first item of MIDDLEWARE so that it wraps all the others, and the setting ADMINS lists the recipients of the error emails.

config/settings.py (logging)
MIDDLEWARE = [
    'orders.logs.RequestIdMiddleware',
    'django.middleware.security.SecurityMiddleware',
    ...
]

...

ADMINS = [('Admin', 'admin@example.com')]

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'filters': {
        'request_id': {'()': 'orders.logs.RequestIdFilter'},
        'require_debug_false': {'()': 'django.utils.log.RequireDebugFalse'},
    },
    'formatters': {
        'simple': {
            'format': '{levelname:<7} {name} [{request_id}] {message}',
            'style': '{',
        },
        'json': {'()': 'orders.logs.JsonFormatter'},
    },
    'handlers': {
        'console': {
            'class': 'orders.logs.StdoutHandler',
            'formatter': 'simple',
            'filters': ['request_id'],
        },
        'file': {
            'class': 'logging.handlers.RotatingFileHandler',
            'filename': BASE_DIR / 'logs' / 'app.log',
            'maxBytes': 1_000_000,
            'backupCount': 3,
            'formatter': 'json',
            'filters': ['request_id'],
        },
        'mail_admins': {
            'class': 'django.utils.log.AdminEmailHandler',
            'level': 'ERROR',
            'filters': ['require_debug_false'],
        },
    },
    'loggers': {
        'orders': {
            'handlers': ['console', 'file'],
            'level': 'DEBUG',
            'propagate': False,
        },
        'django.request': {
            'handlers': ['console', 'file', 'mail_admins'],
            'level': 'WARNING',
            'propagate': False,
        },
        'django.security': {
            'handlers': ['console'],
            'level': 'WARNING',
            'propagate': False,
        },
        'django.db.backends': {
            'handlers': ['console'],
            'level': 'INFO',
            'propagate': False,
        },
    },
    'root': {
        'handlers': ['console'],
        'level': 'WARNING',
    },
}

The logger orders sends everything from DEBUG up to the console and to a file, and, with propagate false, not to the root. The logger django.request receives the warnings about 4xx responses and the errors about 5xx responses, and has a third handler, mail_admins, which is limited to the level ERROR and active only when DEBUG is false. The logger django.db.backends is set to INFO, so that the SQL is not logged until we lower the level. The file handler writes to the directory logs; RotatingFileHandler starts a new file when the size reaches one megabyte and keeps three old ones.

The logger hierarchy and the defaults of Django

Django has its own default configuration, DEFAULT_LOGGING, which is merged with ours (that is why disable_existing_loggers must be false). It sends the messages of the logger django at INFO and above to the console if DEBUG is true, and to the administrators by email if DEBUG is false. The logger django.server prints the request lines of runserver. The script prints these defaults and shows, for some of our loggers, the level, the effective level inherited from the ancestors, the flag propagate, and the handlers.

demos/config.py
import os

import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()

import logging

from django.utils.log import DEFAULT_LOGGING

print('Django default LOGGING:')
print('  handlers:', sorted(DEFAULT_LOGGING['handlers']))
for name, options in DEFAULT_LOGGING['loggers'].items():
    print(f'  logger {name!r}: handlers={options["handlers"]}, level={options.get("level", "-")}')
print()

for name in ('orders.views', 'orders', 'django.request', 'django.db.backends', 'django.template'):
    logger = logging.getLogger(name)
    handlers = [type(handler).__name__ for handler in logger.handlers]
    print(f'{name:<20} level={logging.getLevelName(logger.level):<8} '
          f'effective={logging.getLevelName(logger.getEffectiveLevel()):<8} '
          f'propagate={logger.propagate!s:<5} handlers={handlers}')

The logger orders.views has no level and no handlers of its own; it inherits DEBUG from orders and passes its records to the handlers of that logger. The logger django.template inherits INFO from django and so reports nothing at DEBUG.

$ uv run python -m demos.config
Django default LOGGING:
  handlers: ['console', 'django.server', 'mail_admins']
  logger 'django': handlers=['console', 'mail_admins'], level=INFO
  logger 'django.server': handlers=['django.server'], level=INFO

orders.views         level=NOTSET   effective=DEBUG    propagate=True  handlers=[]
orders               level=DEBUG    effective=DEBUG    propagate=False handlers=['StdoutHandler', 'RotatingFileHandler']
django.request       level=WARNING  effective=WARNING  propagate=False handlers=['StdoutHandler', 'RotatingFileHandler', 'AdminEmailHandler']
django.db.backends   level=INFO     effective=INFO     propagate=False handlers=['StdoutHandler']
django.template      level=NOTSET   effective=INFO     propagate=True  handlers=[]

Logging requests and errors

The script requests five URLs with the test client and gives each request its own identifier. The console handler writes to the standard output with the format {levelname} {name} [{request_id}] {message}.

demos/requests.py
import os

import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()

from django.test import Client
from django.test.utils import setup_test_environment

setup_test_environment()

from django.test import Client

client = Client(raise_request_exception=False)
for number, path in enumerate(['/orders/', '/orders/1/', '/orders/99/', '/orders/1/pay/', '/orders/crash/'], 1):
    print(f'--- GET {path}')
    response = client.get(path, headers={'X-Request-ID': f'req-{number:04d}'})
    print(f'--- {response.status_code}')

The index request writes one INFO message. The view detail writes DEBUG and INFO messages, and for a missing order a WARNING followed by the WARNING of django.request about the 404 response; both carry the same request identifier, so the lines of a request can be found with a search. The failed payment writes our ERROR with the traceback, and then django.request reports the status 502. The last request shows what Django does with an unhandled exception: it logs the ERROR with the traceback and returns the response with the status 500. Django logs the responses with the status 400-499 as WARNING and 500-599 as ERROR.

$ uv run python -m demos.requests
--- GET /orders/
INFO    orders.views [req-0001] The index page was requested
--- 200
--- GET /orders/1/
DEBUG   orders.views [req-0002] Looking for order 1
INFO    orders.views [req-0002] Order 1 has the total 59.90
--- 200
--- GET /orders/99/
DEBUG   orders.views [req-0003] Looking for order 99
WARNING orders.views [req-0003] Order 99 does not exist
WARNING django.request [req-0003] Not Found: /orders/99/
--- 404
--- GET /orders/1/pay/
ERROR   orders.views [req-0004] Payment failed
Traceback (most recent call last):
  File "./orders/views.py", line 37, in pay
    charge(order)
    ~~~~~~^^^^^^^
  File "./orders/views.py", line 15, in charge
    raise PaymentError(f'the card of {order.customer} was declined')
orders.views.PaymentError: the card of Alice was declined
ERROR   django.request [req-0004] Bad Gateway: /orders/1/pay/
--- 502
--- GET /orders/crash/
ERROR   django.request [req-0005] Internal Server Error: /orders/crash/
Traceback (most recent call last):
  File "./.venv/lib/python3.15/site-packages/django/core/handlers/exception.py", line 56, in inner
    response = get_response(request)
  File "./.venv/lib/python3.15/site-packages/django/core/handlers/base.py", line 199, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "./orders/views.py", line 49, in crash
    raise RuntimeError('something unexpected')
RuntimeError: something unexpected
--- 500

JSON logs in a rotating file

Humans read the console output; log collectors prefer a structured format. The handler file uses JsonFormatter, so each record is one JSON object on a line. The script empties the file, makes three requests, and prints the file.

demos/files.py
import os

import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()

from django.test import Client
from django.test.utils import setup_test_environment

setup_test_environment()

import logging
import os

from django.conf import settings
from django.test import Client

log_file = settings.BASE_DIR / 'logs' / 'app.log'
if log_file.exists():
    os.truncate(log_file, 0)

client = Client(raise_request_exception=False)
for number, path in enumerate(['/orders/1/', '/orders/99/', '/orders/2/pay/'], 1):
    client.get(path, headers={'X-Request-ID': f'req-{number:04d}'})
for handler in logging.getLogger('orders').handlers:
    handler.flush()

print(f'{log_file.name}: {log_file.stat().st_size} bytes')
for line in log_file.read_text().splitlines():
    print(line)

The lines contain the time, the level, the logger, the request identifier, and the message; the extra data order_id and the status_code of django.request are fields. The exception is reduced to its last line to keep the line short; a real system might keep the whole traceback in a field. When the file reaches maxBytes it is renamed to app.log.1, the older ones are shifted, and the oldest is deleted. Rotation by time is done by TimedRotatingFileHandler. With several processes writing to one file a rotating handler is unsafe, and it is better to log to the standard output and let the platform collect it.

$ uv run python -m demos.files
DEBUG   orders.views [req-0001] Looking for order 1
INFO    orders.views [req-0001] Order 1 has the total 59.90
DEBUG   orders.views [req-0002] Looking for order 99
WARNING orders.views [req-0002] Order 99 does not exist
WARNING django.request [req-0002] Not Found: /orders/99/
ERROR   orders.views [req-0003] Payment failed
Traceback (most recent call last):
  File "./orders/views.py", line 37, in pay
    charge(order)
    ~~~~~~^^^^^^^
  File "./orders/views.py", line 15, in charge
    raise PaymentError(f'the card of {order.customer} was declined')
orders.views.PaymentError: the card of Bob was declined
ERROR   django.request [req-0003] Bad Gateway: /orders/2/pay/
app.log: 1028 bytes
{"time": "20:54:14", "level": "DEBUG", "logger": "orders.views", "request_id": "req-0001", "message": "Looking for order 1"}
{"time": "20:54:14", "level": "INFO", "logger": "orders.views", "request_id": "req-0001", "message": "Order 1 has the total 59.90"}
{"time": "20:54:14", "level": "DEBUG", "logger": "orders.views", "request_id": "req-0002", "message": "Looking for order 99"}
{"time": "20:54:14", "level": "WARNING", "logger": "orders.views", "request_id": "req-0002", "message": "Order 99 does not exist"}
{"time": "20:54:14", "level": "WARNING", "logger": "django.request", "request_id": "req-0002", "message": "Not Found: /orders/99/", "status_code": 404}
{"time": "20:54:14", "level": "ERROR", "logger": "orders.views", "request_id": "req-0003", "message": "Payment failed", "order_id": 2, "exception": "orders.views.PaymentError: the card of Bob was declined"}
{"time": "20:54:14", "level": "ERROR", "logger": "django.request", "request_id": "req-0003", "message": "Bad Gateway: /orders/2/pay/", "status_code": 502}

SQL queries

The logger django.db.backends logs every SQL statement with its duration at the level DEBUG, but only when DEBUG is true. Our configuration keeps it at INFO. The script lowers the level at run time and silences our own logger, so only the SQL is left. In a project one can set the level in LOGGING to see all queries during development, which is a convenient way to spot N+1 problems (see Django query performance).

demos/sql.py
import os

import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()

from django.test import Client
from django.test.utils import setup_test_environment

setup_test_environment()

import logging

from django.test import Client

logging.getLogger('django.db.backends').setLevel(logging.DEBUG)
logging.getLogger('orders').setLevel(logging.WARNING)

response = Client().get('/orders/count/', headers={'X-Request-ID': 'req-sql'})
print(response.content.decode())

The single query of the view is logged with its time in seconds, the parameters, and the alias of the database, and the line carries the identifier of the request.

$ uv run python -m demos.sql
DEBUG   django.db.backends [req-sql] (0.000) SELECT COUNT(*) AS "__count" FROM "orders_order"; args=(); alias=default
{"orders": 2}

Errors by email and security events

The handler AdminEmailHandler sends a record by email to the addresses in ADMINS. It is attached to django.request at the level ERROR, so a 500 error produces an email, while a 404 does not. The filter RequireDebugFalse keeps the handler quiet during development. The script sets DEBUG to false for the requests; the test environment collects the emails in mail.outbox. Sending is done by the mail system configured in the project (see Django email).

demos/mail.py
import os

import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()

from django.test import Client
from django.test.utils import setup_test_environment

setup_test_environment()

import logging

from django.core import mail
from django.test import Client, override_settings

logging.getLogger('orders').handlers = [logging.NullHandler()]
logging.getLogger('django.request').handlers = [
    handler for handler in logging.getLogger('django.request').handlers
    if type(handler).__name__ == 'AdminEmailHandler'
]

with override_settings(DEBUG=False, ALLOWED_HOSTS=['testserver']):
    Client(raise_request_exception=False).get('/orders/crash/')
    Client(raise_request_exception=False).get('/orders/99/')

print('emails sent:', len(mail.outbox))
message = mail.outbox[0]
print('subject:', message.subject)
print('to     :', message.to)
print('body   :')
for line in message.body.splitlines()[:6]:
    print('  ', line)

One email was sent, for the error 500. The subject contains the level, the kind of the IP address, and the message, and the body has the exception, the request data, and the traceback.

$ uv run python -m demos.mail
emails sent: 1
subject: [Django] ERROR (EXTERNAL IP): Internal Server Error: /orders/crash/
to     : ['admin@example.com']
body   :
   Internal Server Error: /orders/crash/
   
   RuntimeError at /orders/crash/
   something unexpected
   
   Request Method: GET

The loggers below django.security report suspicious requests, such as a forbidden host name, a bad CSRF token, or a tampered session. A request with an unknown Host header is refused with the status 400 and logged in django.security.DisallowedHost. These messages are worth a separate handler or alert in production. The script replaces the handlers of the logger with a small one that collects the records.

demos/security.py
import os

import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()

from django.test import Client
from django.test.utils import setup_test_environment

setup_test_environment()

import logging

from django.test import Client

records = []


class Collect(logging.Handler):
    def emit(self, record):
        records.append(record)


logging.getLogger('django.security').handlers = [Collect()]

response = Client().get('/orders/', headers={'Host': 'evil.example'})
print('status:', response.status_code)
for record in records:
    print(f'{record.name} {record.levelname}: {record.getMessage()}')

The request was rejected and the record was written by the child logger of django.security.

$ uv run python -m demos.security
status: 400
django.security.DisallowedHost ERROR: Invalid HTTP_HOST header: 'evil.example'. You may need to add 'evil.example' to ALLOWED_HOSTS.

Testing logging

The method assertLogs(logger, level) of TestCase collects the records of a logger in a context manager and fails if there are none; assertNoLogs() asserts the opposite. The result has the formatted lines in output and the records in records, from which one can read the extra attributes, such as order_id and the status_code of the request logger. The tests are run with the option --buffer, which hides the output of the passing tests, here the log lines of the console handler, and shows the output of a test that fails.

orders/tests.py
import logging

from django.test import TestCase

from .models import Order


class LoggingTests(TestCase):

    @classmethod
    def setUpTestData(cls):
        cls.order = Order.objects.create(customer='Alice', total='59.90')

    def test_info_message(self):
        with self.assertLogs('orders.views', level='INFO') as captured:
            self.client.get(f'/orders/{self.order.pk}/')
        self.assertEqual(captured.output,
                         [f'INFO:orders.views:Order {self.order.pk} has the total 59.90'])

    def test_warning_for_a_missing_order(self):
        with self.assertLogs('orders.views', level='WARNING') as captured:
            response = self.client.get('/orders/999/')
        self.assertEqual(response.status_code, 404)
        self.assertEqual(captured.records[0].getMessage(), 'Order 999 does not exist')

    def test_exception_is_logged_with_extra_data(self):
        with self.assertLogs('orders.views', level='ERROR') as captured:
            response = self.client.get(f'/orders/{self.order.pk}/pay/')
        self.assertEqual(response.status_code, 502)
        record = captured.records[0]
        self.assertEqual(record.order_id, self.order.pk)
        self.assertEqual(record.exc_info[0].__name__, 'PaymentError')

    def test_request_logger_records_the_status(self):
        with self.assertLogs('django.request', level='WARNING') as captured:
            self.client.get('/orders/999/')
        self.assertEqual(captured.records[0].status_code, 404)

    def test_nothing_is_logged_for_the_index_at_warning_level(self):
        with self.assertNoLogs('orders', level='WARNING'):
            self.client.get('/orders/')

    def test_request_id_header(self):
        response = self.client.get('/orders/', headers={'X-Request-ID': 'abc'})
        self.assertEqual(response['X-Request-ID'], 'abc')

All six tests pass.

$ uv run python manage.py test --buffer
Found 6 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
......
----------------------------------------------------------------------
Ran 6 tests in 0.016s

OK
Destroying test database for alias 'default'...

Source

Django documentation: Logging

Python documentation: Configuration dictionary schema

Python documentation: Logging Cookbook

In this article we have written log messages in views, configured handlers, formatters, and filters with LOGGING, added the request identifier to the records, written JSON lines to a rotating file, looked at the loggers django.request, django.db.backends, and django.security, sent errors to the administrators, and tested the output. Related articles are Django middleware, Django email, and Django query performance.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Django tutorials.