Skip to main content

Django admin log viewer supporting MongoDB and SQL databases.

Project description

django-log-panel

Latest on Django Packages

django-log-panel collects logs from any logger configured in Django's standard LOGGING setting and displays them on a dashboard inspired by a status page. Each logger gets its own health card showing error and warning counts, a colour-coded activity timeline, and a drilldown into searchable, filterable log entries - all inside Django admin, with no separate service to run.

For alerting, it emits a Django signal when a logger crosses a configured threshold, leaving the response - email, Slack, webhook - entirely to the application.

Screenshots

The dashboard shows one card per logger. Clicking a card opens a paginated table of log entries with level filtering and message search.

Log panel dashboard showing per-logger health cards for the last 24 hours

Log panel dashboard showing a 90-day logger timeline

Log detail view with message search and paginated entries Log detail view with the level filter dropdown open

Supports two storage backends:

  • MongoDB - write-heavy, append-only logging with automatic TTL-based retention.
  • SQL - logs stored in any Django-supported relational database via the Panel model.

Key Features

  • Status-page dashboard in Django admin - a health card per logger showing total errors, warnings, and recent issues from the last hour, all without a separate service to run.
  • Colour-coded activity timeline - each card includes a visual timeline strip across configurable time ranges (e.g. last 24h, 30d, 90d).
  • Searchable, filterable log table - click any card to open a paginated list of log entries with level filtering and free-text message search.
  • Two storage backends - write logs to MongoDB (append-only, automatic TTL-based cleanup) or any Django-supported SQL database.
  • Threshold alerting via Django signals - emits a signal when a logger crosses a configured per-level count within a rolling one-hour window, leaving the response entirely to the application.
  • Customisable log level colors - configure hex colours for any log level badge, including custom Python log levels, which are automatically added to the filter dropdown.
  • Configurable timeline ranges - define your own time range slots to match how you think about your traffic patterns.
  • Configurable alert thresholds - set per-level count thresholds before the signal fires, or disable alerting for specific levels entirely.
  • Configurable page title and table size - customise the panel heading and how many rows appear per page in the log detail view.
  • Flexible access control - restrict the panel to specific users or groups via a permission callback, beyond the default Django staff check.
  • High-volume buffering support - optionally wrap handlers with Python's built-in MemoryHandler for batch writes on busy applications.
  • Configurable data retention - automatic TTL expiry for MongoDB; a management command with dry-run and batch options for SQL.

Requirements

  • Python ≥ 3.12
  • Django ≥ 5.2
  • pymongo == 4.16.0 (only required for MongoDB backend)

Installation

# with uv
uv add django-log-panel

# with pip
pip install django-log-panel

For MongoDB support, install the optional extra:

# with uv
uv add "django-log-panel[mongodb]"

# with pip
pip install "django-log-panel[mongodb]"

Local Development

If you want to work on a local checkout, install uv from the official docs.

With uv

cd `Project directory`
uv venv --python=3.13
uv sync --group dev
uv run pytest

Linting & typing

cd `Project directory`
uv run ruff check
uv run ruff format
uv run ty check

Quick Start

1. Add to INSTALLED_APPS:

INSTALLED_APPS = [
    ...
    "log_panel",
]

2. Configure a backend (see MongoDB Setup or SQL Setup below).

3. Add a handler to Django LOGGING (see the relevant setup section).

4. Open Django admin and go to Application Logs, or navigate to:

/admin/log_panel/panel/

How Backend Resolution Works

The admin UI reads data through log_panel.conf.get_backend(). The backend is resolved in this order:

  1. LOG_PANEL["BACKEND"] - if you explicitly provide a backend class path.
  2. SQL backend - if LOG_PANEL["DATABASE_ALIAS"] is set.
  3. MongoDB backend - if LOG_PANEL["CONNECTION_STRING"] is set.
  4. No backend - admin shows an unconfigured state.

Note: LOG_PANEL controls how the admin reads logs. Django LOGGING handlers control where log records are written.

MongoDB Setup

Use this when you want cheap append-only logging with automatic TTL-based retention.

Settings

LOG_PANEL = {
    "CONNECTION_STRING": "mongodb://localhost:27017",
    "DB_NAME": "myapp_logs",
    "COLLECTION": "logs",
    "TTL_DAYS": 90,
}

Django LOGGING Configuration

LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "handlers": {
        "log_panel": {
            "class": "log_panel.handlers.MongoDBHandler",
        },
    },
    "root": {
        "handlers": ["log_panel"],
        "level": "INFO",
    },
}

