""" Главное FastAPI приложение - полная миграция от Sanic Интеграция всех модулей: middleware, routes, системы """ import asyncio import logging import time from contextlib import asynccontextmanager from typing import Dict, Any from datetime import datetime from fastapi import FastAPI, Request, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.trustedhost import TrustedHostMiddleware from fastapi.responses import JSONResponse from fastapi.exceptions import RequestValidationError from sqlalchemy import text import uvicorn # Импорт компонентов приложения from app.core.config import get_settings from app.core.database import db_manager, get_cache_manager from app.core.logging import configure_logging, get_logger from app.core.crypto import get_ed25519_manager # Импорт middleware from app.api.fastapi_middleware import ( FastAPISecurityMiddleware, FastAPIRateLimitMiddleware, FastAPICryptographicMiddleware, FastAPIRequestContextMiddleware, FastAPIAuthenticationMiddleware ) # Импорт роутеров from app.api.fastapi_auth_routes import router as auth_router from app.api.fastapi_content_routes import router as content_router from app.api.fastapi_storage_routes import router as storage_router from app.api.fastapi_node_routes import router as node_router from app.api.fastapi_system_routes import router as system_router # ДОБАВЛЕНО: импорт дополнительных роутеров из app/api/routes/ from app.api.routes.content_access_routes import router as content_access_router from app.api.routes.node_stats_routes import router as node_stats_router # Глобальные переменные для мониторинга _app_start_time = time.time() @asynccontextmanager async def lifespan(app: FastAPI): """ Управление жизненным циклом приложения Startup и shutdown события """ # Startup logger = get_logger(__name__) settings = get_settings() # Флаг для локальной диагностики без БД/кэша import os skip_db_init = bool(os.getenv("SKIP_DB_INIT", "0") == "1") or bool(getattr(settings, "DEBUG", False)) try: await logger.ainfo("=== FastAPI Application Starting ===", skip_db_init=skip_db_init) # === DEBUG: PostgreSQL DRIVERS VALIDATION === await logger.ainfo("=== DEBUGGING psycopg2 ERROR ===") # Проверка psycopg2 try: import psycopg2 await logger.ainfo("✅ psycopg2 импортирован успешно", version=psycopg2.__version__) except ImportError as e: await logger.aerror("❌ ОШИБКА: psycopg2 не найден", error=str(e)) except Exception as e: await logger.aerror("❌ ОШИБКА: psycopg2 другая ошибка", error=str(e)) # Проверка asyncpg try: import asyncpg await logger.ainfo("✅ asyncpg импортирован успешно", version=asyncpg.__version__) except ImportError as e: await logger.aerror("❌ ОШИБКА: asyncpg не найден", error=str(e)) except Exception as e: await logger.aerror("❌ ОШИБКА: asyncpg другая ошибка", error=str(e)) # Проверка SQLAlchemy драйверов try: from sqlalchemy.dialects import postgresql await logger.ainfo("✅ SQLAlchemy PostgreSQL диалект доступен") except ImportError as e: await logger.aerror("❌ ОШИБКА: SQLAlchemy PostgreSQL диалект недоступен", error=str(e)) # Проверка DATABASE_URL from app.core.config import DATABASE_URL await logger.ainfo("🔧 DATABASE_URL конфигурация", url=DATABASE_URL) await logger.ainfo("=== END DEBUGGING ===") if not skip_db_init: # Инициализация базы данных await logger.ainfo("Initializing database connection...") await db_manager.initialize() # Инициализация кэша await logger.ainfo("Initializing cache manager...") cache_manager = await get_cache_manager() await cache_manager.initialize() if hasattr(cache_manager, 'initialize') else None else: await logger.awarning("Skipping DB/Cache initialization (diagnostic mode)", reason="SKIP_DB_INIT=1 or DEBUG=True") # Инициализация криптографии await logger.ainfo("Initializing cryptographic manager...") crypto_manager = get_ed25519_manager() await logger.ainfo(f"Node ID: {crypto_manager.node_id}") # Проверка готовности системы await logger.ainfo("System initialization completed successfully") yield except Exception as e: await logger.aerror(f"Failed to initialize application: {e}") raise finally: # Shutdown await logger.ainfo("=== FastAPI Application Shutting Down ===") # Закрытие соединений с базой данных try: if not skip_db_init: await db_manager.close() await logger.ainfo("Database connections closed") except Exception as e: await logger.aerror(f"Error closing database: {e}") # Закрытие кэша try: if not skip_db_init: cache_manager = await get_cache_manager() if hasattr(cache_manager, 'close'): await cache_manager.close() await logger.ainfo("Cache connections closed") except Exception as e: await logger.aerror(f"Error closing cache: {e}") await logger.ainfo("Application shutdown completed") def create_fastapi_app() -> FastAPI: """ Создание и конфигурация FastAPI приложения """ settings = get_settings() logger = get_logger(__name__) # Создание приложения app = FastAPI( title="MY Network Uploader Bot - FastAPI", description="Decentralized content uploader with web2-client compatibility", version="3.0.0", docs_url="/docs", redoc_url="/redoc", lifespan=lifespan ) # Диагностические логи конфигурации приложения try: debug_enabled = bool(getattr(settings, 'DEBUG', False)) docs_url = app.docs_url if hasattr(app, "docs_url") else None redoc_url = app.redoc_url if hasattr(app, "redoc_url") else None openapi_url = app.openapi_url if hasattr(app, "openapi_url") else None trusted_hosts = getattr(settings, 'TRUSTED_HOSTS', ["*"]) allowed_origins = getattr(settings, 'ALLOWED_ORIGINS', ["*"]) host = getattr(settings, 'HOST', '0.0.0.0') port = getattr(settings, 'PORT', 8000) # Логи о ключевых настройках asyncio.create_task(logger.ainfo( "FastAPI configuration", DEBUG=debug_enabled, docs_url=docs_url, redoc_url=redoc_url, openapi_url=openapi_url, trusted_hosts=trusted_hosts, allowed_origins=allowed_origins, host=host, port=port )) except Exception: pass # Настройка CORS app.add_middleware( CORSMiddleware, allow_origins=getattr(settings, 'ALLOWED_ORIGINS', ["*"]), allow_credentials=True, allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["*"], ) # Безопасность хостов trusted_hosts = getattr(settings, 'TRUSTED_HOSTS', ["*"]) if trusted_hosts != ["*"]: app.add_middleware(TrustedHostMiddleware, allowed_hosts=trusted_hosts) # Кастомные middleware (в правильном порядке) app.add_middleware(FastAPIRequestContextMiddleware) app.add_middleware(FastAPIAuthenticationMiddleware) app.add_middleware(FastAPICryptographicMiddleware) app.add_middleware(FastAPIRateLimitMiddleware) app.add_middleware(FastAPISecurityMiddleware) # Регистрация роутеров app.include_router(auth_router) app.include_router(content_router) app.include_router(storage_router, prefix="/api/storage") app.include_router(node_router) app.include_router(system_router) # ДОБАВЛЕНО: регистрация роутеров из app/api/routes/ # ВНИМАНИЕ: эти роутеры уже имеют собственные префиксы (prefix=...), поэтому include без доп. prefix app.include_router(content_access_router) # /api/content/* app.include_router(node_stats_router) # /api/node/stats/* # Дополнительные обработчики событий setup_exception_handlers(app) setup_middleware_hooks(app) # Диагностика зарегистрированных маршрутов try: routes_info = [] for r in app.router.routes: # У Starlette Route/Router/AAPIRoute разные атрибуты, нормализуем path = getattr(r, "path", getattr(r, "path_format", None)) name = getattr(r, "name", None) methods = sorted(list(getattr(r, "methods", set()) or [])) route_type = type(r).__name__ routes_info.append({ "path": path, "name": name, "methods": methods, "type": route_type }) asyncio.create_task(logger.ainfo("Registered routes snapshot", routes=routes_info)) except Exception: pass return app def setup_exception_handlers(app: FastAPI): """ Настройка обработчиков исключений """ logger = get_logger(__name__) @app.exception_handler(HTTPException) async def http_exception_handler(request: Request, exc: HTTPException): """Обработка HTTP исключений""" await logger.awarning( f"HTTP Exception: {exc.status_code}", path=str(request.url), method=request.method, detail=exc.detail ) return JSONResponse( status_code=exc.status_code, content={ "error": exc.detail, "status_code": exc.status_code, "timestamp": time.time() } ) @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError): """Обработка ошибок валидации""" await logger.awarning( "Validation Error", path=str(request.url), method=request.method, errors=exc.errors() ) return JSONResponse( status_code=422, content={ "error": "Validation failed", "details": exc.errors(), "status_code": 422, "timestamp": time.time() } ) @app.exception_handler(Exception) async def general_exception_handler(request: Request, exc: Exception): """Обработка общих исключений""" await logger.aerror( f"Unhandled exception: {type(exc).__name__}", path=str(request.url), method=request.method, error=str(exc) ) # Увеличиваем счетчик ошибок для мониторинга from app.api.fastapi_system_routes import increment_error_counter await increment_error_counter() return JSONResponse( status_code=500, content={ "error": "Internal server error", "status_code": 500, "timestamp": time.time() } ) def setup_middleware_hooks(app: FastAPI): """ Настройка хуков middleware для мониторинга """ @app.middleware("http") async def monitoring_middleware(request: Request, call_next): """Middleware для мониторинга запросов""" start_time = time.time() # Увеличиваем счетчик запросов from app.api.fastapi_system_routes import increment_request_counter await increment_request_counter() # Проверяем режим обслуживания try: cache_manager = await get_cache_manager() maintenance_mode = await cache_manager.get("maintenance_mode") if maintenance_mode and request.url.path not in ["/api/system/health", "/api/system/live"]: return JSONResponse( status_code=503, content={ "error": "Service temporarily unavailable", "message": maintenance_mode.get("message", "System is under maintenance"), "status_code": 503 } ) except Exception: pass # Продолжаем работу если кэш недоступен # Выполняем запрос try: response = await call_next(request) # ВАЖНО: не менять тело ответа после установки заголовков Content-Length # Добавляем только безопасные заголовки process_time = time.time() - start_time try: response.headers["X-Process-Time"] = str(process_time) response.headers["X-Request-ID"] = getattr(request.state, 'request_id', 'unknown') except Exception: # Никогда не трогаем тело/поток, если ответ уже начал стримиться pass return response except Exception as e: # Логируем ошибку и увеличиваем счетчик logger = get_logger(__name__) await logger.aerror( f"Request processing failed: {e}", path=str(request.url), method=request.method ) from app.api.fastapi_system_routes import increment_error_counter await increment_error_counter() raise # Создание экземпляра приложения app = create_fastapi_app() # Дополнительные корневые эндпоинты для совместимости @app.get("/health") async def simple_health_check(): """ Простой health check endpoint для совместимости со скриптами Дублирует функциональность /api/system/health """ try: # Проверяем подключение к базе данных db_status = "healthy" try: async with db_manager.get_session() as session: await session.execute(text("SELECT 1")) except Exception: db_status = "unhealthy" # Проверяем кэш cache_status = "healthy" try: cache_manager = await get_cache_manager() await cache_manager.set("health_check", "ok", ttl=10) except Exception: cache_status = "unhealthy" # Определяем общий статус overall_status = "healthy" if db_status == "unhealthy" or cache_status == "unhealthy": overall_status = "unhealthy" health_data = { "status": overall_status, "timestamp": datetime.utcnow().isoformat(), "database": db_status, "cache": cache_status, "uptime_seconds": int(time.time() - _app_start_time) } # Возвращаем статус с соответствующим HTTP кодом status_code = 200 if overall_status == "healthy" else 503 return JSONResponse( content=health_data, status_code=status_code ) except Exception as e: logger = get_logger(__name__) await logger.aerror("Simple health check failed", error=str(e)) return JSONResponse( content={ "status": "unhealthy", "error": "Health check system failure", "timestamp": datetime.utcnow().isoformat() }, status_code=503 ) @app.get("/") async def root(): """Корневой эндпоинт""" return { "service": "MY Network Uploader Bot", "version": "3.0.0", "framework": "FastAPI", "status": "running", "uptime_seconds": int(time.time() - _app_start_time), "api_docs": "/docs", "health_check": "/health" } @app.get("/api") async def api_info(): """Информация об API""" crypto_manager = get_ed25519_manager() return { "api_version": "v1", "service": "uploader-bot", "network": "MY Network v3.0", "node_id": crypto_manager.node_id, "capabilities": [ "content_upload", "content_sync", "decentralized_filtering", "ed25519_signatures", "web2_client_api" ], "endpoints": { "authentication": "/api/v1/auth/*", "content": "/api/v1/content/*", "storage": "/api/storage/*", "node_communication": "/api/node/*", "system": "/api/system/*" } } # Совместимость со старыми Sanic роутами @app.get("/api/v1/ping") async def legacy_ping(): """Legacy ping endpoint для совместимости""" return { "status": "ok", "timestamp": time.time(), "framework": "FastAPI" } @app.get("/favicon.ico") async def favicon(): """Заглушка для favicon (без тела ответа)""" from fastapi.responses import Response # Возвращаем пустой ответ 204 без тела, чтобы избежать несоответствия Content-Length return Response(status_code=204) def run_server(): """ Запуск сервера для разработки """ settings = get_settings() # Настройка логирования configure_logging() # Конфигурация uvicorn config = { "app": "app.fastapi_main:app", "host": getattr(settings, 'HOST', '0.0.0.0'), "port": getattr(settings, 'PORT', 8000), "reload": getattr(settings, 'DEBUG', False), "log_level": "info" if not getattr(settings, 'DEBUG', False) else "debug", "access_log": True, "server_header": False, "date_header": False } print(f"🚀 Starting FastAPI server on {config['host']}:{config['port']}") print(f"📚 API documentation: http://{config['host']}:{config['port']}/docs") print(f"🔍 Health check: http://{config['host']}:{config['port']}/api/system/health") uvicorn.run(**config) if __name__ == "__main__": run_server()