diff --git a/.env.example b/.env.example index 2bc29e6..9f38ec0 100644 --- a/.env.example +++ b/.env.example @@ -2,4 +2,7 @@ POSTGRES_USER = POSTGRES_PASSWORD = POSTGRES_DB = DATABASE_URL = -DEBUG = \ No newline at end of file + +DEBUG = + +SLOW_REQUEST_THRESHOLD = \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1cfa58a..4145f82 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,7 @@ env: POSTGRES_DB: ${{ secrets.POSTGRES_DB }} DATABASE_URL: ${{ secrets.DATABASE_URL }} DEBUG: ${{ secrets.DEBUG }} + SLOW_REQUEST_THRESHOLD: ${{ secrets.SLOW_REQUEST_THRESHOLD }} jobs: test: diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 4b17cc3..6a174e7 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -12,6 +12,7 @@ env: POSTGRES_DB: ${{ secrets.POSTGRES_DB }} DATABASE_URL: ${{ secrets.DATABASE_URL }} DEBUG: ${{ secrets.DEBUG }} + SLOW_REQUEST_THRESHOLD: ${{ secrets.SLOW_REQUEST_THRESHOLD }} jobs: diff --git a/.gitignore b/.gitignore index 385aeab..aeca225 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ __pycache__ .pytest_cache *.db .env -check-list-docker.md \ No newline at end of file +check-list-docker.md +ideas.md \ No newline at end of file diff --git a/app/core/config.py b/app/core/config.py index 6c23434..bfb48ac 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -6,6 +6,7 @@ class Settings(BaseSettings): POSTGRES_PASSWORD: str POSTGRES_DB: str DATABASE_URL: str + SLOW_REQUEST_THRESHOLD: float DEBUG: bool = True model_config = ConfigDict(env_file = '.env', case_sensitive = True) diff --git a/app/core/logger.py b/app/core/logger.py new file mode 100644 index 0000000..b63591b --- /dev/null +++ b/app/core/logger.py @@ -0,0 +1,24 @@ +import sys +from pathlib import Path +from loguru import logger + +logger.remove() # removing the old handler to set everything up yourself + +log_dir = Path('logs') +log_dir.mkdir(exist_ok = True) + +log_format = ( + '{time:YYYY-MM-DD HH:mm:ss} | ' + '{level: <8} | ' + '{name}:{function}:{line} - ' + '{message}' +) + +logger.add( + sys.stdout, + format = log_format, + level = "DEBUG", + colorize = True +) + +__all__ = ["logger"] \ No newline at end of file diff --git a/app/core/middleware.py b/app/core/middleware.py new file mode 100644 index 0000000..025bdc5 --- /dev/null +++ b/app/core/middleware.py @@ -0,0 +1,29 @@ +import time +from starlette.middleware.base import BaseHTTPMiddleware +from app.core.logger import logger +from app.core.config import settings + + +class LoggingMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request, call_next): + start_time = time.time() + logger.info(f'-> {request.method} {request.url.path}') + try: + response = await call_next(request) + process_time = time.time() - start_time + + if process_time > settings.SLOW_REQUEST_THRESHOLD: + logger.warning( + f"SLOW REQUEST: {request.method} {request.url.path} - " + f"Took {process_time:.2f}s (threshold: {settings.SLOW_REQUEST_THRESHOLD}s)" + ) + else: + logger.info( + f'<- {request.method} {request.url.path} - ' + f'Status: {response.status_code} - ' + f'Process time: {process_time:.2f}s' + ) + return response + except Exception as e: + logger.error(f'{request.method} {request.url.path} - Error: {str(e)}') + raise \ No newline at end of file diff --git a/app/main.py b/app/main.py index 316a1f8..ee2c6f9 100644 --- a/app/main.py +++ b/app/main.py @@ -1,14 +1,32 @@ -from fastapi import FastAPI +import traceback + +from fastapi import FastAPI, Request, status +from fastapi.responses import JSONResponse from contextlib import asynccontextmanager from app.core.database import engine, Base +from app.core.middleware import LoggingMiddleware from app.api import router +from app.core.logger import logger @asynccontextmanager async def lifespan(app: FastAPI): async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) + logger.info('Application started') yield app = FastAPI(lifespan = lifespan) +@app.exception_handler(Exception) +async def global_exeption_handler(request: Request, exc: Exception): + logger.error( + f'Unhandled error on {request.method} {request.url.path}\n' + f'{traceback.format_exc()}' + ) + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={"detail": "Internal Server Error."} + ) + +app.add_middleware(LoggingMiddleware) app.include_router(router) \ No newline at end of file diff --git a/readme.md b/readme.md index 2360789..94bca97 100644 --- a/readme.md +++ b/readme.md @@ -19,5 +19,9 @@ docker compose up --build **You can view the environment variables in the .env.example file.** +**To get logs in file format, use:** + + docker compose logs > logs.txt + ## License MIT. You can find it in the root of the project. diff --git a/requirements.txt b/requirements.txt index 96aabc4..6166ffe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,6 +13,7 @@ httpcore==1.0.9 httpx==0.28.1 idna==3.18 iniconfig==2.3.0 +loguru==0.7.3 packaging==26.2 pluggy==1.6.0 pydantic==2.13.4