Skip to content

fix(proxy): let HEADROOM_LOG_LEVEL=debug turn on Headroom's own debug logs - #4137

Open
chopratejas wants to merge 38 commits into
mainfrom
fix/headroom-debug-log-level
Open

chopratejas wants to merge 38 commits into
mainfrom
fix/headroom-debug-log-level

Conversation

@chopratejas

@chopratejas chopratejas commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Description

You couldn't turn on Headroom's own debug logs in a running proxy. HEADROOM_LOG_LEVEL set only uvicorn's level. _setup_file_logging pinned the headroom logger and the proxy.log handler to INFO, so no headroom.* DEBUG line was ever emitted. Meanwhile docs/content/docs/metrics.mdx:320 tells operators to run HEADROOM_LOG_LEVEL=debug headroom proxy to diagnose problems.

Now debug and trace lower both the headroom logger and the proxy.log handler 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 drops headroom.* records below INFO, so debug reaches only proxy.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

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Performance improvement
  • Code refactoring (no functional changes)

Changes Made

  • headroom/proxy/helpers.py: _headroom_log_level() (DEBUG for debug/trace, otherwise INFO) sets the headroom logger and the proxy.log handler, including a reused handler. When debug is on, the _HeadroomDebugStaysInProxyLog filter 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 reach proxy.log only for debug/TRACE. (2) A headroom.* 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-in HEADROOM_DEBUG_DUMP=full. HEADROOM_LOG_LEVEL=debug alone no longer writes that content. The startup warning says so.

Testing

  • Unit tests pass (pytest)
  • Linting passes (ruff check .)
  • Type checking passes (mypy headroom)
  • New tests added for new functionality
  • Manual testing performed

Test Output

$ pytest -q tests/test_perf_per_port_logs.py -k log_level        # on origin/main code
FAILED ...[debug-True]  FAILED ...[TRACE-True]   2 failed, 3 passed
$ pytest -q tests/test_perf_per_port_logs.py tests/test_cache_breakpoint_diagnostics.py tests/test_pr208_changes.py tests/test_direct_chat_ccr_resolution.py <every test mentioning HEADROOM_LOG_LEVEL>
125 passed, 1 skipped
$ pytest -q tests/test_perf_per_port_logs.py   # 18 passed; on the previous commit: 4 failed
$ pytest -q <40 test files that use caplog on headroom loggers>   # 956 passed, 1 skipped
$ ruff check / ruff format --check / mypy headroom/proxy/helpers.py
All checks passed! / 2 files already formatted / Success: no issues found in 1 source file

Real Behavior Proof

  • Environment: macOS, Python 3.12. This branch runs as a real headroom proxy on a random port with a temp HOME and workspace, offline, against a local fake Anthropic/OpenAI upstream (the shared smoke harness).
  • Exact command / steps: HEADROOM_LOG_LEVEL=debug smoke.sh <worktree>: one POST /v1/messages and one POST /v1/chat/completions, each with a 400-row tool result. Then compared the proxy's stdout with proxy-<port>.log.
  • Observed result (head 2f43785, HEADROOM_LOG_LEVEL=debug): every run returns HTTP 200 for both requests with 13574 tokens saved (99.82%), and proxy.log is mode 600.
    • Without HEADROOM_DEBUG_DUMP: proxy.log has 19 Headroom DEBUG lines and 0 lines with the tool-output marker item-399.
    • With HEADROOM_DEBUG_DUMP=full: 25 DEBUG lines and 6 marker lines.
    • stdout has 0 DEBUG lines and 0 marker lines in both runs.
    • The startup warning appears in both. With HEADROOM_LOG_LEVEL unset, proxy.log has no DEBUG lines and no warning.
  • Not tested: Windows (owner-only perms there follow the existing fileperms path), and multi-worker mode.

Runtime Rollout Safety

  • Rollout-managed feature(s): None.
  • Minimum rollout channel: N/A; logging only.
  • Stable/default behavior changed: No. The default and every non-debug value keep today's INFO level; only debug/trace change, and they were documented to do this.
  • Kill switch / disable path: Unset HEADROOM_LOG_LEVEL, or set it to anything other than debug/trace.
  • Unsafe override required: No.
  • Qualification impact: None.
  • Rollback path: Revert this commit.

Review Readiness

  • I have performed a self-review
  • This PR is ready for human review

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I did not edit 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_safety helpers). 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=full the tool output the router compresses. It therefore goes only to the owner-only proxy.log, never to stdout. The proxy.mdx knob table will say this in the separate docs PR.

🤖 Generated with Claude Code

… 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
@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

PR governance

This PR does not yet satisfy the required template fields:

  • Fill in Real Behavior Proof → Observed result.

Please update the PR body, or move the PR back to draft while it is still in progress.

@github-actions github-actions Bot added the status: ready for review Pull request body is complete and the author marked it ready for human review label Oct 10, 2026
devin-ai-integration[bot]

This comment was marked as resolved.

@JerrettDavis JerrettDavis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@chopratejas
chopratejas marked this pull request as draft October 10, 2026 03:38
@github-actions github-actions Bot added status: ci failing Required or reported CI checks are failing and removed status: ready for review Pull request body is complete and the author marked it ready for human review labels Oct 10, 2026
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
@github-actions github-actions Bot removed the status: ci failing Required or reported CI checks are failing label Oct 10, 2026
@chopratejas
chopratejas marked this pull request as ready for review October 10, 2026 03:42
@github-actions github-actions Bot added status: ready for review Pull request body is complete and the author marked it ready for human review and removed status: ready for review Pull request body is complete and the author marked it ready for human review labels Oct 10, 2026
devin-ai-integration[bot]

This comment was marked as resolved.

…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>
@github-actions github-actions Bot added status: ready for review Pull request body is complete and the author marked it ready for human review and removed status: ready for review Pull request body is complete and the author marked it ready for human review labels Oct 10, 2026
@github-actions github-actions Bot added status: ready for review Pull request body is complete and the author marked it ready for human review and removed status: ready for review Pull request body is complete and the author marked it ready for human review labels Oct 10, 2026
chopratejas and others added 4 commits October 9, 2026 22:07
…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>
chopratejas and others added 2 commits October 10, 2026 09:37
…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>
devin-ai-integration[bot]

This comment was marked as resolved.

chopratejas and others added 2 commits October 10, 2026 09:45
…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>
KennethWKZ added a commit to KennethWKZ/headroom that referenced this pull request Oct 10, 2026
… 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)
chopratejas and others added 2 commits October 10, 2026 09:51
…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>
devin-ai-integration[bot]

This comment was marked as resolved.

chopratejas and others added 2 commits October 10, 2026 09:56
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>
devin-ai-integration[bot]

This comment was marked as resolved.

chopratejas and others added 2 commits October 10, 2026 10:01
… 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>
devin-ai-integration[bot]

This comment was marked as resolved.

chopratejas and others added 2 commits October 10, 2026 10:06
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>
devin-ai-integration[bot]

This comment was marked as resolved.

chopratejas and others added 2 commits October 10, 2026 10:22
…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>
chopratejas and others added 2 commits October 10, 2026 10:30
- 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>
KennethWKZ added a commit to KennethWKZ/headroom that referenced this pull request Oct 10, 2026
… 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)

This branch was successfully deployed

1 active deployment
Preview — 67885e4c Deployed Oct 10, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: needs author action Pull request body or readiness checklist still needs author updates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants