Patrick's Software Blog

Learning Python Web Application Development with Flask

Python Logging Tutorial

Introduction

Somewhere in between getting your python project to run and getting to the point where even a debugger won’t help you find a bug, you might realize that creating a log file of what your program is doing could be really beneficial. I’ve typically found that once you get a working version of your program, you’ll want to really understand what is happening during subsequent executions of the program. The simplest (note: not best) way to accomplish this is to put lots of print statements in your code. However, this is really a bad idea as you’re going to be getting lots of print out in your console and you are likely going to be deleting these print statements once you get a bug resolved. So what’s a better approach? Logging!

Python has a built-in library called ‘logging’, which is a great library for logging your program’s execution to a file. The tutorial for ‘logging’ provides a good range of examples from basic to more advanced uses of the library. However, I want to illustrate how I got the ‘logging’ module to work successfully in this blog post.

The source code for these examples can be found on GitLab, organized by tags:
https://gitlab.com/patkennedy79/python_logging

Example #1 – Single File Application

tag: v0.1 – https://gitlab.com/patkennedy79/python_logging/tree/v0.1

The first example is a simple program to help illustrate the basic capabilities of the ‘logging’ module. This program is comprised of a single file named app.py that contains a single class:

class FirstClass(object):
    def __init__(self):
        self.current_number = 0

   def increment_number(self):
        self.current_number += 1

    def decrement_number(self):
        self.current_number -= 1

    def clear_number(self):
        self.current_number = 0

number = FirstClass()
number.increment_number()
number.increment_number()
print "Current number: %s" % str(number.current_number)
number.clear_number()
print "Current number: %s" % str(number.current_number)

You can run this program by executing:

$ python run.py 
Current number: 2
Current number: 0

This program uses two print statements to print to the console. This is fine for getting a program to work, but switching over to logging messages is a better long-term approach.

Configuring the Logging Module

The configuration of the logging module can get complex as you start to specify more and more details of how the logging should be performed. I’ve found the following sequence provides a good configuration for:

  • setting the logging severity level
  • setting the file to log messages to
  • setting the format of the log messages

In order to configure the logging module, the constructor for the FirstClass class should be updated to:

    def __init__(self):
        self.current_number = 0

        # Create the Logger
        self.logger = logging.getLogger(__name__)
        self.logger.setLevel(logging.WARNING)

        # Create the Handler for logging data to a file
        logger_handler = logging.FileHandler('python_logging.log')
        logger_handler.setLevel(logging.WARNING)

        # Create a Formatter for formatting the log messages
        logger_formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')

        # Add the Formatter to the Handler
        logger_handler.setFormatter(logger_formatter)

        # Add the Handler to the Logger
        self.logger.addHandler(logger_handler)
        self.logger.info('Completed configuring logger()!')

The constructor configures the usage of the logging module and finishes by logging a message that the configuration of the logger is completed.

Adding Log Messages

In order to add log messages, you can utilize one of the methods from the logging module to log different severity log messages:

    def increment_number(self):
        self.current_number += 1
        self.logger.warning('Incrementing number!')
        self.logger.info('Still incrementing number!!')

    def clear_number(self):
        self.current_number = 0
        self.logger.warning('Clearing number!')
        self.logger.info('Still clearing number!!')

If you run the program again, you’ll still see the same console output.

$ python run.py 
Current number: 2
Current number: 0

Logging Severity Levels

To see what the logging module is doing, check the log file that was created:

$ cat python_logging.log 
__main__ - WARNING - Incrementing number!
__main__ - WARNING - Incrementing number!
__main__ - WARNING - Clearing number!

Interesting, is that what you expected? I initially thought that there should be two “Still incrementing number!!” statements, but they don’t get displayed. Why? Well, the ‘logging’ module has 5 severity levels:

  • DEBUG (lowest)
  • INFO
  • WARNING
  • ERROR
  • CRITICAL (highest)

Our program is using the default setting (WARNING) for the logging severity, which means that any log message with a lower severity than WARNING will not be display. Hence, the INFO messages are not displayed.

Change the following lines in __init__():

        # Create the Logger
        self.logger = logging.getLogger(__name__)
        self.logger.setLevel(logging.WARNING)

        # Create the Handler for logging data to a file
        logger_handler = logging.FileHandler('python_logging.log')
        logger_handler.setLevel(logging.WARNING)

Now check the log file to see that all of the log messages are included:

$ cat python_logging.log 
__main__ - WARNING - Incrementing number!
__main__ - WARNING - Incrementing number!
__main__ - WARNING - Clearing number!
__main__ - INFO - Completed configuring logger()!
__main__ - WARNING - Incrementing number!
__main__ - INFO - Still incrementing number!!
__main__ - WARNING - Incrementing number!
__main__ - INFO - Still incrementing number!!
__main__ - WARNING - Clearing number!
__main__ - INFO - Still clearing number!!

