Problem
PR #114 (part of #113) captures the stack of a timed-out worker and logs per-request durations. That closes the hang attribution gap, but three diagnostic blind spots remain — all observability concerns that were deliberately scoped out of #114 to keep it low-risk and reviewable.
Gaps to close
1. OOM / SIGKILL is a black box (highest value)
scanning-web workers are SIGKILL'd (SCANNING-1W, "perhaps out of memory"), and SIGKILL is uncatchable — the only way to get evidence is to log before the kill. There is currently no in-process memory signal at all. Proposed:
- A lightweight background thread logging worker RSS every N seconds (a memory-over-time trace in pod logs, line up against the kill timestamp).
- A pre-OOM watchdog: when RSS crosses a soft threshold (e.g. 85% of the pod limit), dump all-thread stacks + RSS to stderr/Sentry. The only in-process route to "what was the worker doing when it ran out of memory."
2. No "which request + params" attribution
faulthandler names the view function, but not the request context (which scan.pk, user, URL params). And gunicorn's access log writes a line on response completion, so a request that hangs until the worker is killed never logs — the access log shows everything except the culprit. Proposed:
- An in-flight-request logger: middleware records
(thread, url, scan_pk, user, start_time) on entry into a shared dict; a background thread logs any entry older than N seconds (with elapsed + context) while it's still hung, and clears it on completion. Names the frozen endpoint with its params, before the kill.
3. Threadpool / concurrency saturation is invisible
A leading hypothesis is asgiref-threadpool exhaustion / GIL starvation, but nothing logs it. Proposed:
- Periodically log in-flight request count and
threading.active_count() (pairs naturally with #2's shared state).
Optional
send_default_pii=True in Sentry init to attach user/request details to error events (privacy tradeoff).
- Structured/JSON logs for searchability in the EKS log aggregator.
Definition of done
Notes
Problem
PR #114 (part of #113) captures the stack of a timed-out worker and logs per-request durations. That closes the hang attribution gap, but three diagnostic blind spots remain — all observability concerns that were deliberately scoped out of #114 to keep it low-risk and reviewable.
Gaps to close
1. OOM / SIGKILL is a black box (highest value)
scanning-webworkers are SIGKILL'd (SCANNING-1W, "perhaps out of memory"), and SIGKILL is uncatchable — the only way to get evidence is to log before the kill. There is currently no in-process memory signal at all. Proposed:2. No "which request + params" attribution
faulthandler names the view function, but not the request context (which
scan.pk, user, URL params). And gunicorn's access log writes a line on response completion, so a request that hangs until the worker is killed never logs — the access log shows everything except the culprit. Proposed:(thread, url, scan_pk, user, start_time)on entry into a shared dict; a background thread logs any entry older than N seconds (with elapsed + context) while it's still hung, and clears it on completion. Names the frozen endpoint with its params, before the kill.3. Threadpool / concurrency saturation is invisible
A leading hypothesis is asgiref-threadpool exhaustion / GIL starvation, but nothing logs it. Proposed:
threading.active_count()(pairs naturally with #2's shared state).Optional
send_default_pii=Truein Sentry init to attach user/request details to error events (privacy tradeoff).Definition of done
scan_pk, user) while still in flight.Notes