Follow-ups to the review of PR #1415. The substantive one: degrading to a
NullHandler was completely silent. The 'django' logger has only the 'file'
handler and the 'website' logger's console handler is gated by
require_debug_true (False in prod), so an unwritable log dir means the server
runs blind — and there is no console access on -test or prod to notice it.
Surface the degraded state two web-reachable ways:
- /version.json gains log_to_file + log_file (scriptable deploy check).
- The admin dashboard gains a superuser-only warning callout naming the path
that failed. Red-tinted so it doesn't read as another amber tip box; both
light and dark variants clear WCAG AA (ratios noted inline).
Also from the review:
- Rename _log_dir_is_writable -> _ensure_log_dir_writable; the function calls
os.makedirs, so a pure-predicate name was misleading. Document the os.access
root caveat (the deployed container runs as apache, so the guard still bites).
- Extract _file_log_handler() so both branches are testable and the LOGGING
literal loses its two-dict inline ternary. LOGGING is evaluated once at
import, so the degrade branch was otherwise untestable — settings_test.py
swaps the handler before any test runs.
- Cross-reference LOG_DIR and MEDIA_ROOT at both sites, and add a test pinning
LOG_FILE to MEDIA_ROOT/debug.log — the contract the log's readability rests on.
- Document ML_LOG_DIR (previously undocumented) in DEPLOYMENT.md and CLAUDE.md.
Correcting a stale premise while here: the /logs/ URL that settings.py and
DEPLOYMENT.md cite as the way to read debug.log 404s on BOTH prod and test
(verified by curl, 2026-07-28). SSH to the shared filesystem is the reliable
path; the docs now say so. The INFO-not-DEBUG conservatism stays, since the
file does still live in a web-served tree.
Tests: 724 pass. New coverage for the helper's dir creation, both handler
branches, the MEDIA_ROOT contract, the version.json fields, and the admin
callout (shown to superusers when degraded, hidden otherwise).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fixes #1283.
Problem
The
LOGGINGfilehandler inmakeabilitylab/settings.pyhardcoded an absolute, container-specific path:Django evaluates
LOGGINGatdjango.setup(), so on any host lacking that exact directory (e.g. GitHub Actions CI) startup died withFileNotFoundErrorbefore a single request or test ran. This was papered over insettings_test.py(swapping the handler for aNullHandler), but the source-level fragility remained for any non-/codedeploy or tooling.Fix
Combines the issue's Option 1 (derive from
BASE_DIR) and Option 2 (degrade gracefully):LOG_DIR = os.environ.get('ML_LOG_DIR', os.path.join(BASE_DIR, 'media')),LOG_FILE = LOG_DIR/debug.log.os.makedirs(LOG_DIR, exist_ok=True)and check writability; if the dir can't be created or written, thefilehandler falls back tologging.NullHandlerso startup never dies over a log path.ML_LOG_DIRenv override for future non-/codehosts.Deliberately does not relocate the log out of
MEDIA_ROOT(the issue's Option 3) — the/logs/exposure is intentional (docs/DEPLOYMENT.md) and relocating needs CSE IT coordination.No behavior change on servers/local dev
In the container
BASE_DIRis/code, so the default still resolves to/code/media/debug.log. Prod, test, and local dev are unaffected. TheNullHandlerswap insettings_test.pyis now belt-and-suspenders rather than the only crash guard.Verification
RotatingFileHandlerat/code/media/debug.log, writable.ML_LOG_DIR=/nonexistent/cannot/create→ degrades toNullHandler,django.setup()succeeds, logging still functions.manage.py check --settings=makeabilitylab.settings_test→ no issues.🤖 Generated with Claude Code