Django error logs

The web framework for perfectionists with deadlines.

Logging¶

Python programmers will often use print() in their code as a quick and
convenient debugging tool. Using the logging framework is only a little more
effort than that, but it’s much more elegant and flexible. As well as being
useful for debugging, logging can also provide you with more — and better
structured — information about the state and health of your application.

Overview¶

Django uses and extends Python’s builtin logging module to perform
system logging. This module is discussed in detail in Python’s own
documentation; this section provides a quick overview.

The cast of players¶

A Python logging configuration consists of four parts:

  • Loggers
  • Handlers
  • Filters
  • Formatters

Loggers¶

A logger is the entry point into the logging system. Each logger is a named
bucket to which messages can be written for processing.

A logger is configured to have a log level. This log level describes
the severity of the messages that the logger will handle. Python
defines the following log levels:

  • DEBUG: Low level system information for debugging purposes
  • INFO: General system information
  • WARNING: Information describing a minor problem that has
    occurred.
  • ERROR: Information describing a major problem that has
    occurred.
  • CRITICAL: Information describing a critical problem that has
    occurred.

Each message that is written to the logger is a Log Record. Each log
record also has a log level indicating the severity of that specific
message. A log record can also contain useful metadata that describes
the event that is being logged. This can include details such as a
stack trace or an error code.

When a message is given to the logger, the log level of the message is
compared to the log level of the logger. If the log level of the
message meets or exceeds the log level of the logger itself, the
message will undergo further processing. If it doesn’t, the message
will be ignored.

Once a logger has determined that a message needs to be processed,
it is passed to a Handler.

Handlers¶

The handler is the engine that determines what happens to each message
in a logger. It describes a particular logging behavior, such as
writing a message to the screen, to a file, or to a network socket.

Like loggers, handlers also have a log level. If the log level of a
log record doesn’t meet or exceed the level of the handler, the
handler will ignore the message.

A logger can have multiple handlers, and each handler can have a
different log level. In this way, it is possible to provide different
forms of notification depending on the importance of a message. For
example, you could install one handler that forwards ERROR and
CRITICAL messages to a paging service, while a second handler
logs all messages (including ERROR and CRITICAL messages) to a
file for later analysis.

Filters¶

A filter is used to provide additional control over which log records
are passed from logger to handler.

By default, any log message that meets log level requirements will be
handled. However, by installing a filter, you can place additional
criteria on the logging process. For example, you could install a
filter that only allows ERROR messages from a particular source to
be emitted.

Filters can also be used to modify the logging record prior to being
emitted. For example, you could write a filter that downgrades
ERROR log records to WARNING records if a particular set of
criteria are met.

Filters can be installed on loggers or on handlers; multiple filters
can be used in a chain to perform multiple filtering actions.

Formatters¶

Ultimately, a log record needs to be rendered as text. Formatters
describe the exact format of that text. A formatter usually consists
of a Python formatting string containing
LogRecord attributes; however,
you can also write custom formatters to implement specific formatting behavior.

Security implications¶

The logging system handles potentially sensitive information. For example, the
log record may contain information about a web request or a stack trace, while
some of the data you collect in your own loggers may also have security
implications. You need to be sure you know:

  • what information is collected
  • where it will subsequently be stored
  • how it will be transferred
  • who might have access to it.

To help control the collection of sensitive information, you can explicitly
designate certain sensitive information to be filtered out of error reports –
read more about how to filter error reports.

AdminEmailHandler

The built-in AdminEmailHandler deserves a mention in
the context of security. If its include_html option is enabled, the email
message it sends will contain a full traceback, with names and values of local
variables at each level of the stack, plus the values of your Django settings
(in other words, the same level of detail that is exposed in a web page when
DEBUG is True).

It’s generally not considered a good idea to send such potentially sensitive
information over email. Consider instead using one of the many third-party
services to which detailed logs can be sent to get the best of multiple worlds
– the rich information of full tracebacks, clear management of who is notified
and has access to the information, and so on.

Configuring logging¶

Python’s logging library provides several techniques to configure
logging, ranging from a programmatic interface to configuration files.
By default, Django uses the dictConfig format.

In order to configure logging, you use LOGGING to define a
dictionary of logging settings. These settings describe the loggers,
handlers, filters and formatters that you want in your logging setup,
and the log levels and other properties that you want those components
to have.

By default, the LOGGING setting is merged with Django’s
default logging configuration
using the
following scheme.

If the disable_existing_loggers key in the LOGGING dictConfig is
set to True (which is the dictConfig default if the key is missing)
then all loggers from the default configuration will be disabled. Disabled
loggers are not the same as removed; the logger will still exist, but will
silently discard anything logged to it, not even propagating entries to a
parent logger. Thus you should be very careful using
'disable_existing_loggers': True; it’s probably not what you want. Instead,
you can set disable_existing_loggers to False and redefine some or all
of the default loggers; or you can set LOGGING_CONFIG to None
and handle logging config yourself.

Logging is configured as part of the general Django setup() function.
Therefore, you can be certain that loggers are always ready for use in your
project code.

Examples¶

The full documentation for dictConfig format
is the best source of information about logging configuration dictionaries.
However, to give you a taste of what is possible, here are several examples.

To begin, here’s a small configuration that will allow you to output all log
messages to the console:

settings.py

import os

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
        },
    },
    'root': {
        'handlers': ['console'],
        'level': 'WARNING',
    },
}

This configures the parent root logger to send messages with the
WARNING level and higher to the console handler. By adjusting the level to
INFO or DEBUG you can display more messages. This may be useful during
development.

Next we can add more fine-grained logging. Here’s an example of how to make the
logging system print more messages from just the django named
logger:

settings.py

import os

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
        },
    },
    'root': {
        'handlers': ['console'],
        'level': 'WARNING',
    },
    'loggers': {
        'django': {
            'handlers': ['console'],
            'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'),
            'propagate': False,
        },
    },
}

By default, this config sends messages from the django logger of level
INFO or higher to the console. This is the same level as Django’s default
logging config, except that the default config only displays log records when
DEBUG=True. Django does not log many such INFO level messages. With
this config, however, you can also set the environment variable
DJANGO_LOG_LEVEL=DEBUG to see all of Django’s debug logging which is very
verbose as it includes all database queries.

You don’t have to log to the console. Here’s a configuration which writes all
logging from the django named logger to a local file:

settings.py

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'file': {
            'level': 'DEBUG',
            'class': 'logging.FileHandler',
            'filename': '/path/to/django/debug.log',
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'DEBUG',
            'propagate': True,
        },
    },
}

If you use this example, be sure to change the 'filename' path to a
location that’s writable by the user that’s running the Django application.

Finally, here’s an example of a fairly complex logging setup:

settings.py

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'verbose': {
            'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
            'style': '{',
        },
        'simple': {
            'format': '{levelname} {message}',
            'style': '{',
        },
    },
    'filters': {
        'special': {
            '()': 'project.logging.SpecialFilter',
            'foo': 'bar',
        },
        'require_debug_true': {
            '()': 'django.utils.log.RequireDebugTrue',
        },
    },
    'handlers': {
        'console': {
            'level': 'INFO',
            'filters': ['require_debug_true'],
            'class': 'logging.StreamHandler',
            'formatter': 'simple'
        },
        'mail_admins': {
            'level': 'ERROR',
            'class': 'django.utils.log.AdminEmailHandler',
            'filters': ['special']
        }
    },
    'loggers': {
        'django': {
            'handlers': ['console'],
            'propagate': True,
        },
        'django.request': {
            'handlers': ['mail_admins'],
            'level': 'ERROR',
            'propagate': False,
        },
        'myproject.custom': {
            'handlers': ['console', 'mail_admins'],
            'level': 'INFO',
            'filters': ['special']
        }
    }
}

This logging configuration does the following things:

  • Identifies the configuration as being in ‘dictConfig version 1’
    format. At present, this is the only dictConfig format version.

  • Defines two formatters:

    • simple, that outputs the log level name (e.g., DEBUG) and the log
      message.

      The format string is a normal Python formatting string
      describing the details that are to be output on each logging
      line. The full list of detail that can be output can be
      found in Formatter Objects.

    • verbose, that outputs the log level name, the log
      message, plus the time, process, thread and module that
      generate the log message.

  • Defines two filters:

    • project.logging.SpecialFilter, using the alias special. If this
      filter required additional arguments, they can be provided as additional
      keys in the filter configuration dictionary. In this case, the argument
      foo will be given a value of bar when instantiating
      SpecialFilter.
    • django.utils.log.RequireDebugTrue, which passes on records when
      DEBUG is True.
  • Defines two handlers:

    • console, a StreamHandler, which prints any INFO
      (or higher) message to sys.stderr. This handler uses the simple
      output format.
    • mail_admins, an AdminEmailHandler, which
      emails any ERROR (or higher) message to the site ADMINS.
      This handler uses the special filter.
  • Configures three loggers:

    • django, which passes all messages to the console handler.
    • django.request, which passes all ERROR messages to
      the mail_admins handler. In addition, this logger is
      marked to not propagate messages. This means that log
      messages written to django.request will not be handled
      by the django logger.
    • myproject.custom, which passes all messages at INFO
      or higher that also pass the special filter to two
      handlers – the console, and mail_admins. This
      means that all INFO level messages (or higher) will be
      printed to the console; ERROR and CRITICAL
      messages will also be output via email.

Custom logging configuration¶

If you don’t want to use Python’s dictConfig format to configure your
logger, you can specify your own configuration scheme.

The LOGGING_CONFIG setting defines the callable that will
be used to configure Django’s loggers. By default, it points at
Python’s logging.config.dictConfig() function. However, if you want to
use a different configuration process, you can use any other callable
that takes a single argument. The contents of LOGGING will
be provided as the value of that argument when logging is configured.

Disabling logging configuration¶

If you don’t want to configure logging at all (or you want to manually
configure logging using your own approach), you can set
LOGGING_CONFIG to None. This will disable the
configuration process for Django’s default logging.

Setting LOGGING_CONFIG to None only means that the automatic
configuration process is disabled, not logging itself. If you disable the
configuration process, Django will still make logging calls, falling back to
whatever default logging behavior is defined.

Here’s an example that disables Django’s logging configuration and then
manually configures logging:

settings.py

LOGGING_CONFIG = None

import logging.config
logging.config.dictConfig(...)

Note that the default configuration process only calls
LOGGING_CONFIG once settings are fully-loaded. In contrast, manually
configuring the logging in your settings file will load your logging config
immediately. As such, your logging config must appear after any settings on
which it depends.

  • Getting Help

  • el

  • es

  • fr

  • id

  • it

  • ja

  • ko

  • pl

  • pt-br

  • zh-hans

  • Language: en
  • 4.0

  • 4.2

  • dev

  • Documentation version:
    4.1

Logging¶

Django’s logging module extends Python’s builtin logging.

Logging is configured as part of the general Django django.setup()
function, so it’s always available unless explicitly disabled.

Django’s default logging configuration¶

By default, Django uses Python’s logging.config.dictConfig format.

Default logging conditions¶

The full set of default logging conditions are:

When DEBUG is True:

  • The django logger sends messages in the django hierarchy (except
    django.server) at the INFO level or higher to the console.

When DEBUG is False:

  • The django logger sends messages in the django hierarchy (except
    django.server) with ERROR or CRITICAL level to
    AdminEmailHandler.

Independently of the value of DEBUG:

  • The django.server logger sends messages at the INFO level
    or higher to the console.

All loggers except django.server propagate logging to their
parents, up to the root django logger. The console and mail_admins
handlers are attached to the root logger to provide the behavior described
above.

Python’s own defaults send records of level WARNING and higher
to the console.

Default logging definition¶

Django’s default logging configuration inherits Python’s defaults. It’s
available as django.utils.log.DEFAULT_LOGGING and defined in
django/utils/log.py:

{
    'version': 1,
    'disable_existing_loggers': False,
    'filters': {
        'require_debug_false': {
            '()': 'django.utils.log.RequireDebugFalse',
        },
        'require_debug_true': {
            '()': 'django.utils.log.RequireDebugTrue',
        },
    },
    'formatters': {
        'django.server': {
            '()': 'django.utils.log.ServerFormatter',
            'format': '[{server_time}] {message}',
            'style': '{',
        }
    },
    'handlers': {
        'console': {
            'level': 'INFO',
            'filters': ['require_debug_true'],
            'class': 'logging.StreamHandler',
        },
        'django.server': {
            'level': 'INFO',
            'class': 'logging.StreamHandler',
            'formatter': 'django.server',
        },
        'mail_admins': {
            'level': 'ERROR',
            'filters': ['require_debug_false'],
            'class': 'django.utils.log.AdminEmailHandler'
        }
    },
    'loggers': {
        'django': {
            'handlers': ['console', 'mail_admins'],
            'level': 'INFO',
        },
        'django.server': {
            'handlers': ['django.server'],
            'level': 'INFO',
            'propagate': False,
        },
    }
}

