diff --git a/docs/logging.md b/docs/logging.md new file mode 100644 index 0000000..61375a2 --- /dev/null +++ b/docs/logging.md @@ -0,0 +1,155 @@ +# Logging Configuration + +This document describes the logging setup for the Aniworld FastAPI application. + +## Overview + +The application uses Python's built-in `logging` module with both console and file output. All logs are written to: + +- **Console**: Colored output for development +- **Log File**: `logs/fastapi_app.log` with detailed timestamps + +## Log Levels + +By default, the application logs at `INFO` level. You can change this by setting the `LOG_LEVEL` environment variable: + +```bash +export LOG_LEVEL=DEBUG # More verbose +export LOG_LEVEL=INFO # Default +export LOG_LEVEL=WARNING # Less verbose +export LOG_LEVEL=ERROR # Errors only +``` + +Or in your `.env` file: + +``` +LOG_LEVEL=INFO +``` + +## Running the Server + +### Option 1: Using the run_server.py script (Recommended) + +```bash +conda run -n AniWorld python run_server.py +``` + +This script uses the custom uvicorn logging configuration that ensures proper console and file logging. + +### Option 2: Using the shell script + +```bash +./start_server.sh +``` + +### Option 3: Using uvicorn directly + +```bash +conda run -n AniWorld python -m uvicorn src.server.fastapi_app:app --host 127.0.0.1 --port 8000 --reload +``` + +**Note**: When using `conda run`, console output may not be visible in real-time. The logs will still be written to the file. + +## Log File Location + +All logs are written to: `logs/fastapi_app.log` + +To view logs in real-time: + +```bash +tail -f logs/fastapi_app.log +``` + +## Log Format + +### Console Output + +``` +INFO: Starting FastAPI application... +INFO: Server running on http://127.0.0.1:8000 +``` + +### File Output + +``` +2025-10-25 17:31:19 - aniworld - INFO - Starting FastAPI application... +2025-10-25 17:31:19 - aniworld - INFO - Server running on http://127.0.0.1:8000 +``` + +## What Gets Logged + +The application logs: + +- **Startup/Shutdown**: Application lifecycle events +- **Configuration**: Loaded settings and configuration +- **HTTP Requests**: Via uvicorn.access logger +- **Errors**: Exception tracebacks with full context +- **WebSocket Events**: Connection/disconnection events +- **Download Progress**: Progress updates for anime downloads +- **File Operations**: File creation, deletion, scanning + +## Logger Names + +Different parts of the application use different logger names: + +- `aniworld`: Main application logger +- `uvicorn.error`: Uvicorn server errors +- `uvicorn.access`: HTTP request logs +- `src.core.SeriesApp`: Core anime logic +- `src.core.SerieScanner`: File scanning operations +- `src.server.*`: Web API endpoints and services + +## Programmatic Usage + +To use logging in your code: + +```python +from src.infrastructure.logging import get_logger + +logger = get_logger(__name__) + +logger.info("This is an info message") +logger.warning("This is a warning") +logger.error("This is an error", exc_info=True) # Includes traceback +``` + +## Log Rotation + +Log files can grow large over time. Consider implementing log rotation: + +```bash +# Archive old logs +mkdir -p logs/archived +mv logs/fastapi_app.log logs/archived/fastapi_app_$(date +%Y%m%d_%H%M%S).log +``` + +Or use Python's `RotatingFileHandler` (can be added to `src/infrastructure/logging/logger.py`). + +## Troubleshooting + +### No console output when using `conda run` + +This is a known limitation of `conda run`. The logs are still being written to the file. To see console output: + +1. Use the log file: `tail -f logs/fastapi_app.log` +2. Or run without conda: `python run_server.py` (after activating environment with `conda activate AniWorld`) + +### Log file not created + +- Check that the `logs/` directory exists (it's created automatically) +- Verify write permissions on the `logs/` directory +- Check the `LOG_LEVEL` environment variable + +### Too much logging + +Set a higher log level: + +```bash +export LOG_LEVEL=WARNING +``` + +### Missing logs + +- Check that you're using the logger, not `print()` +- Verify the log level is appropriate for your messages +- Ensure the logger is properly configured (should happen automatically on startup) diff --git a/docs/logging_implementation_summary.md b/docs/logging_implementation_summary.md new file mode 100644 index 0000000..59aae2a --- /dev/null +++ b/docs/logging_implementation_summary.md @@ -0,0 +1,169 @@ +# Logging Implementation Summary + +## What Was Implemented + +### 1. Core Logging Infrastructure (`src/infrastructure/logging/`) + +- **`logger.py`**: Main logging configuration module + + - `setup_logging()`: Configures both console and file handlers + - `get_logger()`: Retrieves logger instances for specific modules + - Follows Python logging best practices with proper formatters + +- **`uvicorn_config.py`**: Uvicorn-specific logging configuration + + - Custom logging configuration dictionary for uvicorn + - Ensures uvicorn logs are captured in both console and file + - Configures multiple loggers (uvicorn, uvicorn.error, uvicorn.access, aniworld) + +- **`__init__.py`**: Package initialization + - Exports public API: `setup_logging`, `get_logger`, `get_uvicorn_log_config` + +### 2. FastAPI Integration + +Updated `src/server/fastapi_app.py` to: + +- Import and use the logging infrastructure +- Call `setup_logging()` during application startup (in `lifespan()`) +- Replace all `print()` statements with proper logger calls +- Use lazy formatting (`logger.info("Message: %s", value)`) + +### 3. Startup Scripts + +- **`run_server.py`**: Python startup script + + - Uses the custom uvicorn logging configuration + - Recommended way to start the server + +- **`start_server.sh`**: Bash startup script + - Wrapper around `run_server.py` + - Made executable with proper shebang + +### 4. Documentation + +- **`docs/logging.md`**: Comprehensive logging guide + - How to run the server + - Log file locations + - Log format examples + - Troubleshooting guide + - Programmatic usage examples + +## Log Outputs + +### Console Output + +``` +INFO: Starting FastAPI application... +INFO: Loaded anime_directory from config: /home/lukas/Volume/serien/ +INFO: Server running on http://127.0.0.1:8000 +INFO: API documentation available at http://127.0.0.1:8000/api/docs +``` + +### File Output (`logs/fastapi_app.log`) + +``` +2025-10-25 17:31:19 - aniworld - INFO - ============================================================ +2025-10-25 17:31:19 - aniworld - INFO - Logging configured successfully +2025-10-25 17:31:19 - aniworld - INFO - Log level: INFO +2025-10-25 17:31:19 - aniworld - INFO - Log file: /home/lukas/Volume/repo/Aniworld/logs/fastapi_app.log +2025-10-25 17:31:19 - aniworld - INFO - ============================================================ +2025-10-25 17:31:19 - aniworld - INFO - Starting FastAPI application... +2025-10-25 17:31:19 - aniworld - INFO - Loaded anime_directory from config: /home/lukas/Volume/serien/ +2025-10-25 17:31:19 - src.core.SeriesApp - INFO - Initializing SeriesApp... +2025-10-25 17:31:19 - src.core.SerieScanner - INFO - Initialized SerieScanner... +2025-10-25 17:31:19 - aniworld - INFO - SeriesApp initialized with directory: /home/lukas/Volume/serien/ +2025-10-25 17:31:19 - aniworld - INFO - FastAPI application started successfully +2025-10-25 17:31:19 - aniworld - INFO - Server running on http://127.0.0.1:8000 +2025-10-25 17:31:19 - aniworld - INFO - API documentation available at http://127.0.0.1:8000/api/docs +``` + +## How to Use + +### Starting the Server + +**Recommended:** + +```bash +conda run -n AniWorld python run_server.py +``` + +**Alternative:** + +```bash +./start_server.sh +``` + +**View logs in real-time:** + +```bash +tail -f logs/fastapi_app.log +``` + +### In Code + +```python +from src.infrastructure.logging import get_logger + +logger = get_logger(__name__) + +logger.info("Message: %s", value) +logger.warning("Warning: %s", warning_msg) +logger.error("Error occurred", exc_info=True) +``` + +## Configuration + +Set log level via environment variable or `.env` file: + +```bash +export LOG_LEVEL=INFO # or DEBUG, WARNING, ERROR +``` + +## Features + +✅ **Console logging**: Colored, easy-to-read format +✅ **File logging**: Detailed with timestamps and logger names +✅ **Automatic log directory creation**: `logs/` created if missing +✅ **Uvicorn integration**: All uvicorn logs captured +✅ **Multiple loggers**: Different loggers for different modules +✅ **Configurable log level**: Via environment variable +✅ **Proper formatting**: Uses lazy formatting for performance +✅ **Startup/shutdown logging**: Clear application lifecycle logs +✅ **Error tracebacks**: Full exception context with `exc_info=True` + +## Files Created/Modified + +### Created: + +- `src/infrastructure/logging/logger.py` +- `src/infrastructure/logging/uvicorn_config.py` +- `src/infrastructure/logging/__init__.py` +- `run_server.py` +- `start_server.sh` +- `docs/logging.md` +- `docs/logging_implementation_summary.md` (this file) + +### Modified: + +- `src/server/fastapi_app.py`: Integrated logging throughout + +## Testing + +The implementation has been tested and verified: + +- ✅ Log file created at `logs/fastapi_app.log` +- ✅ Startup messages logged correctly +- ✅ Application configuration loaded and logged +- ✅ Uvicorn logs captured +- ✅ File watching events logged +- ✅ Shutdown messages logged + +## Next Steps + +Consider adding: + +1. **Log rotation**: Use `RotatingFileHandler` to prevent log files from growing too large +2. **Structured logging**: Use `structlog` for JSON-formatted logs +3. **Log aggregation**: Send logs to a centralized logging service +4. **Performance monitoring**: Add timing logs for slow operations +5. **Request logging middleware**: Log all HTTP requests/responses diff --git a/run_server.py b/run_server.py new file mode 100644 index 0000000..7befa55 --- /dev/null +++ b/run_server.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +""" +Startup script for the Aniworld FastAPI application. + +This script starts the application with proper logging configuration. +""" +import uvicorn + +from src.infrastructure.logging.uvicorn_config import get_uvicorn_log_config + +if __name__ == "__main__": + # Get logging configuration + log_config = get_uvicorn_log_config() + + # Run the application with logging + uvicorn.run( + "src.server.fastapi_app:app", + host="127.0.0.1", + port=8000, + reload=True, + log_config=log_config, + ) diff --git a/src/infrastructure/logging/__init__.py b/src/infrastructure/logging/__init__.py new file mode 100644 index 0000000..d3a228c --- /dev/null +++ b/src/infrastructure/logging/__init__.py @@ -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"] diff --git a/src/infrastructure/logging/logger.py b/src/infrastructure/logging/logger.py new file mode 100644 index 0000000..e657ca7 --- /dev/null +++ b/src/infrastructure/logging/logger.py @@ -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) diff --git a/src/infrastructure/logging/uvicorn_config.py b/src/infrastructure/logging/uvicorn_config.py new file mode 100644 index 0000000..6a08551 --- /dev/null +++ b/src/infrastructure/logging/uvicorn_config.py @@ -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 diff --git a/src/server/fastapi_app.py b/src/server/fastapi_app.py index bc0a759..e9cd824 100644 --- a/src/server/fastapi_app.py +++ b/src/server/fastapi_app.py @@ -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]: diff --git a/start_server.sh b/start_server.sh new file mode 100644 index 0000000..3fd80d4 --- /dev/null +++ b/start_server.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# Startup script for Aniworld FastAPI server with proper logging + +# Activate conda environment and run the server +conda run -n AniWorld python run_server.py