feat(proxy): add an opt-in debug mode to the dashboard #18

Merged
rcsheets merged 1 commit from feat/dashboard-debug into main 2026-10-02 05:40:30 +00:00
Owner

What

An opt-in debug mode for the dashboard, off by default. With it on, each request id in the recent requests table links to /_slp/request/<id>, which shows:

  • What the client sent: request line, headers, body, and the client address.
  • What SLP did with it: the upstream URL, whether model was rewritten and to what, whether stream was forced for aggregation, and the transport error of each attempt that failed.
  • What the client got back: status, headers, and body as SLP wrote them — the backend's response, a reassembled aggregate, or SLP's own error.
dashboard:
  debug: true

or --dashboard-debug for a one-off run (turn-on only, like --log-requests).

An id is a link only when there is a page behind it: plain text with debug off, and for a request whose body could not be read (it never gets an id). An id that has aged out of the last 50 gets a 404 that says so.

What turning it on means

  • Prompts and completions become readable by anyone the dashboard is served to, and backend URLs appear on these pages. This is the deliberate exception to the dashboard's "nothing a client sent" rule. The allow list is still the only gate; the dashboard shows a banner and the startup log a warning while it is on.
  • Credentials in headers are redacted at capture, so the value is never stored. It is by header name — Authorization, Cookie, Set-Cookie, anything containing token, secret, password, credential, or api-key — and shows as [redacted, 21 bytes]. Bodies are not redacted.
  • Memory: up to 128 KiB of each body for the last 50 requests, about 13 MB at worst. Longer bodies are shown truncated and say so.

Worth a look in review

  • serveCompletion now wraps the client's ResponseWriter in a recorder when debug is on. It tees what passes through and implements Flush and Unwrap; MaxBytesReader is still handed the server's own writer. A test sends the same streaming request with debug on and off and compares status, headers, body, and flushing.
  • With debug off nothing new runs on the request path: no body is copied and no response is wrapped.
  • The redaction pattern is broad on purpose; it will also redact a harmless header whose name happens to contain token or key-like words.
  • The main dashboard page is unchanged with debug off; its existing leak test still passes.

Testing

go vet and go test -race ./... pass. New tests cover: nothing kept or linked with debug off; every recent request linked with it on; the request page's contents for a served, a failed, and an unrouted request; credential redaction in both the stored detail and the rendered pages; responses unaltered by recording; body truncation and JSON indentation; and the request pages honouring the allow list. Also run against fake backends and checked in headless Chromium; not yet run against a real deployment.

🤖 Generated with Claude Code

## What An opt-in debug mode for the dashboard, off by default. With it on, each request id in the recent requests table links to `/_slp/request/<id>`, which shows: - **What the client sent**: request line, headers, body, and the client address. - **What SLP did with it**: the upstream URL, whether `model` was rewritten and to what, whether `stream` was forced for aggregation, and the transport error of each attempt that failed. - **What the client got back**: status, headers, and body as SLP wrote them — the backend's response, a reassembled aggregate, or SLP's own error. ```yaml dashboard: debug: true ``` or `--dashboard-debug` for a one-off run (turn-on only, like `--log-requests`). An id is a link only when there is a page behind it: plain text with debug off, and for a request whose body could not be read (it never gets an id). An id that has aged out of the last 50 gets a 404 that says so. ## What turning it on means - **Prompts and completions become readable by anyone the dashboard is served to**, and backend URLs appear on these pages. This is the deliberate exception to the dashboard's "nothing a client sent" rule. The allow list is still the only gate; the dashboard shows a banner and the startup log a warning while it is on. - **Credentials in headers are redacted at capture**, so the value is never stored. It is by header name — `Authorization`, `Cookie`, `Set-Cookie`, anything containing `token`, `secret`, `password`, `credential`, or `api-key` — and shows as `[redacted, 21 bytes]`. Bodies are not redacted. - **Memory**: up to 128 KiB of each body for the last 50 requests, about 13 MB at worst. Longer bodies are shown truncated and say so. ## Worth a look in review - `serveCompletion` now wraps the client's `ResponseWriter` in a recorder when debug is on. It tees what passes through and implements `Flush` and `Unwrap`; `MaxBytesReader` is still handed the server's own writer. A test sends the same streaming request with debug on and off and compares status, headers, body, and flushing. - With debug off nothing new runs on the request path: no body is copied and no response is wrapped. - The redaction pattern is broad on purpose; it will also redact a harmless header whose name happens to contain `token` or `key`-like words. - The main dashboard page is unchanged with debug off; its existing leak test still passes. ## Testing `go vet` and `go test -race ./...` pass. New tests cover: nothing kept or linked with debug off; every recent request linked with it on; the request page's contents for a served, a failed, and an unrouted request; credential redaction in both the stored detail and the rendered pages; responses unaltered by recording; body truncation and JSON indentation; and the request pages honouring the allow list. Also run against fake backends and checked in headless Chromium; not yet run against a real deployment. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(proxy): add an opt-in debug mode to the dashboard
All checks were successful
ci / check (pull_request) Successful in 47s
a7fba4fde2
The dashboard says a request failed with "backend unreachable" and gives an
id to take to the log. That is the right amount for a page served to a whole
network, and not enough when you are the one debugging. dashboard.debug, off
by default, keeps the request and the response for each of the recent
requests and serves a page per request at /_slp/request/<id>, linked from
its id in the recent requests table:

