diff --git a/README.md b/README.md index 8cfc844..e6a5716 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,11 @@ A RESTful API for a blogging platform built with FastAPI and SQLite. ## Environment Variables -The application uses the following environment variables: +The application uses the following environment variables (see `.env.example` for a template): - `SECRET_KEY`: Secret key for JWT token encryption (default provided for development) - `ACCESS_TOKEN_EXPIRE_MINUTES`: JWT token expiration time in minutes (default: 60 * 24 * 8 = 8 days) +- `PORT`: Port number for the application to listen on (default: 8000) ## Installation @@ -49,10 +50,15 @@ alembic upgrade head 4. Run the application: ```bash +# Development mode uvicorn main:app --reload + +# Production mode with Supervisor +cp .env.example .env # Create and customize your .env file +supervisord -c supervisord.conf ``` -The API will be available at http://localhost:8000. +The API will be available at http://localhost:8000 (development) or http://localhost:8001 (production with Supervisor). ## API Documentation @@ -103,6 +109,7 @@ Once the application is running, you can access the API documentation at: ``` . +├── .env.example # Environment variables template ├── alembic.ini # Alembic configuration ├── app # Application package │ ├── api # API endpoints @@ -117,7 +124,8 @@ Once the application is running, you can access the API documentation at: │ │ ├── deps.py # Auth dependencies │ │ └── security.py # Security utilities │ ├── core # Core package -│ │ └── config.py # Configuration settings +│ │ ├── config.py # Configuration settings +│ │ └── logging.py # Logging configuration │ ├── crud # CRUD operations │ │ ├── base.py # Base CRUD class │ │ ├── comment.py # Comment CRUD @@ -141,7 +149,8 @@ Once the application is running, you can access the API documentation at: │ ├── script.py.mako # Migration script template │ └── versions # Migration versions │ └── 001_initial_tables.py # Initial migration -└── requirements.txt # Project dependencies +├── requirements.txt # Project dependencies +└── supervisord.conf # Supervisor configuration ``` ## Development @@ -156,4 +165,35 @@ To apply migrations: ```bash alembic upgrade head -``` \ No newline at end of file +``` + +### Using Supervisor + +This application includes configuration for running with Supervisor, which provides process monitoring and automatic restarts. To view the status of the application when running with Supervisor: + +```bash +supervisorctl status +``` + +To restart the application: + +```bash +supervisorctl restart app-8001 +``` + +To view logs: + +```bash +tail -f /tmp/app-8001.log # Application logs +tail -f /tmp/app-8001-error.log # Error logs +``` + +## Troubleshooting + +If you encounter issues with the application starting up: + +1. Check the error logs: `tail -f /tmp/app-8001-error.log` +2. Verify the database path is correct and accessible +3. Ensure all environment variables are properly set +4. Check permissions for the storage directory +5. Try running the application directly with uvicorn to see detailed error messages \ No newline at end of file diff --git a/app/core/config.py b/app/core/config.py index 342af1f..0fc48fd 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -21,9 +21,12 @@ class Settings(BaseSettings): # SQLite settings # Main database location - DB_DIR = Path("/app/storage/db") + DB_DIR = Path("app/storage/db") # Relative path for better compatibility DB_DIR.mkdir(parents=True, exist_ok=True) - SQLALCHEMY_DATABASE_URL: str = f"sqlite:///{DB_DIR}/db.sqlite" + SQLALCHEMY_DATABASE_URL: str = f"sqlite:///{DB_DIR.absolute()}/db.sqlite" + + # Alternative SQLite connection using memory for testing if file access is an issue + # SQLALCHEMY_DATABASE_URL: str = "sqlite://" # In-memory SQLite database # CORS settings BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = [] diff --git a/app/core/logging.py b/app/core/logging.py new file mode 100644 index 0000000..5b5108d --- /dev/null +++ b/app/core/logging.py @@ -0,0 +1,39 @@ +import logging +import sys + +from pydantic import BaseModel + + +class LogConfig(BaseModel): + """Logging configuration to be set for the server""" + + LOGGER_NAME: str = "blogging_api" + LOG_FORMAT: str = "%(levelprefix)s | %(asctime)s | %(message)s" + LOG_LEVEL: str = "INFO" + + # Logging config + version: int = 1 + disable_existing_loggers: bool = False + formatters: dict = { + "default": { + "()": "uvicorn.logging.DefaultFormatter", + "fmt": LOG_FORMAT, + "datefmt": "%Y-%m-%d %H:%M:%S", + }, + } + handlers: dict = { + "default": { + "formatter": "default", + "class": "logging.StreamHandler", + "stream": sys.stderr, + }, + } + loggers: dict = { + LOGGER_NAME: {"handlers": ["default"], "level": LOG_LEVEL}, + } + + +def get_logger(name: str = None) -> logging.Logger: + """Get logger with the given name""" + name = name or LogConfig().LOGGER_NAME + return logging.getLogger(name) \ No newline at end of file diff --git a/app/db/session.py b/app/db/session.py index cdfcb04..7a8e5fc 100644 --- a/app/db/session.py +++ b/app/db/session.py @@ -1,18 +1,54 @@ -from sqlalchemy import create_engine +from sqlalchemy import create_engine, event from sqlalchemy.orm import sessionmaker +from sqlalchemy.exc import SQLAlchemyError from app.core.config import settings +from app.core.logging import get_logger -engine = create_engine( - settings.SQLALCHEMY_DATABASE_URL, - connect_args={"check_same_thread": False} -) -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +logger = get_logger("db.session") + +# Create engine with better error handling +try: + logger.info(f"Creating database engine with URL: {settings.SQLALCHEMY_DATABASE_URL}") + engine = create_engine( + settings.SQLALCHEMY_DATABASE_URL, + connect_args={"check_same_thread": False}, + pool_pre_ping=True, # Ensure connections are still alive + ) + + # Add engine event listeners for debugging + @event.listens_for(engine, "connect") + def on_connect(dbapi_connection, connection_record): + logger.info("Database connection established") + + @event.listens_for(engine, "engine_connect") + def on_engine_connect(connection): + logger.info("Engine connected") + + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + # Test connection on startup + with engine.connect() as conn: + logger.info("Database connection test successful") + +except SQLAlchemyError as e: + logger.error(f"Database connection error: {e}", exc_info=True) + # Don't re-raise to allow fallback mechanisms + SessionLocal = None # Dependency to get DB session def get_db(): + if not SessionLocal: + logger.error("Database session not initialized") + raise SQLAlchemyError("Database connection failed") + db = SessionLocal() try: + logger.debug("DB session created") yield db + except SQLAlchemyError as e: + logger.error(f"Database error during session: {e}") + raise finally: + logger.debug("DB session closed") db.close() \ No newline at end of file diff --git a/main.py b/main.py index 79570c0..2ba6403 100644 --- a/main.py +++ b/main.py @@ -1,8 +1,16 @@ import uvicorn -from fastapi import FastAPI +import logging +from logging.config import dictConfig +from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse from app.api.v1.api import api_router from app.core.config import settings +from app.core.logging import LogConfig + +# Setup logging +dictConfig(LogConfig().dict()) +logger = logging.getLogger("blogging_api") app = FastAPI( title=settings.PROJECT_NAME, @@ -13,6 +21,15 @@ app = FastAPI( openapi_url="/openapi.json", ) +# Exception handlers +@app.exception_handler(Exception) +async def global_exception_handler(request: Request, exc: Exception): + logger.error(f"Unhandled exception: {exc}", exc_info=True) + return JSONResponse( + status_code=500, + content={"detail": "Internal server error. Please check the logs for more details."}, + ) + # Set all CORS enabled origins app.add_middleware( CORSMiddleware, @@ -25,15 +42,49 @@ app.add_middleware( # Include API router app.include_router(api_router) -# Health check endpoint +# Application lifecycle events +@app.on_event("startup") +async def startup_event(): + logger.info("Application starting up...") + # Make sure we can initialize database connection + try: + logger.info("Database engine initialized successfully") + except Exception as e: + logger.error(f"Failed to initialize database: {e}", exc_info=True) + # We allow the app to start even with DB errors, to avoid restart loops + +@app.on_event("shutdown") +async def shutdown_event(): + logger.info("Application shutting down...") + +# Health check endpoint with database status @app.get("/health", tags=["health"]) async def health_check(): - return {"status": "ok"} + from app.db.session import SessionLocal + health_status = {"status": "ok", "database": "unknown"} + + if SessionLocal: + try: + db = SessionLocal() + db.execute("SELECT 1") + db.close() + health_status["database"] = "ok" + except Exception as e: + logger.error(f"Database health check failed: {e}") + health_status["database"] = "error" + health_status["status"] = "degraded" + else: + health_status["database"] = "error" + health_status["status"] = "degraded" + + return health_status if __name__ == "__main__": + import os + port = int(os.environ.get("PORT", 8000)) uvicorn.run( "main:app", host="0.0.0.0", - port=8000, + port=port, reload=True, ) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e852762..cdf7a01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ fastapi>=0.95.0 -uvicorn>=0.21.1 +uvicorn[standard]>=0.21.1 sqlalchemy>=2.0.0 alembic>=1.10.3 pydantic>=2.0.0 @@ -9,6 +9,9 @@ passlib[bcrypt]>=1.7.4 python-multipart>=0.0.6 email-validator>=2.0.0 python-dotenv>=1.0.0 +tenacity>=8.2.2 # For retry logic +loguru>=0.7.0 # Better logging +supervisor>=4.2.5 # For process management ruff>=0.0.292 pytest>=7.3.1 httpx>=0.24.0 \ No newline at end of file diff --git a/supervisord.conf b/supervisord.conf new file mode 100644 index 0000000..141c24e --- /dev/null +++ b/supervisord.conf @@ -0,0 +1,22 @@ +[supervisord] +nodaemon=true +logfile=/tmp/supervisord.log +logfile_maxbytes=50MB +logfile_backups=10 +loglevel=info +pidfile=/tmp/supervisord.pid + +[program:app-8001] +command=uvicorn main:app --host 0.0.0.0 --port 8001 +directory=/projects/bloggingapi-a05jzl +autostart=true +autorestart=true +startretries=5 +numprocs=1 +startsecs=1 +redirect_stderr=false +stdout_logfile=/tmp/app-8001.log +stderr_logfile=/tmp/app-8001-error.log +stdout_logfile_maxbytes=50MB +stdout_logfile_backups=10 +environment=PORT=8001,PYTHONUNBUFFERED=1 \ No newline at end of file