Jair Trejo

Making the logger available to custom celery Task subclasses

Last week we upgraded our site from django-celery to stock celery. We were overdue, perhaps, but we weren’t looking forward to change the tires of the rolling eight-wheeler.

It went reasonably smooth, but we ran into trouble with a custom celery.Task subclass that needs access to the task logger.

In the past, each celery task had its own logger, accessible by calling self.get_logger. Since a few versions past, the best practice is to create a common logger for all of your tasks at the top of your module. So we have:

# some_package.tasks.py
from celery.utils.log import get_task_logger

import utils


logger = get_task_logger(__name__)


@app.task(base=utils.CustomTask)
def do_stuff():
    # Do some stuff
    logger.info('I did some stuff')

We are using a custom subclass of celery.Task for some of our tasks, and it needs access to the logger:

# utils.py
import celery


class CustomTask(celery.Task):
    def do_custom_thing(self):
        logger = self.get_logger()
        logger.info('I did a custom thing')

But self.get_logger is no more. We could call get_task_logger(__name__) here, but that would mean the logger would be named utils rather than some_package.tasks, which is less than ideal.

It would be nice to set self.logger inside do_stuff, but there is no self there. We could also set it as do_stuff.logger, but do_stuff is a PromiseProxy object, and thus read-only.

But the @app.task decorator can help us. After diving into the source code, I realized that it turns its non-reserved keyword arguments into properties of the class it generates on the fly. So we can do:

@app.task(base=utils.CustomTask, logger=logger)
def do_stuff():
    # Do some stuff
    logger.info('I did some stuff')

And use self.logger in the subclass!

class CustomTask(celery.Task):
    def do_custom_thing(self):
        logger = self.logger
        logger.info('I did a custom thing')

The logger will be the correct one depending on the subclasser task.