From 356e5f77990b6508cceb84376739ab87f5c60c08 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Wed, 17 Jun 2026 09:32:47 -0400 Subject: [PATCH] api: log HTTPException responses (5xx + 4xx) to the journal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Failures surfaced via HTTPException (e.g. wizard dest-provisioning errors raised as HTTPException(500, "dest provisioning failed: …")) were turned into responses by FastAPI and never logged — only the access line showed, so the real DB error went to the browser and vanished from the journal. Register a StarletteHTTPException handler that logs 5xx at ERROR (with exc_info, capturing the chained cause) and 4xx at WARNING, then defers to the default handler. Also configure pipekit's logger to emit to stderr so INFO-level records aren't dropped by uvicorn's last-resort handler. Unhandled (non-HTTPException) errors were already logged by uvicorn. Co-Authored-By: Claude Opus 4.8 --- pipekit/api/app.py | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/pipekit/api/app.py b/pipekit/api/app.py index e201d02..8ea5de6 100644 --- a/pipekit/api/app.py +++ b/pipekit/api/app.py @@ -7,14 +7,36 @@ content-negotiation complexity and keeps the API curl-testable. from __future__ import annotations +import logging +import sys from contextlib import asynccontextmanager -from fastapi import FastAPI +from fastapi import FastAPI, Request +from fastapi.exception_handlers import http_exception_handler +from starlette.exceptions import HTTPException as StarletteHTTPException from .. import __version__, db, jrunner from ..web import mount_web from .routes import connections, introspect, modules, runs, system +_log = logging.getLogger("pipekit.web") + + +def _configure_logging() -> None: + """Route pipekit's loggers to stderr (→ journal) with a consistent format. + + Uvicorn configures only its own loggers, not the root, so without this our + records would fall through to the last-resort handler (WARNING+ only) and + INFO breadcrumbs would vanish. Idempotent — safe to call per create_app.""" + base = logging.getLogger("pipekit") + if not base.handlers: + handler = logging.StreamHandler(sys.stderr) + handler.setFormatter(logging.Formatter( + "%(asctime)s %(levelname)s [%(name)s] %(message)s")) + base.addHandler(handler) + base.setLevel(logging.INFO) + base.propagate = False + @asynccontextmanager async def _lifespan(app: FastAPI): @@ -24,7 +46,24 @@ async def _lifespan(app: FastAPI): def create_app() -> FastAPI: + _configure_logging() app = FastAPI(title="Pipekit", version=__version__, lifespan=_lifespan) + + @app.exception_handler(StarletteHTTPException) + async def _logged_http_exception_handler(request: Request, + exc: StarletteHTTPException): + # FastAPI turns HTTPException into a normal response and never logs it, + # so failures surfaced this way (e.g. wizard dest-provisioning errors) + # were invisible in the journal. Log them, then defer to the default + # handler for the actual response. + if exc.status_code >= 500: + _log.error("%s %s -> %d: %s", request.method, request.url.path, + exc.status_code, exc.detail, exc_info=exc) + elif exc.status_code >= 400: + _log.warning("%s %s -> %d: %s", request.method, request.url.path, + exc.status_code, exc.detail) + return await http_exception_handler(request, exc) + app.include_router(system.router) app.include_router(connections.router, prefix="/api") app.include_router(introspect.router, prefix="/api")