fix(proxy): allow disabling periodic TOIN stats logging (#1265)

## Description

Add an explicit proxy configuration toggle for the periodic TOIN stats
logging loop.

Long-lived proxy workers currently schedule
`_log_toin_stats_periodically()` unconditionally at startup. This change
lets operators disable only that 5-minute stats logging loop via
`HEADROOM_PERIODIC_TOIN_STATS=0` when periodic stats collection creates
avoidable resource pressure. The default remains enabled, and this does
not disable TOIN learning or request-time feedback.

Closes #

## Type of Change

- [x] 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)
- [x] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- Added `ProxyConfig.periodic_toin_stats_enabled`, defaulting to `True`.
- Wired `HEADROOM_PERIODIC_TOIN_STATS` through
`_proxy_config_from_env()`.
- Guarded the proxy lifespan startup so `_log_toin_stats_periodically()`
is only scheduled when the config is enabled.
- Added tests for the default env behavior, disabled env values, and the
disabled lifespan behavior.
- Documented `HEADROOM_PERIODIC_TOIN_STATS` in the configuration
reference.

## Testing

- [x] Unit tests pass (`pytest`)
- [ ] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
$ uv sync --extra dev
Resolved 256 packages in 1m 05s
Built headroom-ai @ file:///C:/Users/wstcz/AppData/Local/Temp/headroom-main-20260622-073524
Installed 124 packages in 50.87s

$ .\.venv\Scripts\python.exe -c "import headroom._core; print('core ok')"
core ok

$ uv run pytest tests/test_proxy_telemetry_env.py -q
============================= test session starts =============================
platform win32 -- Python 3.11.15, pytest-9.0.3, pluggy-1.6.0
rootdir: C:\Users\wstcz\AppData\Local\Temp\headroom-main-20260622-073524
configfile: pyproject.toml
plugins: anyio-4.12.1, langsmith-0.8.0, asyncio-1.3.0, cov-7.0.0
asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collected 9 items

tests\test_proxy_telemetry_env.py .........                              [100%]

============================== warnings summary ===============================
.venv\Lib\site-packages\fastapi\testclient.py:1
  C:\Users\wstcz\AppData\Local\Temp\headroom-main-20260622-073524\.venv\Lib\site-packages\fastapi\testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
    from starlette.testclient import TestClient as TestClient  # noqa

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
======================== 9 passed, 1 warning in 4.30s =========================

$ python -m py_compile headroom\proxy\models.py headroom\proxy\server.py tests\test_proxy_telemetry_env.py
# no output; command exited 0

