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:
7
src/infrastructure/logging/__init__.py
Normal file
7
src/infrastructure/logging/__init__.py
Normal 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"]
|
||||
100
src/infrastructure/logging/logger.py
Normal file
100
src/infrastructure/logging/logger.py
Normal 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)
|
||||
79
src/infrastructure/logging/uvicorn_config.py
Normal file
79
src/infrastructure/logging/uvicorn_config.py
Normal 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
|
||||
@@ -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]:
|
||||
|
||||
Reference in New Issue
Block a user