From 525f091b3a707046ef298352a466b0839dedac36 Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 21 May 2026 16:16:14 +0200 Subject: [PATCH] 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 --- ocr-service/main.py | 17 +++++++++++++++++ ocr-service/test_metrics.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/ocr-service/main.py b/ocr-service/main.py index f7dc495d..bf26ef35 100644 --- a/ocr-service/main.py +++ b/ocr-service/main.py @@ -82,6 +82,23 @@ app = FastAPI(title="Familienarchiv OCR Service", lifespan=lifespan) 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") def health(): """Health endpoint — returns 200 only after models are loaded.""" diff --git a/ocr-service/test_metrics.py b/ocr-service/test_metrics.py index 5c472dbe..271b57d8 100644 --- a/ocr-service/test_metrics.py +++ b/ocr-service/test_metrics.py @@ -442,3 +442,33 @@ async def test_ocr_models_ready_gauge_is_one_after_lifespan_startup(fresh_metric patch("main.load_spell_checker"): async with app.router.lifespan_context(app): 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