deployment

Django Logging and Monitoring

How to implement logging and monitoring in Django production applications. Covers structured logging, error tracking, performance monitoring, health checks, alerting, and observability tools.

⏱ 14 min read production
Application monitoring dashboard showing request metrics and error rates

A Django application in production without logging and monitoring is running blind. When something breaks at 3 AM, the difference between a 5-minute fix and a 3-hour investigation is the quality of your observability setup. This guide covers the logging and monitoring patterns I use on production Django projects: Python’s logging framework configuration, structured log output, error tracking with Sentry-style tools, performance monitoring, health check endpoints, database and queue monitoring, and the alerting strategies that wake you up for the right reasons. For broader deployment patterns, see the Deployment hub.

Good monitoring is not about collecting every metric. It is about answering two questions quickly: is the application healthy right now, and what changed when it stopped being healthy.

Django logging configuration

Django uses Python’s logging module. Configure it in settings with a dictionary:

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'verbose': {
            'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
            'style': '{',
        },
        'json': {
            '()': 'pythonjsonlogger.jsonlogger.JsonFormatter',
            'format': '%(asctime)s %(levelname)s %(name)s %(message)s',
        },
    },
    'filters': {
        'require_debug_false': {
            '()': 'django.utils.log.RequireDebugFalse',
        },
    },
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
            'formatter': 'verbose',
        },
        'json_console': {
            'class': 'logging.StreamHandler',
            'formatter': 'json',
        },
        'mail_admins': {
            'level': 'ERROR',
            'filters': ['require_debug_false'],
            'class': 'django.utils.log.AdminEmailHandler',
        },
    },
    'root': {
        'handlers': ['json_console'],
        'level': 'WARNING',
    },
    'loggers': {
        'django': {
            'handlers': ['json_console'],
            'level': 'WARNING',
            'propagate': False,
        },
        'django.request': {
            'handlers': ['json_console', 'mail_admins'],
            'level': 'ERROR',
            'propagate': False,
        },
        'myproject': {
            'handlers': ['json_console'],
            'level': 'INFO',
            'propagate': False,
        },
    },
}

Structured logging

Plain text logs are difficult to parse and query. Use structured JSON logging in production:

pip install python-json-logger

With JSON logging, each log entry becomes a queryable object:

import logging

logger = logging.getLogger(__name__)

def process_order(order_id):
    logger.info('Processing order', extra={
        'order_id': order_id,
        'action': 'process_order',
    })
    try:
        # ... business logic ...
        logger.info('Order processed', extra={
            'order_id': order_id,
            'status': 'success',
            'duration_ms': elapsed,
        })
    except Exception as e:
        logger.error('Order processing failed', extra={
            'order_id': order_id,
            'error': str(e),
        }, exc_info=True)
        raise

Structured logs integrate with log aggregation services (ELK, Datadog, CloudWatch) and enable filtering, alerting, and dashboards based on structured fields.

Application-level logging patterns

Log at boundaries where failures are likely:

# Log external API calls
logger.info('Calling payment API', extra={'amount': total, 'provider': 'stripe'})
response = payment_api.charge(amount=total)
logger.info('Payment API response', extra={'status': response.status_code})

# Log authentication events
logger.info('Login attempt', extra={'username': username, 'ip': get_client_ip(request)})

# Log slow operations
if elapsed_ms > 1000:
    logger.warning('Slow operation detected', extra={
        'operation': 'generate_report',
        'duration_ms': elapsed_ms,
    })

Do not log sensitive data: passwords, tokens, credit card numbers, or personal identifiers.

Error tracking

Sentry is the standard error tracking tool for Django:

import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration

sentry_sdk.init(
    dsn=os.environ.get('SENTRY_DSN'),
    integrations=[DjangoIntegration()],
    traces_sample_rate=0.1,
    send_default_pii=False,
)

Sentry captures unhandled exceptions, groups them by type and location, tracks occurrence frequency, and provides context like request data and user information.

Health check endpoints

Expose health check endpoints for load balancers and monitoring:

from django.http import JsonResponse
from django.db import connection

def health_check(request):
    checks = {'status': 'healthy'}

    # Database check
    try:
        with connection.cursor() as cursor:
            cursor.execute('SELECT 1')
        checks['database'] = 'ok'
    except Exception:
        checks['database'] = 'error'
        checks['status'] = 'unhealthy'

    # Cache check
    try:
        from django.core.cache import cache
        cache.set('health_check', 'ok', 10)
        if cache.get('health_check') == 'ok':
            checks['cache'] = 'ok'
        else:
            checks['cache'] = 'error'
    except Exception:
        checks['cache'] = 'error'

    status_code = 200 if checks['status'] == 'healthy' else 503
    return JsonResponse(checks, status=status_code)

Map it to a URL that your load balancer polls:

urlpatterns = [
    path('health/', health_check, name='health_check'),
]

Performance monitoring

Track request duration and throughput using middleware:

import time
import logging

logger = logging.getLogger('performance')

class RequestTimingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        start = time.monotonic()
        response = self.get_response(request)
        duration = (time.monotonic() - start) * 1000

        logger.info('Request completed', extra={
            'method': request.method,
            'path': request.path,
            'status': response.status_code,
            'duration_ms': round(duration, 2),
        })

        if duration > 2000:
            logger.warning('Slow request', extra={
                'path': request.path,
                'duration_ms': round(duration, 2),
            })

        return response

Database monitoring

Track slow queries by enabling query logging:

LOGGING['loggers']['django.db.backends'] = {
    'handlers': ['json_console'],
    'level': 'DEBUG' if DEBUG else 'WARNING',
}

In development, Django Debug Toolbar shows all queries per request. In production, use database-level monitoring (pg_stat_statements for PostgreSQL) and application-level tracking. The PostgreSQL production guide covers database monitoring in detail.

Alerting strategy

Not every log message deserves an alert. Tier your alerts:

  • Critical (page immediately): application down, database unreachable, error rate above 5%
  • Warning (notify during hours): elevated error rate, slow responses, disk space below 20%
  • Informational (review weekly): deployment events, dependency update reminders, unusual traffic patterns

Set alert thresholds based on your actual baseline, not arbitrary numbers. A 200ms average response time is normal for your application? Alert at 500ms, not at some generic “over 100ms” threshold.

Frequently asked questions

How much logging is too much? Log at boundaries and decision points, not inside tight loops. A good rule: if you would want to see this data during an incident investigation, log it. If it would just add noise, skip it.

Should I use Sentry or a general logging platform? Both. Sentry excels at error grouping, deduplication, and developer workflow. A logging platform (ELK, Datadog) handles structured log search, metrics, and dashboards. They complement each other.

How do I monitor Celery tasks? Use Flower for real-time monitoring, and send task completion and failure events to your logging pipeline. Track task duration, success rate, and queue depth as metrics.

What about APM (Application Performance Monitoring)? APM tools like New Relic or Datadog APM provide request tracing, database query analysis, and service maps. They are valuable for complex applications with multiple services. The Django integrations are typically straightforward to add.