- what the client sent: request line, headers, body, peer address
- what SLP did with it: the upstream URL, whether model was rewritten and
  to what, whether stream was forced for aggregation, and the transport
  error of each attempt that failed
- what the client got back: status, headers, and body as SLP wrote them --
  the backend's response, a reassembled aggregate, or SLP's own error

--dashboard-debug turns it on for a one-off run; like --log-requests it is
turn-on only.

An id is a link only when there is a page behind it. With debug off there
are none and the pages are a bare 404; a request whose body could not be
read has no id to link. An id that has aged out of the last 50 gets a 404
that says so.

This is the deliberate exception to the dashboard showing nothing a client
sent: with debug on, anyone the dashboard is served to can read prompts and
completions, and backend URLs. The allow list is still the only gate, so the
dashboard carries a banner and the startup log a warning while it is on.

Credentials stay out regardless. A header whose name looks like one
(Authorization, Cookie, Set-Cookie, or anything containing token, secret,
password, credential, or api-key) is replaced by a note of its length when
it is captured, so the value is never stored. Bodies are not redacted.

Recording never changes a response. The client's ResponseWriter is wrapped
by a recorder that tees what passes through and keeps Flush working, so
status, headers, bytes, and per-frame flushing are the same with debug on
or off; MaxBytesReader is still handed the server's own writer. Up to
128 KiB of each body is kept, about 13 MB at worst across 50 requests, and
a longer body is shown truncated and says so. A whole JSON body is indented
with json.Indent, which moves whitespace and nothing else. With debug off
no body is copied and no response is wrapped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator

Automated review by pr-reviewer v0.52.3 | Safety Check | Ministral 3 Instruct | tracking id r-bf4298-d66bf1
This is an AI-generated review and may contain mistakes.

Status: ❌ Failed


This review couldn't be completed: that model isn't loaded on the inference service right now, and no alternate model was able to review it either. Consider splitting this PR into smaller changes. Tracking id r-bf4298-d66bf1.

Comment @pr-reviewer-bot retry to try again.

<!-- pr-reviewer:review --> *Automated review by [pr-reviewer](https://git.brooktrails.org/brooktrails/pr-reviewer) v0.52.3 | Safety Check | Ministral 3 Instruct | tracking id `r-bf4298-d66bf1`* *This is an AI-generated review and may contain mistakes.* **Status:** ❌ Failed --- This review couldn't be completed: that model isn't loaded on the inference service right now, and no alternate model was able to review it either. Consider splitting this PR into smaller changes. Tracking id `r-bf4298-d66bf1`. Comment `@pr-reviewer-bot retry` to try again.
rcsheets deleted branch feat/dashboard-debug 2026-10-02 05:40:30 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
brooktrails/slp!18
No description provided.