Backend logging: stderr + configurable rotating file

Add configure_logging() (main.py): always logs to stderr, plus a rotating
file when PIRATS_LOG_FILE is set; level from PIRATS_LOG_LEVEL (default INFO).
Called from both the CLI entrypoint and the app lifespan (idempotent), which
also logs startup/migration/shutdown.

Targeted server-side events for ops/debugging (not every game event): game
created, player joined, player removed (kick/leave), game ended.

NixOS service: new logFile (/var/log/pirats/pirats.log) and logLevel options,
wired to the env vars; LogsDirectory=pirats makes the path writable under the
sandboxed DynamicUser. README documents the env vars/options.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 15:42:06 -07:00
parent b1a559cf39
commit 5e9f9bfb13
8 changed files with 107 additions and 3 deletions

View File

@@ -6,7 +6,9 @@ from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from sqlmodel import Session
import logging
from logging.handlers import RotatingFileHandler
import re
import sys
import uvicorn
import os
from pathlib import Path
@@ -21,10 +23,58 @@ EVENT_PAGE_SIZE = 50
BASE_DIR = Path(__file__).parent
_logging_configured = False
def configure_logging() -> None:
"""Set up backend logging: always to stderr, plus a rotating file when
PIRATS_LOG_FILE is set (the NixOS service points it at /var/log/pirats/).
Level comes from PIRATS_LOG_LEVEL (default INFO). Idempotent so it's safe to
call from both the CLI entrypoint and the app's startup lifespan."""
global _logging_configured
if _logging_configured:
return
_logging_configured = True
level_name = os.environ.get("PIRATS_LOG_LEVEL", "INFO").upper()
level = getattr(logging, level_name, logging.INFO)
root = logging.getLogger()
root.setLevel(level)
fmt = logging.Formatter(
"%(asctime)s %(levelname)s [%(name)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
stderr_handler = logging.StreamHandler(sys.stderr)
stderr_handler.setFormatter(fmt)
root.addHandler(stderr_handler)
log_file = os.environ.get("PIRATS_LOG_FILE")
if log_file:
try:
Path(log_file).parent.mkdir(parents=True, exist_ok=True)
file_handler = RotatingFileHandler(
log_file, maxBytes=10 * 1024 * 1024, backupCount=5, encoding="utf-8"
)
file_handler.setFormatter(fmt)
root.addHandler(file_handler)
logger.info("Logging to %s", log_file)
except OSError:
# A bad/unwritable path mustn't take the server down — keep stderr.
logger.exception("Could not open log file %s; logging to stderr only", log_file)
@asynccontextmanager
async def lifespan(app: FastAPI):
configure_logging()
logger.info(
"Starting Rats with Gats backend (db=%s, dev_mode_default=%s)",
engine.url.render_as_string(hide_password=True),
crud.dev_mode_default(),
)
run_migrations()
logger.info("Database migrations applied")
yield
logger.info("Shutting down")
app = FastAPI(title="Rats with Gats Remote Play API", lifespan=lifespan)
api = APIRouter()
@@ -189,6 +239,7 @@ if os.path.exists(static_dir):
app.mount("/", StaticFiles(directory=static_dir, html=True), name="static")
def main():
configure_logging()
import argparse
parser = argparse.ArgumentParser(description="Run the Rats with Gats web app")
parser.add_argument("--host", default="0.0.0.0", help="Host address to bind to")