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 messagestack_info
defaults False, if True logging message contains stack information till logger is calledstacklevel
defaults 1, if not 1, the exact number of stack frames are skipped when computing the line number and function nameextra
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.
Django logging tips and best practices
- 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!