Skip to content

Celery Logging: Structured Logging for Tasks and Workers

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Celery Logging: Structured Logging for Tasks and Workers. We cover key concepts, practical examples, and best practices to help you master this topic.

Celery logging provides structured, task-aware log output that includes task IDs, names, and execution context in every log message, enabling effective debugging, monitoring, and log aggregation across distributed worker fleets.

flowchart LR
    Task[Task Execution] --> Logger[Logger]
    Logger --> Fields[Task ID, Name, Args]
    Fields --> Structured[JSON Log Format]
    Structured --> Console[stdout / stderr]
    Structured --> File[Log File]
    Structured --> ELK[Log Aggregator]

What You'll Learn

  • Configuring Celery logging levels and formats
  • Adding task context to log messages
  • Structured logging with JSON output
  • Log aggregation for distributed workers

Why It Matters

Without structured logging, debugging a failed task in a 50-worker fleet means grepping through flat log files trying to match task IDs. Structured logging produces JSON entries with task ID, name, duration, and worker hostname — enabling precise querying like "show all logs for task X on worker Y".

Real-World Use

DodaTech's Celery workers emit structured JSON logs with task_id, task_name, worker_hostname, duration_ms, and result fields. Logs are shipped to Elasticsearch. When a task fails, engineers query task_id: "abc-123" and see every log line from that task across any worker, in chronological order.

Basic Logging Configuration

Configure Celery logging for workers:

from celery import Celery, signals
import logging
import sys

app = Celery('logging_demo', broker='redis://localhost:6379/0')

@signals.setup_logging.connect
def setup_celery_logging(loglevel=None, **kwargs):
    """Configure Celery logging with custom format."""
    formatter = logging.Formatter(
        fmt='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S'
    )
    handler = logging.StreamHandler(sys.stdout)
    handler.setFormatter(formatter)

    root_logger = logging.getLogger()
    root_logger.addHandler(handler)
    root_logger.setLevel(loglevel or logging.INFO)

    celery_logger = logging.getLogger('celery')
    celery_logger.setLevel(logging.DEBUG)

@signals.task_prerun.connect
def log_task_start(sender=None, task_id=None, task=None, **kwargs):
    logger = logging.getLogger('celery.task')
    logger.info(f"Starting task {task.name} [{task_id}]")

@signals.task_success.connect
def log_task_success(sender=None, result=None, **kwargs):
    logger = logging.getLogger('celery.task')
    logger.info(f"Task completed with result: {result}")

@signals.task_failure.connect
def log_task_failure(sender=None, task_id=None, exception=None, **kwargs):
    logger = logging.getLogger('celery.task')
    logger.error(f"Task {task_id} failed: {exception}", exc_info=True)

@app.task
def sample_logging_task(item_id, count=1):
    import time
    logger = logging.getLogger('celery.task')
    logger.debug(f"Processing item {item_id} with count {count}")
    time.sleep(0.1)
    logger.info(f"Item {item_id} processed successfully")
    return {"item_id": item_id, "status": "done"}

import time
print("Worker logging example:")
result = sample_logging_task.delay(42, count=3)
time.sleep(0.3)

Expected output:

2026-06-28 12:00:00 [INFO] celery.task: Starting task logging_demo.sample_logging_task [550e8400-...]
2026-06-28 12:00:00 [DEBUG] celery.task: Processing item 42 with count 3
2026-06-28 12:00:00 [INFO] celery.task: Item 42 processed successfully
2026-06-28 12:00:00 [INFO] celery.task: Task completed with result: {'item_id': 42, 'status': 'done'}

Structured JSON Logging

Emit structured JSON logs for log aggregators:

from celery import Celery, signals
import logging
import json
import sys
import time

app = Celery('logging_demo', broker='redis://localhost:6379/0')

class StructuredJSONFormatter(logging.Formatter):
    def format(self, record):
        log_entry = {
            "timestamp": self.formatTime(record, "%Y-%m-%dT%H:%M:%S"),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
            "module": record.module,
            "line": record.lineno,
        }

        if hasattr(record, 'task_id'):
            log_entry["task_id"] = record.task_id
        if hasattr(record, 'task_name'):
            log_entry["task_name"] = record.task_name
        if record.exc_info and record.exc_info[0]:
            log_entry["exception"] = self.formatException(record.exc_info)

        return json.dumps(log_entry)

@signals.setup_logging.connect
def setup_json_logging(**kwargs):
    handler = logging.StreamHandler(sys.stdout)
    handler.setFormatter(StructuredJSONFormatter())
    logging.getLogger('celery').addHandler(handler)
    logging.getLogger('celery').setLevel(logging.DEBUG)

class TaskAdapter(logging.LoggerAdapter):
    def process(self, msg, kwargs):
        kwargs.setdefault('extra', {})
        if 'extra' in kwargs:
            kwargs['extra'].update({
                'task_id': self.extra.get('task_id', ''),
                'task_name': self.extra.get('task_name', ''),
            })
        return msg, kwargs

@signals.task_prerun.connect
def setup_task_logging(sender=None, task_id=None, task=None, **kwargs):
    logger = logging.getLogger('celery.task')
    adapter = TaskAdapter(logger, {'task_id': task_id, 'task_name': task.name})
    task.request.logger = adapter
    adapter.info(f"Task started")

@app.task
def json_logging_task(data):
    logger = getattr(getattr(json_logging_task, 'request', None), 'logger', None)
    if not logger:
        logger = logging.getLogger('celery.task')
    logger.info(f"Processing data: {data}")
    time.sleep(0.1)
    logger.info("Processing complete")
    return {"result": "ok"}

import time
print("Structured JSON logging:")
json_logging_task.delay({"key": "value"})
time.sleep(0.3)