See Configuring logging on how to complement or replace this default
logging configuration.

Django logging extensions¶

Django provides a number of utilities to handle the particular requirements of
logging in a web server environment.

Loggers¶

Django provides several built-in loggers.

django

The parent logger for messages in the django named logger hierarchy. Django does not post messages using this name.
Instead, it uses one of the loggers below.

django.request

Log messages related to the handling of requests. 5XX responses are
raised as ERROR messages; 4XX responses are raised as WARNING
messages. Requests that are logged to the django.security logger aren’t
logged to django.request.

Messages to this logger have the following extra context:

  • status_code: The HTTP response code associated with the request.
  • request: The request object that generated the logging message.

django.server

Log messages related to the handling of requests received by the server invoked
by the runserver command. HTTP 5XX responses are logged as ERROR
messages, 4XX responses are logged as WARNING messages, and everything else
is logged as INFO.

Messages to this logger have the following extra context:

  • status_code: The HTTP response code associated with the request.
  • request: The request object that generated the logging message.

django.template

Log messages related to the rendering of templates.

  • Missing context variables are logged as DEBUG messages.

django.db.backends

Messages relating to the interaction of code with the database. For example,
every application-level SQL statement executed by a request is logged at the
DEBUG level to this logger.

Messages to this logger have the following extra context:

  • duration: The time taken to execute the SQL statement.
  • sql: The SQL statement that was executed.
  • params: The parameters that were used in the SQL call.
  • alias: The alias of the database used in the SQL call.

For performance reasons, SQL logging is only enabled when
settings.DEBUG is set to True, regardless of the logging
level or handlers that are installed.

This logging does not include framework-level initialization (e.g.
SET TIMEZONE) or transaction management queries (e.g. BEGIN,
COMMIT, and ROLLBACK). Turn on query logging in your database if you
wish to view all database queries.

Changed in Django 4.0:

The database alias was added to log messages.

django.security.*

The security loggers will receive messages on any occurrence of
SuspiciousOperation and other security-related
errors. There is a sub-logger for each subtype of security error, including all
SuspiciousOperations. The level of the log event depends on where the
exception is handled. Most occurrences are logged as a warning, while
any SuspiciousOperation that reaches the WSGI handler will be logged as an
error. For example, when an HTTP Host header is included in a request from
a client that does not match ALLOWED_HOSTS, Django will return a 400
response, and an error message will be logged to the
django.security.DisallowedHost logger.

These log events will reach the django logger by default, which mails error
events to admins when DEBUG=False. Requests resulting in a 400 response due
to a SuspiciousOperation will not be logged to the django.request
logger, but only to the django.security logger.

To silence a particular type of SuspiciousOperation, you can override that
specific logger following this example:

'handlers': {
    'null': {
        'class': 'logging.NullHandler',
    },
},
'loggers': {
    'django.security.DisallowedHost': {
        'handlers': ['null'],
        'propagate': False,
    },
},

Other django.security loggers not based on SuspiciousOperation are:

  • django.security.csrf: For CSRF failures.

django.db.backends.schema

Logs the SQL queries that are executed during schema changes to the database by
the migrations framework. Note that it won’t log the
queries executed by RunPython.
Messages to this logger have params and sql in their extra context (but
unlike django.db.backends, not duration). The values have the same meaning
as explained in django.db.backends.

Handlers¶

Django provides one log handler in addition to those provided by the
Python logging module
.

class AdminEmailHandler(include_html=False, email_backend=None, reporter_class=None

This handler sends an email to the site ADMINS for each log
message it receives.

If the log record contains a request attribute, the full details
of the request will be included in the email. The email subject will
include the phrase “internal IP” if the client’s IP address is in the
INTERNAL_IPS setting; if not, it will include “EXTERNAL IP”.

If the log record contains stack trace information, that stack
trace will be included in the email.

The include_html argument of AdminEmailHandler is used to
control whether the traceback email includes an HTML attachment
containing the full content of the debug web page that would have been
produced if DEBUG were True. To set this value in your
configuration, include it in the handler definition for
django.utils.log.AdminEmailHandler, like this:

'handlers': {
    'mail_admins': {
        'level': 'ERROR',
        'class': 'django.utils.log.AdminEmailHandler',
        'include_html': True,
    },
},

Be aware of the security implications of logging when using the AdminEmailHandler.

By setting the email_backend argument of AdminEmailHandler, the
email backend that is being used by the
handler can be overridden, like this:

'handlers': {
    'mail_admins': {
        'level': 'ERROR',
        'class': 'django.utils.log.AdminEmailHandler',
        'email_backend': 'django.core.mail.backends.filebased.EmailBackend',
    },
},

By default, an instance of the email backend specified in
EMAIL_BACKEND will be used.

The reporter_class argument of AdminEmailHandler allows providing
an django.views.debug.ExceptionReporter subclass to customize the
traceback text sent in the email body. You provide a string import path to
the class you wish to use, like this:

'handlers': {
    'mail_admins': {
        'level': 'ERROR',
        'class': 'django.utils.log.AdminEmailHandler',
        'include_html': True,
        'reporter_class': 'somepackage.error_reporter.CustomErrorReporter',
    },
},
send_mail(subject, message, *args, **kwargs

Sends emails to admin users. To customize this behavior, you can
subclass the AdminEmailHandler class and
override this method.

Filters¶

Django provides some log filters in addition to those provided by the Python
logging module.

class CallbackFilter(callback

This filter accepts a callback function (which should accept a single
argument, the record to be logged), and calls it for each record that
passes through the filter. Handling of that record will not proceed if the
callback returns False.

For instance, to filter out UnreadablePostError
(raised when a user cancels an upload) from the admin emails, you would
create a filter function:

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

and then add it to your logging config:

'filters': {
    'skip_unreadable_posts': {
        '()': 'django.utils.log.CallbackFilter',
        'callback': skip_unreadable_post,
    },
},
'handlers': {
    'mail_admins': {
        'level': 'ERROR',
        'filters': ['skip_unreadable_posts'],
        'class': 'django.utils.log.AdminEmailHandler',
    },
},
class RequireDebugFalse

This filter will only pass on records when settings.DEBUG is False.

This filter is used as follows in the default LOGGING
configuration to ensure that the AdminEmailHandler only sends
error emails to admins when DEBUG is False:

'filters': {
    'require_debug_false': {
        '()': 'django.utils.log.RequireDebugFalse',
    },
},
'handlers': {
    'mail_admins': {
        'level': 'ERROR',
        'filters': ['require_debug_false'],
        'class': 'django.utils.log.AdminEmailHandler',
    },
},
class RequireDebugTrue

This filter is similar to RequireDebugFalse, except that records are
passed only when DEBUG is True.

Back to Top

Some time has passed since EMP’s most helpful code submission. I just now implemented it, and while thrashing around with some manage.py option, to try to chase down a bug, I got a deprecation warning to the effect that with my current version of Django (1.5.?) a require_debug_false filter is now needed for the mail_admins handler.

Here is the revised code:

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'filters': {
         'require_debug_false': {
             '()': 'django.utils.log.RequireDebugFalse'
         }
     },
    'handlers': {
        # Include the default Django email handler for errors
        # This is what you'd get without configuring logging at all.
        'mail_admins': {
            'class': 'django.utils.log.AdminEmailHandler',
            'level': 'ERROR',
            'filters': ['require_debug_false'],
             # But the emails are plain text by default - HTML is nicer
            'include_html': True,
        },
        # Log to a text file that can be rotated by logrotate
        'logfile': {
            'class': 'logging.handlers.WatchedFileHandler',
            'filename': '/home/username/public_html/djangoprojectname/logfilename.log'
        },
    },
    'loggers': {
        # Again, default Django configuration to email unhandled exceptions
        'django.request': {
            'handlers': ['mail_admins'],
            'level': 'ERROR',
            'propagate': True,
        },
        # Might as well log any errors anywhere else in Django
        'django': {
            'handlers': ['logfile'],
            'level': 'ERROR',
            'propagate': False,
        },
        # Your own app - this assumes all your logger names start with "myapp."
        'myapp': {
            'handlers': ['logfile'],
            'level': 'DEBUG', # Or maybe INFO or WARNING
            'propagate': False
        },
    },
}

21 января 2022 г. | Python

Правильное ведение логирования играет решающее значение для процесса отладки и устранения неполадок. Оно не только полезно для локального девелопменга, но и для продашен. При просмотре журналов в поиске проблемы, редко кто-то говорит: «Наше приложение слишком много логирует.» Но чаще можно услышать иное. Итак, принимая это во внимание, давайте начнем.

Введение в логирование на Python

В верхней части каждого файла должно быть что-то подобное:

import logging
logger = logging.getLogger(__name__)

Переменная name будет заменена точечным путём до Python модуля, например, скрипт myproject/myapp/views.py, то будет использоваться myproject.myapp.views. Теперь можно применять logger по всему файлу:

# Простая строка, зарегистрированная на уровне «warning»
logger.warning("Your log message is here")

# Строка с переменной на уровне «info»
logger.info("The value of var is %s", var)

# Ведение журнала трассировки для пойманного исключения
try:
    function_that_might_raise_index_error()
except IndexError:
    # эквивалентно logger.error(msg, exc_info=True)
    logger.exception("Something bad happened")

Примечание: логеры в Python будут обрабатывать вставки переменных в сообщение журнала, если передать их в качестве аргументов в функцию логирования. Если не требуется выводить журнал, замена переменной производиться не будет, что поможет избежать небольшого падения производительности для журнала, который никогда не будет использоваться.

Перепишите свой код с помощью операторов логирования. Краткие рекомендации для выбора уровней журналирования:

  • debug. Информация не нужна для повседневной работы, но полезна в разработке.
  • info: Информация, которая полезна во время обычной работы.
  • warning: Информация, которая может быть проблематичной, но не является срочной.
  • error: Информация, которая важна и, вероятно, требует срочного внимания.
  • critical: на практике используется редко, но если вам нужно больше, чем ошибка, то это то что нужно.

Куда логировать

Ваше приложение не должно беспокоиться о том, куда идут log сообщения. Вместо этого оно должно записывать все в консоль (stdout / stderr), и пусть сервер сам решает, что дальше делать с этой информацией. Обычно они помещаются в выделенный файл, захваченный журналом Systemd или Docker, с последующей отправкой на отдельный сервис, такой как ElasticSearch, или некоторое их сочетание. Хранение файлов журналов – это задача развертывания, а не приложения.

Единственное, что нужно вашему приложению – это формат журналов. Обычно это всего лишь строка с соответствующими данными, но если ваш сервер уже добавляет отметку времени в журнал, то вероятно, нужно будет исключить ее из своего собственного форматирования. Аналогично, если ваш агрегатор журналов может принимать JSON, возможно, более подходящим является форматирование, такое как python-json-logger.

Конфигурирование логирования. Настройка Sentry

Служба отчетов об ошибках играют решающее значение для любого нетривиального сайта. По умолчанию они хранят необработанные исключения, уведомляя о проблеме (только один раз за инцидент) и предоставляют удобный интерфейс для просмотра состояния приложения, когда возникло исключение. Примером такого сервиса может быть Sentry.

Настроим Sentry, отправив любые сообщения службы в журнал, уровня warning или выше. В противном случае они были бы потеряны в море журнальных файлов, которые на практике редко проверяются. Для этого добавим «корневой» журнал, который будет использоваться для всех log сообщений, которые отправляются из любого модуля Python. Пример настройки Django:

import logging.config
LOGGING_CONFIG = None
logging.config.dictConfig({
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'console': {
            # точный формат не важен, это минимальная информация
            'format': '%(asctime)s %(name)-12s %(levelname)-8s %(message)s',
        },
    },
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
            'formatter': 'console',
        },
        # Добавить обработчик для Sentry для `warning` и выше
        'sentry': {
            'level': 'WARNING',
            'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler',
        },
    },
    'loggers': {
    # корневой логгер
        '': {
            'level': 'WARNING',
            'handlers': ['console', 'sentry'],
        },
    },
})

Логирование из приложения

