Simplified logging, debugging, and troubleshooting
Project description
logarhythm
Goals
The goals of this module are to:
-
Make it hassle free to set up logging in the recommended way for the most common configurations
-
Provide some ways of using logging to support debugging, data collection (telemetry), and profiling
Minimal Example
You are writing some code and you want to see something without doing tons of prints or looking at logger documentation.
import logarhythm
logger = logarhythm.get_logger()
logger.level = logarhythm.DEBUG
for x in big_loop():
logger.debug('x is %s' % repr(x))
do_something_with(x)
Usage
#imports
import logarhythm
from logarhtym.levels import * #imports logging level constants (i.e. CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET)
#basic logger usage for an importable module
logger = logarhythm.getLogger() #corresponds to logging.getLogger(__name__)
#basic logger configuration
logger.stderr = False # turn off logging to stderr (it is on by default)
logger.stdout = True # turn on logging to stdout (it is off by default)
logger.format = logarhtyhm.build_format(time='elapsed_msec',process_name=True,level=False)
#optional arguments: time is elapsed msec, process name is displayed, level is not displayed
#other optional arguments not mentioned are not changed
#logging to a file
with logger.file_open('/path/to/file.log'):
logger.warning('goes to the file')
Logger Hierarchy and Level Overview
The following logging levels exist:
- CRITICAL = 50
- ERROR = 40
- WARNING = 30
- INFO = 20
- DEBUG = 10
- NOTSET = 0
- Loggers have a level and messages themselves have a level.
- Loggers have a hierarchy with a root logger on top.
- The root logger's level by default is WARNING. All other logger's levels by default are NOTSET.
- If a logger's level is NOTSET, it will pass the message to its parent logger unless it is the root logger.
- If the root logger's level is NOTSET, it will process any message it receives.
- If a logger's level is something other than NOTSET, it will process the message if the message's level is greater or equal to the logger's level. Otherwise it will discard the message.
import logarhtym
from logarhtym.levels import * #imports logging level constants (i.e. CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET)
parent_logger = logarhythm.getLogger('parent') #initialized level = NOTSET
child1_logger = logarhythm.getLogger('parent.child1') #initialized level = NOTSET
child2_logger = parent_logger.child('child2') #parent.child2 #initialized level = NOTSET
parent_logger.level = DEBUG #parent and both children loggers will log every message at debug level or higher
child1.info('this will show')
child1_logger.level = CRITICAL
child1.info('this will not show')
child2.info('this will show')
child1_logger.level = NOTSET
child1_logger.level = NOTSET
child1.info('this will not show')
child2.info('this will not show')
#root logger's level is WARNING by default
child1.warning('this will show')
Logging Improvements
The default logger should be name not the root logger
The recommended convention for naming and organizing loggers in the logging HOWTO documentation is to use __name__
i.e.
logger = logging.getLogger(__name__)
However, the default logger returned when no name is provided is the root logger. This can introduce problems because changing settings for the root logger can affect all loggers at every level which may result in seeing unexpected logging messages from imported libraries if those libraries were not careful to set up their logging correctly. Having messages from libraries can inhibit troubleshooting and development of the package or module of interest to the developer.
Logarhythm resolves this by changing the behavior of the logging module when logarhythm is first loaded. logarhythm replaces the logging module-level functions with versions that use name to determine the default logger instead of returning the root logger. The result of this is that badly behaved third party libraries will use these replaced functions which will return loggers specific to the library and prevent logging messages from being mixed together unless deliberately set to do so.
It is possible to undo this and restore the original logging module functions by calling logarhythm.set_disarm_logging_module(False).
Hassle-free logging to stdout/stderr/files/streams
To make a logger send messages to an additional streams or files in the standard logging module, one needs to perform several steps including creating one or more handlers, creating formatters, associating each handler with the correct formatter, and adding the handlers to the logger. While this allows for powerful customization, doing the most common situations of stderr/stdout/file/stream should be simpler (i.e. one line).
With logarhythm, to turn on logging to stderr or stdout, simply toggle the corresponding property:
logger = logarhythm.getLogger() # corresponds to \_\_name\_\_ by default
logger.stderr = False # turn off logging to stderr (it is on by default)
logger.stdout = True # turn on logging to stdout (it is off by default)
For logging to a file, the procedure is similar to how to normally open files in python:
logger = logarhythm.getLogger() # corresponds to \_\_name\_\_ by default
# can use as a context manager (closes file and stops logging at end of block)
with logger.file_open('/path/to/file.log'):
logger.warning('goes to the file')
# can use with a logarhythm file handle object
fh = logger.file_open('/path/to/file.log')
logger.warning('this also goes to the file')
fh.close() # closes file and stops logging
Streams are handled similarly to files:
logger = logarhythm.getLogger() # corresponds to \_\_name\_\_ by default
#can use with a context manager (stops logging to stream at end of block)
with logger.stream_open() as sh:
logger.warning('goes to the stream')
print(sh.getvalue()) # prints the content of the stream
from io import StringIO
sio = StringIO()
sh = logger.stream_open(sio)
logger.warning('goes to the stream')
sh.close() # stops future logging to stream - does not clear/release the stream itself
assert(sio.getvalue() == sh.getvalue())
Simplified formatting
In the standard logging module, every handler must be individually associated with a formatter object via the setFormatter() method. Additionally, for most people, writing the format string requires reviewing the online documentation to determine what formatting variables are present, since these variable names have inconsistent capitalization and non-straightforward names. Finally one must decide how to order all the desired information the format string, and may end up doing this inconsistently from project to project since there is no standard/default way of constructing these strings.
In logarhythm, each logger has a format assigned to it, and all handlers by default will use the same format. It is still possible in logarhythm to individually assign formatters to handlers if that flexibility is desired. Additionally, there is a utility function called build_format() to automatically construct format strings based on input parameters that specify what information to include or not. These parameters and the valid settings are available by calling help(logarhythm.build_format).
With build_format(), one can choose to include time as
-
"full" = YYYY-MM-DD/hh:mm:ss
-
"hms" = hh:mm:ss
-
"elapsed_msec" = number of milliseconds since the logging module or logarhythm was loaded
-
None = no time
Additionally, one can add or remove via True/False flags the following information:
-
logger_name (default True)
-
process_name (default False - useful for multiprocessing)
-
thread_name (default False - useful for multithreading)
-
level (default True)
To apply a format to a logger:
logger = logarhythm.getLogger()
logger.format = build_format(time='elapsed_msec',process_name=True)
Logger configuration convenience functions
Mode properties
The main rationale for using logging as opposed to print statements is that it gives you the power to turn on or off different levels of diagnostic detail depending on where you are in development. In the quest to make the common situation easier, there are a few special attributes that configure multiple aspects of a given logger into a mode to match a development phase.
The dev_mode property configures the following logger properties suited for development and/or troubleshooting:
.stderr = True log
messages are displayed and go to stderr
.stdout = False
.auto_debug = True auto
debug (post-mortem for unhandled exceptions) is turned on
.level = DEBUG debug
level messages will show
.captures_disabled=False capture
patterns are enabled
.profiling_disabled=False profiling
is enabled
.monitoring_disabled=False monitoring
is enabled
.debugging_disabled=False debugging
through logarhythm breakpoints/captures/callbacks is enabled
The prod_mode property reduces/eliminates diagnostic data as possible to make the program suitable for production and external users:
.stderr = True log
messages are displayed and go to stderr
.stdout = False
.auto_debug = False automatic
debug is disabled
.end_interactive_mode = False ending
in interactive mode is disabled
.level = NOTSET all
loggers are at the NOTSET level by default (only WARNINGS and higher level messages are displayed)
.captures_disabled = True capture
patterns are disabled
.profiling_disabled = True profiling
is disabled
.monitoring_disabled = True monitoring
is disabled
.debugging_disabled = True debugging
through logarhythm breakpoints/captures/callbacks is disabled
For writing doctests in which it is desired to include logging messages, the doctest_mode is provided.
.stderr = False doctests
do not capture stderr, so this is disabled
.stdout = True doctests
capture stdout, so this is enabled
.format = build_format(time=None) this
removes timestamps which allows for test repeatability
.debugging_disabled = True typically
debug mode is not wanted when doctests are being run
set(), set_all(), and use()
The following properties/attributes can be set en masse with the set() function using them as keyword arguments. auto_debug captures_disabled debugging_disabled disarm_logging_module end_interactive format level profiling_disabled monitoring_disabled stderr stdout
The set_all() function is the same as set() except it applies the values to the current logger as well as all descendant loggers in the logger tree.
The use() function temporarily sets these properties to values with the context of a 'with' block. At the end of the block, all attributes are set back to what they were before the 'with' block.
Utilities to use logging to support debugging (with pdb)
Captures
With logarhythm, one can associate a capture pattern (regular expression) that can be triggered when the pattern matches a generated log message. The capture pattern can be specified with a callback to perform any kind of function, however the default behavior is to enter debug mode.
logger = logarthyhm.getLogger()
logger.capture('is 41')
logger.level = DEBUG
for x in range(100):
logger.debug('x is %d',x) # will enter debug mode when x is 41
Monitoring
With logarhythm, one can mark an object's attribute to be monitored for changes. When the attribute is changed, a log message will be generated and an optional callback, if specified, will be called.
logger = logarhythm.getLogger()
obj.x = 5
logger.monitor_attr(obj,'x')
obj.x = 6 # generates a logging message
Additionally, one can mark a callable (function, object method, class method, static method, or object with call() defined) to be monitored. When the callable is called, a log message will be generated, and an optional callback, if specified will be called.
logger = logarhythm.getLogger()
logger.monitor_call(func)
func() # generates a logging message
Miscellaneous convenience utilities
These items are admittedly not directly related to logging, however they are very useful for development and are included for convenience.
Auto Debug
One useful feature in python for development is to automatically enter debug mode (post-mortem) following an unhandled exception. This allows one to use pdb to examine the variables at the time of failure in the failed context as well as in calling stack frames (with the "up" command in pdb). To do this in standard python requires importing several modules, writing a handler function using it to replace the default exception handling hook.
In logarhythm, one can use this feature by calling:
import logarhythm;
logarhythm.set_auto_debug(True)
End in Interactive Mode
Once a program has finished running it is common during development to occasionally want to inspect or examine some of the generated variables. By default, a python program will terminate when it is finished executing. This can be frustrating for Windows users because the command line window simply disappears if the script was not run from an existing command line instance. Additionally, it can be useful for users on any platform in general to end in interactive mode to write doctests (see the doctestify package on pypi for more convenience functions for making doctests).
In logarhythm, one can make a script end in interactive mode by calling:
import logarhythm;
logarhythm.set_end_interactive(True)
breakpoint() function
When debugging, one can programmatically enter debug mode by calling pdb.set_trace(). In Python 3.7 additionally a built-in breakpoint() function was introduced to do the same thing. logarhythm provides a similar function for convenience with two extra caveats:
-
the breakpoint() function can take an optional condition e.g. breakpoint(x==41)
-
when invoked, a logging message will be generated saying that a breakpoint was tripped
Utilities for capturing data from your programs
Capturing structured data (telemetry)
Logarhythm provides two special functions to store and retrieve structure data to and from log files. The tlm() function takes multiple keyword arguments and values and encodes it into a message that can be sent via one of the logging commands. Given a collection of logging messages that include some of these tlm messages - the parse_tlm function can locate, extract and decode the tlm data points into dictionaries of key-value pairs. Both functions allow an optional tlm_channel field, which is a string label that can be used to filter on specific messages.
logger.level = INFO
with logger.stream_open() as sh:
logger.info(tlm('channel A',x=5,y=10))
logger.info(tlm('channel A',x=9,y=11))
logger.info(tlm(z='trying to be tricky with )00000000_tlm_end inside the data itself'))
assert(list(parse_tlm(sh.getvalue())) == [{'x':5,'y':10},{'x':9,'y':11},{'z':'trying to be tricky with )00000000_tlm_end inside the data itself'}])
assert(list(parse_tlm(sh.getvalue(),'channel A')) == [{'x':5,'y':10},{'x':9,'y':11}])
Profiling
The python standard library comes with some profiling modules to help identify performance bottlenecks in code. logarhythm provides a convenience function that can wrap around a code block to assess performance within that code block and generate a logging message containing the results.
logger.level = INFO
with logger.profile():
do_some_slow_function()
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file logarhythm-1.0.5.tar.gz
.
File metadata
- Download URL: logarhythm-1.0.5.tar.gz
- Upload date:
- Size: 95.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.3.0 pkginfo/1.6.1 requests/2.22.0 setuptools/46.1.3 requests-toolbelt/0.9.1 tqdm/4.55.1 CPython/3.8.5
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 26e42e8b31bdc2dd943a6f1240a78675d8d7964539964dc893050d5ba66e9766 |
|
MD5 | b5db72ccfccda961fc429070ad0d5bdb |
|
BLAKE2b-256 | 2c3073d56d779264c8ae59502edd6c22a0379747d72a7972adbcac810783e37a |
File details
Details for the file logarhythm-1.0.5-py3-none-any.whl
.
File metadata
- Download URL: logarhythm-1.0.5-py3-none-any.whl
- Upload date:
- Size: 29.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.3.0 pkginfo/1.6.1 requests/2.22.0 setuptools/46.1.3 requests-toolbelt/0.9.1 tqdm/4.55.1 CPython/3.8.5
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 2685e9be3e0ff0fae23b58e2c5229668fea2453b9a89475c04fe18f82a280b9b |
|
MD5 | 8f8682026fdac1cb526696bb1684ff25 |
|
BLAKE2b-256 | 5fb0550703e763bdcfce3b04b85493ee64ccd499f2e5936b1874835542f7eb0b |