Log to a file instead of only stdout

All app and uvicorn logging now goes to logs/app.log (rotating,
10MB x5), and a catch-all exception handler logs full tracebacks there
before returning a generic 500 — so an error is traceable to its cause
without depending on how the process was launched. logs/ is gitignored,
like the other runtime artifacts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 10:46:47 +02:00
co-authored by Claude Sonnet 5
parent ddaca5520e
commit 7e5c372833
3 changed files with 44 additions and 2 deletions
+1
View File
@@ -6,3 +6,4 @@ __pycache__/
master.xprv.enc
.pytest_cache/
*.egg-info/
logs/
+28
View File
@@ -0,0 +1,28 @@
import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path
LOG_DIR = Path(__file__).resolve().parent.parent / "logs"
LOG_FILE = LOG_DIR / "app.log"
_FORMAT = "%(asctime)s %(levelname)s [%(name)s] %(message)s"
def setup_logging() -> None:
"""Route all app + uvicorn logging to logs/app.log so errors are traceable
without depending on however the process happens to be launched."""
LOG_DIR.mkdir(exist_ok=True)
handler = RotatingFileHandler(LOG_FILE, maxBytes=10 * 1024 * 1024, backupCount=5)
handler.setFormatter(logging.Formatter(_FORMAT))
root = logging.getLogger()
root.setLevel(logging.INFO)
root.addHandler(handler)
# "uvicorn.error"/"uvicorn.access" have propagate=False in uvicorn's default
# logging config, so they never reach the root handler above and need their
# own. The parent "uvicorn" logger is deliberately skipped: uvicorn.error
# already bubbles into it, so attaching there too would double every line.
for logger_name in ("uvicorn.error", "uvicorn.access"):
logging.getLogger(logger_name).addHandler(handler)
+15 -2
View File
@@ -1,10 +1,15 @@
import asyncio
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.responses import FileResponse
from fastapi import FastAPI, Request
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from app.logging_config import setup_logging
setup_logging()
import app.bets.confirmation # noqa: F401 (registers the "bet" confirmation handler)
import app.rounds.confirmation # noqa: F401 (registers the "payout" confirmation handler)
import app.withdrawals.confirmation # noqa: F401 (registers the "withdrawal" confirmation handler)
@@ -22,6 +27,8 @@ from app.tx.broadcast import RbfBumper
from app.tx.confirmation import ConfirmationPoller
from app.tx.locks import UserLocks
logger = logging.getLogger(__name__)
def _make_electrum_client() -> ElectrumClient:
return ElectrumClient(settings.electrum_host, settings.electrum_port, settings.electrum_use_ssl)
@@ -60,6 +67,12 @@ app.include_router(withdrawals_router)
app.include_router(admin_router)
@app.exception_handler(Exception)
async def log_unhandled_exception(request: Request, exc: Exception) -> JSONResponse:
logger.exception("Unhandled error on %s %s", request.method, request.url.path)
return JSONResponse(status_code=500, content={"detail": "internal server error"})
@app.get("/health")
async def health() -> dict[str, str]:
return {"status": "ok"}