Quick Note: Adding Custom Attributes to LogRecord Objects

As I've told in the previous QN , Quick Note is a series where I take a quick note and put some code samples about how I have solved a particular problem.

The Definition of The Problem

Today, I needed to add a UUID instance to a LogRecord object.

If the reader asks about "How on earth do you even get an instance of a LogRecord?", it is simple. If you write your custom handler and formatter, these instances are already provided to you. Masnun, in this article , gives great examples about how you do that.

Why do I need to do that? What I wanted to do was to get an identifier of a LogRecord instance. You can think it like primary key for database-driven applications (you could even do that yourself). My requirements were not about database, it was to distinct one LogRecord from another, so UUID instances were great in my case.

The Solution

It is pretty simple, actually. Standard logging library provides you two useful methods:

  • getLogRecordFactory : It helps you to get the factory for creating LogRecord instances.
  • setLogRecordFactory: It helps you to pass a factory method which will be used to create a LogRecord.

At first, let me give an example on how getLogRecordFactory works. It returns a method that we can give the same parameters as LogRecord class.

log_record_factory = logging.getLogRecordFactory()
log_record = log_record_factory(
    name="example.logger",
    level=logging.ERROR,
    pathname="/foo/bar.py",
    lineno=1,
    msg="An error occured.",
    args=(),
    exc_info=None
)

Of course, it is not developer's business to create LogRecord instances by hand. It is just to give you insight about how it works.

And since we know how it works, to add a UUID instance to the LogRecord, what we do is:

  1. Get the former log record factory.
  2. Create a method that receives all args and kwargs that a LogRecord would receive.
  3. Create LogRecord instance inside the method with args and kwargs.
  4. Inject UUID instance to LogRecord instance.
  5. Return LogRecord instance.
  6. Set the method as new log record factory.

The below is the implementation:

_former_log_record_factory = logging.getLogRecordFactory()

def _log_record_uuid_injector(*args, **kwargs):
    record = _former_log_record_factory(*args, **kwargs)
    record.uuid = uuid.uuid4()  # or another implementation of uuid
    return record

# set the new factory
logging.setLogRecordFactory(_log_record_uuid_injector)

With this method, whenever we receive a LogRecord instance in Handler::emit or Formatter::format method.

Where should the code live?

This also might lead the developer to confusion as to where the code should live. Wherever it lives, it should be somewhere with global scope, possibly under a module. I implemented this above my handler so whenever I call from foo import handlers, the code runs.