Files
MediaManager/media_manager/main.py
Maximilian Dorninger c45c9e5873 add correlation id to logging (#398)
This PR adds Correlation IDs to logs and request responses.

```
2026-02-04 12:40:32,793 - [afd825081d874d6e835b5c59a6ddb371] DEBUG - media_manager.movies - get_importable_movies(): Found 5 importable movies.
2026-02-04 12:40:32,794 - [afd825081d874d6e835b5c59a6ddb371] INFO - uvicorn.access - send(): 172.19.0.1:64094 - "GET /api/v1/movies/importable HTTP/1.1" 200
2026-02-04 12:40:47,322 - [41d30b7003fd45288c6a4bb1cfba5e7a] INFO - uvicorn.access - send(): 127.0.0.1:52964 - "GET /api/v1/health HTTP/1.1" 200
2026-02-04 12:41:17,408 - [157027ea5dde472a9e620f53739ccd53] INFO - uvicorn.access - send(): 127.0.0.1:39850 - "GET /api/v1/health HTTP/1.1" 200
```
2026-02-04 13:55:05 +01:00

191 lines
6.0 KiB
Python

import logging
import os
import uvicorn
from asgi_correlation_id import CorrelationIdMiddleware
from fastapi import APIRouter, FastAPI, Request, Response
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from psycopg.errors import UniqueViolation
from sqlalchemy.exc import IntegrityError
from starlette.responses import FileResponse, RedirectResponse
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
import media_manager.movies.router as movies_router
import media_manager.torrent.router as torrent_router
import media_manager.tv.router as tv_router
from media_manager.auth.router import (
auth_metadata_router,
get_openid_router,
)
from media_manager.auth.router import (
users_router as custom_users_router,
)
from media_manager.auth.schemas import UserCreate, UserRead, UserUpdate
from media_manager.auth.users import (
bearer_auth_backend,
cookie_auth_backend,
fastapi_users,
)
from media_manager.config import MediaManagerConfig
from media_manager.exceptions import (
ConflictError,
InvalidConfigError,
MediaAlreadyExistsError,
NotFoundError,
conflict_error_handler,
invalid_config_error_exception_handler,
media_already_exists_exception_handler,
not_found_error_exception_handler,
sqlalchemy_integrity_error_handler,
)
from media_manager.filesystem_checks import run_filesystem_checks
from media_manager.logging import LOGGING_CONFIG, setup_logging
from media_manager.notification.router import router as notification_router
from media_manager.scheduler import setup_scheduler
setup_logging()
config = MediaManagerConfig()
log = logging.getLogger(__name__)
if config.misc.development:
log.warning("Development Mode activated!")
scheduler = setup_scheduler(config)
run_filesystem_checks(config, log)
BASE_PATH = os.getenv("BASE_PATH", "")
FRONTEND_FILES_DIR = os.getenv("FRONTEND_FILES_DIR")
DISABLE_FRONTEND_MOUNT = os.getenv("DISABLE_FRONTEND_MOUNT", "").lower() == "true"
FRONTEND_FOLLOW_SYMLINKS = os.getenv("FRONTEND_FOLLOW_SYMLINKS", "").lower() == "true"
log.info("Hello World!")
app = FastAPI(root_path=BASE_PATH)
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts="*")
origins = config.misc.cors_urls
log.info(f"CORS URLs activated for following origins: {origins}")
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["GET", "PUT", "POST", "DELETE", "PATCH", "HEAD", "OPTIONS"],
)
app.add_middleware(CorrelationIdMiddleware, header_name="X-Correlation-ID")
api_app = APIRouter(prefix="/api/v1")
@api_app.get("/health")
async def hello_world() -> dict:
return {"message": "Hello World!", "version": os.getenv("PUBLIC_VERSION")}
api_app.include_router(
fastapi_users.get_auth_router(bearer_auth_backend),
prefix="/auth/jwt",
tags=["auth"],
)
api_app.include_router(
fastapi_users.get_auth_router(cookie_auth_backend),
prefix="/auth/cookie",
tags=["auth"],
)
api_app.include_router(
fastapi_users.get_register_router(UserRead, UserCreate),
prefix="/auth",
tags=["auth"],
)
api_app.include_router(
fastapi_users.get_reset_password_router(), prefix="/auth", tags=["auth"]
)
api_app.include_router(
fastapi_users.get_verify_router(UserRead), prefix="/auth", tags=["auth"]
)
api_app.include_router(custom_users_router, tags=["users"])
api_app.include_router(
fastapi_users.get_users_router(UserRead, UserUpdate),
prefix="/users",
tags=["users"],
)
api_app.include_router(auth_metadata_router, tags=["openid"])
if get_openid_router():
api_app.include_router(get_openid_router(), tags=["openid"], prefix="/auth/oauth")
api_app.include_router(tv_router.router, prefix="/tv", tags=["tv"])
api_app.include_router(torrent_router.router, prefix="/torrent", tags=["torrent"])
api_app.include_router(movies_router.router, prefix="/movies", tags=["movie"])
api_app.include_router(
notification_router, prefix="/notification", tags=["notification"]
)
# serve static image files
app.mount(
"/api/v1/static/image",
StaticFiles(directory=config.misc.image_directory),
name="static-images",
)
app.include_router(api_app)
# handle static frontend files
if not DISABLE_FRONTEND_MOUNT:
app.mount(
"/web",
StaticFiles(
directory=FRONTEND_FILES_DIR,
html=True,
follow_symlink=FRONTEND_FOLLOW_SYMLINKS,
),
name="frontend",
)
log.debug(f"Mounted frontend at /web from {FRONTEND_FILES_DIR}")
else:
log.info("Frontend mounting disabled (DISABLE_FRONTEND_MOUNT is set)")
@app.get("/")
async def root() -> RedirectResponse:
return RedirectResponse(url="/web/")
@app.get("/dashboard")
async def dashboard() -> RedirectResponse:
return RedirectResponse(url="/web/")
@app.get("/login")
async def login() -> RedirectResponse:
return RedirectResponse(url="/web/")
# this will serve the custom 404 page for frontend routes, so SvelteKit can handle routing
@app.exception_handler(404)
async def not_found_handler(request: Request, _exc: Exception) -> Response:
if not DISABLE_FRONTEND_MOUNT and any(
base_path in ["/web", "/dashboard", "/login"] for base_path in request.url.path
):
return FileResponse(f"{FRONTEND_FILES_DIR}/404.html")
return Response(content="Not Found", status_code=404)
# Register exception handlers for custom exceptions
app.add_exception_handler(NotFoundError, not_found_error_exception_handler)
app.add_exception_handler(
MediaAlreadyExistsError, media_already_exists_exception_handler
)
app.add_exception_handler(InvalidConfigError, invalid_config_error_exception_handler)
app.add_exception_handler(IntegrityError, sqlalchemy_integrity_error_handler)
app.add_exception_handler(UniqueViolation, sqlalchemy_integrity_error_handler)
app.add_exception_handler(ConflictError, conflict_error_handler)
if __name__ == "__main__":
uvicorn.run(
app,
host="127.0.0.1",
port=5049,
log_config=LOGGING_CONFIG,
proxy_headers=True,
forwarded_allow_ips="*",
)