Python logging
last modified January 29, 2024
Python logging tutorial shows how to do logging in Python with the logging module.
Logging
Logging is the process of writing information into log files. Log files contain information about various events that happened in operating system, software, or in communication.
Purpose of logging
Logging is done for the following purposes:
- Information gathering
- Troubleshooting
- Generating statistics
- Auditing
- Profiling
Logging is not limited to identifying errors in software development. It is also used in detecting security incidents, monitoring policy violations, providing information in case of problems, finding application bottlenecks, or generating usage data.
Which events to log
Events that should be logged include input validation failures, authentication and authorization failures, application errors, configuration changes, and application start-ups and shut-downs.
Which events not to log
Events that should not be logged include application source code, session identification values, access tokens, sensitive personal data, passwords, database connection strings, encryption keys, bank account and card holder data.
Logging best practices
The following are some best practices for doing logging:
- Logging should be meaningful.
- Logging should contain context.
- Logging should structured and done at different levels.
- Logging should be balanced; it should not include too little or too much information.
- Logging messages should be understandable to humans and parseable by machines.
- Logging in more complex applications should be done into several log files.
- Logging should be adapted to development and to production.
The logging module
Python logging module defines functions and classes which implement a flexible event logging system for applications and libraries.
The logging module components
The logging module has four main components: loggers, handlers, filters, and formatters. Loggers expose the interface that application code directly uses. Handlers send the log records (created by loggers) to the appropriate destination. Filters provide a finer grained facility for determining which log records to output. Formatters specify the layout of log records in the final output.
Python logging hierarchy
Python loggers form a hierarchy. A logger named main
is a parent
of main.new
.
Child loggers propagate messages up to the handlers associated with their ancestor loggers. Because of this, it is unnecessary to define and configure handlers for all the loggers in the application. It is sufficient to configure handlers for a top-level logger and create child loggers as needed.
Python logging levels
Levels are used for identifying the severity of an event. There are six logging levels:
- CRITICAL
- ERROR
- WARNING
- INFO
- DEBUG
- NOTSET
If the logging level is set to WARNING
, all WARNING
,
ERROR
, and CRITICAL
messages are written to the log file
or console. If it is set to ERROR
, only ERROR
and
CRITICAL
messages are logged.
Loggers have a concept of effective level. If a level is not explicitly set on a logger, the level of its parent is used instead as its effective level. If the parent has no explicit level set, its parent is examined, and so on - all ancestors are searched until an explicitly set level is found.
When the logger is created with getLogger
, the level is set to
NOTSET
. If the logging level is not set explicitly with setLevel
,
the messages are propagated to the logger parents. The logger's chain of ancestor loggers
is traversed until either an ancestor with a level other than NOTSET
is found,
or the root is reached. The root logger has a default WARNING
level set.
Root logger
All loggers are descendants of the root logger. Each logger passes log messages
on to its parent. New loggers are created with the getLogger(name)
method. Calling the function without a name (getLogger
) returns
the root logger.
The root logger always has an explicit level set, which is WARNING
by default.
The root looger sits at the top of the hierarchy and is always present, even if not configured. In general, the program or library should not log directly against the root logger. Instead a specific logger for the program should be configured. Root logger can be used to easily turn all loggers from all libraries on and off.
Python logging simple example
The logging
module has simple methods that can be used right
away without any configuration. This can be used for simple logging.
#!/usr/bin/python import logging logging.debug('This is a debug message') logging.info('This is an info message') logging.warning('This is a warning message') logging.error('This is an error message') logging.critical('This is a critical message')
The example calls five methods of the logging
module.
The messages are written to the console.
$ simple.py WARNING:root:This is a warning message ERROR:root:This is an error message CRITICAL:root:This is a critical message
Notice that root logger is used and only three messages were written. This is because by default, only messages with level warning and up are written.
Python set logging level
The logging level is set with setLevel
.
It sets the threshold for this logger to lvl
.
Logging messages which are less severe than lvl
will be ignored.
#!/usr/bin/python import logging logger = logging.getLogger('dev') logger.setLevel(logging.DEBUG) logger.debug('This is a debug message') logger.info('This is an info message') logger.warning('This is a warning message') logger.error('This is an error message') logger.critical('This is a critical message')
In the example, we change the logging level to DEBUG
.
logger = logging.getLogger('dev')
The getLogger
returns a logger with the specified name.
If name is None
, it returns the root logger. The name can
be a dot separated string defining logging hierarchy; for instance
'a', 'a.b', or 'a.b.c'. Note that there is an implicit root name, which
is not shown.
$ set_level.py This is a warning message This is an error message This is a critical message
Now all the messages were written.
Python effective logging level
The effective logging level is the level set explicitly or determined from the logger parents.
#!/usr/bin/python import logging main_logger = logging.getLogger('main') main_logger.setLevel(5) dev_logger = logging.getLogger('main.dev') print(main_logger.getEffectiveLevel()) print(dev_logger.getEffectiveLevel())
In the example, we examine the effective logging level of two loggers.
dev_logger = logging.getLogger('main.dev')
The level of the dev_logger
is not set; the level of
its parent is then used.
$ effective_level.py 5 5
Python logging handlers
Handler is an object responsible for dispatching the appropriate log messages (based on the log messages' severity) to the handler's specified destination.
Handlers are propagated like levels. If the logger has no handler set, its chain of ancestors is search for a handler.
#!/usr/bin/python import logging logger = logging.getLogger('dev') logger.setLevel(logging.INFO) fileHandler = logging.FileHandler('test.log') fileHandler.setLevel(logging.INFO) consoleHandler = logging.StreamHandler() consoleHandler.setLevel(logging.INFO) logger.addHandler(fileHandler) logger.addHandler(consoleHandler) logger.info('information message')
The example creates two handlers for a logger: a file handler and a console handler.
fileHandler = logging.FileHandler('test.log')
FileHandler
sends log records to the test.log
file.
consoleHandler = logging.StreamHandler()
StreamHandler
sends log records to a stream.
If the stream is not specified, the sys.stderr
is used.
logger.addHandler(fileHandler)
The handler is added to the logger with addHandler
.
Python logging formatters
Formatter is an object which configures the final order, structure, and contents of the log record. In addition to the message string, log records also include date and time, log names, and log level severity.
#!/usr/bin/python import logging logger = logging.getLogger('dev') logger.setLevel(logging.INFO) consoleHandler = logging.StreamHandler() consoleHandler.setLevel(logging.INFO) logger.addHandler(consoleHandler) formatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s') consoleHandler.setFormatter(formatter) logger.info('information message')
The example creates a console logger and adds a formatter to its handler.
formatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s')
The formatter is created. It includes the date time, logger name, logging level name, and log message.
consoleHandler.setFormatter(formatter)
The formatter is set to the handler with setFormatter
.
$ formatter.py 2019-03-28 14:53:27,446 dev INFO: information message
The message with the defined format is shown in the console.
Python logging basicConfig
The basicConfig
configures the root logger. It does basic
configuration for the logging system by creating a stream handler with a default
formatter. The debug
, info
,
warning
, error
and critical
call
basicConfig
automatically if no handlers are defined for the root logger.
#!/usr/bin/python import logging logging.basicConfig(filename='test.log', format='%(filename)s: %(message)s', level=logging.DEBUG) logging.debug('This is a debug message') logging.info('This is an info message') logging.warning('This is a warning message') logging.error('This is an error message') logging.critical('This is a critical message')
The example configures the root logger with basicConfig
.
logging.basicConfig(filename='test.log', format='%(filename)s: %(message)s', level=logging.DEBUG)
With filename
, we set the file to which we write the log messages.
The format
determines what is logged into the file; we have
the filename and the message. With level
, we set the logging treshold.
$ basic_config.py $ cat test.log basic_config.py: This is a debug message basic_config.py: This is an info message basic_config.py: This is a warning message basic_config.py: This is an error message basic_config.py: This is a critical message
After running the program, we have five messages written into the
test.log
file.
Python logging fileConfig
The fileConfig
reads the logging configuration from
a configparser format file.
[loggers] keys=root,dev [handlers] keys=consoleHandler [formatters] keys=extend,simple [logger_root] level=INFO handlers=consoleHandler [logger_dev] level=INFO handlers=consoleHandler qualname=dev propagate=0 [handler_consoleHandler] class=StreamHandler level=INFO formatter=extend args=(sys.stdout,) [formatter_extend] format=%(asctime)s - %(name)s - %(levelname)s - %(message)s [formatter_simple] format=%(asctime)s - %(message)s
The log.conf
defines a logger, handler, and formatter.
#!/usr/bin/python import logging import logging.config logging.config.fileConfig(fname='log.conf') logger = logging.getLogger('dev') logger.info('This is an information message')
The example reads the logging configuration file from
the log.conf
.
$ file_config.py 2019-03-28 15:26:31,137 - dev - INFO - This is an information message
Python logging variable
Dynamic data is logged by using string formatting.
#!/usr/bin/python import logging root = logging.getLogger() root.setLevel(logging.INFO) log_format = '%(asctime)s %(filename)s: %(message)s' logging.basicConfig(filename="test.log", format=log_format) # incident happens error_message = 'authentication failed' root.error(f'error: {error_message}')
The example writes custom data to the log message.
2019-03-21 14:17:23,196 log_variable.py: error: authentication failed
This is the log message.
Python logging format datetime
The datetime is included in the log message with the asctime
log record. With the datefmt
configuration option, we can format
the datetime string.
#!/usr/bin/python import logging logger = logging.getLogger() logger.setLevel(logging.DEBUG) log_format = '%(asctime)s %(filename)s: %(message)s' logging.basicConfig(filename="test.log", format=log_format, datefmt='%Y-%m-%d %H:%M:%S') logger.info("information message")
The example formats the datetime of the log message.
log_format = '%(asctime)s %(filename)s: %(message)s'
We include the datetime string into the log with asctime
.
logging.basicConfig(filename="test.log", format=log_format, datefmt='%Y-%m-%d %H:%M:%S')
The datefmt
option formats the datetime string.
2019-03-21 14:17:23,196 log_variable.py: error: authentication failed 2019-03-21 14:23:33 date_time.py: information message
Notice the difference in the datetime string format.
Python logging stack trace
The stack trace is a call stack of functions that were run
up to the point of a thrown exceptions. The stack trace is
included with the exc_info
option.
#!/usr/bin/python import logging log_format = '%(asctime)s %(filename)s: %(message)s' logging.basicConfig(filename="test.log", format=log_format) vals = [1, 2] try: print(vals[4]) except Exception as e: logging.error("exception occurred", exc_info=True)
In the example, we log the exception that is thrown when we try to access a non-existing list index.
logging.error("exception occurred", exc_info=True)
The stack trace is included in the log by setting the exc_info
to True
.
2019-03-21 14:56:21,313 stack_trace.py: exception occurred Traceback (most recent call last): File "C:\Users\Jano\Documents\pyprogs\pylog\stack_trace.py", line 11, in <module> print(vals[4]) IndexError: list index out of range
The stack trace is included in the log.
Python logging getLogger
The getLogger
returns a logger with the specified name.
If no name is specified, it returns the root logger. It is a common practice
to put the module name there with __name__
.
All calls to this function with a given name return the same logger instance. This means that logger instances never need to be passed between different parts of an application.
#!/usr/bin/python import logging import sys main = logging.getLogger('main') main.setLevel(logging.DEBUG) handler = logging.FileHandler('my.log') format = logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s') handler.setFormatter(format) main.addHandler(handler) main.info('info message') main.critical('critical message') main.debug('debug message') main.warning('warning message') main.error('error message')
The example creates a new logger with getLogger
.
It is given a file handler and a formatter.
main = logging.getLogger('main') main.setLevel(logging.DEBUG)
A logger named main
is created; we set the
logging level to DEBUG
.
handler = logging.FileHandler('my.log')
A file handler is created. The messages will be written to the
my.log
file.
format = logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s') handler.setFormatter(format)
A formatter is created. It includes the time, the logger name,
the logging level, and the message in to log. The formatter is
set to the handler with setFormatter
.
main.addHandler(handler)
The handler is added to the logger with addHandler
.
$ cat my.log 2019-03-21 14:15:45,439 main INFO: info message 2019-03-21 14:15:45,439 main CRITICAL: critical message 2019-03-21 14:15:45,439 main DEBUG: debug message 2019-03-21 14:15:45,439 main WARNING: warning message 2019-03-21 14:15:45,439 main ERROR: error message
These are the written log messages.
Python logging YAML configuration
Logging details can be defined in a YAML configuration file. YAML is a human-readable data serialization language. It is commonly used for configuration files.
$ pip install pyyaml
We need to install pyyaml
module.
version: 1 formatters: simple: format: "%(asctime)s %(name)s: %(message)s" extended: format: "%(asctime)s %(name)s %(levelname)s: %(message)s" handlers: console: class: logging.StreamHandler level: INFO formatter: simple file_handler: class: logging.FileHandler level: INFO filename: test.log formatter: extended propagate: false loggers: dev: handlers: [console, file_handler] test: handlers: [file_handler] root: handlers: [file_handler]
In the configuration file, we have defined various formatters, handlers, and
loggers. The propagate
option prevents from propagating
log messages to the parent loggers; in our case, to the root logger.
Otherwise, the messages would be duplicated.
#!/usr/bin/python import logging import logging.config import yaml with open('config.yaml', 'r') as f: log_cfg = yaml.safe_load(f.read()) logging.config.dictConfig(log_cfg) logger = logging.getLogger('dev') logger.setLevel(logging.INFO) logger.info('This is an info message') logger.error('This is an error message')
In the example, we read the configuration file and use
the dev
logger.
$ log_yaml.py 2019-03-28 11:36:54,854 dev: This is an info message 2019-03-28 11:36:54,855 dev: This is an error message
When we run the program, there are two messages on the console. The console handlers use the simple formatter with less information.
... 2019-03-28 11:36:54,854 dev INFO: This is an info message 2019-03-28 11:36:54,855 dev ERROR: This is an error message
There are log messages inside the test.log
file.
They are produced by the extended formatter with more information.
Source
In this article we have worked with the Python logging library.
Author
List all Python tutorials.