Как правило требуется сведения о предупреждениях и ошибках о сторонних зависимостей, но, как правило, требуется гораздо глубокое понимание кода приложения. В идеале, код целиком живет в одном пространстве имён, чтобы его можно было охватить одним логером. Предположим, что проект использует пространство имен myproject, основываясь на коде выше.

logging.config.dictConfig({
    # ...
    'loggers': {
        '': {
            'level': 'WARNING',
            'handlers': ['console', 'sentry'],
        },
        'myproject': {
            'level': 'INFO',
            'handlers': ['console', 'sentry'],
            # требуется, чтобы избежать двойного ведения журнала с помощью корневого логгера
            'propagate': False,
        },
    },
})

Как быть, если нудно исследовать что-то в своем приложении глубже, чем debug уровень журналированя? Коммит нового кода и его развертывая ощущается излишним. Лучшим вариантом в этом случае воспользоваться переменной окружения среды. Модифицируем предыдущий код, приведя к такому виду:

import os
LOGLEVEL = os.environ.get('LOGLEVEL', 'info').upper()
logging.config.dictConfig({
    # ...
    'loggers': {
        '': {
            'level': 'WARNING',
            'handlers': ['console', 'sentry'],
        },
        'myproject': {
            'level': LOGLEVEL,
            'handlers': ['console', 'sentry'],
            # требуется, чтобы избежать двойного ведения журнала с помощью корневого регистратора
            'propagate': False,
        },
    },
})

Теперь ведение журнала по умолчанию будет info, но может быть легко временно изменено, установив переменную среды LOGLEVEL = debug. В качестве альтернативы, если хранилище журналов не является проблемой, учитывайте возможность ведения журнала на уровне debug. Их достаточно легко отфильтровать с помощью простого grep или с помощью инструмента визуализации журнала, например, Kibana.

Отфильтровывание шума

После того, как установлено и запущено логирование, может появляться информация о некоторых модулях, которые не являются важными, продуцирующими дополнительный шум в журнальные файлы. Для таких модулей можно добавить еще один регистратор в настройки. Первый вариант заключается в том, чтобы вывести их на консоль, но не распространять их на корневой журнал, который отправит их в Sentry:

logging.config.dictConfig({
    # ...
    'loggers': {
        '': {
            'level': 'WARNING',
            'handlers': ['console', 'sentry'],
        },
        'myproject': {
            'level': LOGLEVEL,
            'handlers': ['console', 'sentry'],
            # требуется, чтобы избежать двойного ведения журнала с помощью корневого регистратора
            'propagate': False,
        },
        # Не отправляйте журналы этого модуля в Sentry
        'noisy_module': {
            'level':'ERROR',
            'handlers': ['console'],
            'propagate': False,
        },
    },
})

Если обнаружится, что они слишком шумные, даже для консоли, можно полностью отказаться от них логирования:

logging.config.dictConfig({
    # ...
    'loggers': {
        # ...
        # Не логгировать этот модуль
        'noisy_module': {
            'level': 'NOTSET',
            'propagate': False,
        },
    },
})

Логирование локальный запросов

По умолчанию в Django будет выполняться журналирование запросов с runserver. Переопределяя конфигурацию Django происходит потеря этой возможности. Но её достаточно легко добавить:

from django.utils.log import DEFAULT_LOGGING
logging.config.dictConfig({
    # ...
    'formatters': {
        # ...
        'django.server': DEFAULT_LOGGING['formatters']['django.server'],
    },
    'handlers' {
        # ...
        'django.server': DEFAULT_LOGGING['handlers']['django.server'],
    },
    'loggers': {
        # ...
        'django.server': DEFAULT_LOGGING['loggers']['django.server'],
    },
})

Этот метод немного хрупкий, поскольку зависит от некоторых внутренних компонентов, которые могут меняться между версиями, но поломки легко исправить используя прямое копирование/вставку из исходников Django.

Django logging is one of few basic Django concepts that developers usually neglect but is important to master.

So what exactly is logging?
Logging in Django is the process of storing certain records in some form of container (file, database, memory,…) which then helps us in certain stages of development.

Firstly, we need to understand the concept of logging, its advantages, disadvantages, and why we use it.

A good understanding, writing, and reading of logs can in the future make debugging easier and, among other things, prevent problems that may occur. While it may not seem like it, the key to creating large stable Django applications lies in logging.

What is Django logging?

Django logging module consists of 4 parts:

  • Loggers (interface to log events from the application)
  • Handlers (describes logging behavior, such as writing a message to the screen, to a file, or a network socket)
  • Filters (providing additional control over recording logs)
  • Formatters (provides control over rendered text – custom formats, etc.)

The simplest example of using a logging module is shown in the following example:

Example 1.

import logging

# logger instance with name of module where it’s used (good practice)
logger = logging.getLogger(__name__)

logger.error(“Dummy text”)

And that’s it, the logger instance is defined and ready to use! No need to install additional libraries or modules. Plug and play!

Official Django documentation on logging.

Loggers, Log levels, and when to use one?

Loggers have certain log levels that describe events on our backend, let’s dive into it!

  • DEBUG
    • It would be a good practice to use debug method instead of print (read Logging vs printing section)
  • INFO
    • Used to log some general information as well as capture bottlenecks
  • WARNING
    • Describes problems that are small and potentially dangerous, such as missing CSRF_COOKIE
  • ERROR
    • Used when some exception is raised but not caught
  • CRITICAL
    • Never practically used in Django projects
Level Numeric value
CRITICAL 50
ERROR 40
WARNING 30
INFO 20
DEBUG 10

All Django logging log levels in the implementation receive the same arguments, ie the structure of arguments (args) and keyword arguments (kwargs) is the same for all -> (msg, * args, ** kwargs).

Kwargs can receive 4 arguments:

  • exc_info defaults False, if True it causes exception information to be added to logging message
  • stack_info defaults False, if True logging message contains stack information till logger is called
  • stacklevel defaults 1, if not 1, the exact number of stack frames are skipped when computing the line number and function name
  • extra dictionary in which we can put whatever information we find useful 🙂

Let’s see some basic logging examples:

Example 1.

from django.http import HttpResponse
import logging

logger = logging.getLogger(__name__)


def my_view(request):

    logging.debug("DEBUG")
    logging.info("INFO")
    logging.warning("WARNING")
    logging.error("ERROR")
    logging.critical("CRITICAL")

    return HttpResponse("Django logging example")

We come to an interesting output. WARNING, ERROR, and CRITICAL log levels are recorded but we don’t have DEBUG and INFO in the record.
This happens because the default level logging is WARNING and all “less critical” levels will not be recorded.

We can change this with a simple configuration.

Example 2.

from django.http import HttpResponse
import logging

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)


def my_view(request):
    logging.debug("DEBUG")
    logging.info("INFO")
    logging.warning("WARNING")
    logging.error("ERROR")
    logging.critical("CRITICAL")
    return HttpResponse("Django logging example")
Output:

Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

DEBUG:root:DEBUG
INFO:root:INFO
WARNING:root:WARNING
ERROR:root:ERROR
CRITICAL:root:CRITICAL

[26/Nov/2021 15:35:59] "GET /example/logging/ HTTP/1.1" 200 22

Next example shows arguments and keyword arguments usage:

Example 3.

from django.http import HttpResponse
import logging

logger = logging.getLogger(__name__)

def my_view(request):
    logging.error('Internal server error: %s', request.path,
                  exc_info=True, #default False
                  stack_info=True, #default True
                  stacklevel=1, #default 1
                  extra={
                      "status_code": 500,
                      "request": request
                  }
     )

    return HttpResponse("Django logging example")
Output:

