Logging¶
As you probably noticed almost no package, e.g. numpy
has print
functions. Most packages use so called logging. Logging enables you to turn
on/off printing depending on the logging mode. It is a standard python built-in,
so I suggest you make use of it
Setting up your logging¶
A standard practice is to set the logger in the matpy.__init__
using
Python’s built-in logging
module.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | from __future__ import (absolute_import, print_function, division)
import logging
from .log_util import VERBOSE
from .log_util import modify_logger
# Setup the logger
logger = logging.getLogger("matpy")
modify_logger(logger)
'''The different levels are
0: logging.NOTSET
10: logging.DEBUG
15: logging.VERBOSE
20: logging.INFO
30: logging.WARNING
40: logging.ERROR
50: logging.CRITICAL
'''
logger.setLevel(logging.VERBOSE)
'''The following line is simply used to make it possible to
.. code-block::
from matpy import MatrixMultiplication
instead of
.. code-block::
from matpy.matrixmultiplication import MatrixMultiplication
which is still a valid statement.
Note the "#NOQA" after the statement? This is used to note make an additional
automatic documentation entry.
'''
from .matrixmultiplication import MatrixMultiplication # NOQA
|
The outline is as follows:
Retrieve the module’s logger
Use how it outputs stuff using the
matpy.log_util.modify_logger()
Set logging level
All necessary code for modifying the logging is located in the
matpy.log_util
. The code is shown below and not all too complicated,
but it is ‘long’. Do not get intimidated and continue below!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | #!/usr/bin/env python
"""
This module includes a custom logger to make it easier to identify errors and
debug etc.
The module adds both another level to the logger and the corresponding
formatter. If you want to remove or add any logging level make sure to edit
both the CustomLogger and the CustomFormatter to accommodate your changes!
:author:
Lucas Sawade (lsawade@princeton.edu, 2019)
:license:
GNU Lesser General Public License, Version 3
(http://www.gnu.org/copyleft/lgpl.html)
"""
import logging
from logging import getLoggerClass
from logging import NOTSET
VERBOSE = 15
def addLoggingLevel(levelName, levelNum, methodName=None):
"""
Comprehensively adds a new logging level to the `logging` module and the
currently configured logging class.
`levelName` becomes an attribute of the `logging` module with the value
`levelNum`. `methodName` becomes a convenience method for both `logging`
itself and the class returned by `logging.getLoggerClass()` (usually just
`logging.Logger`). If `methodName` is not specified, `levelName.lower()` is
used.
To avoid accidental clobberings of existing attributes, this method will
raise an `AttributeError` if the level name is already an attribute of the
`logging` module or if the method name is already present
.. rubric:: Example
.. code-block:: python
addLoggingLevel('TRACE', logging.DEBUG - 5)
logging.getLogger(__name__).setLevel("TRACE")
logging.getLogger(__name__).trace('that worked')
logging.trace('so did this')
logging.TRACE
5
Taken from StackOverflow because the code was beautifully simple.
Author: Mad Physicist (Mar 4, 2016)
"""
if not methodName:
methodName = levelName.lower()
if hasattr(logging, levelName):
raise AttributeError(
'{} already defined in logging module'.format(levelName))
if hasattr(logging, methodName):
raise AttributeError(
'{} already defined in logging module'.format(methodName))
if hasattr(logging.getLoggerClass(), methodName):
raise AttributeError(
'{} already defined in logger class'.format(methodName))
# This method was inspired by the answers to Stack Overflow post
# http://stackoverflow.com/q/2183233/2988730, especially
# http://stackoverflow.com/a/13638084/2988730
def logForLevel(self, message, *args, **kwargs):
if self.isEnabledFor(levelNum):
self._log(levelNum, message, args, **kwargs)
def logToRoot(message, *args, **kwargs):
logging.log(levelNum, message, *args, **kwargs)
logging.addLevelName(levelNum, levelName)
setattr(logging, levelName, levelNum)
setattr(logging.getLoggerClass(), methodName, logForLevel)
setattr(logging, methodName, logToRoot)
class CustomLogger(getLoggerClass()):
"""
This class is just created ot add the VERBOSE level. More
level could be added to this class to accommodate other levels.
The variable VERBOSE is given at the top of the module. That way it can be
changed for all depending function
The class makes it possible to add extra levels to the classic logger
The line `addLoggingLevel("VERBOSE", VERBOSE)` in the initalization is
an example on how to add a level using the `addLoggingLevel` function
located in this module.
Don't forget to edit the `CustomFormatter` to accommodate for your
introduced levels if you are using the `CustomFormatter`.
An example is given in the class under `# EXTRA LEVELS` for the
`VERBOSE` level.
"""
def __init__(self, name, level=NOTSET):
super().__init__(name, level)
addLoggingLevel("VERBOSE", VERBOSE)
class CustomFormatter(logging.Formatter):
"""
Logging Formatter to add colors and count warning / errors
This class organizes the customization of the logging output.
The formatter as of now outputs the logs in the following manner in
order of Loglevel:
.. rubric:: Example Output
.. code-block:: python
[2020-04-03 14:17:18] -- matpy.matrixmultiplication ----- [INFO]: Initializing matrices...
[2020-04-03 14:17:18] -- matpy.matrixmultiplication ---- [ERROR]: Test Error Level (matrixmultiplication.py:60)
[2020-04-03 14:17:18] -- matpy.matrixmultiplication - [CRITICAL]: Test Critical Level (matrixmultiplication.py:61)
[2020-04-03 14:17:18] -- matpy.matrixmultiplication -- [VERBOSE]: Test Verbose Level
[2020-04-03 14:17:18] -- matpy.matrixmultiplication -- [VERBOSE]: A:
[2020-04-03 14:17:18] -- matpy.matrixmultiplication -- [VERBOSE]: [1 2]
[2020-04-03 14:17:18] -- matpy.matrixmultiplication -- [VERBOSE]: [3 4]
[2020-04-03 14:17:18] -- matpy.matrixmultiplication -- [VERBOSE]: B:
[2020-04-03 14:17:18] -- matpy.matrixmultiplication -- [VERBOSE]: [2 3 5]
[2020-04-03 14:17:18] -- matpy.matrixmultiplication -- [VERBOSE]: [4 5 6]
[2020-04-03 14:17:18] -- matpy.matrixmultiplication -- [WARNING]: Matrix size exceeds 4 elements.
These outputs are colored in the actual output but the formatting is just
as shown above. VERBOSE is an extra added LogLevel formatting. More can be
added below the comment `EXTRA LEVELS` in the same way the VERBOSE is added.
The variable VERBOSE is given at the top of the module. That way it can be
changed for all depending function
"""
# Setting up the different ANSI color escape sequences for color terminals:
# Codes are 3/4-bit codes from here:
# https://en.wikipedia.org/wiki/ANSI_escape_code
# For 8-colors: use "\x1b[38;5;<n>m" where <n> is the number of the color.
grey = "\x1b[38;21m"
green = "\x1b[38;5;64m"
light_grey = "\x1b[38;5;240m"
dark_blue = "\x1b[38;5;25m"
light_blue = "\x1b[38;5;69m"
yellow = "\x1b[33;21m"
red = "\x1b[31;21m"
dark_red = "\x1b[38;5;97m"
bold_red = "\x1b[31;1m"
reset = "\x1b[0m"
# Formats The spaces accommodate the different length of the words and
# amount of detail wanted in the message:
time_fmt = light_grey + "[%(asctime)s]" + reset
name_fmt = "-- %(name)s -"
pre_fmt = time_fmt + " " + name_fmt
debug_fmt = "--- [" + light_blue + "%(levelname)s" + reset + "]:" \
+ light_blue + " %(message)s (%(filename)s:%(lineno)d)" + reset
info_fmt = "---- [%(levelname)s]: %(message)s"
warning_fmt = "- [" + yellow + "%(levelname)s" + reset + "]:" \
+ yellow + " %(message)s" + reset
error_fmt = "--- [" + red + "%(levelname)s" + reset + "]:" \
+ red + " %(message)s (%(filename)s:%(lineno)d)" + reset
critical_fmt = " [" + bold_red + "%(levelname)s" + reset + "]:" \
+ bold_red + " %(message)s (%(filename)s:%(lineno)d)" + reset
# Create format dictionary
FORMATS = {
logging.DEBUG: pre_fmt + debug_fmt,
logging.INFO: pre_fmt + info_fmt,
logging.WARNING: pre_fmt + warning_fmt,
logging.ERROR: pre_fmt + error_fmt,
logging.CRITICAL: pre_fmt + critical_fmt
}
# EXTRA LEVELS
# Verbose
addLoggingLevel('VERBOSE', VERBOSE)
verbose_fmt = "- [%(levelname)s]: %(message)s"
FORMATS[logging.VERBOSE] = pre_fmt + verbose_fmt # add to format dictionary
# Initialize with a default logging.Formatter
def __init__(self):
super().__init__(fmt="-- %(name)s ---- [%(levelname)s]: %(message)s",
datefmt=None, style='%')
def format(self, record):
# Use the logging.LEVEL to get the right formatting
log_fmt = self.FORMATS.get(record.levelno)
# Create new formatter with modified timestamp formatting.
formatter = logging.Formatter(log_fmt, "%Y-%m-%d %H:%M:%S")
# Return
return formatter.format(record)
def modify_logger(logger):
# Make sure only this module prints the logger information.
logger.propagate = 0
# Add formatter
ch = logging.StreamHandler()
ch.setFormatter(CustomFormatter())
logger.addHandler(ch)
return logger
|
There are 3-4 important things here. The first one is the
matpy.log_util.CustomLogger
. The matpy.log_util.CustomLogger
simply takes logging.Logger
and adds another Logging (see below) level
by using the function matpy.log_util.addLoggingLevel()
. That’s all the
functions are doing. Of course there are some details in
matpy.log_util.addLoggingLevel()
, but those are simply there to handle
the adding if the matpy.log_util.CustomLogger
is called multiple times
so that the Logging level is not overwritten.
The most important part (why I started writing the logging docs) is the
matpy.log_util.CustomFormatter
.
The matpy.log_util.CustomFormatter
is a basic class containing some
info that specifies the nature of the logged print statements, i.e., colors and
whether a certain statement should be print depending on the logging levels etc.
It’s basic string formatting that depends on the level of the statements.
There are a number 6 different logging levels:
Not-set
Debug
Info
Warning
Error
Critical
In general, the lower the debug level the larger the number of messages. Meaning, if the level is set to 50, only Critical messages will be printed, but if the level is set to 20, Critical, Error, Warning, and Info messages will be printed.
We can in theory create a different format for any of those message-levels. When debugging for examples this can be a huge advantage, because you can assign a different color to the debugging messages and find the correct lines faster.
The logging level of any package can be set by any script from anywhere by retrieving the logger and setting the level:
import logging
# Retrieve logger
logger = logging.getLogger("matpy")
# Set level
logger.setLevel(logging.INFO)
For this package, we put set the level to VERBOSE, so that all messages are printed. Except the DEBUG message. An example output:
[2020-04-03 14:17:18] -- matpy.matrixmultiplication ----- [INFO]: Initializing matrices...
[2020-04-03 14:17:18] -- matpy.matrixmultiplication ---- [ERROR]: Test Error Level (matrixmultiplication.py:60)
[2020-04-03 14:17:18] -- matpy.matrixmultiplication - [CRITICAL]: Test Critical Level (matrixmultiplication.py:61)
[2020-04-03 14:17:18] -- matpy.matrixmultiplication -- [VERBOSE]: Test Verbose Level
[2020-04-03 14:17:18] -- matpy.matrixmultiplication -- [VERBOSE]: A:
[2020-04-03 14:17:18] -- matpy.matrixmultiplication -- [VERBOSE]: [1 2]
[2020-04-03 14:17:18] -- matpy.matrixmultiplication -- [VERBOSE]: [3 4]
[2020-04-03 14:17:18] -- matpy.matrixmultiplication -- [VERBOSE]: B:
[2020-04-03 14:17:18] -- matpy.matrixmultiplication -- [VERBOSE]: [2 3 5]
[2020-04-03 14:17:18] -- matpy.matrixmultiplication -- [VERBOSE]: [4 5 6]
[2020-04-03 14:17:18] -- matpy.matrixmultiplication -- [WARNING]: Matrix size exceeds 4 elements.
Then, just as in matpy.matrixmultiplication.matmul()
, the logger can
print statements much like the normal print function.
# Setup Logger at the top of your module
import logging
from .log_util import modify_logger
logger = logging.getLogger(__name__)
modify_logger(logger)
logger.debug("Test Debug")
logger.error("Test Error")
logger.critical("Test Critical")
logger.info("Test info")
logger.warning("Test Warning")
The last important this is modify logger. I wish it wasn’t so but it also has
to be run to apply the matpy.log_util.CustomFormatter
because the
intrinsic logging facility has its one format. Just to explain it see the
code below.
def modify_logger(logger):
# Make sure only this module prints the logger information.
logger.propagate = 0
# Add formatter
ch = logging.StreamHandler()
ch.setFormatter(CustomFormatter())
logger.addHandler(ch)
return logger
The propagate module sets only that the logging statements are not propagated
to a parent module, so that this is the only module to print the messages. The
loggingStreamHandler
handles how stuff is output, in our case we
want it to be output in a specific format, so we set our
matpy.log_util.CustomFormatter
. The formatter is optional but
convenient. instead of the matpy.log_util.CustomFormatter
, a “simple”
# Customformatter
FORMAT = "%(name)s - %(levelname)s: %(message)s"
formatter = logging.Formatter(FORMAT)
ch.setFormatter(formatter)
would have worked as well, but let me elaborate the benefits (beauty) of a truly custom formatter.
So, now you are all set for logging! Feel free to modify the logging!