Logging
Overview
All logs live in the logs/ directory at the project root. This directory is gitignored and created automatically when Django starts (via LOGS_DIR.mkdir(exist_ok=True) in settings).
Django app logs go through Python’s standard logging module and are written to logs/django.log. Shell scripts (bin/update.sh, cron jobs) write directly to their respective log files in the same directory.
Log Directory Layout
| File | Source | Contents |
|---|---|---|
logs/django.log | Django (Python logging) | App logs from all apps.* loggers |
logs/update.log | bin/update.sh | Update script output |
logs/update-history.jsonl | bin/update.sh | Structured update history (JSON lines) |
logs/cron.log | cron job | Health check pipeline output |
logs/push.log | cron job | Cluster push-to-hub output |
Django Logging
The logging configuration lives in config/settings.py under the LOGGING dict. It defines two handlers:
- file – writes to
logs/django.logusing theverboseformatter - console – writes to stderr using the same formatter
The verbose format produces lines like:
2026-04-04 12:34:56,789 [INFO] apps.alerts.drivers.grafana: Parsed 3 alerts from webhook
The root logger and the apps logger both use level INFO by default.
Changing the log level
Edit config/settings.py and change the level value in the relevant logger:
"loggers": {
"apps": {
"handlers": ["console", "file"],
"level": "DEBUG", # was "INFO"
"propagate": False,
},
},
Adding a logger for a specific app
Add a new entry under loggers in the LOGGING dict:
"loggers": {
"apps": { ... },
"apps.intelligence": {
"handlers": ["console", "file"],
"level": "DEBUG",
"propagate": False,
},
},
Then use it in your code:
import logging
logger = logging.getLogger("apps.intelligence")
logger.debug("Provider returned %d tokens", token_count)
Update History
bin/update.sh records every update attempt as a JSON line in logs/update-history.jsonl. Use the --history flag to view it:
bin/update.sh --history # last 20 updates
bin/update.sh --history -n 5 # last 5
bin/update.sh --history --json # raw JSONL
The formatted output looks like:
Update History (last 3):
DATE FROM TO STATUS COMMITS MODE AUTO
--------------------------------------------------------------------------------
2026-04-03 09:15:22 a1b2c3d e4f5g6h success 4 venv no
2026-04-02 14:30:01 e4f5g6h i7j8k9l failed 0 venv yes
2026-04-01 08:00:00 i7j8k9l i7j8k9l up_to_date 0 venv no
JSONL field reference
| Field | Type | Description |
|---|---|---|
timestamp | string | ISO 8601 timestamp of the update attempt |
old_sha | string | Short SHA before the update |
new_sha | string | Short SHA after the update |
status | string | success, failed, or up_to_date |
commits | number | Number of commits pulled |
mode | string | Python environment mode (venv, system, etc.) |
auto_update | boolean | Whether --auto-env was used |
failed_step | string or null | Which step failed (e.g., pull, migrate) |
rolled_back | boolean | Whether a rollback was performed |
Debugging
Common commands for tailing and searching logs:
tail -f logs/django.log # follow Django logs
tail -f logs/update.log # follow update output
tail -f logs/cron.log # follow cron output
grep ERROR logs/django.log # find errors
grep '"status":"failed"' logs/update-history.jsonl # find failed updates
Log Rotation
For production deployments, configure logrotate to prevent log files from growing indefinitely:
/path/to/project/logs/*.log {
weekly
rotate 4
compress
missingok
notifempty
}
Save this as /etc/logrotate.d/server-maintanence and logrotate will handle rotation automatically on its next run.