LogPy

Unorthodox logging for python


License
Other
Install
pip install LogPy==1.0.1

Documentation

LogPy

Unorthodox logging for Python

Author: Michal Hordecki
URL: http://github.com/MHordecki/LogPy

Introduction

LogPy is an alternative for standard Python logging facilities, loosely based on Lisp's log5. LogPy is based on KISS principles - therefore I wanted it to be as most transparent as possible.

The main difference when compared to stdlib's logging is tag-based architecture. In logging, each log has assigned a certain level (be it debug, error, etc.). That's all. LogPy, on the other hand, sports tags - you can attach short strings to each message. Tag can represent variety of things: severity level, module name, or some custom log categorization.

LogPy requires Python 2.6 or higher. It works seamlessly on Python 3 too (in fact, it's developed with py3k in mind and then backported to Python 2.6).

Getting started

Using LogPy is dead simple:

from logpy import LogPy
import sys

log = LogPy()
log.add_output(sys.stderr.write)

log('debug')('Hello World!')

Voila! LogPy instances are callable. To output a log, call log "twice" - in first call pass all tags of the log, and everything passed to the second one will be considered a part of the message. The example will output logs to the standard error output. Easy, isn't it?

Under the hood

LogPy has a few layers of abstraction:

  1. LogPy - it accepts data from the user, combines them into a Message instance and passes them down to all outputs.
  2. Output - it filters messages based on some predefined conditions, and if the message passes them all, it's formatted by the Formatter and then passed to the actual output.
  3. Formatter - takes message and formats it ;) (in standard implementation it uses string.format for the job).
  4. Actual output - a callable that, for example, outputs the Formatter's output to the screen.

All those layers/objects are callables.

Common tasks

Output filtering

With multiple outputs, you probably want to filter out some logs in each of them. There is support for that:

log = LogPy()

log.add_output(my_output, filter = lambda m: 'error' in m.tags)

# Equivalent to:

log.add_output(my_output, filter = [lambda m: 'error' in m.tags])

As you can see, filters are callables, taking Message object as an argument and returning bool. Multiple filters can be provided by a list.

Custom formatting

You can customize formatting by either replacing the format string or by replacing the Formatting object altogether. Your choice.

Custom format string

This one will meet 90% of your needs. You can change your format string with keyword argument to the add_output method of LogPy (also possible when directly instantiating Output objects):

log.add_output(..., formatter = 'my custom format string!')

When processing a message, method format of the string will be called with following, predefined arguments:

  • date - datetime object
  • tags - space-delimited list of tags (string)
  • args - list of arguments in the message
  • kwargs - dict of keyword arguments in the message
  • message - the actual message object. All arguments above are actually just a syntactic sugar, as they are all attributes of this object.

Default format string looks like this: {date} : {tags} : {args} {kwargs}\n

Don't forget to put a newline at the ending, or your logs will look crippled.

Working with multiple modules

You can help yourself while using LogPy with multiple modules by predefining some of the tags:

# Main module

log = LogPy()

# Child module

import mainmodule

log = mainmodule.log('module:childmodule', curry = True)

# Now:

log('debug')('Hello World!')

# is equivalent to

log('module:childmodule', 'debug')('Hello World')

Custom format object

In case you want the full power - you can get rid of the default formatter:

log.add_output(..., formatter = my_formatter_object)

Formatter objects must comply to the simple protocol:

class Formatter:
    def __call__(message: Message) -> Someting reasonable:
        pass

class Message:
    tags = set(str)
    args = [] # passed by the user
    kwargs = {} # passed by the user
    date = datetime.datetime

(I have no idea whatsoever if there's standard formal notation for describing protocols in Python besides things like zope.interface. I hope my ramblings are clear.)

Where something reasonable means: everything that will be accepted by the output of the Output (sounds kinda silly) - it usually means str, but not always.

Custom Output object

If you're willing to scrap 50% of the LOC of LogPy, feel free to do so:

log.add_raw_output(my_customized_output_object)

Worth mentioning is the fact that LogPy.add_output is just a wrapper for:

log.add_output(...)
# Equivalent to
log.add_raw_output(Output(...))

Output protocol looks as follows:

class Output:
    def __call__(message: Message):
        pass

In other words: you will be called with every log issued by the user.

Note: Please, treat messages as immutable objects - they are being reused for all Outputs.

Thread safety

LogPy employs some basic thread safety; a threading.Lock is used in __call__ method of LogPy. It can be easily replaced:

from threading import RLock

log = LogPy()
log.lock = RLock()