diff --git a/webmain.py b/webmain.py index 30c06d7..55d1145 100644 --- a/webmain.py +++ b/webmain.py @@ -5,13 +5,73 @@ and the same runs/ artifacts. Example: python webmain.py python webmain.py --host 0.0.0.0 --port 8800 + python webmain.py --host 0.0.0.0 --port 8800 --log-level debug """ from __future__ import annotations import argparse +import logging +import logging.config +from datetime import datetime +from pathlib import Path -import uvicorn + +REPO_ROOT = Path(__file__).resolve().parent + + +def _build_log_config(log_file: Path, level: str) -> dict: + """Build a uvicorn-compatible logging config dict. + + Writes to both stderr (console) and a rotating daily log file. + All webapp.* and rag_eval.* loggers inherit from root so every + logger.info() call in the API routes is captured. + """ + level_upper = level.upper() + return { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "detailed": { + "format": "%(asctime)s %(levelname)-8s %(name)s %(message)s", + "datefmt": "%Y-%m-%d %H:%M:%S", + }, + "console": { + "format": "%(asctime)s %(levelname)-8s %(name)-30s %(message)s", + "datefmt": "%H:%M:%S", + }, + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "stream": "ext://sys.stderr", + "formatter": "console", + "level": level_upper, + }, + "file": { + "class": "logging.handlers.RotatingFileHandler", + "filename": str(log_file), + "maxBytes": 50 * 1024 * 1024, # 50 MB per file + "backupCount": 7, # keep 7 rotated files + "encoding": "utf-8", + "formatter": "detailed", + "level": "DEBUG", # file always captures everything + }, + }, + "loggers": { + # Our application loggers — detailed level + "webapp": {"handlers": ["console", "file"], "level": level_upper, "propagate": False}, + "rag_eval": {"handlers": ["console", "file"], "level": level_upper, "propagate": False}, + # uvicorn access log — captured to file, shown on console + "uvicorn.access": {"handlers": ["console", "file"], "level": "INFO", "propagate": False}, + "uvicorn.error": {"handlers": ["console", "file"], "level": "INFO", "propagate": False}, + "uvicorn": {"handlers": ["console", "file"], "level": "INFO", "propagate": False}, + }, + "root": { + "handlers": ["console", "file"], + "level": "WARNING", # suppress noisy third-party libs at WARNING + }, + } def parse_args() -> argparse.Namespace: @@ -24,17 +84,52 @@ def parse_args() -> argparse.Namespace: action="store_true", help="Enable auto-reload for local development.", ) + parser.add_argument( + "--log-level", + default="info", + choices=["debug", "info", "warning", "error"], + help="Console log level (default: info). File always captures DEBUG.", + ) + parser.add_argument( + "--log-file", + default=None, + help="Log file path (default: logs/server_YYYY-MM-DD.log).", + ) return parser.parse_args() def main() -> None: - """Start uvicorn with the configured application.""" + """Start uvicorn with the configured application and logging.""" + import uvicorn + args = parse_args() + + # Resolve log file path + logs_dir = REPO_ROOT / "logs" + logs_dir.mkdir(parents=True, exist_ok=True) + if args.log_file: + log_file = Path(args.log_file) + else: + date_str = datetime.now().strftime("%Y-%m-%d") + log_file = logs_dir / f"server_{date_str}.log" + + log_config = _build_log_config(log_file, args.log_level) + + # Apply config before uvicorn starts so our loggers are ready immediately + logging.config.dictConfig(log_config) + + logger = logging.getLogger("webapp.server") + logger.info( + "Starting RAGAS Console host=%s port=%d log_level=%s log_file=%s", + args.host, args.port, args.log_level, log_file, + ) + uvicorn.run( "webapp.server:app", host=args.host, port=args.port, reload=args.reload, + log_config=log_config, # hand our config to uvicorn so it uses same handlers )