Expected output:

Structured JSON logging:
{"timestamp": "2026-06-28T12:00:00", "level": "INFO", "logger": "celery.task", "message": "Task started", "module": "celery_signals", "line": 42, "task_id": "550e8400-...", "task_name": "logging_demo.json_logging_task"}
{"timestamp": "2026-06-28T12:00:00", "level": "INFO", "logger": "celery.task", "message": "Processing data: {'key': 'value'}", ...}
{"timestamp": "2026-06-28T12:00:00", "level": "INFO", "logger": "celery.task", "message": "Processing complete", ...}

Log Aggregation Strategy

Collect logs from distributed workers:

from celery import Celery, signals
import logging
import sys
import json

app = Celery('logging_demo', broker='redis://localhost:6379/0')

class AggregationFormatter(logging.Formatter):
    def format(self, record):
        import socket
        base = {
            "timestamp": self.formatTime(record),
            "level": record.levelname,
            "message": record.getMessage(),
            "worker": socket.gethostname(),
            "process": record.process,
        }
        if hasattr(record, 'task_id'):
            base["task_id"] = record.task_id
        if hasattr(record, 'task_name'):
            base["task_name"] = record.task_name
        return json.dumps(base)

@signals.setup_logging.connect
def setup_aggregation_logging(**kwargs):
    handler = logging.StreamHandler(sys.stdout)
    handler.setFormatter(AggregationFormatter())
    logging.getLogger('celery').addHandler(handler)

    import socket
    hostname = socket.gethostname()
    print(f"[INIT] Worker starting on {hostname}", file=sys.stderr)

@app.task
def aggregation_task(task_num):
    import logging
    import time

    logger = logging.getLogger('celery.task')
    logger.info(f"Processing task number {task_num}")
    time.sleep(0.05)
    logger.info(f"Task {task_num} complete")

    return task_num

import time
print("Log aggregation demo:")
for i in range(3):
    aggregation_task.delay(i)

time.sleep(0.5)

import socket
print(f"\nSearch all logs for: worker={socket.gethostname()}")
print("(In production, use Elasticsearch or Loki for cross-worker search)")

Expected output:

Log aggregation demo:
{"timestamp": "2026-06-28 12:00:00", "level": "INFO", "message": "Processing task number 0", "worker": "my-host", "process": 12345, "task_id": "550e8400-...", "task_name": "logging_demo.aggregation_task"}
{"timestamp": "2026-06-28 12:00:00", "level": "INFO", "message": "Task 0 complete", ...}
...

Search all logs for: worker=my-host
(In production, use Elasticsearch or Loki for cross-worker search)

Common Mistakes

  • Configuring logging only in the task module, not the worker — Celery workers may use a different Python environment. Configure logging in a worker initialization signal or in the Celery app module.
  • Using print() for task logging — print() statements do not include task IDs, timestamps, or log levels. Always use logging.getLogger() for task output.
  • Forgetting to configure logging for the worker Process — the Celery worker process has its own logging configuration. Logging set up in Django or Flask settings may not apply to Celery workers.
  • Logging sensitive data — task arguments often contain user data, API keys, or PII. Redact sensitive fields before logging using a custom formatter or filter.
  • Not rotating log files — Celery workers can generate gigabytes of logs per day. Configure log rotation (size-based or time-based) or use stdout with a log shipper (Fluentd, Logstash) that handles rotation.

Practice Questions

  1. How do you configure structured JSON logging for Celery tasks?
  2. Why should you avoid using print() for task logging?
  3. How do you include task ID in every log message?
  4. What is log aggregation and why is it important for distributed workers?
  5. How do you redact sensitive data from task logs?

Challenge

Build a comprehensive Celery logging system that: (1) configures structured JSON logging for all Celery tasks, (2) includes task_id, task_name, worker_hostname, duration_ms, and result in every log entry, (3) supports different log levels per task type (debug for internal tasks, info for user-facing tasks), (4) redacts sensitive fields (password, token, ssn) from log output, (5) ships logs to a file with rotation and to stdout for container environments, and (6) adds trace_id for distributed tracing correlation.

FAQ

How do I enable Celery task logging?

Celery tasks inherit the worker's logger. Use import logging; logger = logging.getLogger(__name__) at the top of your task module, then logger.info() inside the task. Celery automatically adds task context.

How do I include task ID in log messages?

Use the CELERYD_LOG_FORMAT setting: --log-format='[%(asctime)s] [%(task_id)s] %(message)s'. Or configure a custom formatter that reads record.task_id attribute set by Celery.

What is the best log format for Celery?

JSON structured logging with fields: timestamp, level, logger, message, task_id, task_name, worker_hostname, duration_ms. This format works with all log aggregators (ELK, Loki, Datadog) and enables precise querying.

How do I handle Celery worker log rotation?

Option 1: Configure --logfile with the logging.handlers.RotatingFileHandler. Option 2: Log to stdout and use an external log shipper (Fluentd, Logstash, Vector) that handles rotation and shipping separately.

Can I have different log levels per task?

Yes. Use a task_prerun signal that adjusts the task's logger level based on task name or arguments. Or use separate loggers per task module and configure levels independently.

Mini Project

Build a Celery logging library that provides: (1) a setup_logging() function called from worker_init that configures JSON or text format, (2) automatic task_id and task_name injection into every log record, (3) sensitive data redaction based on field name patterns (password, token, secret), (4) log level override per task type via configuration, (5) async log shipping to Elasticsearch via background thread, and (6) a log viewer web UI that queries Elasticsearch by task_id, time range, or log level.

What's Next

Continue with Celery Serialization to learn about task argument Serialization. Then explore Celery Security for securing Celery communications.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro