feat: Add comprehensive logging system with console and file output

- Create logging infrastructure in src/infrastructure/logging/
  - logger.py: Main logging setup with console and file handlers
  - uvicorn_config.py: Custom uvicorn logging configuration
  - __init__.py: Export public logging API

- Update FastAPI application to use logging
  - Replace all print() statements with proper logger calls
  - Initialize logging during application startup
  - Add detailed startup/shutdown logging

- Add startup scripts
  - run_server.py: Python script with uvicorn logging config
  - start_server.sh: Bash wrapper script

- Add comprehensive documentation
  - docs/logging.md: User guide for logging system
  - docs/logging_implementation_summary.md: Technical implementation details

Features:
- Console logging with clean, readable format
- File logging with timestamps to logs/fastapi_app.log
- Configurable log level via LOG_LEVEL environment variable
- Proper lazy formatting for performance
- Captures all uvicorn, application, and module logs
- Automatic log directory creation
This commit is contained in:
2025-10-25 17:40:20 +02:00
parent a41c86f1da
commit 94c53e9555
8 changed files with 560 additions and 12 deletions

View File

@@ -0,0 +1,7 @@
"""
Logging infrastructure for the Aniworld application.
"""
from src.infrastructure.logging.logger import get_logger, setup_logging
from src.infrastructure.logging.uvicorn_config import get_uvicorn_log_config
__all__ = ["setup_logging", "get_logger", "get_uvicorn_log_config"]

View File

@@ -0,0 +1,100 @@
"""
Logging configuration for the Aniworld application.
This module provides a centralized logging setup with both console and file
logging, following Python logging best practices.
"""
import logging
import sys
from pathlib import Path
from typing import Optional
from src.config.settings import settings
def setup_logging(
log_file: Optional[str] = None,
log_level: Optional[str] = None,
log_dir: Optional[Path] = None
) -> logging.Logger:
"""
Configure application logging with console and file handlers.
Args:
log_file: Name of the log file (default: "fastapi_app.log")
log_level: Logging level (default: from settings or "INFO")
log_dir: Directory for log files (default: "logs" in project root)
Returns:
Configured logger instance
"""
# Determine log level
level_name = log_level or settings.log_level or "INFO"
level = getattr(logging, level_name.upper(), logging.INFO)
# Determine log directory and file
if log_dir is None:
# Default to logs directory in project root
log_dir = Path(__file__).parent.parent.parent.parent / "logs"
log_dir.mkdir(parents=True, exist_ok=True)
if log_file is None:
log_file = "fastapi_app.log"
log_path = log_dir / log_file
# Create formatters
detailed_formatter = logging.Formatter(
fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
console_formatter = logging.Formatter(
fmt="%(levelname)s: %(message)s"
)
# Configure root logger
root_logger = logging.getLogger()
root_logger.setLevel(level)
# Remove existing handlers to avoid duplicates
root_logger.handlers.clear()
# Console handler (stdout)
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(level)
console_handler.setFormatter(console_formatter)
root_logger.addHandler(console_handler)
# File handler
file_handler = logging.FileHandler(log_path, mode='a', encoding='utf-8')
file_handler.setLevel(level)
file_handler.setFormatter(detailed_formatter)
root_logger.addHandler(file_handler)
# Create application logger
logger = logging.getLogger("aniworld")
logger.setLevel(level)
# Log startup information
logger.info("=" * 60)
logger.info("Logging configured successfully")
logger.info("Log level: %s", level_name.upper())
logger.info("Log file: %s", log_path)
logger.info("=" * 60)
return logger
def get_logger(name: str) -> logging.Logger:
"""
Get a logger instance for a specific module.
Args:
name: Name of the logger (typically __name__)
Returns:
Logger instance
"""
return logging.getLogger(name)

View File

@@ -0,0 +1,79 @@
"""
Uvicorn logging configuration for the Aniworld application.
This configuration ensures that uvicorn logs are properly formatted and
written to both console and file.
"""
from pathlib import Path
# Get the logs directory
LOGS_DIR = Path(__file__).parent.parent.parent.parent / "logs"
LOGS_DIR.mkdir(parents=True, exist_ok=True)
LOG_FILE = LOGS_DIR / "fastapi_app.log"
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"format": "%(levelprefix)s %(message)s",
"use_colors": None,
},
"detailed": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "INFO",
"formatter": "default",
"stream": "ext://sys.stdout",
},
"file": {
"class": "logging.FileHandler",
"level": "INFO",
"formatter": "detailed",
"filename": str(LOG_FILE),
"mode": "a",
"encoding": "utf-8",
},
},
"loggers": {
"uvicorn": {
"handlers": ["console", "file"],
"level": "INFO",
"propagate": False,
},
"uvicorn.error": {
"handlers": ["console", "file"],
"level": "INFO",
"propagate": False,
},
"uvicorn.access": {
"handlers": ["console", "file"],
"level": "INFO",
"propagate": False,
},
"aniworld": {
"handlers": ["console", "file"],
"level": "INFO",
"propagate": False,
},
},
"root": {
"handlers": ["console", "file"],
"level": "INFO",
},
}
def get_uvicorn_log_config() -> dict:
"""
Get the uvicorn logging configuration dictionary.
Returns:
Dictionary containing logging configuration
"""
return LOGGING_CONFIG

View File

@@ -5,6 +5,7 @@ This module provides the main FastAPI application with proper CORS
configuration, middleware setup, static file serving, and Jinja2 template
integration.
"""
import logging
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Optional
@@ -18,6 +19,7 @@ from src.config.settings import settings
# Import core functionality
from src.core.SeriesApp import SeriesApp
from src.infrastructure.logging import setup_logging
from src.server.api.analytics import router as analytics_router
from src.server.api.anime import router as anime_router
from src.server.api.auth import router as auth_router
@@ -51,8 +53,13 @@ from src.server.services.websocket_service import get_websocket_service
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Manage application lifespan (startup and shutdown)."""
# Setup logging first
logger = setup_logging()
# Startup
try:
logger.info("Starting FastAPI application...")
# Load configuration from config.json and sync with settings
try:
from src.server.services.config_service import get_config_service
@@ -62,25 +69,25 @@ async def lifespan(app: FastAPI):
# Sync anime_directory from config.json to settings
if config.other and config.other.get("anime_directory"):
settings.anime_directory = str(config.other["anime_directory"])
print(
f"Loaded anime_directory from config: "
f"{settings.anime_directory}"
logger.info(
"Loaded anime_directory from config: %s",
settings.anime_directory
)
except Exception as e:
print(f"Warning: Failed to load config from config.json: {e}")
logger.warning("Failed to load config from config.json: %s", e)
# Initialize SeriesApp with configured directory and store it on
# application state so it can be injected via dependencies.
if settings.anime_directory:
app.state.series_app = SeriesApp(settings.anime_directory)
print(
f"SeriesApp initialized with directory: "
f"{settings.anime_directory}"
logger.info(
"SeriesApp initialized with directory: %s",
settings.anime_directory
)
else:
# Log warning when anime directory is not configured
print(
"WARNING: ANIME_DIRECTORY not configured. "
logger.warning(
"ANIME_DIRECTORY not configured. "
"Some features may be unavailable."
)
@@ -100,16 +107,20 @@ async def lifespan(app: FastAPI):
progress_service.set_broadcast_callback(broadcast_callback)
print("FastAPI application started successfully")
logger.info("FastAPI application started successfully")
logger.info("Server running on http://127.0.0.1:8000")
logger.info(
"API documentation available at http://127.0.0.1:8000/api/docs"
)
except Exception as e:
print(f"Error during startup: {e}")
logger.error("Error during startup: %s", e, exc_info=True)
raise # Re-raise to prevent app from starting in broken state
# Yield control to the application
yield
# Shutdown
print("FastAPI application shutting down")
logger.info("FastAPI application shutting down")
def get_series_app() -> Optional[SeriesApp]: