Celery: log each task run to its own file?

Question:

I want each job running to log to its own file in the logs/ directory where the filename is the taskid.

logger = get_task_logger(__name__)

@app.task(base=CallbackTask)
def calc(syntax):
    some_func()
    logger.info('started')

In my worker, I set the log file to output to by using the -f argument. I want to make sure that it outputs each task to its own log file.

Asked By: user299709

||

Answers:

Below is my crude, written out-of-my-head, untested approach. Think it more as guidelining than production-grade code.

def get_or_create_task_logger(func):
    """ A helper function to create function specific logger lazily. """

    # https://docs.python.org/2/library/logging.html?highlight=logging#logging.getLogger
    # This will always result the same singleton logger
    # based on the task's function name (does not check cross-module name clash, 
    # for demo purposes only)
    logger = logging.getLogger(func.__name__)

    # Add our custom logging handler for this logger only
    # You could also peek into Celery task context variables here
    #  http://celery.readthedocs.org/en/latest/userguide/tasks.html#context
    if len(logger.handlers) == 0:
        # Log to output file based on the function name
        hdlr = logging.FileHandler('%s.log' % func.__name__)
        formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
        hdlr.setFormatter(formatter)
        logger.addHandler(hdlr) 
        logger.setLevel(logging.DEBUG)

    return logger

@app.task(base=CallbackTask)
def calc(syntax):
    logger = get_or_create_task_logger(calc)
    some_func()
    logger.info('started')
Answered By: Mikko Ohtamaa

Seems like I am 3 years late. Nevertheless here’s my solution inspired from @Mikko Ohtamaa idea. I just made it little different by using Celery Signals and python’s inbuilt logging framework for preparing and cleaning logging handle.

from celery.signals import task_prerun, task_postrun
import logging

# to control the tasks that required logging mechanism
TASK_WITH_LOGGING = ['Proj.tasks.calc']
@task_prerun.connect(sender=TASK_WITH_LOGGING)
def prepare_logging(signal=None, sender=None, task_id=None, task=None, args=None, kwargs=None):
    logger = logging.getLogger(task_id)
    formatter = logging.Formatter('[%(asctime)s][%(levelname)s] %(message)s')
    # optionally logging on the Console as well as file
    stream_handler = logging.StreamHandler()
    stream_handler.setFormatter(formatter)
    stream_handler.setLevel(logging.INFO)
    # Adding File Handle with file path. Filename is task_id
    task_handler = logging.FileHandler(os.path.join('/tmp/', task_id+'.log'))
    task_handler.setFormatter(formatter)
    task_handler.setLevel(logging.INFO)
    logger.addHandler(stream_handler)
    logger.addHandler(task_handler)

@task_postrun.connect(sender=TASK_WITH_LOGGING)
def close_logging(signal=None, sender=None, task_id=None, task=None, args=None, kwargs=None, retval=None, state=None):
    # getting the same logger and closing all handles associated with it
    logger = logging.getLogger(task_id)
    for handler in logger.handlers:
        handler.flush()
        handler.close()
    logger.handlers = []

@app.task(base=CallbackTask, bind=True)
def calc(self, syntax):
    # getting logger with name Task ID. This is already
    # created and setup in prepare_logging
    logger = logging.getLogger(self.request.id)
    some_func()
    logger.info('started')

The bind=True is necessary here in order to have id available within task. This will create individual log file with <task_id>.log every time the task calc is executed.

Answered By: Saurabh

Here is my code,

import logging

from celery.app.log import TaskFormatter
from celery.signals import task_prerun, task_postrun

@task_prerun.connect
def overload_task_logger(task_id, task, args, **kwargs):
    logger = logging.getLogger("celery.task")
    file_handler = logging.FileHandler(f'/tmp/{task_id}.log')
    file_handler.setLevel(logging.INFO)
    file_handler.setFormatter(
        TaskFormatter("[%(asctime)s: %(levelname)s/%(processName)s] %(task_name)s[%(task_id)s]: %(message)s")
    )
    logger.addHandler(file_handler)


@task_postrun.connect
def cleanup_logger(task_id, task, args, **kwargs):
    logger = logging.getLogger("celery.task")
    for handler in logger.handlers:
        if isinstance(handler, logging.FileHandler) and handler.baseFilename == f'/tmp/{task_id}.log':
            logger.removeHandler(handler)

tested on celery: v5.2.7

Answered By: Mohamed Zumair
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.