539 lines
20 KiB
Python
539 lines
20 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/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() |