from fastapi import FastAPI, Depends, HTTPException, Response, Request import uvicorn from github_api import find_package_version_with_tag as find_package_version_with_tag_github from dockerhub_api import find_package_version_with_tag as find_package_version_with_tag_dockerhub import uvicorn from contextlib import asynccontextmanager from db_module import get_db, create_connection_pool, close_connection_pool, start_healthcheck_thread from simple_logger_handler import setup_logger, LOG_LEVEL from send_notification import send_notification from metrics_server import REQUEST_COUNTER import asyncio from uvicorn_logger_config import LOGGING_CONFIG from secret_manager import cleanup_secret_files logger = setup_logger(__name__) @asynccontextmanager async def lifespan(app: FastAPI): logger.info("[App] Starting application...") logger.info("[DB] Creating MySQL connection pool...") create_connection_pool("repositoryQueryPool") start_healthcheck_thread() logger.info("[DB] MySQL healthcheck thread started.") SECRET_PATHS = frozenset({ "/etc/secrets/api_key", "/etc/secrets/db_username", "/etc/secrets/db_password", "/etc/secrets/dockerhub_token", "/etc/secrets/dockerhub_username", "/etc/secrets/github_token" }) cleanup_secret_files(SECRET_PATHS) yield logger.info("[App] Closing MySQL connection pool...") close_connection_pool() logger.info("[App] Shutdown complete.") api = FastAPI( title="Docker Repository Query", description="Queries Dockerhub and GHCR for new docker images", version="1.0.0", lifespan=lifespan ) @api.middleware("http") async def prometheus_middleware(request: Request, call_next): logger.debug(f"[Metrics] Incoming request: {request.method} {request.url.path}") status = 500 try: response = await call_next(request) status = response.status_code logger.debug(f"[Metrics] Request processed with status {status}") except Exception as e: logger.error(f"[Metrics] Exception occurred: {e}", exc_info=True) raise finally: REQUEST_COUNTER.labels(request.method, request.url.path, status).inc() logger.debug(f"[Metrics] Counter incremented for {request.method} {request.url.path} [{status}]") return response @api.get("/health") def return_health(): return Response(status_code=200) @api.get("/suwayomi") def handle_suwayomi( request: Request, db = Depends(get_db) ): try: logger.info("[App] Suwayomi handler invoked") online_version = find_package_version_with_tag_github("Suwayomi", "tachidesk", "stable") logger.debug(f"[App] Fetched latest Suwayomi version from GitHub: {online_version}") cursor = db.cursor() cursor.execute("SELECT latest_version FROM docker_repositories WHERE app='suwayomi'") local_state = cursor.fetchone() local_version = str(local_state[0]) if local_state and local_state[0] is not None else None logger.debug(f"[App] Comparing versions: local='{local_version}' vs online='{online_version}'") if local_version != online_version: logger.debug("[App] Version mismatch detected. Updating database.") cursor.execute ("UPDATE docker_repositories SET latest_version=%s WHERE app='suwayomi'", (online_version,)) db.commit() logger.info("[App] New Suwayomi version recorded in database") send_notification("New Suwayomi version has been found") logger.debug("[App] Notification sent for Suwayomi update") return Response(status_code=200) logger.debug("[App] No new Suwayomi version found") return Response(status_code=204) except Exception as e: logger.error(f"[App] Error in Suwayomi handler: {e}", exc_info=True) raise HTTPException(status_code=500, detail=str(e)) @api.get("/pihole") def handle_pihole( request: Request, db = Depends(get_db) ): try: logger.info("[App] Pi-hole handler invoked") online_version = find_package_version_with_tag_dockerhub("pihole/pihole", "latest") logger.debug(f"Fetched latest Pi-hole version from Docker Hub: {online_version}") cursor = db.cursor() cursor.execute("SELECT latest_version FROM docker_repositories WHERE app='pihole'") local_state = cursor.fetchone() local_version = str(local_state[0]) if local_state and local_state[0] is not None else None logger.debug(f"[App] Comparing versions: local='{local_version}' vs online='{online_version}'") if local_version != online_version: logger.debug("[App] Version mismatch detected. Updating database.") cursor.execute ("UPDATE docker_repositories SET latest_version=%s WHERE app='pihole'", (online_version,)) db.commit() logger.info("[App] New Pi-hole version recorded in database") send_notification("New Pi-hole version has been found") logger.debug("[App] Notification sent for Pi-hole update") return Response(status_code=200) logger.debug("[App] No new Pi-hole version found") return Response(status_code=204) except Exception as e: logger.error(f"[App] Error in Pi-hole handler: {e}", exc_info=True) raise HTTPException(status_code=500, detail=str(e)) async def start_servers(): logger.info("Starting main and metrics servers") config_main = uvicorn.Config("main:api", host="0.0.0.0", port=5000, log_level=LOG_LEVEL.lower(), log_config=LOGGING_CONFIG) config_metrics = uvicorn.Config("metrics_server:metrics_api", host="0.0.0.0", port=9000, log_level=LOG_LEVEL.lower(), log_config=LOGGING_CONFIG) server_main = uvicorn.Server(config_main) server_metrics = uvicorn.Server(config_metrics) logger.debug("Launching servers concurrently") await asyncio.gather(server_main.serve(), server_metrics.serve()) logger.info("Both servers have stopped") if __name__ == "__main__": asyncio.run(start_servers())