Rollbar monitoring of Celery in a Django app

By Matt Layman on February 7, 2017

The last Rollbar post that I made covered how to integrate Rollbar into your Ember application. As a reminder, Rollbar is a service that let’s you record your errors wherever they happen. In that original post, I didn’t cover how to include Rollbar in a Django application because the Rollbar team does a great job of that in their own documentation. But what happens when you want to do asynchronous task management with Celery in your Django app? I encountered some gotchas in making that work so I’m going to describe what I learned here.

College Conductor uses Celery to make sure that slow tasks like sending email do not happen in the web request. I still want the safety net of error reporting in my async tasks, but I discovered that my Celery workers were not sending errors to Rollbar. This initally surprised me because I was using the Django middleware in the Rollbar documentation.

When I looked closely at the problem, I realized that it made perfect sense that Celery was not running Rollbar code. The Celery worker reads the Django settings to initialize its tasks, but it does not execute the Django middleware. Thus, the Rollbar configuration is skipped. After searching, I found Rollbar’s example code for configuring a Celery worker and that is when I encountered my problem. The sample code and the Django middleware configure the rollbar.BASE_DATA_HOOK attribute, but they use different Python functions for the hook. Both the Celery worker and the Django application need access to the Celery app instance so I had to resolve how to have two different BASE_DATA_HOOK functions.

The best solution I could devise was to do Celery specific configuration if a certain environment variable was set.

In my Django settings file, I have the necessary ROLLBAR dictionary:

ROLLBAR = {
    'access_token': os.environ['ROLLBAR_ACCESS_TOKEN'],
    'environment': os.environ['ROLLBAR_ENVIRONMENT'],
    'root': BASE_DIR,
    'enabled': bool(os.environ.get('ROLLBAR_ENABLED', False)),
}

These settings, in combination with the Django middleware, make the Django application report errors to Rollbar. (Note: my Django app follows the twelve factor app design so all of the environment specific stuff like acccess tokens are injected as environment variables. It’s a very powerful pattern.)

Over in my celery.py module, the code looks like:

import os

from celery import Celery
from celery.signals import task_failure

os.environ.setdefault(
    'DJANGO_SETTINGS_MODULE', 'conductor.settings.development')

app = Celery('conductor')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()

if bool(os.environ.get('CELERY_WORKER_RUNNING', False)):
    from django.conf import settings
    import rollbar
    rollbar.init(**settings.ROLLBAR)

    def celery_base_data_hook(request, data):
        data['framework'] = 'celery'

    rollbar.BASE_DATA_HOOK = celery_base_data_hook

    @task_failure.connect
    def handle_task_failure(**kw):
        rollbar.report_exc_info(extra_data=kw)

The linchpin of this setup is the CELERY_WORKER_RUNNING environment variable. All the code within the if statement will look very similar to Rollbar’s Celery example. In my configuration management tool, I add the CELERY_WORKER_RUNNING variable for the Celery worker and exclude it for the Django app. By doing this, the system is able to get the proper BASE_DATA_HOOK depending on the execution context.

Without too much extra work, I managed to make my project send Django and Celery errors to Rollbar in a shared module. I hope this setup can help anyone else struggling to make the two contexts play nicely together.

If you want to chat about this with me, I'm @mblayman on Twitter.



Matt Layman

Matt is the lead software engineer at Storybird.

Always eager to talk about Python and other technology topics, Matt organizes Python Frederick in Frederick, Maryland (NW of Washington D.C.) and seeks to grow software skills for people in his community.