feat(ocr): suppress uvicorn access logs for /metrics and /health
Adds a logging.Filter on uvicorn.access that drops records whose request path is /metrics or /health. Each is hit on a tight schedule (Prometheus scrape interval and Docker healthcheck), so unfiltered they dominate the access log without carrying any information about real traffic. Refs #652 (Nora's recommendation) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -82,6 +82,23 @@ app = FastAPI(title="Familienarchiv OCR Service", lifespan=lifespan)
|
|||||||
Instrumentator(excluded_handlers=["/health", "/metrics"]).instrument(app).expose(app)
|
Instrumentator(excluded_handlers=["/health", "/metrics"]).instrument(app).expose(app)
|
||||||
|
|
||||||
|
|
||||||
|
class MetricsPathFilter(logging.Filter):
|
||||||
|
"""Drop uvicorn.access entries for /metrics and /health to keep logs focused."""
|
||||||
|
|
||||||
|
_SUPPRESSED_PATHS = {"/metrics", "/health"}
|
||||||
|
|
||||||
|
def filter(self, record: logging.LogRecord) -> bool:
|
||||||
|
# uvicorn.access formats as: '%s - "%s %s HTTP/%s" %d'
|
||||||
|
if record.args and len(record.args) >= 3:
|
||||||
|
path = record.args[2]
|
||||||
|
if isinstance(path, str) and path in self._SUPPRESSED_PATHS:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
logging.getLogger("uvicorn.access").addFilter(MetricsPathFilter())
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
def health():
|
def health():
|
||||||
"""Health endpoint — returns 200 only after models are loaded."""
|
"""Health endpoint — returns 200 only after models are loaded."""
|
||||||
|
|||||||
@@ -442,3 +442,33 @@ async def test_ocr_models_ready_gauge_is_one_after_lifespan_startup(fresh_metric
|
|||||||
patch("main.load_spell_checker"):
|
patch("main.load_spell_checker"):
|
||||||
async with app.router.lifespan_context(app):
|
async with app.router.lifespan_context(app):
|
||||||
assert fresh_metrics.ocr_models_ready._value.get() == 1.0
|
assert fresh_metrics.ocr_models_ready._value.get() == 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_uvicorn_access_log_filter_skips_metrics_path():
|
||||||
|
"""The MetricsPathFilter drops uvicorn.access log records that target /metrics."""
|
||||||
|
import logging as _logging
|
||||||
|
from main import MetricsPathFilter
|
||||||
|
|
||||||
|
filt = MetricsPathFilter()
|
||||||
|
metrics_record = _logging.LogRecord(
|
||||||
|
name="uvicorn.access", level=_logging.INFO, pathname="", lineno=0,
|
||||||
|
msg='%s - "%s %s HTTP/%s" %d',
|
||||||
|
args=("127.0.0.1:1234", "GET", "/metrics", "1.1", 200),
|
||||||
|
exc_info=None,
|
||||||
|
)
|
||||||
|
health_record = _logging.LogRecord(
|
||||||
|
name="uvicorn.access", level=_logging.INFO, pathname="", lineno=0,
|
||||||
|
msg='%s - "%s %s HTTP/%s" %d',
|
||||||
|
args=("127.0.0.1:1234", "GET", "/health", "1.1", 200),
|
||||||
|
exc_info=None,
|
||||||
|
)
|
||||||
|
ocr_record = _logging.LogRecord(
|
||||||
|
name="uvicorn.access", level=_logging.INFO, pathname="", lineno=0,
|
||||||
|
msg='%s - "%s %s HTTP/%s" %d',
|
||||||
|
args=("127.0.0.1:1234", "POST", "/ocr", "1.1", 200),
|
||||||
|
exc_info=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert filt.filter(metrics_record) is False
|
||||||
|
assert filt.filter(health_record) is False
|
||||||
|
assert filt.filter(ocr_record) is True
|
||||||
|
|||||||
Reference in New Issue
Block a user