Notes

  • Formatters configured in LOGGING have no effect on MongoDBHandler. The message is always stored as the raw log text; structured fields (level, timestamp, module, etc.) are captured directly from the log record. Exception tracebacks are appended automatically when present.
  • MongoDBHandler creates three indexes automatically on the first write:
    • A TTL index on timestamp for automatic record expiry.
    • A compound index on (timestamp, logger_name, level) to speed up timeline aggregations (covered index, no document fetch needed).
    • A compound index on (logger_name, timestamp DESC) to speed up table-view queries filtered by logger.
  • MongoDB cleanup runs asynchronously; no Django management command is needed.
  • LogsRouter.allow_migrate() returns False for log_panel in MongoDB-only mode, so no SQL migration is needed.
  • For large collections with long time ranges (e.g. 90 days over millions of records), set LOG_PANEL["ALLOW_DISK_USE"] = True if aggregation queries hit MongoDB's 100 MB in-memory limit.

SQL Setup

Use this when logs must live in a relational database.

Database Configuration

Point LOG_PANEL["DATABASE_ALIAS"] at the database you want to use for log storage:

DATABASES["logs"] = {
    "ENGINE": "django.db.backends.postgresql",
    "NAME": "myapp_logs",
    "USER": "...",
    "PASSWORD": "...",
    "HOST": "...",
    "PORT": "...",
}

DATABASE_ROUTERS = [
    "log_panel.routers.LogsRouter",
]

LOG_PANEL = {
    "DATABASE_ALIAS": "logs",
    "TTL_DAYS": 90,
}

Django LOGGING Configuration

LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "handlers": {
        "log_panel": {
            "class": "log_panel.handlers.DatabaseHandler",
        },
    },
    "root": {
        "handlers": ["log_panel"],
        "level": "INFO",
    },
}

Notes

  • Formatters configured in LOGGING have no effect on DatabaseHandler. The message is always stored as the raw log text; structured fields (level, timestamp, module, etc.) are captured directly from the log record. Exception tracebacks are appended automatically when present.

Migration

LogsRouter prevents the migration from running on the wrong database, but Django's migrate command only targets default unless told otherwise. You still need to point it at your alias explicitly:

python manage.py migrate log_panel --database=logs

If your logging alias is default, the normal migration flow is sufficient.

Retention Cleanup

SQL storage does not have automatic TTL cleanup. Use the management command instead:

# Dry run - prints count without deleting
python manage.py delete_old_logs --dry-run

# Delete logs older than 30 days
python manage.py delete_old_logs --days 30

# Custom batch size (default: 1000)
python manage.py delete_old_logs --days 30 --batch-size 5000

See delete_old_logs for full option reference.

Writing Logs

Any Python logger writes into log_panel as long as your Django LOGGING configuration routes to MongoDBHandler or DatabaseHandler.

import logging

logger = logging.getLogger(__name__)

logger.info("Info log")
logger.warning("Warning log")
logger.error("Error log")
logger.critical("Critical log")

Named loggers work the same way:

import logging

sql_logger = logging.getLogger("myapp.sql")
sql_logger.debug("Manual SQL diagnostic message")

High-Volume Logging

By default, both handlers write each record immediately. This is safest - no records are lost on a crash - but one database write per log call can become a bottleneck on busy applications.

Python's standard library includes logging.handlers.MemoryHandler, which buffers records and flushes in two situations:

  • The buffer reaches capacity (number of records).
  • A record at or above flushLevel is emitted - high-severity records are never held.

Example - SQL

LOGGING = {
    "handlers": {
        "log_panel_db": {
            "class": "log_panel.handlers.DatabaseHandler",
        },
        "log_panel": {
            "class": "logging.handlers.MemoryHandler",
            "capacity": 50,         # flush after 50 records
            "flushLevel": "ERROR",  # always flush immediately on ERROR or CRITICAL
            "target": "log_panel_db",
        },
    },
    "root": {
        "handlers": ["console", "log_panel"],
        "level": "INFO",
    },
}

Example - MongoDB

LOGGING = {
    "handlers": {
        "log_panel_mongo": {
            "class": "log_panel.handlers.MongoDBHandler",
        },
        "log_panel": {
            "class": "logging.handlers.MemoryHandler",
            "capacity": 100,
            "flushLevel": "ERROR",
            "target": "log_panel_mongo",
        },
    },
}

Trade-offs

Setting Effect
Lower capacity Smaller exposure window; more frequent writes
Higher capacity Better throughput; more records at risk on a hard crash
flushLevel="ERROR" Errors always written immediately, regardless of buffer state
flushLevel="CRITICAL" Only critical records bypass buffering

MemoryHandler flushes on close(), which Django calls during a clean shutdown - normal process termination does not lose buffered records. Only an abrupt crash (OOM kill, power loss) can lose records that have not yet been flushed.