ERROR:root:Internal server error: /example/logging/
NoneType: None
Stack (most recent call last):
	. . .
          File ".../django_logging/venv/lib/python3.8/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File ".../django_logging/example/views.py", line 8, in my_view
    logging.error('Internal server error: %s', request.path,

[26/Nov/2021 16:08:15] "GET /example/logging/ HTTP/1.1" 200 22

exc_info=True outputs NoneType:None because no exception is caught.

stack_info=True produces Stack output (most recent call last)

Documentation on Module-level functions.

Handlers and handler classes

Handlers determine what happens to messages, whether we print them to standard output, to a file, or something else.

Several handler classes are set by default within the logging module, each of which I will briefly describe, and the choice depends on the use case.

Handler Outputs
StreamHandler Streams, any object that supports write() and flush()
FileHandler File
NullHandler None
usage explained: If you don’t want to produce any logs, it’s easier to change handler to NullHandler instead of removing logs
WatchedFileHandler File
BaseRotatingHandler None
Used only for extending (to override methods)
RotatingFileHandler File
When the given size is about to be exceeded, a new file is opened
TimedRotatingFileHandler File
The new file is opened based on interval or when
SockerHandler Network socket
DatagramHandler Logging messages over UDP sockets
SysLogHandler Logging messages to a remote or local Unix Syslog.
NTEventLogHandler Logging messages to a local Windows NT, Windows 2000, or Windows XP event log.
SMTPHandler Logging messages to an email address
MemoryHandler Logging records in memory
HTTPHandler Logging messages to a web server
QueueHandler Logging messages to a queue
QueueListener Not itself a handler, it is documented here because it works hand-in-hand with QueueHandler
TIP - If unsure use RotatingFileHandler class.
WARNING - WatchedFileHandler should not be used on Windows.

Filter overview

Filters exist to give us extra control over handlers and loggers.

To create your filter inherit the Filter class.

Filter class has a filter method that receives a record in arguments. Also, the Filter class must return True if we want it to enter the logs, otherwise, the log is ignored.

Enough theory, let’s show what it looks like in practice!

Example 1.

import logging
from logging import Filter


class CustomFilter(Filter):
    MODULE = ['example.views']

    def filter(self, record: logging.LogRecord) -> bool:
        if record.name in self.MODULE:
            return False
        return True

We defined CustomFilter class in which we inherited Filter, in the CustomFilter class we defined the module from which we do not want to receive logs.

In the filter method, we checked whether record.name (name of the module) is in the defined MODULE list, if located we do not record that log.

Let’s see how we added this filter to the logger instance.

from django.http import HttpResponse
import logging

from core.logging_filters import CustomFilter

logger = logging.getLogger(__name__)
custom_filter = CustomFilter()
logger.addFilter(custom_filter)


def my_view(request):
    logger.error("ERROR")
    return HttpResponse("Django logging example")

Documentation on filters.

Formatter overview

Formatter works similarly to the filter, we initialize our Custom class in which we inherit the Formatter class, we have more methods in it that we can override, I will focus on the most used format method.

Let’s look at the implementation!

Example 1.

from logging import Formatter, LogRecord


class CustomFormatter(Formatter):

    def format(self, record: LogRecord) -> str:
        return "Any type of information you want in your logs"

format method returns a string (what will be written in the log file).

The formatter is not added directly to the logger but the handler, as shown in the example:

from logging.handlers import RotatingFileHandler

from django.http import HttpResponse
import logging

from core.logging_formatters import CustomFormatter

logger = logging.getLogger(__name__)

handler = RotatingFileHandler('example.log', maxBytes=1000, backupCount=2)
custom_formatter = CustomFormatter()
handler.setFormatter(custom_formatter)
logger.addHandler(handler)


def my_view(request):
    logger.error("ERROR")
    return HttpResponse("Django logging example")

Documentation on formatters.

Logging vs printing

Why is it recommended to use a logging library instead of a print?
The goal is to have the most detailed view of what we want to show through the application.
Logging solves it through the log level, in each stage of the code, we can know whether we want to display error, warning, debug, or info.

Additionally, logging can be set to display eg timestamps and other application contexts with a fairly simple configuration. We have more control over the logger, which is always a better thing for a developer.

Print statements are not recorded, unlike logs.

Finally, depending on the webserver, a forgotten print statement may crash your entire server.

  • f-strings don’t work with loggers
  • use ISO-8601 Format for Timestamps
import logging
logging.basicConfig(format='%(asctime)s %(message)s') #could be added as formatter
  • Add logging before the app grows too much 🙂
  • When DEBUG=False and ERROR log level is triggered, email is sent to every email listed ADMINS
  • Don’t import and reuse logger from other modules, define a new logger
    • gives you the ability to turn off and on certain loggers
  • Use log rotation (RotatingFileHandler class) to prevent logs to grow too much
    • the mechanism that is used to prevent full disks
    • works by compressing and deleting old log files
import logging
from logging.handlers import RotatingFileHandler

logger = logging.getLogger(__name__)
handler = RotatingFileHandler('log_file.log', maxBytes=1000, backupCount=5)
logger.addHandler(handler)
  • Use correct log levels!
  • Logutils package ( https://pypi.org/project/logutils/ ) contains useful new and improved handlers

Use Django logging alternatives?

The logging module of the standard pyhon implementation has been tested and brought to perfection.

For each new version of Python, there is a possibility that each “third party module” breaks, and thus the logging in your application does not work.

I would not recommend using anything other than the above module for the pure reason that the alternatives are practically the same.

Don’t invent the wheel if it’s already invented. 🙂

Django projects with logging and analysis tools hide great power – so don’t be afraid to use it to improve your application!

From Get docs

Django/docs/3.0.x/topics/logging

Jump to:navigation, search

Logging

A quick logging primer

Django uses Python’s builtin logging module to perform system logging. The usage of this module is discussed in detail in Python’s own documentation. However, if you’ve never used Python’s logging framework (or even if you have), here’s a quick primer.

The cast of players

A Python logging configuration consists of four parts:

  • Loggers
  • Handlers
  • Filters
  • Formatters

Loggers

A logger is the entry point into the logging system. Each logger is a named bucket to which messages can be written for processing.

A logger is configured to have a log level. This log level describes the severity of the messages that the logger will handle. Python defines the following log levels:

  • DEBUG: Low level system information for debugging purposes
  • INFO: General system information
  • WARNING: Information describing a minor problem that has occurred.
  • ERROR: Information describing a major problem that has occurred.
  • CRITICAL: Information describing a critical problem that has occurred.

Each message that is written to the logger is a Log Record. Each log record also has a log level indicating the severity of that specific message. A log record can also contain useful metadata that describes the event that is being logged. This can include details such as a stack trace or an error code.

When a message is given to the logger, the log level of the message is compared to the log level of the logger. If the log level of the message meets or exceeds the log level of the logger itself, the message will undergo further processing. If it doesn’t, the message will be ignored.

Once a logger has determined that a message needs to be processed, it is passed to a Handler.

Handlers

The handler is the engine that determines what happens to each message in a logger. It describes a particular logging behavior, such as writing a message to the screen, to a file, or to a network socket.

Like loggers, handlers also have a log level. If the log level of a log record doesn’t meet or exceed the level of the handler, the handler will ignore the message.

A logger can have multiple handlers, and each handler can have a different log level. In this way, it is possible to provide different forms of notification depending on the importance of a message. For example, you could install one handler that forwards ERROR and CRITICAL messages to a paging service, while a second handler logs all messages (including ERROR and CRITICAL messages) to a file for later analysis.

Filters

A filter is used to provide additional control over which log records are passed from logger to handler.

By default, any log message that meets log level requirements will be handled. However, by installing a filter, you can place additional criteria on the logging process. For example, you could install a filter that only allows ERROR messages from a particular source to be emitted.

Filters can also be used to modify the logging record prior to being emitted. For example, you could write a filter that downgrades ERROR log records to WARNING records if a particular set of criteria are met.

Filters can be installed on loggers or on handlers; multiple filters can be used in a chain to perform multiple filtering actions.

Formatters

Ultimately, a log record needs to be rendered as text. Formatters describe the exact format of that text. A formatter usually consists of a Python formatting string containing LogRecord attributes; however, you can also write custom formatters to implement specific formatting behavior.

Using logging

Once you have configured your loggers, handlers, filters and formatters, you need to place logging calls into your code. Using the logging framework works like this:

# import the logging library
import logging

# Get an instance of a logger
logger = logging.getLogger(__name__)

def my_view(request, arg1, arg):
    ...
    if bad_mojo:
        # Log an error message
        logger.error('Something went wrong!')

And that’s it! Every time the bad_mojo condition is activated, an error log record will be written.

Naming loggers

The call to logging.getLogger() obtains (creating, if necessary) an instance of a logger. The logger instance is identified by a name. This name is used to identify the logger for configuration purposes.

By convention, the logger name is usually __name__, the name of the Python module that contains the logger. This allows you to filter and handle logging calls on a per-module basis. However, if you have some other way of organizing your logging messages, you can provide any dot-separated name to identify your logger:

# Get an instance of a specific named logger
logger = logging.getLogger('project.interesting.stuff')

The dotted paths of logger names define a hierarchy. The project.interesting logger is considered to be a parent of the project.interesting.stuff logger; the project logger is a parent of the project.interesting logger.

Why is the hierarchy important? Well, because loggers can be set to propagate their logging calls to their parents. In this way, you can define a single set of handlers at the root of a logger tree, and capture all logging calls in the subtree of loggers. A logger defined in the project namespace will catch all logging messages issued on the project.interesting and project.interesting.stuff loggers.

This propagation can be controlled on a per-logger basis. If you don’t want a particular logger to propagate to its parents, you can turn off this behavior.

Making logging calls

The logger instance contains an entry method for each of the default log levels:

  • logger.debug()
  • logger.info()
  • logger.warning()
  • logger.error()
  • logger.critical()

There are two other logging calls available:

  • logger.log(): Manually emits a logging message with a specific log level.
  • logger.exception(): Creates an ERROR level logging message wrapping the current exception stack frame.

Configuring logging

Of course, it isn’t enough to just put logging calls into your code. You also need to configure the loggers, handlers, filters and formatters to ensure that logging output is output in a useful way.

Python’s logging library provides several techniques to configure logging, ranging from a programmatic interface to configuration files. By default, Django uses the dictConfig format.

In order to configure logging, you use :setting:`LOGGING` to define a dictionary of logging settings. These settings describes the loggers, handlers, filters and formatters that you want in your logging setup, and the log levels and other properties that you want those components to have.

By default, the :setting:`LOGGING` setting is merged with Django’s default logging configuration using the following scheme.

If the disable_existing_loggers key in the :setting:`LOGGING` dictConfig is set to True (which is the dictConfig default if the key is missing) then all loggers from the default configuration will be disabled. Disabled loggers are not the same as removed; the logger will still exist, but will silently discard anything logged to it, not even propagating entries to a parent logger. Thus you should be very careful using 'disable_existing_loggers': True; it’s probably not what you want. Instead, you can set disable_existing_loggers to False and redefine some or all of the default loggers; or you can set :setting:`LOGGING_CONFIG` to None and handle logging config yourself.

Logging is configured as part of the general Django setup() function. Therefore, you can be certain that loggers are always ready for use in your project code.

Examples

The full documentation for dictConfig format is the best source of information about logging configuration dictionaries. However, to give you a taste of what is possible, here are several examples.

To begin, here’s a small configuration that will allow you to output all log messages to the console:

settings.py

import os

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
        },
    },
    'root': {
        'handlers': ['console'],
        'level': 'WARNING',
    },
}

This configures the parent root logger to send messages with the WARNING level and higher to the console handler. By adjusting the level to INFO or DEBUG you can display more messages. This may be useful during development.

Next we can add more fine-grained logging. Here’s an example of how to make the logging system print more messages from just the django named logger:

settings.py

import os

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
        },
    },
    'root': {
        'handlers': ['console'],
        'level': 'WARNING',
    },
    'loggers': {
        'django': {
            'handlers': ['console'],
            'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'),
            'propagate': False,
        },
    },
}

By default, this config sends messages from the django logger of level INFO or higher to the console. This is the same level as Django’s default logging config, except that the default config only displays log records when DEBUG=True. Django does not log many such INFO level messages. With this config, however, you can also set the environment variable DJANGO_LOG_LEVEL=DEBUG to see all of Django’s debug logging which is very verbose as it includes all database queries.

You don’t have to log to the console. Here’s a configuration which writes all logging from the django named logger to a local file:

settings.py

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'file': {
            'level': 'DEBUG',
            'class': 'logging.FileHandler',
            'filename': '/path/to/django/debug.log',
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'DEBUG',
            'propagate': True,
        },
    },
}

If you use this example, be sure to change the 'filename' path to a location that’s writable by the user that’s running the Django application.

Finally, here’s an example of a fairly complex logging setup:

settings.py

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'verbose': {
            'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
            'style': '{',
        },
        'simple': {
            'format': '{levelname} {message}',
            'style': '{',
        },
    },
    'filters': {
        'special': {
            '()': 'project.logging.SpecialFilter',
            'foo': 'bar',
        },
        'require_debug_true': {
            '()': 'django.utils.log.RequireDebugTrue',
        },
    },
    'handlers': {
        'console': {
            'level': 'INFO',
            'filters': ['require_debug_true'],
            'class': 'logging.StreamHandler',
            'formatter': 'simple'
        },
        'mail_admins': {
            'level': 'ERROR',
            'class': 'django.utils.log.AdminEmailHandler',
            'filters': ['special']
        }
    },
    'loggers': {
        'django': {
            'handlers': ['console'],
            'propagate': True,
        },
        'django.request': {
            'handlers': ['mail_admins'],
            'level': 'ERROR',
            'propagate': False,
        },
        'myproject.custom': {
            'handlers': ['console', 'mail_admins'],
            'level': 'INFO',
            'filters': ['special']
        }
    }
}

This logging configuration does the following things:

  • Identifies the configuration as being in ‘dictConfig version 1’ format. At present, this is the only dictConfig format version.

  • Defines two formatters:

    • simple, that outputs the log level name (e.g., DEBUG) and the log message.

      The format string is a normal Python formatting string describing the details that are to be output on each logging line. The full list of detail that can be output can be found in formatter-objects.

    • verbose, that outputs the log level name, the log message, plus the time, process, thread and module that generate the log message.

  • Defines two filters:

    • project.logging.SpecialFilter, using the alias special. If this filter required additional arguments, they can be provided as additional keys in the filter configuration dictionary. In this case, the argument foo will be given a value of bar when instantiating SpecialFilter.

    • django.utils.log.RequireDebugTrue, which passes on records when :setting:`DEBUG` is True.

  • Defines two handlers:

    • console, a StreamHandler, which prints any INFO (or higher) message to sys.stderr. This handler uses the simple output format.

    • mail_admins, an AdminEmailHandler, which emails any ERROR (or higher) message to the site :setting:`ADMINS`. This handler uses the special filter.

  • Configures three loggers:

    • django, which passes all messages to the console handler.

    • django.request, which passes all ERROR messages to the mail_admins handler. In addition, this logger is marked to not propagate messages. This means that log messages written to django.request will not be handled by the django logger.

    • myproject.custom, which passes all messages at INFO or higher that also pass the special filter to two handlers – the console, and mail_admins. This means that all INFO level messages (or higher) will be printed to the console; ERROR and CRITICAL messages will also be output via email.

Custom logging configuration

If you don’t want to use Python’s dictConfig format to configure your logger, you can specify your own configuration scheme.

The :setting:`LOGGING_CONFIG` setting defines the callable that will be used to configure Django’s loggers. By default, it points at Python’s logging.config.dictConfig() function. However, if you want to use a different configuration process, you can use any other callable that takes a single argument. The contents of :setting:`LOGGING` will be provided as the value of that argument when logging is configured.

Disabling logging configuration

If you don’t want to configure logging at all (or you want to manually configure logging using your own approach), you can set :setting:`LOGGING_CONFIG` to None. This will disable the configuration process for Django’s default logging. Here’s an example that disables Django’s logging configuration and then manually configures logging:

settings.py

LOGGING_CONFIG = None

import logging.config
logging.config.dictConfig(...)

Setting :setting:`LOGGING_CONFIG` to None only means that the automatic configuration process is disabled, not logging itself. If you disable the configuration process, Django will still make logging calls, falling back to whatever default logging behavior is defined.

Django’s logging extensions

Django provides a number of utilities to handle the unique requirements of logging in Web server environment.

Loggers

Django provides several built-in loggers.

django

The catch-all logger for messages in the django hierarchy. No messages are posted using this name but instead using one of the loggers below.

django.request

Log messages related to the handling of requests. 5XX responses are raised as ERROR messages; 4XX responses are raised as WARNING messages. Requests that are logged to the django.security logger aren’t logged to django.request.

Messages to this logger have the following extra context:

  • status_code: The HTTP response code associated with the request.
  • request: The request object that generated the logging message.

django.server

Log messages related to the handling of requests received by the server invoked by the :djadmin:`runserver` command. HTTP 5XX responses are logged as ERROR messages, 4XX responses are logged as WARNING messages, and everything else is logged as INFO.

Messages to this logger have the following extra context:

  • status_code: The HTTP response code associated with the request.
  • request: The request object that generated the logging message.

django.template

Log messages related to the rendering of templates.

  • Missing context variables are logged as DEBUG messages.

django.db.backends

