uploader-bot/app/fastapi_main.py

539 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Главное 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()