Threshold Alert Signals

django-log-panel emits a Django signal, log_panel.signals.log_threshold_reached, each time a logger crosses a configured count threshold. Connect any receiver to act on it - send an email, post to Slack, call a webhook, or anything else.

How it works

After each log record is written, the handler counts how many records at that level the same logger has emitted in the last rolling hour. When the count exactly matches the configured threshold, log_threshold_reached is dispatched with a ThresholdAlertEvent payload.

Thresholds are configured per log level via LOG_PANEL["THRESHOLDS"]. By default, WARNING, ERROR, and CRITICAL each have a threshold of 1 - the signal fires on the first occurrence of each. Set a level to None to disable it, or raise the value to require more occurrences before the signal fires:

LOG_PANEL = {
    "THRESHOLDS": {
        "CRITICAL": 1,   # fire on first critical
        "ERROR": 10,     # fire after 10 errors in the rolling hour
        "WARNING": None, # no signal for warnings
    }
}

ThresholdAlertEvent fields

Field Type Description
logger_name str Name of the logger that crossed the threshold
threshold_level LogLevel The level that was configured (e.g.ERROR)
record_level LogLevel The actual level of the triggering record (e.g.CRITICAL)
threshold int The configured count that was reached
matching_count int Actual count within the window (equals threshold)
window_start datetime Start of the one-hour rolling window (UTC)
window_end datetime End of the window - the timestamp of the triggering record (UTC)
message str Formatted message of the triggering record
module str Module where the record was emitted
pathname str Full path of the source file
line_number int Line number within the source file

Connecting a receiver

Define a receiver function and import it during app startup:

# myapp/log_alerts.py
from django.dispatch import receiver

from log_panel.signals import ThresholdAlertEvent, log_threshold_reached


@receiver(log_threshold_reached)
def on_threshold_reached(sender, event: ThresholdAlertEvent, **kwargs):
    ...

Notes

  • The signal uses send_robust - exceptions in receivers are caught and do not affect log writing.
  • When DatabaseHandler or MongoDBHandler is wrapped in logging.handlers.MemoryHandler, the signal fires when the buffered record is flushed into the real handler, not at the point of the original logger.error(...) call.

Admin UI

The admin view is optimised for browsing logger health first and raw entries second.

  • The landing page shows one card per logger.
  • Each card shows total errors, total warnings, recent issues from the last hour, and a colour-coded timeline strip.
  • Available time ranges are configured through LOG_PANEL["RANGES"] (default: 24h, 30d, 90d).
  • Clicking a logger opens a paginated table view.
  • The table view supports filtering by level and free-text search against the message body.
  • Timestamps are displayed in Django's configured default timezone.

Admin URL: /admin/log_panel/panel/

LOG_PANEL Settings Reference

Setting Default Description Example
BACKEND None Dotted path to a custom backend class. Overrides auto-detection. "myapp.logging.MyBackend"
CONNECTION_STRING None MongoDB connection string. "mongodb://localhost:27017"
DB_NAME "log_panel" MongoDB database name. "myapp_logs"
COLLECTION "logs" MongoDB collection name. "app_logs"
TTL_DAYS 90 Retention window in days. 30
SERVER_SELECTION_TIMEOUT_MS 2000 Milliseconds before MongoDBConnectionError is raised. Applies to both MongoDBBackend and MongoDBHandler. 5000
ALLOW_DISK_USE False Pass allowDiskUse=True to MongoDB aggregation pipelines. Enable this when queries on large collections (millions of records, long time ranges) exceed MongoDB's 100 MB in-memory aggregation limit. MongoDB-only. True
DATABASE_ALIAS None Explicit SQL database alias for log storage. "logs"
TITLE "Panel Logs" Page title shown in the admin UI. "Production Logs"
PAGE_SIZE 10 Rows per page in the detail table. 25
RANGES {"24h": ..., "30d": ..., "90d": ...} Timeline range definitions for the logger cards. See Custom RANGES
THRESHOLDS {"WARNING": 1, "ERROR": 1, "CRITICAL": 1} Per-level alert thresholds. The log_threshold_reached signal fires when a level's count in the rolling one-hour window hits the configured value. Omit a level to keep its default; set a level to None to disable it. {"CRITICAL": 1, "ERROR": 5, "WARNING": None}
LEVEL_COLORS See Log Level Colors Hex colors for each log level in the admin table view. Merge with defaults - only override the levels you want to change. Custom level names are supported. {"CRITICAL": "#9b00d3", "MY_AUDIT": "#0055aa"}
PERMISSION_CALLBACK None Dotted path to a callable (request: HttpRequest) -> bool that controls who can view the panel. When not set, any active staff user may view it. "myapp.utils.superusers_only"

