Repository navigation
fix(proxy): let HEADROOM_LOG_LEVEL=debug turn on Headroom's own debug logs - #4137
chopratejas wants to merge 38 commits into
Conversation
… logs HEADROOM_LOG_LEVEL only set uvicorn's level. The headroom logger and the proxy.log handler were pinned to INFO, so no headroom.* DEBUG line could ever be seen in a running proxy, even though the docs tell operators to set HEADROOM_LOG_LEVEL=debug to diagnose it. debug and trace now lower both to DEBUG. Every other value keeps INFO, so the default (warning, which only quiets uvicorn) still shows Headroom's INFO events. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assisted-by: claude-code:claude-opus-5-5 Headline-Session: 6e77a807-8dd4-4a77-a2fa-ddce6dc2515e Headline-AI-Share: 71% strict / 71% weighted
PR governanceThis PR does not yet satisfy the required template fields:
Please update the PR body, or move the PR back to draft while it is still in progress. |
JerrettDavis
left a comment
There was a problem hiding this comment.
Please update the reused file handler's level before returning from the same-path initialization branch. The parent logger is updated to DEBUG, but an existing proxy-log handler remains at INFO.
I reproduced this in a temporary workspace: set HEADROOM_LOG_LEVEL=info, call _setup_file_logging(port=9911), switch the environment to debug, call setup again for 9911, then emit INFO and DEBUG sentinels. The file contained INFO but not DEBUG. All 16 current focused tests pass because their handler setup starts fresh; they do not cover this transition. The reverse transition can also leave the handler configuration inconsistent.
Keep the existing handler/file-descriptor reuse, adjust its level to the newly resolved value, and add a same-path reconfiguration regression. This confirms the existing inline report. Update the configuration documentation for the new Headroom-level behavior too. The branch contains main a9815bb. I stopped qualification at this reproduced blocker; no contributor logic was changed and no contributor CI action is requested.
Headroom's debug lines can carry request-derived data: the content router logs the tool output it compresses. With debug now reachable, a headroom.* DEBUG record also propagated to the root handlers, i.e. stdout, container logs and wrap's stdio capture. A filter on the root handlers now drops headroom.* records below INFO, so they reach only proxy.log (mode 0600), and stdout shows exactly what it showed before. Turning debug on logs one startup warning that names the file. A same-port re-setup now also lowers the reused handler's level. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assisted-by: claude-code:claude-opus-5 Assisted-by: claude-code:claude-opus-5-5 Headline-Session: 6e77a807-8dd4-4a77-a2fa-ddce6dc2515e Headline-Session: 88f3a7f6-986c-4fa8-ab24-0c064dbb438a Headline-AI-Share: 6% strict / 7% weighted
…rs too With HEADROOM_LOG_LEVEL=debug, a handler attached to the headroom logger or a child logger received debug records before the root filters ran. Filter every such handler except proxy.log, and test a child-logger handler. Refs #4136 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…repeat warnings Log records are kept and shared, so no level may carry request content, tool output or credentials. headroom/log_safety.py describes an exception by its types and code locations without the message (full traceback only with HEADROOM_DEBUG_DUMP=full), redacts URL userinfo and query, bounds ids, and gives a warn-once guard that stays bounded instead of resetting. The request debug dump now uses the shared URL redactor and opt-in check. Refs #4136 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… fix/headroom-debug-log-level
…to fix/headroom-debug-log-level
…gs in the overflow notice exec/compile code chooses its own file and function names, which can quote request data, so such frames show as <dynamic code>. The overflow notice now counts warnings, matching what WarnOnce limits, instead of distinct keys. Refs #4136 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… fix/headroom-debug-log-level
…d module Runtime-compiled code can claim a real file name, so checking the file on disk let its chosen function name through, and it hid every frame of a .pyc-only or zip install. A frame is now logged only when its globals are the namespace of the module sys.modules holds under that name; the location comes from the module, so sourceless and zip installs keep theirs. Refs #4136 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… logs Local carry of upstream headroomlabs-ai#4137 (open, unmerged), applied as its net diff against main (git diff origin/main...pr-4137), the same change a squash-merge lands. Includes the headroomlabs-ai#4138 log-safety helpers it merges in. Drop this commit once headroomlabs-ai#4137 is on main. (cherry picked from PR headroomlabs-ai#4137 head e6d0913)
…pers - redact_url: a string with no "//" (a pasted "sk-key:secret") is not logged at all; unknown schemes and hosts with line breaks are replaced. - describe_exception: a class name that is not printable is replaced; an int-subclass errno is not formatted; frame data is read from the module namespace with dict's own methods; the helper returns a fixed token rather than raise. - safe_id never raises; WarnOnce rejects an infinite or NaN window. Refs #4136 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… fix/headroom-debug-log-level
describe_exception no longer needs a catch-all: frame data is read through dict's own methods with exact type checks, so the odd inputs it guarded against cannot raise (their tests still pass). safe_id catches only the errors repr raises in practice. Refs #4136 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… fix/headroom-debug-log-level
… in URLs Every text a frame carries (module, file, function name) can be set by code compiled at runtime, so describe_exception now names a frame only when it is Headroom's own code: registered module namespace, plain headroom.* name, the module's own file, and an identifier function name. Library frames are skipped and only the innermost three Headroom frames are kept, in bounded memory. redact_url accepts hostless file/sqlite/unix URLs and a host only when it is a DNS name or IP address. Refs #4136 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… fix/headroom-debug-log-level
describe_exception reads __cause__, __context__, __traceback__, errno and the class name through the base classes' own descriptors, so a subclass property that raises never runs. safe_id uses the built-in repr of str, bytes and numbers and shows any other type by name, so no __repr__ of the caller's object runs. No blind except is needed for either. Refs #4136 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… fix/headroom-debug-log-level
…n safe_id - redact_url drops an IPv6 zone id (free text), accepts "_" in host labels (container service names) and gives non-ASCII hosts in IDNA form; a hostless URL must have ":///" right after its scheme. - safe_id checks the real type, not a fakeable __class__, and respects the process's int-to-text digit limit. - Class names are logged only when shaped like source identifiers. Refs #4136 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… fix/headroom-debug-log-level
- A host that is not an IP address or a strict DNS name (no "_") may be a pasted key, so it is shown as <host>; scheme and port are kept. - The int digit-limit getter only exists from 3.10.7; earlier releases have no limit, so safe_id no longer raises AttributeError there. - The hostless check cleans the URL the same way urlsplit does. Refs #4136 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… fix/headroom-debug-log-level
… logs Local carry of upstream headroomlabs-ai#4137 (open, unmerged), applied as its net diff against main (git diff origin/main...pr-4137), the same change a squash-merge lands. Includes the headroomlabs-ai#4138 log-safety helpers it merges in. Drop this commit once headroomlabs-ai#4137 is on main. (cherry picked from PR headroomlabs-ai#4137 head e6d0913)
Description
You couldn't turn on Headroom's own debug logs in a running proxy.
HEADROOM_LOG_LEVELset only uvicorn's level._setup_file_loggingpinned theheadroomlogger and theproxy.loghandler to INFO, so noheadroom.*DEBUG line was ever emitted. Meanwhiledocs/content/docs/metrics.mdx:320tells operators to runHEADROOM_LOG_LEVEL=debug headroom proxyto diagnose problems.Now
debugandtracelower both theheadroomlogger and theproxy.loghandler to DEBUG. Every other value keeps INFO. The default,warning, only quiets uvicorn, so Headroom's INFO events (PERF lines and the like) are still written as before.Debug lines stay in the owner-only file. Some Headroom debug lines carry request-derived data, such as error messages that quote a request. With
HEADROOM_DEBUG_DUMP=full, they also include the tool output the router compresses. Those lines had never been reachable before. Without a guard they would also propagate to the root handlers, meaning stdout, container logs and wrap's stdio capture. A filter on the root handlers dropsheadroom.*records below INFO, so debug reaches onlyproxy.log(mode 0600), and stdout shows exactly what it showed before. Turning debug on logs one startup WARNING that names the file and says what it now records. A same-port re-setup also lowers the reused handler's level, so switching from info to debug takes effect.Refs #4136
Type of Change
Changes Made
headroom/proxy/helpers.py:_headroom_log_level()(DEBUG fordebug/trace, otherwise INFO) sets theheadroomlogger and theproxy.loghandler, including a reused handler. When debug is on, the_HeadroomDebugStaysInProxyLogfilter goes on the root handlers, plus one startup warning.tests/test_perf_per_port_logs.py: three regression tests. (1) Parametrized: debug lines and the startup warning reachproxy.logonly fordebug/TRACE. (2) Aheadroom.*debug line reaches proxy.log but not a root stdout handler, while INFO and third-party warnings still do. (3) Same-port re-setup from info to debug. All four checks fail on the previous commit.headroom/transforms/content_router.py:_log_router_debug, which dumps the tool output being routed, now also needs the explicit content opt-inHEADROOM_DEBUG_DUMP=full.HEADROOM_LOG_LEVEL=debugalone no longer writes that content. The startup warning says so.Testing
pytest)ruff check .)mypy headroom)Test Output
Real Behavior Proof
headroom proxyon a random port with a temp HOME and workspace, offline, against a local fake Anthropic/OpenAI upstream (the shared smoke harness).HEADROOM_LOG_LEVEL=debug smoke.sh <worktree>: onePOST /v1/messagesand onePOST /v1/chat/completions, each with a 400-row tool result. Then compared the proxy's stdout withproxy-<port>.log.HEADROOM_LOG_LEVEL=debug): every run returns HTTP 200 for both requests with 13574 tokens saved (99.82%), and proxy.log is mode 600.HEADROOM_DEBUG_DUMP: proxy.log has 19 Headroom DEBUG lines and 0 lines with the tool-output markeritem-399.HEADROOM_DEBUG_DUMP=full: 25 DEBUG lines and 6 marker lines.HEADROOM_LOG_LEVELunset, proxy.log has no DEBUG lines and no warning.filepermspath), and multi-worker mode.Runtime Rollout Safety
debug/tracechange, and they were documented to do this.HEADROOM_LOG_LEVEL, or set it to anything other thandebug/trace.Review Readiness
Checklist
CHANGELOG.md— it is generated by release-please from my Conventional Commit PR title (a CI guard enforces this)Screenshots (if applicable)
N/A
Additional Notes
Depends on #4138 (shared
headroom.log_safetyhelpers). Its files show in this diff until it merges.Part of maintainer-requested cleanup #4136. Many Headroom failure paths log at DEBUG, so before this change nobody running the proxy could see them. Debug output can include request-derived data, and with
HEADROOM_DEBUG_DUMP=fullthe tool output the router compresses. It therefore goes only to the owner-only proxy.log, never to stdout. Theproxy.mdxknob table will say this in the separate docs PR.🤖 Generated with Claude Code