471 lines
15 KiB
Python
471 lines
15 KiB
Python
"""
|
||
Enhanced Sanic API application with async support and monitoring
|
||
"""
|
||
import asyncio
|
||
from contextlib import asynccontextmanager
|
||
from datetime import datetime
|
||
from typing import Dict, Any, Optional
|
||
|
||
from sanic import Sanic, Request, HTTPResponse
|
||
from sanic.response import json as json_response
|
||
from sanic.exceptions import SanicException
|
||
import structlog
|
||
|
||
from app.core.config import settings
|
||
from app.core.database import init_database, close_database
|
||
from app.core.logging import get_logger, AsyncContextLogger
|
||
from app.api.middleware import (
|
||
request_middleware,
|
||
response_middleware,
|
||
exception_middleware,
|
||
maintenance_middleware
|
||
)
|
||
|
||
logger = get_logger(__name__)
|
||
|
||
|
||
class EnhancedSanic(Sanic):
|
||
"""Enhanced Sanic application with additional features"""
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
self.ctx.startup_tasks = []
|
||
self.ctx.shutdown_tasks = []
|
||
self.ctx.background_tasks = set()
|
||
|
||
async def startup(self):
|
||
"""Application startup tasks"""
|
||
async with AsyncContextLogger("app_startup"):
|
||
# Initialize database
|
||
await init_database()
|
||
logger.info("Database initialized")
|
||
|
||
# Initialize cache connections
|
||
from app.core.database import get_cache
|
||
cache = await get_cache()
|
||
await cache.redis.ping()
|
||
logger.info("Redis cache initialized")
|
||
|
||
# Initialize ed25519 cryptographic module
|
||
try:
|
||
from app.core.crypto import init_ed25519_manager
|
||
await init_ed25519_manager()
|
||
logger.info("Ed25519 cryptographic module initialized")
|
||
except ImportError:
|
||
logger.warning("Ed25519 module not available")
|
||
except Exception as e:
|
||
logger.error("Failed to initialize ed25519 module", error=str(e))
|
||
|
||
# Run custom startup tasks
|
||
for task in self.ctx.startup_tasks:
|
||
try:
|
||
await task()
|
||
except Exception as e:
|
||
logger.error("Startup task failed", task=task.__name__, error=str(e))
|
||
|
||
logger.info("Application startup completed")
|
||
|
||
async def shutdown(self):
|
||
"""Application shutdown tasks"""
|
||
async with AsyncContextLogger("app_shutdown"):
|
||
# Cancel background tasks
|
||
for task in self.ctx.background_tasks:
|
||
if not task.done():
|
||
task.cancel()
|
||
|
||
# Wait for tasks to complete
|
||
if self.ctx.background_tasks:
|
||
await asyncio.gather(*self.ctx.background_tasks, return_exceptions=True)
|
||
|
||
# Run custom shutdown tasks
|
||
for task in self.ctx.shutdown_tasks:
|
||
try:
|
||
await task()
|
||
except Exception as e:
|
||
logger.error("Shutdown task failed", task=task.__name__, error=str(e))
|
||
|
||
# Close database connections
|
||
await close_database()
|
||
logger.info("Database connections closed")
|
||
|
||
logger.info("Application shutdown completed")
|
||
|
||
def add_startup_task(self, task):
|
||
"""Add startup task"""
|
||
self.ctx.startup_tasks.append(task)
|
||
|
||
def add_shutdown_task(self, task):
|
||
"""Add shutdown task"""
|
||
self.ctx.shutdown_tasks.append(task)
|
||
|
||
def add_background_task(self, coro):
|
||
"""Add background task"""
|
||
task = asyncio.create_task(coro)
|
||
self.ctx.background_tasks.add(task)
|
||
task.add_done_callback(self.ctx.background_tasks.discard)
|
||
return task
|
||
|
||
|
||
# Create Sanic app instance
|
||
app = EnhancedSanic(
|
||
name="my_uploader_bot",
|
||
configure_logging=False # We handle logging ourselves
|
||
)
|
||
|
||
# Configure app settings
|
||
app.config.update({
|
||
"REQUEST_MAX_SIZE": settings.MAX_FILE_SIZE,
|
||
"REQUEST_TIMEOUT": 60,
|
||
"RESPONSE_TIMEOUT": 60,
|
||
"KEEP_ALIVE_TIMEOUT": 5,
|
||
"KEEP_ALIVE": True,
|
||
"ACCESS_LOG": False, # We handle access logging in middleware
|
||
"AUTO_RELOAD": settings.DEBUG,
|
||
"DEBUG": settings.DEBUG,
|
||
})
|
||
|
||
|
||
# Register middleware
|
||
app.register_middleware(maintenance_middleware, "request")
|
||
app.register_middleware(request_middleware, "request")
|
||
app.register_middleware(response_middleware, "response")
|
||
|
||
|
||
# Global exception handler
|
||
@app.exception(Exception)
|
||
async def handle_exception(request: Request, exception: Exception):
|
||
"""Global exception handler"""
|
||
return await exception_middleware(request, exception)
|
||
|
||
|
||
# Health check endpoint
|
||
@app.get("/health")
|
||
async def health_check(request: Request):
|
||
"""Health check endpoint"""
|
||
try:
|
||
# Check database connection
|
||
from app.core.database import db_manager
|
||
async with db_manager.get_session() as session:
|
||
await session.execute("SELECT 1")
|
||
|
||
# Check Redis connection
|
||
from app.core.database import get_cache
|
||
cache = await get_cache()
|
||
await cache.redis.ping()
|
||
|
||
return json_response({
|
||
"status": "healthy",
|
||
"version": settings.PROJECT_VERSION,
|
||
"timestamp": datetime.utcnow().isoformat()
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error("Health check failed", error=str(e))
|
||
return json_response({
|
||
"status": "unhealthy",
|
||
"error": str(e),
|
||
"timestamp": datetime.utcnow().isoformat()
|
||
}, status=503)
|
||
|
||
|
||
# Metrics endpoint (if enabled)
|
||
if settings.METRICS_ENABLED:
|
||
@app.get("/metrics")
|
||
async def metrics_endpoint(request: Request):
|
||
"""Prometheus metrics endpoint"""
|
||
try:
|
||
from prometheus_client import generate_latest, CONTENT_TYPE_LATEST
|
||
metrics_data = generate_latest()
|
||
return HTTPResponse(
|
||
body=metrics_data,
|
||
headers={"Content-Type": CONTENT_TYPE_LATEST},
|
||
status=200
|
||
)
|
||
except Exception as e:
|
||
logger.error("Metrics generation failed", error=str(e))
|
||
return json_response({"error": "Metrics unavailable"}, status=503)
|
||
|
||
|
||
# System info endpoint
|
||
@app.get("/api/system/info")
|
||
async def system_info(request: Request):
|
||
"""System information endpoint"""
|
||
try:
|
||
import psutil
|
||
import sys
|
||
|
||
# Get system metrics
|
||
memory = psutil.virtual_memory()
|
||
disk = psutil.disk_usage('/')
|
||
|
||
info = {
|
||
"application": {
|
||
"name": settings.PROJECT_NAME,
|
||
"version": settings.PROJECT_VERSION,
|
||
"python_version": sys.version,
|
||
"debug": settings.DEBUG,
|
||
},
|
||
"system": {
|
||
"cpu_percent": psutil.cpu_percent(),
|
||
"memory": {
|
||
"total": memory.total,
|
||
"available": memory.available,
|
||
"percent": memory.percent
|
||
},
|
||
"disk": {
|
||
"total": disk.total,
|
||
"free": disk.free,
|
||
"percent": (disk.used / disk.total) * 100
|
||
}
|
||
},
|
||
"services": {
|
||
"database": "connected",
|
||
"redis": "connected",
|
||
"indexer": "running" if settings.INDEXER_ENABLED else "disabled",
|
||
"ton_daemon": "running" if settings.TON_DAEMON_ENABLED else "disabled",
|
||
}
|
||
}
|
||
|
||
return json_response(info)
|
||
|
||
except Exception as e:
|
||
logger.error("System info failed", error=str(e))
|
||
return json_response({"error": "System info unavailable"}, status=500)
|
||
|
||
|
||
# Register API routes
|
||
def register_routes():
|
||
"""Register all API routes"""
|
||
# Import main blueprints
|
||
from app.api.routes.auth_routes import auth_bp
|
||
from app.api.routes.content_routes import content_bp
|
||
from app.api.routes.storage_routes import storage_bp
|
||
from app.api.routes.blockchain_routes import blockchain_bp
|
||
|
||
# Import node communication blueprint
|
||
from app.api.node_communication import node_bp
|
||
|
||
# Импортировать существующие маршруты
|
||
try:
|
||
from app.api.routes._system import bp as system_bp
|
||
except ImportError:
|
||
system_bp = None
|
||
|
||
try:
|
||
from app.api.routes.account import bp as user_bp
|
||
except ImportError:
|
||
user_bp = None
|
||
|
||
# Register main route blueprints
|
||
app.blueprint(auth_bp)
|
||
app.blueprint(content_bp)
|
||
app.blueprint(storage_bp)
|
||
app.blueprint(blockchain_bp)
|
||
app.blueprint(node_bp) # Межузловое общение с ed25519
|
||
|
||
# Register optional blueprints
|
||
if user_bp:
|
||
app.blueprint(user_bp)
|
||
if system_bp:
|
||
app.blueprint(system_bp)
|
||
|
||
# Попробовать добавить MY Network маршруты
|
||
try:
|
||
from app.api.routes import my_network_sanic, my_monitoring_sanic
|
||
|
||
# Создать MY Network blueprint'ы
|
||
app.blueprint(my_network_sanic.bp)
|
||
app.blueprint(my_monitoring_sanic.bp)
|
||
|
||
logger.info("MY Network routes registered")
|
||
|
||
except ImportError as e:
|
||
logger.warning("MY Network routes not available", error=str(e))
|
||
|
||
logger.info("API routes registered")
|
||
|
||
|
||
# Application lifecycle hooks
|
||
@app.before_server_start
|
||
async def before_server_start(app, loop):
|
||
"""Tasks to run before server starts"""
|
||
await app.startup()
|
||
|
||
|
||
@app.after_server_stop
|
||
async def after_server_stop(app, loop):
|
||
"""Tasks to run after server stops"""
|
||
await app.shutdown()
|
||
|
||
|
||
# Background task management
|
||
class BackgroundTaskManager:
|
||
"""Manager for background tasks"""
|
||
|
||
def __init__(self, app: EnhancedSanic):
|
||
self.app = app
|
||
self.tasks: Dict[str, asyncio.Task] = {}
|
||
|
||
async def start_service(self, name: str, service_func, *args, **kwargs):
|
||
"""Start a background service"""
|
||
if name in self.tasks:
|
||
logger.warning("Service already running", service=name)
|
||
return
|
||
|
||
logger.info("Starting background service", service=name)
|
||
task = self.app.add_background_task(service_func(*args, **kwargs))
|
||
self.tasks[name] = task
|
||
|
||
# Add error handling
|
||
task.add_done_callback(lambda t: self._handle_task_completion(name, t))
|
||
|
||
def _handle_task_completion(self, name: str, task: asyncio.Task):
|
||
"""Handle background task completion"""
|
||
if name in self.tasks:
|
||
del self.tasks[name]
|
||
|
||
if task.cancelled():
|
||
logger.info("Background service cancelled", service=name)
|
||
elif task.exception():
|
||
logger.error("Background service failed", service=name, error=str(task.exception()))
|
||
else:
|
||
logger.info("Background service completed", service=name)
|
||
|
||
async def stop_service(self, name: str):
|
||
"""Stop a background service"""
|
||
if name not in self.tasks:
|
||
logger.warning("Service not running", service=name)
|
||
return
|
||
|
||
logger.info("Stopping background service", service=name)
|
||
task = self.tasks[name]
|
||
task.cancel()
|
||
|
||
try:
|
||
await task
|
||
except asyncio.CancelledError:
|
||
pass
|
||
|
||
async def stop_all_services(self):
|
||
"""Stop all background services"""
|
||
for name in list(self.tasks.keys()):
|
||
await self.stop_service(name)
|
||
|
||
def get_service_status(self) -> Dict[str, str]:
|
||
"""Get status of all services"""
|
||
status = {}
|
||
for name, task in self.tasks.items():
|
||
if task.done():
|
||
if task.cancelled():
|
||
status[name] = "cancelled"
|
||
elif task.exception():
|
||
status[name] = "failed"
|
||
else:
|
||
status[name] = "completed"
|
||
else:
|
||
status[name] = "running"
|
||
return status
|
||
|
||
|
||
# Initialize background task manager
|
||
task_manager = BackgroundTaskManager(app)
|
||
app.ctx.task_manager = task_manager
|
||
|
||
|
||
# Service startup functions
|
||
async def start_background_services():
|
||
"""Start all background services"""
|
||
from app.core.background import (
|
||
indexer_service,
|
||
ton_service,
|
||
license_service,
|
||
convert_service,
|
||
uploader_service
|
||
)
|
||
|
||
if settings.INDEXER_ENABLED:
|
||
await task_manager.start_service("indexer", indexer_service.main_fn)
|
||
|
||
if settings.TON_DAEMON_ENABLED:
|
||
await task_manager.start_service("ton_daemon", ton_service.main_fn)
|
||
|
||
if settings.LICENSE_SERVICE_ENABLED:
|
||
await task_manager.start_service("license_service", license_service.main_fn)
|
||
|
||
if settings.CONVERT_SERVICE_ENABLED:
|
||
await task_manager.start_service("convert_service", convert_service.main_fn)
|
||
|
||
# Попробовать запустить MY Network сервис
|
||
try:
|
||
await start_my_network_service()
|
||
except Exception as e:
|
||
logger.warning("MY Network service not started", error=str(e))
|
||
|
||
logger.info("Background services started")
|
||
|
||
|
||
async def start_my_network_service():
|
||
"""Запустить MY Network сервис."""
|
||
try:
|
||
from app.core.my_network.node_service import initialize_my_network, shutdown_my_network
|
||
|
||
# Добавить как фоновую задачу
|
||
async def my_network_task():
|
||
try:
|
||
logger.info("Initializing MY Network service...")
|
||
await initialize_my_network()
|
||
logger.info("MY Network service initialized successfully")
|
||
|
||
# Держать сервис активным
|
||
while True:
|
||
await asyncio.sleep(60) # Проверять каждую минуту
|
||
|
||
except asyncio.CancelledError:
|
||
logger.info("MY Network service shutdown requested")
|
||
await shutdown_my_network()
|
||
raise
|
||
except Exception as e:
|
||
logger.error("MY Network service error", error=str(e))
|
||
await shutdown_my_network()
|
||
raise
|
||
|
||
await task_manager.start_service("my_network", my_network_task)
|
||
logger.info("MY Network service started successfully")
|
||
|
||
except ImportError as e:
|
||
logger.info("MY Network modules not available", error=str(e))
|
||
except Exception as e:
|
||
logger.error("Failed to start MY Network service", error=str(e))
|
||
# Не поднимаем исключение, чтобы не блокировать запуск остального сервера
|
||
|
||
|
||
# Add startup task
|
||
app.add_startup_task(start_background_services)
|
||
app.add_shutdown_task(task_manager.stop_all_services)
|
||
|
||
# Register routes
|
||
register_routes()
|
||
|
||
|
||
# Main application factory
|
||
def create_app() -> EnhancedSanic:
|
||
"""Application factory"""
|
||
return app
|
||
|
||
|
||
# Development server runner
|
||
async def run_dev_server():
|
||
"""Run development server"""
|
||
await app.create_server(
|
||
host="0.0.0.0",
|
||
port=settings.SANIC_PORT,
|
||
debug=settings.DEBUG,
|
||
auto_reload=settings.DEBUG,
|
||
access_log=False
|
||
)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
logger.info("Starting development server")
|
||
asyncio.run(run_dev_server())
|