Example #2 – Logging in a Module

tag: v0.2 – https://gitlab.com/patkennedy79/python_logging/tree/v0.2

The second example is slightly more complex, as it updates the structure of the program to include a package with a single module:

python_logging
    python_logging
        __init__.py
        first_class.py
    run.py

The first_class.py file basically contains the FirstClass class that was created in the first example:

import logging


class FirstClass(object):
    def __init__(self):
        self.current_number = 0
        self.logger = logging.getLogger(__name__)

        # Create the Logger
        self.logger = logging.getLogger(__name__)
        self.logger.setLevel(logging.DEBUG)

        # Create the Handler for logging data to a file
        logger_handler = logging.FileHandler('python_logging.log')
        logger_handler.setLevel(logging.DEBUG)

        # Create a Formatter for formatting the log messages
        logger_formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')

        # Add the Formatter to the Handler
        logger_handler.setFormatter(logger_formatter)

        # Add the Handler to the Logger
        self.logger.addHandler(logger_handler)
        self.logger.info('Completed configuring logger()!')

    def increment_number(self):
        self.current_number += 1
        self.logger.warning('Incrementing number!')
        self.logger.info('Still incrementing number!!')

    def decrement_number(self):
        self.current_number -= 1

    def clear_number(self):
        self.current_number = 0
        self.logger.warning('Clearing number!')
        self.logger.info('Still clearing number!!')

In order to utilize this module, update the run.py file in the top-level directory to import the FirstClass class to utilize it:

from python_logging.first_class import FirstClass


number = FirstClass()
number.increment_number()
number.increment_number()
print "Current number: %s" % str(number.current_number)
number.clear_number()
print "Current number: %s" % str(number.current_number)

Check that the log file is unchanged.

Example #3: Logging in a Package (Part I)

tag: v0.3 – https://gitlab.com/patkennedy79/python_logging/tree/v0.3

The third example adds a second class to the python_logging package to show how to configure the logging module for a whole package. Here is the structure of this example:

python_logging
    python_logging
        __init__.py
        first_class.py
        second_class.py
    run.py

Here is the basic version of the second_class.py file:

class SecondClass(object):
    def __init__(self):
        self.enabled = False

    def enable_system(self):
        self.enabled = True

    def disable_system(self):
        self.enabled = False

Your first inclination might be to duplicate the configuration of the logger in the constructor of this class (copy from first_class.py). This would result in a lot of unnecessary, repetitive code. A better option is to move the configuration of the logger to the __init__.py file:

from os import path, remove
import logging
import logging.config

from .first_class import FirstClass
from .second_class import SecondClass


# If applicable, delete the existing log file to generate a fresh log file during each execution
if path.isfile("python_logging.log"):
    remove("python_logging.log")

# Create the Logger
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)

# Create the Handler for logging data to a file
logger_handler = logging.FileHandler('python_logging.log')
logger_handler.setLevel(logging.DEBUG)

# Create a Formatter for formatting the log messages
logger_formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')

# Add the Formatter to the Handler
logger_handler.setFormatter(logger_formatter)

# Add the Handler to the Logger
logger.addHandler(logger_handler)
logger.info('Completed configuring logger()!')

Depending on the type of program you are creating, you might find it beneficial to delete any existing log files prior to logging any new messages from this execution of the program. One option to consider if you want to maintain an on-going log of an application is the RotatingFileHandler within the logging module.

Now that the configuration of the logging module is done in __init__.py, the second_class.py file can be greatly simplified to utilize the logger, instead of having to worry about configuring it first:

import logging


class SecondClass(object):
    def __init__(self):
        self.enabled = False
        self.logger = logging.getLogger(__name__)

    def enable_system(self):
        self.enabled = True
        self.logger.warning('Enabling system!')
        self.logger.info('Still enabling system!!')

    def disable_system(self):
        self.enabled = False
        self.logger.warning('Disabling system!')
        self.logger.info('Still disabling system!!')

There are similar updates to the first_class.py file.

Finally, the updates to __init__.py result in needing the following updates to run.py:

from python_logging import FirstClass, SecondClass


number = FirstClass()
number.increment_number()
number.increment_number()
print "Current number: %s" % str(number.current_number)
number.clear_number()
print "Current number: %s" % str(number.current_number)

system = SecondClass()
system.enable_system()
system.disable_system()
print "Current system configuration: %s" % str(system.enabled)

Try running the program again and looking at the log file:

