What is Proper Logging?
Having a proper logger is essential for any production application. In the Java world, almost every framework automatically pulls in Logback or Log4J, and libraries tend to use SLF4J in order to be logger agnostic and to wire up to these loggers. So, I had to set out to see how to do similar logging in python.
While it can get fancier, I think the following things are essential when setting up a logger; so they were what I was looking for:
- It should be externally configured from a file that your operations team can change.
- It should write to a file automatically, not just console.
- It should roll the file it writes to at a regular size (date or time rolling on top of that can be beneficial too; but the size restriction ensures you won’t fill up your disk with a ton of logs and break your applications).
- It should keep a history of a few previous rolled files to aid debugging.
- It should use a format that specifies both the time of the logs and the class that logged them.
On top of these, obviously we must be able to log at different levels and filter out which logs go to the file easily. This way, when we have issues, operations can jack up the logging level and figure out what is going wrong as needed.
How Do We Do it in Python 3?
It turns out that Python actually has a strong logging library built into its core distribution. The only extra library I had to add to use it was PyYAML, and even that could have been avoided (Python supports JSON out of the box and that could be used instead, but people seem to prefer YAML configuration in the community).
In the place where your app starts up, write the following code. Note that you have to install the PyYAML module yourself. Also, this expects the “logging.yaml” to be in the same directory as the startup code (change that if you like though). We’ll show the “logging.yaml” content lower.
import logging import logging.config import yaml # Initialize the logger once as the application starts up. with open("logging.yaml", 'rt') as f: config = yaml.safe_load(f.read()) logging.config.dictConfig(config) # Get an instance of the logger and use it to write a log! # Note: Do this AFTER the config is loaded above or it won't use the config. logger = logging.getLogger(__name__) logger.info("Configured the logger!")
Then, when you want to use the logger in other modules, simply do this:
import logging logger.info("Using the logger from another module.")
Of course, you just have to import logging once at the top of each module, not every time you write a log.
This code uses “logging.yaml” which contains the following settings. Note that:
- It defines a formatter with the time, module name, level name, and the logging message.
- It defines a rotating file handler which writes to my.log and rolls the file at 10MB, keeping 5 historic copies. The handler is set up to use our simple format from above.
- The “root” logger writes to our handler and allows only INFO messages through.
- The handler is set to DEBUG, so if the root logger is increased to DEBUG during an investigation, it will let the messages through to the log file.
Here is the “logging.yaml” example file:
--- version: 1 disable_existing_loggers: False # Define format of output logs (named 'simple'). formatters: simple: format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s" handlers: # Create rotating file handler using 'simple' format. file_handler: class: logging.handlers.RotatingFileHandler level: INFO formatter: simple filename: operations-audit-query-service.log maxBytes: 10485760 # 10MB backupCount: 5 encoding: utf8 root: level: INFO handlers: [file_handler]
The code and YAML for this was adapted from this very good blog which I recommend reading: https://fangpenlin.com/posts/2012/08/26/good-logging-practice-in-python/.