Messages relating to the interaction of code with the database. For example, every application-level SQL statement executed by a request is logged at the DEBUG level to this logger.

Messages to this logger have the following extra context:

  • duration: The time taken to execute the SQL statement.
  • sql: The SQL statement that was executed.
  • params: The parameters that were used in the SQL call.

For performance reasons, SQL logging is only enabled when settings.DEBUG is set to True, regardless of the logging level or handlers that are installed.

This logging does not include framework-level initialization (e.g. SET TIMEZONE) or transaction management queries (e.g. BEGIN, COMMIT, and ROLLBACK). Turn on query logging in your database if you wish to view all database queries.

django.security.*

The security loggers will receive messages on any occurrence of SuspiciousOperation and other security-related errors. There is a sub-logger for each subtype of security error, including all SuspiciousOperations. The level of the log event depends on where the exception is handled. Most occurrences are logged as a warning, while any SuspiciousOperation that reaches the WSGI handler will be logged as an error. For example, when an HTTP Host header is included in a request from a client that does not match :setting:`ALLOWED_HOSTS`, Django will return a 400 response, and an error message will be logged to the django.security.DisallowedHost logger.

These log events will reach the django logger by default, which mails error events to admins when DEBUG=False. Requests resulting in a 400 response due to a SuspiciousOperation will not be logged to the django.request logger, but only to the django.security logger.

To silence a particular type of SuspiciousOperation, you can override that specific logger following this example:

'handlers': {
    'null': {
        'class': 'logging.NullHandler',
    },
},
'loggers': {
    'django.security.DisallowedHost': {
        'handlers': ['null'],
        'propagate': False,
    },
},

Other django.security loggers not based on SuspiciousOperation are:

  • django.security.csrf: For CSRF failures.

django.db.backends.schema

Logs the SQL queries that are executed during schema changes to the database by the migrations framework. Note that it won’t log the queries executed by RunPython. Messages to this logger have params and sql in their extra context (but unlike django.db.backends, not duration). The values have the same meaning as explained in django.db.backends.

Handlers

Django provides one log handler in addition to those provided by the Python logging module.

class AdminEmailHandler(include_html=False, email_backend=None, reporter_class=None)

This handler sends an email to the site :setting:`ADMINS` for each log message it receives.

If the log record contains a request attribute, the full details of the request will be included in the email. The email subject will include the phrase “internal IP” if the client’s IP address is in the :setting:`INTERNAL_IPS` setting; if not, it will include “EXTERNAL IP”.

If the log record contains stack trace information, that stack trace will be included in the email.

The include_html argument of AdminEmailHandler is used to control whether the traceback email includes an HTML attachment containing the full content of the debug Web page that would have been produced if :setting:`DEBUG` were True. To set this value in your configuration, include it in the handler definition for django.utils.log.AdminEmailHandler, like this:

'handlers': {
    'mail_admins': {
        'level': 'ERROR',
        'class': 'django.utils.log.AdminEmailHandler',
        'include_html': True,
    }
},

Note that this HTML version of the email contains a full traceback, with names and values of local variables at each level of the stack, plus the values of your Django settings. This information is potentially very sensitive, and you may not want to send it over email. Consider using something such as Sentry to get the best of both worlds – the rich information of full tracebacks plus the security of not sending the information over email. You may also explicitly designate certain sensitive information to be filtered out of error reports – learn more on Filtering error reports.

By setting the email_backend argument of AdminEmailHandler, the email backend that is being used by the handler can be overridden, like this:

'handlers': {
    'mail_admins': {
        'level': 'ERROR',
        'class': 'django.utils.log.AdminEmailHandler',
        'email_backend': 'django.core.mail.backends.filebased.EmailBackend',
    }
},

By default, an instance of the email backend specified in :setting:`EMAIL_BACKEND` will be used.

The reporter_class argument of AdminEmailHandler allows providing an django.views.debug.ExceptionReporter subclass to customize the traceback text sent in the email body. You provide a string import path to the class you wish to use, like this:

'handlers': {
    'mail_admins': {
        'level': 'ERROR',
        'class': 'django.utils.log.AdminEmailHandler',
        'include_html': True,
        'reporter_class': 'somepackage.error_reporter.CustomErrorReporter'
    }
},

New in version 3.0: The reporter_class argument was added.

send_mail(subject, message, *args, **kwargs)

Sends emails to admin users. To customize this behavior, you can subclass the AdminEmailHandler class and override this method.

Filters

Django provides some log filters in addition to those provided by the Python logging module.

class CallbackFilter(callback)

This filter accepts a callback function (which should accept a single argument, the record to be logged), and calls it for each record that passes through the filter. Handling of that record will not proceed if the callback returns False.

For instance, to filter out UnreadablePostError (raised when a user cancels an upload) from the admin emails, you would create a filter function:

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

and then add it to your logging config:

'filters': {
    'skip_unreadable_posts': {
        '()': 'django.utils.log.CallbackFilter',
        'callback': skip_unreadable_post,
    }
},
'handlers': {
    'mail_admins': {
        'level': 'ERROR',
        'filters': ['skip_unreadable_posts'],
        'class': 'django.utils.log.AdminEmailHandler'
    }
},
class RequireDebugFalse

This filter will only pass on records when settings.DEBUG is False.

This filter is used as follows in the default :setting:`LOGGING` configuration to ensure that the AdminEmailHandler only sends error emails to admins when :setting:`DEBUG` is False:

'filters': {
    'require_debug_false': {
        '()': 'django.utils.log.RequireDebugFalse',
    }
},
'handlers': {
    'mail_admins': {
        'level': 'ERROR',
        'filters': ['require_debug_false'],
        'class': 'django.utils.log.AdminEmailHandler'
    }
},
class RequireDebugTrue
This filter is similar to RequireDebugFalse, except that records are passed only when :setting:`DEBUG` is True.

Django’s default logging configuration

By default, Django configures the following logging:

When :setting:`DEBUG` is True:

  • The django logger sends messages in the django hierarchy (except django.server) at the INFO level or higher to the console.

When :setting:`DEBUG` is False:

  • The django logger sends messages in the django hierarchy (except django.server) with ERROR or CRITICAL level to AdminEmailHandler.

Independent of the value of :setting:`DEBUG`:

  • The django.server logger sends messages at the INFO level or higher to the console.

All loggers except django.server propagate logging to their parents, up to the root django logger. The console and mail_admins handlers are attached to the root logger to provide the behavior described above.

See also Configuring logging to learn how you can complement or replace this default logging configuration defined in :source:`django/utils/log.py`.

Программисты Python часто используют print() в своем коде как быстрый и удобный инструмент отладки. Использование фреймворка протоколирования требует лишь немного больше усилий, но оно гораздо более элегантно и гибко. Помимо полезности для отладки, протоколирование также может предоставить вам больше — и лучше структурированной — информации о состоянии и работоспособности вашего приложения.

Быстрый обзор¶

Django использует и расширяет встроенный модуль Python logging для ведения системного журнала. Этот модуль подробно рассматривается в собственной документации Python; в данном разделе представлен краткий обзор.

Состав игроков¶

Конфигурация ведения журнала Python состоит из четырех частей:

  • Логеры
  • Обработчики
  • Фильтры
  • Форматировщики

Логеры¶

Журнал logger — это точка входа в систему протоколирования. Каждый логгер представляет собой именованное ведро, в которое можно записывать сообщения для обработки.

Логер настроен на уровень журнала. Этот уровень журнала описывает серьезность сообщений, обрабатываемых логером. Python определяет следующие уровни журнала:

  • DEBUG: системная информация низкого уровня для отладки
  • INFO: Общая информация о системе
  • WARNING: Информация, описывающая возникшую незначительную проблему.
  • ERROR: Информация, описывающая возникшую серьезную проблему.
  • CRITICAL: Информация, описывающая возникшую критическую проблему.

Каждое сообщение, которое записывается в регистратор, является записью журнала. Каждая запись журнала также имеет уровень журнала, указывающий на серьезность данного конкретного сообщения. Запись журнала может также содержать полезные метаданные, описывающие регистрируемое событие. Это могут быть такие детали, как трассировка стека или код ошибки.

Когда сообщение поступает в регистратор, уровень журнала сообщения сравнивается с уровнем журнала регистратора. Если уровень журнала сообщения соответствует уровню журнала самого регистратора или превышает его, сообщение будет обработано. Если нет, то сообщение будет проигнорировано.

Как только регистратор определил, что сообщение должно быть обработано, оно передается в Handler.

Обработчики¶

Обработчик* — это механизм, определяющий, что происходит с каждым сообщением в регистраторе. Он описывает определенное поведение регистратора, например, запись сообщения на экран, в файл или в сетевой сокет.

Как и регистраторы, обработчики также имеют уровень журнала. Если уровень записи журнала не соответствует уровню обработчика или превышает его, обработчик проигнорирует сообщение.

У регистратора может быть несколько обработчиков, и каждый обработчик может иметь свой уровень регистрации. Таким образом, можно обеспечить различные формы уведомления в зависимости от важности сообщения. Например, можно установить один обработчик, который пересылает сообщения ERROR и CRITICAL в службу подкачки, а второй обработчик записывает все сообщения (включая ERROR и CRITICAL) в файл для последующего анализа.

Фильтры¶

Для обеспечения дополнительного контроля над тем, какие записи журнала передаются от регистратора к обработчику, используется фильтр.

По умолчанию обрабатывается любое сообщение журнала, отвечающее требованиям уровня журнала. Однако, установив фильтр, вы можете задать дополнительные критерии для процесса протоколирования. Например, вы можете установить фильтр, который позволяет выдавать только сообщения ERROR из определенного источника.

Фильтры также можно использовать для изменения записи журнала перед ее выдачей. Например, вы можете написать фильтр, который понижает уровень ERROR записей журнала до WARNING записей, если выполняется определенный набор критериев.

Фильтры можно устанавливать на регистраторы или на обработчики; несколько фильтров можно использовать в цепочке для выполнения нескольких действий фильтрации.

Форматировщики¶

В конечном счете, запись журнала должна быть представлена в виде текста. Форматировщики описывают точный формат этого текста. Форматтер обычно состоит из строки форматирования Python, содержащей LogRecord attributes; однако вы также можете написать собственные форматтеры для реализации специфического поведения форматирования.

Последствия для безопасности¶

Система протоколирования обрабатывает потенциально конфиденциальную информацию. Например, запись журнала может содержать информацию о веб-запросе или трассировке стека, а некоторые данные, которые вы собираете в собственных регистраторах, также могут иметь отношение к безопасности. Вы должны быть уверены в том, что знаете:

  • какая информация собирается
  • где он будет впоследствии храниться
  • как она будет передана
  • кто может иметь к нему доступ.

Чтобы помочь контролировать сбор конфиденциальной информации, вы можете явно указать определенную конфиденциальную информацию, которая будет отфильтрована из отчетов об ошибках — читайте подробнее о том, как это сделать filter error reports.

AdminEmailHandler

Встроенный AdminEmailHandler заслуживает упоминания в контексте безопасности. Если его опция include_html включена, то отправляемое им сообщение электронной почты будет содержать полный трассировочный откат, с именами и значениями локальных переменных на каждом уровне стека, плюс значения ваших настроек Django (другими словами, тот же уровень детализации, который раскрывается на веб-странице, когда DEBUG становится True).

Обычно считается, что отправлять такую потенциально важную информацию по электронной почте не стоит. Вместо этого воспользуйтесь одним из многочисленных сторонних сервисов, на которые можно отправлять подробные журналы, чтобы получить лучшее из нескольких миров — богатую информацию о полном отслеживании, четкое управление тем, кто уведомлен и имеет доступ к информации, и так далее.

Настройка протоколирования¶

Библиотека протоколирования Python предоставляет несколько способов настройки протоколирования, начиная от программного интерфейса и заканчивая конфигурационными файлами. По умолчанию Django использует dictConfig format.

Для настройки протоколирования вы используете LOGGING для определения словаря настроек протоколирования. Эти настройки описывают регистраторы, обработчики, фильтры и форматеры, которые вы хотите использовать в своей настройке протоколирования, а также уровни протоколирования и другие свойства, которыми должны обладать эти компоненты.

По умолчанию установка LOGGING объединяется с Django’s default logging configuration по следующей схеме.

