467 lines
16 KiB
Python
467 lines
16 KiB
Python
"""
|
||
Главное 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_start_time = time.time()
|
||
|
||
|
||
@asynccontextmanager
|
||
async def lifespan(app: FastAPI):
|
||
"""
|
||
Управление жизненным циклом приложения
|
||
Startup и shutdown события
|
||
"""
|
||
# Startup
|
||
logger = get_logger(__name__)
|
||
settings = get_settings()
|
||
|
||
try:
|
||
await logger.ainfo("=== FastAPI Application Starting ===")
|
||
|
||
# === 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 ===")
|
||
|
||
# Инициализация базы данных
|
||
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
|
||
|
||
# Инициализация криптографии
|
||
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:
|
||
await db_manager.close()
|
||
await logger.ainfo("Database connections closed")
|
||
except Exception as e:
|
||
await logger.aerror(f"Error closing database: {e}")
|
||
|
||
# Закрытие кэша
|
||
try:
|
||
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()
|
||
|
||
# Создание приложения
|
||
app = FastAPI(
|
||
title="MY Network Uploader Bot - FastAPI",
|
||
description="Decentralized content uploader with web2-client compatibility",
|
||
version="3.0.0",
|
||
docs_url="/docs" if getattr(settings, 'DEBUG', False) else None,
|
||
redoc_url="/redoc" if getattr(settings, 'DEBUG', False) else None,
|
||
lifespan=lifespan
|
||
)
|
||
|
||
# Настройка 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)
|
||
|
||
# Дополнительные обработчики событий
|
||
setup_exception_handlers(app)
|
||
setup_middleware_hooks(app)
|
||
|
||
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)
|
||
process_time = time.time() - start_time
|
||
|
||
# Добавляем заголовки мониторинга
|
||
response.headers["X-Process-Time"] = str(process_time)
|
||
response.headers["X-Request-ID"] = getattr(request.state, 'request_id', 'unknown')
|
||
|
||
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"""
|
||
return JSONResponse(status_code=204, content=None)
|
||
|
||
|
||
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() |