You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Address code-review findings on the #1283 log-path fix
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>
Copy file name to clipboardExpand all lines: CLAUDE.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -130,6 +130,7 @@ the existing viewset/serializer pattern and keep `v1` fields additive-only
130
130
-**Prod/test `config.ini` has only a `[Django]` section — no `[Postgres]` section.** Per `settings.py`, a missing `[Postgres]` section means Django uses the fallback `DATABASES` default (`HOST='db'`) — i.e. the dockerized `db` service of the active compose file. A `[Postgres]` section, if added, would override it. So the DB is the in-stack `db` container in **every** environment (no external Postgres); on the servers that's the `db` service in `docker-compose.yml`.
-`TIME_ZONE = 'America/Los_Angeles'`. `ML_WEBSITE_VERSION` in settings is shown in the admin header and used in release tagging.
133
+
-**Logging (#1283):**`debug.log` lives at `LOG_DIR/debug.log`, where `LOG_DIR` is `$ML_LOG_DIR` or `<BASE_DIR>/media` (`/code/media` in the container). Keep it inside `MEDIA_ROOT` — the web-served `/logs/debug.log` depends on that. `ML_LOG_DIR` is unset everywhere today; it exists for non-`/code` hosts. If the dir isn't writable the file handler degrades to a `NullHandler` rather than crashing `django.setup()`, and since there's no console on the servers that state surfaces via `/version.json` (`log_to_file`) and a superuser-only callout on the admin dashboard.
133
134
134
135
### Container startup side effects (`docker-entrypoint.sh`)
# Makeability Lab Global Variables, including Makeability Lab version
89
-
ML_WEBSITE_VERSION="2.31.0"# Keep this updated with each release and also change the short description below
90
-
ML_WEBSITE_VERSION_DESCRIPTION="A project's roster in the public API now reports each person's title over the span of their role (#1435), so someone on a project since 2012 reads as what they are today rather than the title they held back then. Finished stints still read as what that person was at the time."
89
+
ML_WEBSITE_VERSION="2.31.1"# Keep this updated with each release and also change the short description below
90
+
ML_WEBSITE_VERSION_DESCRIPTION="The Django log path is now derived from the project root instead of a hardcoded container path, and an unwritable log directory degrades gracefully instead of killing startup (#1283). Because the servers have no console, /version.json now reports whether file logging is actually live."
91
91
DATE_MAKEABILITYLAB_FORMED=datetime.date(2012, 1, 1) # Date Makeability Lab was formed
92
92
MAX_BANNERS=7# Maximum number of banners on a page
93
93
@@ -110,21 +110,39 @@
110
110
# an absolute container-specific path. Django evaluates LOGGING at django.setup(),
111
111
# so on any host lacking that exact directory (e.g. GitHub Actions CI) startup died
112
112
# with FileNotFoundError before a single request/test ran. Derive the path from
113
-
# BASE_DIR instead (still under media/ so it stays reachable via the intentional
114
-
# /logs/ URL — see docs/DEPLOYMENT.md), allow an ML_LOG_DIR env override, and if
113
+
# BASE_DIR instead (still under media/, the bind-mounted tree, so the file stays
114
+
# readable over SSH — see docs/DEPLOYMENT.md), allow an ML_LOG_DIR env override, and if
115
115
# the directory can't be created or written, fall back to a NullHandler so a bad
116
116
# log path never crashes startup.
117
-
def_log_dir_is_writable(log_dir):
118
-
"""Return True if ``log_dir`` exists (or can be created) and looks writable.
119
-
120
-
Used to decide whether the file log handler is active or degrades to a
121
-
NullHandler so a bad log path never crashes ``django.setup()`` (issue #1283).
122
-
123
-
Note: this checks the *directory*, not the eventual log file. A dir that is
124
-
writable but already holds a root-owned, read-only ``debug.log`` would still
125
-
let RotatingFileHandler raise on open — an edge case we accept, since it is
126
-
strictly better than the previous unconditional crash and matches the real
127
-
deploy model (media/ is owned by the app's own user).
117
+
#
118
+
# Degrading is silent by default, and that is dangerous here: the 'django' logger
119
+
# has only the 'file' handler, and the 'website' logger's console handler is gated
120
+
# by require_debug_true (False in prod), so an unwritable log dir means the app runs
121
+
# completely blind. We have no console access on the -test or prod servers, so the
122
+
# degraded state is surfaced two web-reachable ways instead: the 'log_to_file' field
123
+
# on /version.json (website/views/version.py) and a warning callout on the admin
124
+
# dashboard (website/templates/admin/index.html).
125
+
def_ensure_log_dir_writable(log_dir):
126
+
"""Create ``log_dir`` if needed and return True if it looks writable.
127
+
128
+
Named for the side effect: this *creates* the directory (``os.makedirs``)
129
+
rather than merely inspecting it. Used to decide whether the file log handler
130
+
is active or degrades to a NullHandler, so a bad log path never crashes
131
+
``django.setup()`` (issue #1283).
132
+
133
+
Two known limits, both accepted as strictly better than the previous
134
+
unconditional crash:
135
+
136
+
1. This checks the *directory*, not the eventual log file. A dir that is
137
+
writable but already holds a root-owned, read-only ``debug.log`` would
138
+
still let RotatingFileHandler raise on open. That doesn't match the real
139
+
deploy model, where media/ is owned by the app's own user.
140
+
2. ``os.access(dir, os.W_OK)`` returns True for root regardless of the
141
+
directory mode, so a mode-555 dir wouldn't be caught when running as root.
142
+
The deployed container runs as ``apache`` (UID 48, see Dockerfile), so the
143
+
guard is meaningful where it matters; only the root devcontainer bypasses
144
+
it. The common failures — missing dir, uncreatable dir, read-only
0 commit comments