Если ключ disable_existing_loggers в директ-конфиге LOGGING установлен в True (что является dictConfig по умолчанию, если ключ отсутствует), то все регистраторы из конфигурации по умолчанию будут отключены. Отключенные регистраторы — это не то же самое, что удаленные; регистратор будет по-прежнему существовать, но будет молча отбрасывать все, что записывается в него, даже не передавая записи в родительский регистратор. Таким образом, вы должны быть очень осторожны, используя 'disable_existing_loggers': True; вероятно, это не то, что вам нужно. Вместо этого вы можете установить disable_existing_loggers в False и переопределить некоторые или все логгеры по умолчанию; или вы можете установить LOGGING_CONFIG в None и handle logging config yourself.

Логирование настраивается как часть общей функции Django setup(). Поэтому вы можете быть уверены, что логгеры всегда готовы к использованию в коде вашего проекта.

Примеры¶

Полная документация по dictConfig format является лучшим источником информации о словарях конфигурации журнала. Однако, чтобы дать вам представление о том, что возможно, приведем несколько примеров.

Для начала, вот небольшая конфигурация, которая позволит вам выводить все сообщения журнала на консоль:

settings.py

import os

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
        },
    },
    'root': {
        'handlers': ['console'],
        'level': 'WARNING',
    },
}

Это настраивает родительский регистратор root на отправку сообщений с уровнем WARNING и выше в обработчик консоли. Изменив уровень до INFO или DEBUG, можно вывести больше сообщений. Это может быть полезно во время разработки.

Далее мы можем добавить более тонкое протоколирование. Вот пример того, как заставить систему протоколирования выводить больше сообщений только из именованного регистратора django:

settings.py

import os

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
        },
    },
    'root': {
        'handlers': ['console'],
        'level': 'WARNING',
    },
    'loggers': {
        'django': {
            'handlers': ['console'],
            'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'),
            'propagate': False,
        },
    },
}

По умолчанию этот конфиг отправляет сообщения от логгера django уровня INFO или выше на консоль. Это тот же уровень, что и стандартная конфигурация логирования Django, за исключением того, что стандартная конфигурация отображает записи журнала только при уровне DEBUG=True. Django не записывает много сообщений такого уровня INFO. Однако с этой конфигурацией вы также можете установить переменную окружения DJANGO_LOG_LEVEL=DEBUG, чтобы увидеть все отладочные журналы Django, которые очень подробны, поскольку включают все запросы к базе данных.

Вам не обязательно вести журнал в консоль. Вот конфигурация, которая записывает все логи из django именованного логгера в локальный файл:

settings.py

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'file': {
            'level': 'DEBUG',
            'class': 'logging.FileHandler',
            'filename': '/path/to/django/debug.log',
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'DEBUG',
            'propagate': True,
        },
    },
}

Если вы используете этот пример, не забудьте изменить путь 'filename' на место, доступное для записи пользователю, который запускает приложение Django.

Наконец, вот пример довольно сложной настройки протоколирования:

settings.py

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'verbose': {
            'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
            'style': '{',
        },
        'simple': {
            'format': '{levelname} {message}',
            'style': '{',
        },
    },
    'filters': {
        'special': {
            '()': 'project.logging.SpecialFilter',
            'foo': 'bar',
        },
        'require_debug_true': {
            '()': 'django.utils.log.RequireDebugTrue',
        },
    },
    'handlers': {
        'console': {
            'level': 'INFO',
            'filters': ['require_debug_true'],
            'class': 'logging.StreamHandler',
            'formatter': 'simple'
        },
        'mail_admins': {
            'level': 'ERROR',
            'class': 'django.utils.log.AdminEmailHandler',
            'filters': ['special']
        }
    },
    'loggers': {
        'django': {
            'handlers': ['console'],
            'propagate': True,
        },
        'django.request': {
            'handlers': ['mail_admins'],
            'level': 'ERROR',
            'propagate': False,
        },
        'myproject.custom': {
            'handlers': ['console', 'mail_admins'],
            'level': 'INFO',
            'filters': ['special']
        }
    }
}

Эта конфигурация протоколирования выполняет следующие действия:

  • Идентифицирует конфигурацию как имеющую формат „dictConfig версии 1“. В настоящее время это единственная версия формата dictConfig.

  • Определяет два форматера:

    • simple, который выводит имя уровня журнала (например, DEBUG) и сообщение журнала.

      Строка format — это обычная строка форматирования Python, описывающая детали, которые должны быть выведены в каждой строке журнала. Полный список деталей, которые могут быть выведены, можно найти в Formatter Objects.

    • verbose, который выводит имя уровня журнала, сообщение журнала, а также время, процесс, поток и модуль, которые генерируют сообщение журнала.

  • Определяет два фильтра:

    • project.logging.SpecialFilter, используя псевдоним special. Если для данного фильтра требуются дополнительные аргументы, они могут быть предоставлены как дополнительные ключи в словаре конфигурации фильтра. В этом случае аргументу foo будет присвоено значение bar при инстанцировании SpecialFilter.
    • django.utils.log.RequireDebugTrue, который передает записи, когда DEBUG становится True.
  • Определяет два обработчика:

    • console, обработчик StreamHandler, который печатает любое сообщение INFO (или выше) в sys.stderr. Этот обработчик использует формат вывода simple.
    • mail_admins, обработчик AdminEmailHandler, который отправляет любое сообщение ERROR (или выше) на сайт ADMINS. Этот обработчик использует фильтр special.
  • Настраивает три регистратора:

    • django, который передает все сообщения обработчику console.
    • django.request, который передает все сообщения ERROR обработчику mail_admins. Кроме того, этот логгер помечен как не распространяющий сообщения. Это означает, что сообщения журнала, записанные в django.request, не будут обработаны регистратором django.
    • myproject.custom, который передает все сообщения уровня INFO или выше, которые также проходят фильтр special, двум обработчикам — console и mail_admins. Это означает, что все сообщения уровня INFO (или выше) будут выведены на консоль; ERROR и CRITICAL сообщения также будут выведены по электронной почте.

Пользовательская конфигурация протоколирования¶

Если вы не хотите использовать формат Python dictConfig для настройки регистратора, вы можете задать свою собственную схему конфигурации.

Параметр LOGGING_CONFIG определяет вызываемую функцию, которая будет использоваться для настройки логгеров Django. По умолчанию он указывает на функцию Python logging.config.dictConfig(). Однако, если вы хотите использовать другой процесс конфигурации, вы можете использовать любую другую вызываемую функцию, принимающую один аргумент. Содержимое LOGGING будет предоставлено в качестве значения этого аргумента при настройке логирования.

Отключение конфигурации ведения журнала¶

Если вы не хотите настраивать ведение журнала вообще (или хотите вручную настроить ведение журнала, используя свой собственный подход), вы можете установить LOGGING_CONFIG на None. Это отключит процесс конфигурирования для Django’s default logging.

Установка LOGGING_CONFIG в None означает только отключение процесса автоматической настройки, но не самого протоколирования. Если вы отключите процесс конфигурирования, Django все равно будет выполнять вызовы логирования, возвращаясь к тому поведению логирования, которое определено по умолчанию.

Вот пример, который отключает конфигурацию логирования Django, а затем вручную настраивает логирование:

settings.py

LOGGING_CONFIG = None

import logging.config
logging.config.dictConfig(...)

Обратите внимание, что процесс настройки по умолчанию вызывает LOGGING_CONFIG только после того, как настройки полностью загружены. В отличие от этого, ручная настройка логирования в вашем файле настроек загрузит ваш конфиг логирования немедленно. Таким образом, конфигурация логирования должна появиться после всех настроек, от которых она зависит.

Logging in Python – How to Use Logs to Debug Your Django Projects

The only perfect code is code that has never been written. As a developer, you are bound to face errors and will be responsible for debugging them.

If you’re coding in Python, you can always look at its error messages to figure out what’s going on. But what if an error occurs that you have no idea what’s breaking your code?

Something might be strangely wrong in the background, but you are unable to recognize it. You can always turn it off and on again – or even better, you can check the logs.

What is Logging?

If an error occurs or your app decides to work strangely, your log files will come in handy. You can traverse through them and find out where exactly the application is having problems and how you can replicate those problems.

By reproducing the problem, you can dig deeper and find a reasonable solution for the errors. Something which might otherwise take several hours to detect might just take few minutes to diagnose with the presence of log files.

Thankfully, Django has support for logging and most of the hard work has already been done by its developers. Django comes with Python’s built-in logging module to leverage system logging.

The Python logging module has four main parts:

  1. Loggers
  2. Handlers
  3. Filters
  4. Formatters

Every component is explained meticulously in the Django Official Documentation. I don’t want you to be overwhelmed with its complexity, so I’ll explain every single part briefly:

1. Loggers

Loggers are basically the entry point of the logging system. This is what you’ll actually work with as a developers.

When a message is received by the logger, the log level is compared to the log level of the logger. If it is the same or exceeds the log level of the logger, the message is sent to the handler for further processing. The log levels are:

  • DEBUG: Low-level system information
  • INFO: General system information
  • WARNING: Minor problems related information
  • ERROR: Major problems related information
  • CRITICAL: Critical problems related information

2. Handlers

Handlers basically determine what happens to each message in a logger. It has log levels the same as Loggers. But, we can essentially define what way we want to handle each log level.

For example: ERROR log level messages can be sent in real-time to the developer, while INFO log levels can just be stored in a system file.

It essentially tells the system what to do with the message like writing it on the screen, a file, or to a network socket

3. Filters

A filter can sit between a Logger and a Handler. It can be used to filter the log record.

For example: in CRITICAL messages, you can set a filter which only allows a particular source to be processed.

4. Formatters

As the name suggests, formatters describe the format of the text which will be rendered.

Now that we have covered the basics, let’s dig deeper with an actual example. Click here for the source code.

Please note that this tutorial assumes that you are already familiar with the basics of Django.

Project Setup

First, create a virtual environment called venv inside your project folder django-logging-tutorial with the command below and activate it.

mkdir django-logging-tutorial
virtualenv venv
source venv/bin/activate

Create a new Django project called django_logging_tutorial. Notice that the project folder name is with a dash while the project name is with an underscore (- vs _). We will also run a series of commands quickly to set up our project.

How to Configure Your Log Files

Let’s first set up the settings.py file of our project. Heads up – notice my comments in the code which will help you understand this process better.

This code is also mentioned in the 3rd example of the official documentation and in most of our projects, it will serve just fine. I have slightly modified it to make it more robust.


LOGGING = {
    'version': 1,
    # The version number of our log
    'disable_existing_loggers': False,
    # django uses some of its own loggers for internal operations. In case you want to disable them just replace the False above with true.
    # A handler for WARNING. It is basically writing the WARNING messages into a file called WARNING.log
    'handlers': {
        'file': {
            'level': 'WARNING',
            'class': 'logging.FileHandler',
            'filename': BASE_DIR / 'warning.log',
        },
    },
    # A logger for WARNING which has a handler called 'file'. A logger can have multiple handler
    'loggers': {
       # notice the blank '', Usually you would put built in loggers like django or root here based on your needs
        '': {
            'handlers': ['file'], #notice how file variable is called in handler which has been defined above
            'level': 'WARNING',
            'propagate': True,
        },
    },
}

If you read my comments above, you might have noticed that the logger part was just blank. Which essentially means any logger.

Be careful with this approach as most of our work can be satisfied with in-built Django loggers like django.request or django.db.backends.

Also, for the sake of simplicity, I only used a file for storing the logs. Depending on your use case you might also choose to drop an email when CRITICAL or ERROR messages are encountered.

To learn more about this, I would encourage you to read the handler part of the docs. The docs might feel overwhelming at the start, but the more you get used to reading them the more you might discover other interesting or better approaches.

Don’t worry if it’s your first time working with documentation. There is always a first time for everything.

I’ve explained most of the code in the comments, but we still haven’t touched upon propagate yet. What is it?

When propagate is set as True, a child will propagate all their logging calls to the parent. This means that we can define a handler at the root (parent) of the logger tree and all logging calls in the subtree (child) go to the handler defined in the parent.

It is also important to note that hierarchy is important here. We can also just set it up as True in our project as it won’t matter in our case since there is no subtree.

How to Trigger Logs in Python

Now, we need to create some log messages so we can try out our configuration in settings.py.

Let’s have a default homepage that just displays ‘Hello FreeCodeCamp.org Reader :)’ and every time someone visits the page we note down a WARNING message in our warning.log file as ‘Homepage was accessed at 2021-08-29 22:23:33.551543 hours!’

Go to your app logging_example, and in views.py include the following code. Make sure you have added logging_example in the INSTALLED_APPS in setting.py.


from django.http import HttpResponse
import datetime
# import the logging library
import logging
# Get an instance of a logger
logger = logging.getLogger(__name__)

