diff --git a/src/chatmemory/logging.py b/src/chatmemory/logging.py index 48bb61a..59b9e89 100644 --- a/src/chatmemory/logging.py +++ b/src/chatmemory/logging.py @@ -6,9 +6,16 @@ import structlog +#: HTTP client libraries that log every request URL at INFO. Infura (and other +#: providers) put the API key in the URL path, so at INFO these wrote the key +#: into production logs on every chain read. WARNING keeps their errors. +QUIET_HTTP_LOGGERS = ("httpx", "httpx2", "httpcore", "httpcore2") + def configure(level: str = "INFO") -> None: logging.basicConfig(format="%(message)s", level=getattr(logging, level.upper())) + for name in QUIET_HTTP_LOGGERS: + logging.getLogger(name).setLevel(max(logging.WARNING, getattr(logging, level.upper()))) structlog.configure( processors=[ structlog.contextvars.merge_contextvars, diff --git a/tests/unit/test_logging_secrets.py b/tests/unit/test_logging_secrets.py new file mode 100644 index 0000000..63c2cb0 --- /dev/null +++ b/tests/unit/test_logging_secrets.py @@ -0,0 +1,29 @@ +"""Regression: request URLs, which carry provider API keys, stay out of the logs. + +Production logs held `POST https://arbitrum-mainnet.infura.io/v3/` on every +chain read, written by httpx's INFO request log. +""" + +from __future__ import annotations + +import logging + +import pytest + +from chatmemory import logging as log_setup + + +@pytest.mark.parametrize("name", log_setup.QUIET_HTTP_LOGGERS) +def test_http_client_request_lines_are_not_logged_at_info(name: str) -> None: + log_setup.configure("INFO") + assert not logging.getLogger(name).isEnabledFor(logging.INFO) + assert logging.getLogger(name).isEnabledFor(logging.WARNING) + + +def test_an_infura_request_line_never_reaches_the_log(caplog: pytest.LogCaptureFixture) -> None: + log_setup.configure("INFO") + with caplog.at_level(logging.INFO): + logging.getLogger("httpx").info( + 'HTTP Request: POST https://base-mainnet.infura.io/v3/%s "HTTP/1.1 200 OK"', "k" * 32 + ) + assert "infura.io/v3" not in caplog.text