loggerutils

Celery optimised log formatter


License
BSD-3-Clause
Install
pip install loggerutils==0.1.0

Documentation

loggerutils

loggerutils is a Python package that provides a drop-in replacement for the logging.Formatter class. loggerutils enriches the log message with extra information when running inside a Celery task.

Configure your logger using the loggerutils.Formatter class:

import logging
import loggerutils

logger = logging.getLogger()
sh = logging.StreamHandler()
sh.setFormatter(loggerutils.Formatter('%(asctime)s - %(task_id)s - %(task_name)s - %(name)s - %(levelname)s - %(message)s'))
logger.setLevel(logging.INFO)
logger.addHandler(sh)

And you automatically get task_id and task_name when logging within a Celery task execution context.

# funcs.py
import logging

logger = logging.getLogger(__name__)

def do_something():
    logger.info('Do something')
    return True

When calling from within a Celery task

# tasks.py
import logging
import funcs
from worker import app

logger = logging.getLogger(__name__)

@app.task(bind=True)
def task(self):
    return funcs.do_something()

Logs

2018-11-06 07:33:16,140 - 7d2ec1a7-0af2-4e8c-8354-02cd0975c906 - tasks.task - tasks - INFO - Do something

When calling do_something from somewhere outside Celery, you get:

2018-11-06 07:33:16,140 -  -  - tasks - INFO - Do something

loggerutils extends the built-in Formatter styles (PercentStyle (%), StrFormatStyle ({) and StringTemplateStyle ($)) with Jinja2Style ({{). Jinja2Style leverages Jinja2 templates and allows you to use conditions to prettify your log messages:

...
sh.setFormatter(loggerutils.Formatter('{{asctime}}{% if task_id %} - {{task_id}} - {{task_name}}{% endif %} - {{name}} - {{levelname}} - {{message}}', style='{{'))
...

Motivation

Celery provides a special task logger in celery.utils.log.get_task_logger.

This task logger enriches the log message with the Celery task's id and name.

# tasks.py
import os
from celery.utils.log import get_task_logger
from worker import app

logger = get_task_logger(__name__)

@app.task()
def add(x, y):
    result = x + y
    logger.info(f'Add: {x} + {y} = {result}')
    return result

Produces this log output:

[2018-11-06 07:30:13,545: INFO/MainProcess] Received task: tasks.get_request[9c332222-d2fc-47d9-adc3-04cebbe145cb]
[2018-11-06 07:30:13,546: INFO/MainProcess] tasks.get_request[9c332222-d2fc-47d9-adc3-04cebbe145cb]: Add: 3 + 5 = 8
[2018-11-06 07:30:13,598: INFO/MainProcess] Task tasks.get_request[9c332222-d2fc-47d9-adc3-04cebbe145cb] succeeded in 0.052071799989789724s: None

But what to do with lower level code?

# models.py
import logging

from passlib.hash import sha256_crypt
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import validates
from sqlalchemy import text
from . import db

logger = logging.getLogger(__name__)

class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(UUID(as_uuid=True), primary_key=True, server_default=text("uuid_generate_v4()"))
    name = db.Column(db.String(64), unique=False, nullable=True)
    email = db.Column(db.String(256), unique=True, nullable=False)

    @validates('email')
    def validate_email(self, key, value):
        logger.info(f'Validate email address: {value}')
        if value is not None:
            assert '@' in value
            return value.lower()

The method validate_email could be called inside a Celery task. Or inside a Django or Flask request. Your model code should not know about the runtime context it is executed in.

For debugging, it is helpful to get a task id and task name if the model code runs inside a Celery task. Especially if you process a high volume of Celery tasks.

Requirements

loggerutils currently works with:

  • Python 3.6+

Installation

To install Requests, simply use pip (or pipenv:

$ pip install loggerutils
✨🍰✨

Links

Contributions

Pull Requests welcome! Please open an issue to report bugs or suggest improvements.