def hello_reader(request):
    logger.warning('Homepage was accessed at '+str(datetime.datetime.now())+' hours!')
    return HttpResponse("<h1>Hello FreeCodeCamp.org Reader :)</h1>")

In the project’s urls.py, add the following code so that when we access the homepage the right function is called.

from django.contrib import admin
from django.urls import path
from logging_example import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('',views.hello_reader, name="hello_reader")
]

Time for Some Testing

Finally, our simple setup is done. All we need to do now is to fire up our server and test our log.

Run the development server with this command:

python manage.py runserver

Now, go to your homepage 127.0.0.1:8000 where you will be greeted with the message we have coded. Now check your warning.log file in the path created. Sample output is shown below:

Homepage was accessed at 2021-08-29 22:38:29.922510 hours!
Homepage was accessed at 2021-08-29 22:48:35.088296 hours!

That’s it! Now you know how to perform logging in Django. If you have any questions, just drop me a message. I promise to help :)

If you found my article helpful and want to read more, please check out some Django tutorials at my blog Techflow360.com.



Learn to code for free. freeCodeCamp’s open source curriculum has helped more than 40,000 people get jobs as developers. Get started

In a previous article, we dived into the fundamentals of logging in
Python using the built-in logging module.
This tutorial will build on those fundamentals by exploring how they are applied
in a real-world application built using
Django, a Python web framework that aids with
the rapid design and development of web applications.

By following through with this tutorial, you will gain an understanding of the
following concepts:

  1. Setting up a logging system that is independent of Django.
  2. Using the built-in logging system in Django.
  3. Integrating Django projects with a log aggregation service.

🔭 Want to centralize and monitor your Django application logs?

Head over to Logtail and start ingesting your logs in 5 minutes.

Prerequisites

While this article explains its concepts in detail, we recommend reading the
previous article which discusses the fundamentals of logging in
Python as basic concepts such as
understanding the use cases of logging and writing meaningful log messages were
covered in that article. However, this is optional if you’re already familiar
with Python’s logging module.

To follow through with this article, you should install the latest version of
Python on your machine. If you are missing Python, you can
find the installation instructions here.

You will also need SQLite. Some machines come with SQLite pre-installed, but you
can
find the installation instructions here
if you don’t have it already.

Finally,
clone the sample code repository
and set up the project on your machine:

git clone https://github.com/betterstack-community/horus.git
pip install -r requirements.txt

Finally, you should sign up for a free
OpenWeather account as our sample application
relies on it to function. Your OpenWeather API key can be
accessed from here:

Copy the key and add the following file and add it to a .env file at the root
of your project:

.env

OPEN_WEATHER_API_KEY=<api key>

Breaking down the sample application

For this tutorial, we will implement a basic logging system in a Django-powered
weather search application — Horus. This section will introduce the project
architecture, technologies used, and discuss some important points about the
project.

Project structure

The horus directory follows the
standard Django configuration,
except for the hierarchy of the views. Instead of having a traditional large
views.py file, each view corresponds to a Python file in the horus/views/
folder to make logging easier to implement.

The critical files for this project are as follows:

> horus/
    > views/
        * index.py      — houses the Index view
        * search.py     — houses the Search view
        * weather.py    — houses the Weather view
    * openweather.py    — OpenWeather API access wrapper
    * settings.py       — configurations for the various components of the project
* .env                  — environment variables for the project
* requirements.txt      — required packages for the project

Core views

Each view corresponds to a page in Horus, and there are three in total:

  1. Index: the homepage with a search input.
  2. Search: displays the search results, allowing users to select the intended
    location.
  3. Weather: displays the weather of the selected location.

To understand the basic construction of a page, open horus/views/search.py in
your text editor:

horus/views/search.py

# ...

def search(request):
    if request.method != 'POST':
        return redirect('/')

    location = request.POST['location']

    try:
        locations = search_countries(location)
        return render(request, 'search.html', {'success': True, 'search': location, 'results': locations})
    except OpenWeatherError:
        return render(request, 'search.html', {'success': False})

The routing logic is as follows:

The other pages also rely on their respective templates (found in
horus/templates/) to render the data accordingly. However, for this article,
it is not crucial to understand how templating works in Django. If you wish to
learn more, refer to the
Django documentation on templates..

Overall, the general program flow is as follows:

The OpenWeatherMap API

Horus relies on the OpenWeatherMap API to provide up-to-date information about
the current weather at a location. It uses two endpoints from the API —
geocoding to match the user
search query with possible locations and
current weather to retrieve the current
weather of the selected location.

A basic API wrapper has been built around the endpoints mentioned above and can
be found in horus/openweather.py. The built-in requests module is used to
query each endpoint with the given parameters and their responses are parsed
accordingly:

horus/openweather.py

# ...

def search_countries(search, limit=5):

response = __request__(

'http://api.openweathermap.org/geo/1.0/direct',

{'q': search, 'limit': limit}

)

if not response.ok or len(response.json()) == 0: raise OpenWeatherError( 'OpenWeather could not find matching locations - response not OK or response is empty') locations = [{ 'name': location['name'], 'lat': location['lat'], 'lon': location['lon'], 'country': pycountry.countries.get(alpha_2=location['country']).name, 'state': location['state'] if 'state' in location else '' } for location in response.json()] return locations def get_current_weather(location, lat, lon):

response = __request__(

'https://api.openweathermap.org/data/2.5/weather',

{'lat': lat, 'lon': lon, 'units': 'metric'}

)

if not response.ok: raise OpenWeatherError( 'OpenWeather could not find the weather of the location - response not OK') weather = { 'current': response.json()['weather'][0]['description'].lower(), 'temp': response.json()['main']['temp'], 'feels_like': response.json()['main']['feels_like'], 'location': location } return weather

Note that this wrapper utilizes error handling for response management. If a
request is successful, the wrapper function will return the parsed response from
the API. If unsuccessful, the wrapper will raise an error instead. It is up to
the developer to catch and handle the error accordingly (which we do).

Limitations-wise, we will use the free tier of the OpenWeatherMap API with
certain limits. In our case, however, we will not run into these limits.

Design decisions

We have explicitly avoided using JavaScript or building a Single-Page
Application (SPA) to minimize complexity. We’re also relying on the SQLite
database provided by Django as set up in the Prerequisites section.

To demonstrate multi-user access, each user will have a unique UUID assigned
when they first access the Horus homepage. We will explore the usefulness of
this approach when integrating Horus with a log aggregation system.

Now that you understand how the project works, let’s revise the advanced logging
features in Python next.

Recap of Python logging basics

In the introductory piece on logging in
Python, we briefly covered the basics of its
standard logging module. To recap, we touched base on Loggers, Formatters,
Handlers, and Filters, with an emphasis on the first three.

Here’s a brief recap of what they all do:

  1. Logger: writes log records.
  2. Formatter: specifies the structure of each log record.
  3. Handler: determines the destination for each records.
  4. Filter: determines which log records get sent to the configured
    destination.

These components follow the general setup process:

Now that we have a general idea of how logging works in Python, we will begin to
implement these concepts in Horus.

Creating a Django-independent logging system

In this section, we will explore how to create a basic logging infrastructure
that doesn’t rely on Django-specific features, and we will use the flowchart
above to guide our implementation. Let’s take a look at the views/index.py
file in the project directory for instructions on achieving this in code.

Throughout this tutorial, we will use
Visual Studio Code as our default editor but
feel free to use any editor that you are comfortable with.

code horus/views/index.py

When you first open the file in your editor, you will see that we’ve already set
up the general logging infrastructure that will be replicated elsewhere. Let us
go through the infrastructure one step at a time.

horus/views/index.py

. . .

logger = logging.getLogger('horus.views.index')

logger.setLevel(logging.INFO)

. . .

First, a new Logger is instantiated and named horus.views.index. This name
is included in every log entry produced by the logger so its good to give a
descriptive name to easily locate the origin of a record. The minimum log level
of the logger is set to INFO so that only messages logged at the INFO
level or above are recorded.

horus/views/index.py

. . .
formatter = logging.Formatter('%(name)s at %(asctime)s (%(levelname)s) :: %(message)s')
. . .

Next, a Formatter is created to specify the structure of each log message like
this:

<name> at <timestamp> (<level>) :: <message>

There are many other
formatting attributes
that can be used to format your logs so do check them out.

horus/views/index.py

sh = logging.StreamHandler()

sh.setFormatter(formatter)

An instance of the StreamHandler class also created and used to direct all
logs to the standard error. On the next line, the formatter we created is set
on the StreamHandler instance so that the correct formatting is applied to
each record.

Lastly, the StreamHandler is added instance to our logger such that all logs
are created through it are sent to the console.

At this point, we have a basic infrastructure for logging to the console and we
can begin to log messages in our project. For example, let’s log the unique UUID
that is assigned to each new request to the homepage.

horus/views/index.py

def index(request):
    request.session['id'] = str(uuid.uuid4())

logger.info(f'New user with ID: {request.session["id"]}')

return render(request, 'index.html', {})

You can see it in action by starting the Django application with the following
command:

python manage.py runserver

Output

Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
August 16, 2022 - 15:13:50
Django version 4.0.4, using settings 'horus.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Head over to http://localhost:8000 in your browser and
observe the following log entry in the terminal:

Output

horus.views.index at 2022-06-21 18:39:00,970 (INFO) :: New user with ID: 889c2532-27df-4b31-99c2-245fd9fe3b8d

Normally, Django projects will have other logs printed to the console, but we
have disabled them in the settings.py project so that we can focus solely on
the logs generated by Horus itself.

Now that we have implemented a logging infrastructure that is independent of
Django-specific features, let’s practice by repeating the same process in the
openweather.py file. Open the file, and follow the TODO prompts to set up
the necessary logging infrastructure (the solution is discussed below).

Notes for practice: Django-independent logging

To create a FileHandler, refer to the
documentation for the necessary configurations
(Hint: fh = logging.FileHandler('logs.txt')).

One of the prompts requires you to log the OpenWeather API key to the console.
This is a bad practice so once you have written up the code for this, ensure
that you remove it before moving on to the next section.

For the prompts about logging various information about the program state,
follow these general guidelines:

  1. Be as specific as possible about the site of the log.
  2. Use appropriate log levels that correspond to the respective severities of
    the events.
  3. Include as much information about the log and problem, instead of just
    printing results.

Once you have completed the prompts, you can refer to the solution by changing
to the solution branch.

git switch independent-solutions

Return to the openweather.py file in your editor and observe how each prompt
was addressed.

horus/openweather.py

logger = logging.getLogger('horus.openweather')
logger.setLevel(logging.INFO)
formatter = logging.Formatter(
    '%(name)s at %(asctime)s (%(levelname)s) :: %(message)s')

The initialization of the Logger and Formatter is the same as in the
index.py example except that the name of the logger is horus.openweather.

horus/openweather.py

sh = logging.StreamHandler()

fh = logging.FileHandler(filename='logging-logs.txt')

The creation of the StreamHandler follows the same the same pattern as in the
index.py file. We also created an instance of the FileHandler class for
outputting our logs to a file. This handler requires a filename argument that
represents the name of the log file that is saved to the filesystem.

horus/openweather.py

fh.setLevel(logging.WARNING)

The next prompt specifies that only logs of WARNING level and above should to
be logged to the file, so the minimum log level of the newly created
FileHandler is set to WARNING.

horus/openweather.py

sh.setFormatter(formatter)
fh.setFormatter(formatter)

logger.addHandler(sh)
logger.addHandler(fh)

Afterward, the formatter is set on both the StreamHandler and FileHandler
instances, and both handlers are subsequently added to the logger.

The prompts about logging program state do not have fixed answers, but it is
advisable to follow the above guidelines. The two most important prompts in this
section are:

  1. Bad example logging:

Remember to delete the log after you have ensured that it works as it is bad
practice to log tokens or API keys.

  1. Logging when an API request fails:

Use logger.error() instead of logger.info() as we are logging something
that goes against the normal program flow. When we are logging on the server,
it is important that any failed API requests are logged as errors, not as
warnings, so that future debugging will be easier.

horus/openweather.py

   # ...
   if not response.ok or len(response.json()) == 0:

logger.error(

'search_countries failed as response returned not OK or no results in returned search')

# ...

Once you are done inspecting the solutions branch, switch back to your main
branch with the command below:

Now that we’ve set up a basic logging infrastructure independent of Django, we
can look how to construct a Django-based logging setup in our application. This
should be the preferred approach as it saves you from writing a lot of
boilerplate code.