$ cat python_logging.log 
python_logging - INFO - Completed configuring logger()!
python_logging.first_class - WARNING - Incrementing number!
python_logging.first_class - INFO - Still incrementing number!!
python_logging.first_class - WARNING - Incrementing number!
python_logging.first_class - INFO - Still incrementing number!!
python_logging.first_class - WARNING - Clearing number!
python_logging.first_class - INFO - Still clearing number!!
python_logging.second_class - WARNING - Enabling system!
python_logging.second_class - INFO - Still enabling system!!
python_logging.second_class - WARNING - Disabling system!
python_logging.second_class - INFO - Still disabling system!!

Notice how the module names are printed! This is a really handy feature to quickly identify where specific operations are happening.

Example #4: Logging in a Package (Part II)

tag: v0.4 – https://gitlab.com/patkennedy79/python_logging/tree/v0.4

The fourth (and final) example expands upon the logging capability that was added to a package by adding an input file (JSON) to configure the logger. Keep an eye out for how the log messages are unaffected by this configuration change…

The first change for this example is to __init__.py to change the configuration of the logger to utilize a JSON input file:

from os import path, remove
import logging
import logging.config
import json

from .first_class import FirstClass
from .second_class import SecondClass


# If applicable, delete the existing log file to generate a fresh log file during each execution
if path.isfile("python_logging.log"):
    remove("python_logging.log")

with open("python_logging_configuration.json", 'r') as logging_configuration_file:
    config_dict = json.load(logging_configuration_file)

logging.config.dictConfig(config_dict)

# Log that the logger was configured
logger = logging.getLogger(__name__)
logger.info('Completed configuring logger()!')

Now that we have the code to process the input file, let’s define the input file (python_logging_configuration.json). Make sure to include this file in your top-level folder so that it can be easily identified by the python interpreter. You should be running this program from the top-level folder, so that will make the input JSON file accessible by the python interpreter since it is in the current/working directory.

Here is the configuration file:

{
    "version": 1,
    "disable_existing_loggers": false,
    "formatters": {
        "simple": {
            "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
        }
    },

    "handlers": {
        "file_handler": {
            "class": "logging.FileHandler",
            "level": "DEBUG",
            "formatter": "simple",
            "filename": "python_logging.log",
            "encoding": "utf8"
        }
    },

    "root": {
        "level": "DEBUG",
        "handlers": ["file_handler"]
    }
}

Run the program again and take a look at the log file:

$ cat python_logging.log 
2017-03-09 22:32:01,846 - python_logging - INFO - Completed configuring logger()!
2017-03-09 22:32:01,847 - python_logging.first_class - WARNING - Incrementing number!
2017-03-09 22:32:01,847 - python_logging.first_class - INFO - Still incrementing number!!
2017-03-09 22:32:01,847 - python_logging.first_class - WARNING - Incrementing number!
2017-03-09 22:32:01,847 - python_logging.first_class - INFO - Still incrementing number!!
2017-03-09 22:32:01,847 - python_logging.first_class - WARNING - Clearing number!
2017-03-09 22:32:01,847 - python_logging.first_class - INFO - Still clearing number!!
2017-03-09 22:32:01,848 - python_logging.second_class - WARNING - Enabling system!
2017-03-09 22:32:01,848 - python_logging.second_class - INFO - Still enabling system!!
2017-03-09 22:32:01,848 - python_logging.second_class - WARNING - Disabling system!
2017-03-09 22:32:01,848 - python_logging.second_class - INFO - Still disabling system!!

The log file is almost the same, but the date/time stamp has been added to each log message. I like this format of logging messages: date/time – package.module – log message

If JSON is not your favorite, the input file can also be defined as a YAML file. Here’s an example: Good Logging Practice in Python

Conclusion

The idea of moving beyond print statements to actually logging messages is made so easy thanks to the logging module that is built-in to python. The logging module requires a bit of configuration, but this is a small price to pay to such an easy-to-use module.

I’ve found that having a log file for a program, especially command-line applications, provides a great way to understand what the program is doing. You might go weeks without looking at the log file if things are working well, but having a lot of data about your program’s execution readily available is so beneficial when things go wrong.

References

Python Logging Module Documentation: https://docs.python.org/3/howto/logging.html

Blog Post on Good Logging Practice in Python: https://fangpenlin.com/posts/2012/08/26/good-logging-practice-in-python/
 
Blog Post on Logging in Python: https://www.relaxdiego.com/2014/07/logging-in-python.html

2 Comments

  1. There appears to be a duplicate initialisation of self.logger with the getLogger calls in the second code listing of example 1 (lines 3 and 6). Very interesting article otherwise though, especially covering the different ways to implement logging.

    • patkennedy79@gmail.com

      March 20, 2017 at 5:47 am

      Great catch on the duplicate calls to getLogger() in the 1st example! I updated the code listing.

Leave a Reply

Your email address will not be published.

*