Log Level Colors

The table view colors each log level badge using the LEVEL_COLORS setting. The defaults map Python's six standard levels:

Level Default color
NOTSET #888 (gray)
DEBUG #888 (gray)
INFO #417690 (blue)
WARNING #c0a000 (amber)
ERROR #c47900 (orange)
CRITICAL #ba2121 (red)

Override individual levels or add entirely custom ones - you do not need to specify the full set:

LOG_PANEL = {
    "LEVEL_COLORS": {
        "CRITICAL": "#9b00d3",
        "MY_AUDIT": "#0055aa",
    },
}

Any level name with no entry in LEVEL_COLORS falls back to gray.

Custom log levels are supported. Register a custom level with Python's logging module, add it to LEVEL_COLORS, and it will automatically appear in the filter dropdown and receive its configured color in the table:

# Register the custom level
import logging
MY_AUDIT = 25
logging.addLevelName(MY_AUDIT, "MY_AUDIT")

# Configure the panel
LOG_PANEL = {
    "LEVEL_COLORS": {
        "MY_AUDIT": "#0055aa",
    },
}

Note: Python's logger.exception() method logs at ERROR level - records stored from it carry level = "ERROR", not "EXCEPTION".

Permissions

By default, any active staff user (is_staff=True) can view the log panel. Use PERMISSION_CALLBACK to restrict access to a specific subset of users:

LOG_PANEL = {
    "PERMISSION_CALLBACK": "myapp.utils.can_view_logs",
}

The callback receives the current HttpRequest and must return True to grant access:

def can_view_logs(request):
    return request.user.is_superuser
# Allow only users in a specific group
def can_view_logs(request):
    return request.user.groups.filter(name="log-viewers").exists()

Custom RANGES

Override the timeline ranges by providing RangeConfig instances or plain dicts:

from datetime import timedelta
from log_panel.types import RangeConfig, RangeUnit

LOG_PANEL = {
    "RANGES": {
        "1h": RangeConfig(
            delta=timedelta(hours=1),
            unit=RangeUnit.HOUR,
            slots=12,
            format="%H:%M",
            label="Last hour",
        ),
        "7d": RangeConfig(
            delta=timedelta(days=7),
            unit=RangeUnit.DAY,
            slots=7,
            format="%b %d",
            label="Last 7 days",
        ),
    },
}

delete_old_logs

Deletes Panel entries older than the configured TTL. Only relevant for the SQL backend - MongoDB uses its built-in TTL index.

python manage.py delete_old_logs [--days DAYS] [--batch-size BATCH_SIZE] [--dry-run]
Option Default Description
--days LOG_PANEL["TTL_DAYS"] Override the retention window for this run.
--batch-size 1000 Number of records to delete per batch. Deleting millions of rows in a single query locks the table and spikes I/O. Batching keeps each delete small so the database stays responsive. Increase for faster cleanup on idle systems, decrease if you see lock contention.
--dry-run - Print how many records would be deleted without deleting them.

Support & Donate

If you found django-log-panel helpful, consider supporting its development.

Ko-fi Page

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_log_panel-0.1.4.tar.gz (44.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_log_panel-0.1.4-py3-none-any.whl (40.0 kB view details)

Uploaded Python 3

File details

Details for the file django_log_panel-0.1.4.tar.gz.

File metadata

  • Download URL: django_log_panel-0.1.4.tar.gz
  • Upload date:
  • Size: 44.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for django_log_panel-0.1.4.tar.gz
Algorithm Hash digest
SHA256 b7c0dfc71987fb0800ce329b299862f50e055572bd07c5aab2cd46e4438a6e7a
MD5 3cf25dcda83d91495a3c0606175e58c0
BLAKE2b-256 da5e50839d5c3736eafbe87f7fe859868f46f9879a592aa051ece888e8ea328d

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_log_panel-0.1.4.tar.gz:

Publisher: release.yml on rreiter3/django-log-panel

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file django_log_panel-0.1.4-py3-none-any.whl.

File metadata

File hashes

Hashes for django_log_panel-0.1.4-py3-none-any.whl
Algorithm Hash digest
SHA256 539cdae473fceaeb608e363c516168483709664871435612b72df6b4fa6c4575
MD5 1aadfecb4a554142455c73dae7e76787
BLAKE2b-256 a845b128f0217d5a7a0908b8a41999f6845193b950024d1b86fb4ef6071833a1

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_log_panel-0.1.4-py3-none-any.whl:

Publisher: release.yml on rreiter3/django-log-panel

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page