diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index edc1fca..a9582e8 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,4 +1,46 @@ import ReactDOM from "react-dom/client"; import App from "./App"; +const backendBase = + (import.meta.env.VITE_BACKEND_URL as string | undefined) ?? "http://localhost:8000"; + +const sendFrontLog = (message: string, level: "info" | "warn" | "error" = "info") => { + fetch(`${backendBase}/logs`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ message, level, source: "frontend" }), + }).catch(() => {}); +}; + +window.addEventListener("error", (event) => { + const err = event.error instanceof Error ? event.error : null; + const details = err?.stack || err?.message || String(event.message || "unknown error"); + sendFrontLog(`window.error: ${details}`, "error"); +}); + +window.addEventListener("unhandledrejection", (event) => { + const reason = event.reason instanceof Error ? event.reason.stack || event.reason.message : String(event.reason); + sendFrontLog(`unhandledrejection: ${reason}`, "error"); +}); + +const _consoleError = console.error.bind(console); +console.error = (...args: unknown[]) => { + try { + sendFrontLog(`console.error: ${args.map(String).join(" ")}`, "error"); + } catch { + // ignore + } + _consoleError(...args); +}; + +const _consoleWarn = console.warn.bind(console); +console.warn = (...args: unknown[]) => { + try { + sendFrontLog(`console.warn: ${args.map(String).join(" ")}`, "warn"); + } catch { + // ignore + } + _consoleWarn(...args); +}; + ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(); diff --git a/gnss/__main__.py b/gnss/__main__.py index 125ad7c..bcd7e52 100644 --- a/gnss/__main__.py +++ b/gnss/__main__.py @@ -1,5 +1,7 @@ import argparse import os +import sys +from pathlib import Path from typing import Optional import uvicorn @@ -12,19 +14,80 @@ def _env(name: str, default: str) -> str: return v if v is not None and v != "" else default +class _TeeStream: + def __init__(self, primary, secondary): + self._primary = primary + self._secondary = secondary + + def write(self, data): + try: + self._primary.write(data) + except Exception: + pass + try: + self._secondary.write(data) + except Exception: + pass + return len(data) + + def flush(self): + try: + self._primary.flush() + except Exception: + pass + try: + self._secondary.flush() + except Exception: + pass + + def isatty(self): + try: + return self._primary.isatty() + except Exception: + return False + + +def _ensure_uvicorn_logs_to_file(log_path: Path) -> None: + import logging + + log_path.parent.mkdir(parents=True, exist_ok=True) + file_handler = logging.FileHandler(log_path, encoding="utf-8") + file_handler.setLevel(logging.INFO) + file_handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s")) + + for name in ("uvicorn", "uvicorn.error", "uvicorn.access"): + logger = logging.getLogger(name) + logger.setLevel(logging.INFO) + if not any( + isinstance(h, logging.FileHandler) and getattr(h, "baseFilename", None) == str(log_path) + for h in logger.handlers + ): + logger.addHandler(file_handler) + logger.propagate = True + + def main(argv: Optional[list[str]] = None) -> int: parser = argparse.ArgumentParser(prog="gnss", description="GNSS Backend (FastAPI)") parser.add_argument("--host", default=_env("GNSS_HOST", "0.0.0.0")) parser.add_argument("--port", type=int, default=int(_env("GNSS_PORT", "8000"))) parser.add_argument("--log-level", default=_env("GNSS_LOG_LEVEL", "info")) + parser.add_argument("--log-file", default=_env("GNSS_BACKEND_LOG_FILE", "logs/backend.log")) args = parser.parse_args(argv) + log_path = Path(args.log_file).expanduser() + log_path.parent.mkdir(parents=True, exist_ok=True) + backend_log_file = log_path.open("a", encoding="utf-8", buffering=1) + sys.stdout = _TeeStream(sys.stdout, backend_log_file) # type: ignore[assignment] + sys.stderr = _TeeStream(sys.stderr, backend_log_file) # type: ignore[assignment] + _ensure_uvicorn_logs_to_file(log_path) + uvicorn.run( app, host=args.host, port=args.port, log_level=args.log_level, access_log=True, + log_config=None, workers=1, ) return 0