Implementing a Django-specific logging system

Something you might have noticed about the Django-independent logging system
above is that setting up the logger, handler and formatter often has to be done
within the file that requires logging.

To address this, Python’s logging module has a built-in system for
centralizing log configurations through the logging.dictConfig() function.
This function loads a
dictionary of logger configurations
as a «global» store of loggers. This centralizes the logging infrastructure and
avoids the problem of repeating the setup steps per file.

Django integrates with this built-in dictConfig system so that you can
centralize your logging configuration in the settings.py file. Let us begin by
looking at a basic example in horus/settings.py. It houses the logging
configurations under the LOGGING field towards the end of the file.

Before the LOGGING field in settings.py, the LOGGING_CONFIG field is set
to None to disable the automatic configuration process since we’re going to
set up a custom configuration shortly.

Next, we configured the logging behaviour of the requests library by setting
it to only display logs equal to or above the ERROR log level.

horus/settings.py

logging.getLogger("requests").setLevel(logging.ERROR)

Lastly, the LOGGING field is used to setup the dictConfig() method:

horus/settings.py

logging.config.dictConfig(LOGGING)

These three steps effectively disable the default logging setup provided by
Django and allows our application logs to take center stage.

horus/settings.py

# ...

LOGGING = {

'version': 1,

'disable_existing_loggers': False,

'formatters': {

},

'handlers': {

},

'loggers': {

}

}

Let’s now shift our focus to defining a basic logging setup in Django. We can
configure several loggers, formatters, and handlers in a single location and
then import them into our code as needed. The disable_existing_loggers key is
set to False so that our loggers from the previous section will still work,
but you may disable them if you want to restrict the use of loggers in your
Django application to only the ones defined in the settings.py file
(recommended).

Let’s start by defining a new Formatter. Add the following code to the
formatters dictionary shown below:

horus/settings.py

. . .
'formatters': {

'base': {

'format': '{name} at {asctime} ({levelname}) :: {message}',

'style': '{'

}

}, . . .

The new Formatter is called base and it uses the same log message format in
the previous section. Setting the style key to { ensures that we can
reference the log message attributes using curly braces.

Similarly, a new Handler is created by adding it to the handlers dictionary
within the LOGGING dictionary:

horus/settings.py

. . .
'handlers': {

'console': {

'class': 'logging.StreamHandler',

'formatter': 'base'

},

}, . . .

The console handler is a StreamHandler instance that logs to the console,
and it uses the base Formatter created earlier to format its output.

Finally, let’s define a new Logger under the loggers dictionary as follows:

horus/settings.py

. . .
'loggers': {

'horus.views.search': {

'handlers': ['console'],

'level': 'INFO'

}

} . . .

Our new logger is called horus.views.search and it uses the console handler
defined earlier. It is also configured to only record messages with a log level
of INFO and above.

With this configuration in place, we can access the horus.views.search logger
in any file like this:

logger = logging.getLogger('horus.views.search')

Open the views/search.py file in your editor and observe how this logger was
imported and used in several places:

horus/views/search.py

. . .

logger = logging.getLogger('horus.views.search')

def search(request): if request.method != 'POST':

logger.info(

'Invalid access made to /search, redirecting to /')

return redirect('/')

logger.info(f'User {request.session["id"]} has navigated to /search')

location = request.POST['location']

logger.info(f'User {request.session["id"]} searched for {location}')

try: locations = search_countries(location) return render(request, 'search.html', {'success': True, 'search': location, 'results': locations}) except OpenWeatherError:

logger.error(

'Unable to retrieve matching locations for search', exc_info=True)

return render(request, 'search.html', {'success': False})

When you search for a city in the application, you will observe that the
following logs are recorded to the console:

Output

horus.views.search at 2022-08-16 18:34:00,184 (INFO) :: User b617a3b3-773b-42aa-b679-fc12e2370818 has navigated to /search
horus.views.search at 2022-08-16 18:34:00,184 (INFO) :: User b617a3b3-773b-42aa-b679-fc12e2370818 searched for london

If you try to make a GET request to the /search route, it will redirect you
back to the homepage and a INFO message will be logged to the console:

horus.views.search at 2022-08-16 18:36:00,702 (INFO) :: Invalid access made to /search, redirecting to /

In the latter part of the function, logger.error() is used in the except
block so that if the search fails, an error is logged to the console. The
exc_info argument is used to provide contextual information about the
exception within the log message itself. We don’t need to raise the exception
since it does not impede the overall operation of the application and it is
sufficiently handled in the except block by rendering an error page.

You can test it out by typing some gibberish in the search input instead of a
location:

Once you click the search button, you will notice that the search was not
successful:

And the following error will be logged to the console:

Output

horus.views.search at 2022-08-16 18:42:43,842 (ERROR) :: Unable to retrieve matching locations for search
Traceback (most recent call last):
  File "/home/user/horus/views/search.py", line 35, in search
    locations = search_countries(location)
  File "/home/user/horus/openweather.py", line 61, in search_countries
    raise OpenWeatherError(
horus.openweather.OpenWeatherError: OpenWeather could not find matching locations - response not OK or response is empty

Now that we have seen how a Django-specific logging configuration works in
practice, we can dive into some practice in both horus/settings.py and
horus/views/weather.py, following the prompts provided in both files.

Notes for practice: Django-specific logging

First, head over to the settings.py file and complete the TODOs to create
and configure a new Logger. Afterward, use the newly created Logger in
horus/views/weather.py, following the given prompts to add logging to the
file.

Once you have completed the prompts, you can refer to the solution by changing
to the solution branch using the command below:

git switch specific-solutions

Return to the settings.py file in your text editor:

settings.py

LOGGING = {
    # ...
    'handlers': {
        # ...
        'file': {
            'class': 'logging.FileHandler',
            'formatter': 'base',

'level': 'WARNING',

'filename': 'django-logs.txt'

} }, # ... }

In settings.py, a new FileHandler called file is added to the handlers
dictionary. Like the FileHandler from the previous section on
Django-independent logging, this FileHandler will have a minimum level of
WARNING and above. It also needs to have a filename value specified and it
uses the base formatter declared earlier.

horus/settings.py

LOGGING = {
    # ...
    'loggers': {
        # ...
        'horus.views.weather': {
            'handlers': ['console', 'file'],
            'level': 'INFO'
        }
    }
}

Secondly, the horus.views.weather logger is also created and it logs to both
the console and file handlers.

For the prompts in views/weather.py, follow the same guidelines as the ones
highlighted in the previous section. Because of Django’s centralized logging
infrastructure, we can use logging.getLogger() to specify the name of the
Logger we’d like to use as long as it is defined in settings.py.

horus/views/weather.py

logger = logging.getLogger('horus.views.weather')

Once you have explored the suggested solution for this section, change back to
the main branch.

We have just explored how Django-independent and Django-specific logging works
and the differences between them. Let us now look at log aggregation systems,
why they are necessary, and how to get started quickly with aggregating your
Django logs in the cloud

Using Log Aggregation systems

Since production applications are often deployed to multiple servers, it is
necessary to centralize the logs so that you don’t have to log into each server
to view them and also so they can be analyzed and monitored for problems
automatically. There are many solutions for centralizing application logs but we
will go with the simplest option here which is using a hosted cloud service. For
this section, we have opted to use Logtail
due to its simplicity in getting started.

Setting up a Logtail account

To begin, create a free Logtail account and
set up a new log source:

Retrieve the source token and copy it to your system clipboard:

Add the source token to your .env file as shown below:

.env

...
LOGTAIL_SOURCE_TOKEN=<source token>

This step is incredibly important as without it, your project will not be able
to integrate with Logtail.

Finally, create a new branch on the repository and call it logtail:

We have now successfully configured a Logtail source so we will integrate it
with our Horus application to ensure that all logs are sent to the service.

Integrating Horus with Logtail

To get started, let us work within views/index.py by adding a Logtail handler:

horus/views/index.py

import environ

from logtail import LogtailHandler

# ...

env = environ.Env()

env.read_env(env.str('ENV_PATH', '.env'))

lh = LogtailHandler(source_token=env('LOGTAIL_SOURCE_TOKEN'))

lh.setFormatter(formatter)

logger.addHandler(lh)

Notice that the general steps to set up Logtail in views/index.py is similar
to the steps taken in previous sections. Now, we can kill the Horus server with
Ctrl-C and start it up once again:

python manage.py runserver

Visit the Horus homepage to generate a log record. Afterward, head over to the
Live tail section on the Logtail dashboard. You should notice the log entry
in the dashboard as follows:

Now that you have a general notion of how Logtail is integrated with
Django-independent logging infrastructure, try modifying the other files to use
LogtailHandler as well. Hint: You only need to modify openweather.py and
settings.py.

For Django-specific logging, note that the source_token argument is set as a
key in the corresponding handler in settings.py. Also remember to add the
handler to the respective loggers. When properly configured, you will only need
to add the LogtailHandler to the respective Loggers once in
horus/settings.py and the centralized logging infrastructure will handle the
rest accordingly.

To view the complete solutions, change to the solution branch:

git switch logtail-solutions

horus/openweather.py

. . .
lh = LogtailHandler(source_token=env('LOGTAIL_SOURCE_TOKEN'))
lh.setFormatter(formatter)
logger.addHandler(lh)
. . .

Setting up Logtail in the openweather.py file involves adding a
LogtailHandler to the existing Logger as demonstrated above.

horus/settings.py

. . .
LOGGING = {
    # ...
    'handlers': {
        # ...
        'logtail': {
            'class': 'logtail.LogtailHandler',
            'formatter': 'base',
            'source_token': env('LOGTAIL_SOURCE_TOKEN')
        },
        # ...
    },
    # ...
}
. . .

In settings.py, we need to add a LogtailHandler to our handlers
dictionary, as we have done for both StreamHandler and FileHandler. This
time, we specified the source_token key to be the LOGTAIL_SOURCE_TOKEN added
to .env earlier.

Afterward, we can add this new logtail handler to any of the loggers as shown
below:

horus/settings.py

LOGGING = {
    # ...
    'loggers': {
        'horus.views.search': {

'handlers': ['console', 'logtail'],

'level': 'INFO' }, 'horus.views.weather': {

'handlers': ['console', 'file', 'logtail'],

'level': 'INFO' } } }

Once you successfully integrated Logtail in your application, you will observe
the application logs in the Live tail section.

Demonstrating multi-user access and logging

To visualize how log aggregation systems enhance logging for concurrently
accessed applications, we can simulate multi-user access using the following
variations of browser tabs:

  1. 1 incognito tab + 1 non-incognito tab of the same browser
  2. 1 non-incognito tab from two separate browsers

Note that any other variation may cause Django to mix up the UUIDs assigned per
tab, so use only the two variations provided above.

Access http://localhost:8000 on each tab and you will find that the user
sessions are maintained across each tab (simulating a unique user) and whenever
an action is performed in a tab, it is properly attributed and logged under the
corresponding user.

From the logs above, we can understand that one user attempted to search for an
invalid location while the other searched for «Singapore».

Notice how exc_info displayed the full exception caused by the invalid search,
providing as much contextual information as possible about the problem for
further diagnosis.

You can see that with a log aggregation system like Logtail, all the logs are
centralized and easily viewed, making problem diagnosis a lot easier.

Final thoughts

While you can build your logging infrastructure from the ground up, it is often
best to consult the documentation of the library/framework you are using as they
usually provide built-in mechanisms to enhance the logging process.

In Django, it comes in the form of modifying a central settings.py file with
the relevant logging infrastructure and retrieving the relevant configuration
with logging.getLogger() in any program file.

I hope this article has provided sufficient details to help you get started with
logging in your Django application. If you are interested in learning more about
logging in Django, please refer to the
official documentation.

This article was contributed by guest author
Woo Jia Hao, a Software Developer from
Singapore! He is an avid learner who loves solving and talking about complex and
interesting problems. Lately, he has been working with Go and Elixir!

Centralize all your logs into one place.

Analyze, correlate and filter logs with SQL.

Create actionable

dashboards.

Share and comment with built-in collaboration.

Got an article suggestion?
Let us know

Share on Twitter

Share on Facebook

Share via e-mail

Next article

How to Get Started with Logging in Flask

Learn how to start logging with Flask and go from basics to best practices in no time.

Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Понравилась статья? Поделить с друзьями:
  • Django custom error page
  • Django core exceptions improperlyconfigured error loading psycopg2 module no module named psycopg2
  • Django core exceptions improperlyconfigured error loading mysqldb module
  • Django authenticate error
  • Django assert error