$ git diff --check
# no output; command exited 0
```

## Real Behavior Proof

- Environment: Windows, Python 3.11.15 uv-managed `.venv`, source
checkout at `bc12ace` plus this branch.
- Exact command / steps: synced dev dependencies with `uv sync --extra
dev`, verified `headroom._core` imports, then ran `uv run pytest
tests/test_proxy_telemetry_env.py -q`.
- Observed result: all 9 tests in `tests/test_proxy_telemetry_env.py`
passed. The new tests prove the env var defaults to enabled, `0` /
`false` / `off` / `no` disable it, and disabled config avoids requesting
the periodic TOIN stats coroutine during app lifespan startup.
- Not tested: full repository pytest, ruff, and mypy were not run for
this narrowly scoped proxy config change.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable

## Screenshots (if applicable)

N/A.

## Additional Notes

- Default behavior is unchanged: periodic TOIN stats logging remains
enabled unless explicitly disabled.
- This only disables the periodic stats logging task. It does not
disable TOIN learning, request-time feedback, or stats retrieval
elsewhere.
- `My changes generate no new warnings` is left unchecked because the
focused pytest run emits a pre-existing Starlette/FastAPI TestClient
deprecation warning from dependencies.
- `CHANGELOG.md` is left unchecked because this is a small unreleased
configuration toggle and docs/tests cover the behavior.
This commit is contained in:
wstczyw 2026-06-22 10:55:23 +08:00 committed by GitHub
parent d480c464e9
commit b5f63d8fa9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 69 additions and 2 deletions

View file

@ -235,6 +235,7 @@ headroom proxy --learn --min-evidence 3
| `HEADROOM_TOIN_PATH` | Override TOIN telemetry file location. Always wins when set. | derived from `${HEADROOM_WORKSPACE_DIR}` |
| `HEADROOM_SUBSCRIPTION_STATE_PATH` | Override subscription tracker state file. Always wins when set. | derived from `${HEADROOM_WORKSPACE_DIR}` |
| `HEADROOM_TELEMETRY` | Set to `on` to opt in to anonymous telemetry | `off` |
| `HEADROOM_PERIODIC_TOIN_STATS` | Controls periodic TOIN stats logging in long-lived proxy workers. Set to `0`, `false`, `off`, or `no` to disable the 5-minute stats loop without disabling TOIN learning or request-time feedback. | `true` |
| `HEADROOM_MEMORY_INJECTION_MODE` | Memory-context routing mode: `live_zone_tail` (default) or `disabled`. The legacy `system_prompt` mode was retired by PR-A2; supplying it raises. | `live_zone_tail` |
| `HEADROOM_PROXY_PYTHON_FORWARDER_MODE` | Python forwarder serialization mode. `byte_faithful` (default) forwards original request bytes verbatim when no transform mutated the body and re-serializes canonically only when needed — keeps Anthropic prompt-cache hit-rate intact. `legacy_json_kwarg` is an explicit operator opt-in for emergency rollback to the historical `httpx ... json=body` behavior. NOT a fallback — only flip on explicit operator decision. | `byte_faithful` |
| `HEADROOM_STRIP_INTERNAL_HEADERS` | Python proxy: whether to strip internal `x-headroom-*` request headers (e.g. `x-headroom-bypass`, `x-headroom-mode`, `x-headroom-user-id`, `x-headroom-stack`, `x-headroom-base-url`) before every upstream forwarder call (PR-A5, fixes P5-49). `enabled` (default) stops fingerprinting / leakage. `disabled` is an explicit operator opt-in for diagnostic shadow tracing — NOT a fallback. Inbound reads of these headers (bypass gating, memory user-id resolution) are unaffected because they read `request.headers` directly. | `enabled` |

View file

@ -294,6 +294,12 @@ class ProxyConfig:
subscription_poll_interval_s: int = 300
subscription_active_window_s: int = 60
# Periodic TOIN stats logging. Enabled by default for observability, but
# operators of long-lived proxies can disable it if TOIN stats collection
# causes avoidable memory pressure on their platform.
# Env: HEADROOM_PERIODIC_TOIN_STATS=0.
periodic_toin_stats_enabled: bool = True
# Stateless mode — disable all filesystem writes for read-only / container deployments
stateless: bool = False

View file

@ -1810,7 +1810,8 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI:
try:
# Startup
await proxy.startup()
asyncio.create_task(_log_toin_stats_periodically())
if config.periodic_toin_stats_enabled:
asyncio.create_task(_log_toin_stats_periodically())
if proxy.usage_reporter:
await proxy.usage_reporter.start(proxy)
if proxy.traffic_learner:
@ -3740,6 +3741,7 @@ def _proxy_config_from_env() -> ProxyConfig:
max_connections=_get_env_int("HEADROOM_MAX_CONNECTIONS", 500),
max_keepalive_connections=_get_env_int("HEADROOM_MAX_KEEPALIVE", 100),
http2=_get_env_bool("HEADROOM_HTTP2", True),
periodic_toin_stats_enabled=_get_env_bool("HEADROOM_PERIODIC_TOIN_STATS", True),
mode=normalize_proxy_mode(_get_env_str("HEADROOM_MODE", PROXY_MODE_TOKEN)),
)

View file

@ -1,12 +1,15 @@
"""Tests for proxy telemetry environment variable handling."""
import asyncio
from unittest.mock import patch
import pytest
pytest.importorskip("fastapi")
from headroom.proxy.server import ProxyConfig, create_app
from fastapi.testclient import TestClient
from headroom.proxy.server import ProxyConfig, _proxy_config_from_env, create_app
class TestProxyTelemetrySDKEnv:
@ -56,3 +59,58 @@ class TestProxyTelemetrySDKEnv:
)
assert mock_beacon.call_args.kwargs["sdk"] == "proxy"
class TestProxyPeriodicTOINStatsEnv:
"""Test HEADROOM_PERIODIC_TOIN_STATS handling for long-lived proxy workers."""
def test_periodic_toin_stats_enabled_by_default(self, monkeypatch):
"""Periodic TOIN stats logging remains enabled unless explicitly disabled."""
monkeypatch.delenv("HEADROOM_PERIODIC_TOIN_STATS", raising=False)
config = _proxy_config_from_env()
assert config.periodic_toin_stats_enabled is True
@pytest.mark.parametrize("value", ["0", "false", "off", "no"])
def test_periodic_toin_stats_can_be_disabled_by_env(self, monkeypatch, value):
"""HEADROOM_PERIODIC_TOIN_STATS=0/false/off/no disables periodic logging."""
monkeypatch.setenv("HEADROOM_PERIODIC_TOIN_STATS", value)
config = _proxy_config_from_env()
assert config.periodic_toin_stats_enabled is False
def test_lifespan_skips_periodic_toin_stats_when_disabled(self, monkeypatch):
"""Disabling periodic TOIN stats avoids scheduling the stats loop."""
monkeypatch.setenv("HEADROOM_SKIP_UPSTREAM_CHECK", "1")
requested = False
def fake_periodic_toin_stats():
nonlocal requested
requested = True
async def noop():
await asyncio.sleep(0)
return noop()
monkeypatch.setattr(
"headroom.proxy.server._log_toin_stats_periodically",
fake_periodic_toin_stats,
)
app = create_app(
ProxyConfig(
optimize=False,
cache_enabled=False,
rate_limit_enabled=False,
cost_tracking_enabled=False,
periodic_toin_stats_enabled=False,
)
)
with TestClient(app):
pass
assert requested is False