Implement user-specific question numbering and update database schema. Added triggers for automatic question numbering and adjustments upon deletion. Enhanced CRUD operations to manage user_question_number effectively.
This commit is contained in:
350
services/infrastructure/http_server.py
Normal file
350
services/infrastructure/http_server.py
Normal file
@@ -0,0 +1,350 @@
|
||||
"""
|
||||
HTTP сервер для эндпоинтов метрик и health check
|
||||
"""
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from aiohttp import ClientSession, web
|
||||
from aiohttp.web import Request, Response
|
||||
from loguru import logger
|
||||
|
||||
from config.constants import DEFAULT_HTTP_HOST, DEFAULT_HTTP_PORT, APP_VERSION, HTTP_STATUS_OK, HTTP_STATUS_SERVICE_UNAVAILABLE, HTTP_STATUS_INTERNAL_SERVER_ERROR
|
||||
from dependencies import get_database_service
|
||||
from .metrics import get_metrics_service
|
||||
|
||||
|
||||
class HTTPServer:
|
||||
"""HTTP сервер для метрик и health check"""
|
||||
|
||||
def __init__(self, host: str = DEFAULT_HTTP_HOST, port: int = DEFAULT_HTTP_PORT):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.app = web.Application()
|
||||
self.metrics_service = get_metrics_service()
|
||||
self.database_service = get_database_service()
|
||||
self.start_time = time.time()
|
||||
self._setup_routes()
|
||||
|
||||
def _setup_routes(self):
|
||||
"""Настройка маршрутов"""
|
||||
self.app.router.add_get('/metrics', self.metrics_handler)
|
||||
self.app.router.add_get('/health', self.health_handler)
|
||||
self.app.router.add_get('/ready', self.ready_handler)
|
||||
self.app.router.add_get('/status', self.status_handler)
|
||||
self.app.router.add_get('/', self.root_handler)
|
||||
|
||||
async def metrics_handler(self, request: Request) -> Response:
|
||||
"""Обработчик эндпоинта /metrics"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# Получаем метрики
|
||||
metrics_data = self.metrics_service.get_metrics()
|
||||
content_type = self.metrics_service.get_content_type()
|
||||
|
||||
# Записываем метрику HTTP запроса
|
||||
duration = time.time() - start_time
|
||||
self.metrics_service.record_http_request_duration("GET", "/metrics", duration)
|
||||
self.metrics_service.increment_http_requests("GET", "/metrics", HTTP_STATUS_OK)
|
||||
|
||||
return Response(
|
||||
text=metrics_data,
|
||||
content_type=content_type,
|
||||
status=HTTP_STATUS_OK
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in metrics handler: {e}")
|
||||
duration = time.time() - start_time
|
||||
self.metrics_service.record_http_request_duration("GET", "/metrics", duration)
|
||||
self.metrics_service.increment_http_requests("GET", "/metrics", HTTP_STATUS_INTERNAL_SERVER_ERROR)
|
||||
self.metrics_service.increment_errors(type(e).__name__, "metrics_handler")
|
||||
|
||||
return Response(
|
||||
text="Internal Server Error",
|
||||
status=HTTP_STATUS_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
async def health_handler(self, request: Request) -> Response:
|
||||
"""Обработчик эндпоинта /health"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# Проверяем состояние сервисов
|
||||
health_status = {
|
||||
"status": "healthy",
|
||||
"timestamp": time.time(),
|
||||
"uptime": time.time() - self.start_time,
|
||||
"version": APP_VERSION,
|
||||
"services": {}
|
||||
}
|
||||
|
||||
# Проверяем базу данных
|
||||
try:
|
||||
await self.database_service.check_connection()
|
||||
health_status["services"]["database"] = "healthy"
|
||||
except Exception as e:
|
||||
health_status["services"]["database"] = f"unhealthy: {str(e)}"
|
||||
health_status["status"] = "unhealthy"
|
||||
|
||||
# Проверяем метрики
|
||||
try:
|
||||
self.metrics_service.get_metrics()
|
||||
health_status["services"]["metrics"] = "healthy"
|
||||
except Exception as e:
|
||||
health_status["services"]["metrics"] = f"unhealthy: {str(e)}"
|
||||
health_status["status"] = "unhealthy"
|
||||
|
||||
# Определяем HTTP статус
|
||||
http_status = HTTP_STATUS_OK if health_status["status"] == "healthy" else HTTP_STATUS_SERVICE_UNAVAILABLE
|
||||
|
||||
# Записываем метрику HTTP запроса
|
||||
duration = time.time() - start_time
|
||||
self.metrics_service.record_http_request_duration("GET", "/health", duration)
|
||||
self.metrics_service.increment_http_requests("GET", "/health", http_status)
|
||||
|
||||
return Response(
|
||||
json=health_status,
|
||||
status=http_status
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in health handler: {e}")
|
||||
duration = time.time() - start_time
|
||||
self.metrics_service.record_http_request_duration("GET", "/health", duration)
|
||||
self.metrics_service.increment_http_requests("GET", "/health", 500)
|
||||
self.metrics_service.increment_errors(type(e).__name__, "health_handler")
|
||||
|
||||
return Response(
|
||||
json={"status": "error", "message": str(e)},
|
||||
status=HTTP_STATUS_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
async def ready_handler(self, request: Request) -> Response:
|
||||
"""Обработчик эндпоинта /ready (readiness probe)"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# Проверяем готовность сервисов
|
||||
ready_status = {
|
||||
"status": "ready",
|
||||
"timestamp": time.time(),
|
||||
"services": {}
|
||||
}
|
||||
|
||||
# Проверяем базу данных
|
||||
try:
|
||||
await self.database_service.check_connection()
|
||||
ready_status["services"]["database"] = "ready"
|
||||
except Exception as e:
|
||||
ready_status["services"]["database"] = f"not_ready: {str(e)}"
|
||||
ready_status["status"] = "not_ready"
|
||||
|
||||
# Определяем HTTP статус
|
||||
http_status = HTTP_STATUS_OK if ready_status["status"] == "ready" else HTTP_STATUS_SERVICE_UNAVAILABLE
|
||||
|
||||
# Записываем метрику HTTP запроса
|
||||
duration = time.time() - start_time
|
||||
self.metrics_service.record_http_request_duration("GET", "/ready", duration)
|
||||
self.metrics_service.increment_http_requests("GET", "/ready", http_status)
|
||||
|
||||
return Response(
|
||||
json=ready_status,
|
||||
status=http_status
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in ready handler: {e}")
|
||||
duration = time.time() - start_time
|
||||
self.metrics_service.record_http_request_duration("GET", "/ready", duration)
|
||||
self.metrics_service.increment_http_requests("GET", "/ready", 500)
|
||||
self.metrics_service.increment_errors(type(e).__name__, "ready_handler")
|
||||
|
||||
return Response(
|
||||
json={"status": "error", "message": str(e)},
|
||||
status=HTTP_STATUS_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
async def status_handler(self, request: Request) -> Response:
|
||||
"""Handle /status endpoint for process status information."""
|
||||
try:
|
||||
import os
|
||||
import time
|
||||
import psutil
|
||||
|
||||
# Получаем PID текущего процесса
|
||||
current_pid = os.getpid()
|
||||
|
||||
try:
|
||||
# Получаем информацию о процессе
|
||||
process = psutil.Process(current_pid)
|
||||
create_time = process.create_time()
|
||||
uptime_seconds = time.time() - create_time
|
||||
|
||||
# Логируем для диагностики
|
||||
import datetime
|
||||
create_time_str = datetime.datetime.fromtimestamp(create_time).strftime('%Y-%m-%d %H:%M:%S')
|
||||
current_time_str = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
logger.info(f"Process PID {current_pid}: created at {create_time_str}, current time {current_time_str}, uptime {uptime_seconds:.1f}s")
|
||||
|
||||
# Форматируем uptime
|
||||
if uptime_seconds < 60:
|
||||
uptime_str = f"{int(uptime_seconds)}с"
|
||||
elif uptime_seconds < 3600:
|
||||
minutes = int(uptime_seconds // 60)
|
||||
uptime_str = f"{minutes}м"
|
||||
elif uptime_seconds < 86400:
|
||||
hours = int(uptime_seconds // 3600)
|
||||
minutes = int((uptime_seconds % 3600) // 60)
|
||||
uptime_str = f"{hours}ч {minutes}м"
|
||||
else:
|
||||
days = int(uptime_seconds // 86400)
|
||||
hours = int((uptime_seconds % 86400) // 3600)
|
||||
uptime_str = f"{days}д {hours}ч"
|
||||
|
||||
# Проверяем, что процесс активен
|
||||
if process.is_running():
|
||||
status = "running"
|
||||
else:
|
||||
status = "stopped"
|
||||
|
||||
# Формируем ответ
|
||||
response_data = {
|
||||
"status": status,
|
||||
"pid": current_pid,
|
||||
"uptime": uptime_str,
|
||||
"memory_usage_mb": round(process.memory_info().rss / 1024 / 1024, 2),
|
||||
"cpu_percent": process.cpu_percent(),
|
||||
"timestamp": time.time()
|
||||
}
|
||||
|
||||
import json
|
||||
return Response(
|
||||
text=json.dumps(response_data, ensure_ascii=False),
|
||||
content_type='application/json',
|
||||
status=200
|
||||
)
|
||||
|
||||
except psutil.NoSuchProcess:
|
||||
# Процесс не найден
|
||||
response_data = {
|
||||
"status": "not_found",
|
||||
"error": "Process not found",
|
||||
"timestamp": time.time()
|
||||
}
|
||||
|
||||
import json
|
||||
return Response(
|
||||
text=json.dumps(response_data, ensure_ascii=False),
|
||||
content_type='application/json',
|
||||
status=404
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Status check failed: {e}")
|
||||
import json
|
||||
response_data = {
|
||||
"status": "error",
|
||||
"error": str(e),
|
||||
"timestamp": time.time()
|
||||
}
|
||||
|
||||
return Response(
|
||||
text=json.dumps(response_data, ensure_ascii=False),
|
||||
content_type='application/json',
|
||||
status=500
|
||||
)
|
||||
|
||||
async def root_handler(self, request: Request) -> Response:
|
||||
"""Обработчик корневого эндпоинта"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
info = {
|
||||
"service": "AnonBot",
|
||||
"version": APP_VERSION,
|
||||
"endpoints": {
|
||||
"metrics": "/metrics",
|
||||
"health": "/health",
|
||||
"ready": "/ready",
|
||||
"status": "/status"
|
||||
},
|
||||
"uptime": time.time() - self.start_time
|
||||
}
|
||||
|
||||
# Записываем метрику HTTP запроса
|
||||
duration = time.time() - start_time
|
||||
self.metrics_service.record_http_request_duration("GET", "/", duration)
|
||||
self.metrics_service.increment_http_requests("GET", "/", 200)
|
||||
|
||||
return Response(
|
||||
json=info,
|
||||
status=HTTP_STATUS_OK
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in root handler: {e}")
|
||||
duration = time.time() - start_time
|
||||
self.metrics_service.record_http_request_duration("GET", "/", duration)
|
||||
self.metrics_service.increment_http_requests("GET", "/", 500)
|
||||
self.metrics_service.increment_errors(type(e).__name__, "root_handler")
|
||||
|
||||
return Response(
|
||||
json={"error": str(e)},
|
||||
status=HTTP_STATUS_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
async def start(self):
|
||||
"""Запуск HTTP сервера"""
|
||||
try:
|
||||
runner = web.AppRunner(self.app)
|
||||
await runner.setup()
|
||||
site = web.TCPSite(runner, self.host, self.port)
|
||||
await site.start()
|
||||
|
||||
logger.info(f"HTTP server started on {self.host}:{self.port}")
|
||||
logger.info(f"Metrics endpoint: http://{self.host}:{self.port}/metrics")
|
||||
logger.info(f"Health endpoint: http://{self.host}:{self.port}/health")
|
||||
logger.info(f"Ready endpoint: http://{self.host}:{self.port}/ready")
|
||||
logger.info(f"Status endpoint: http://{self.host}:{self.port}/status")
|
||||
|
||||
return runner
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start HTTP server: {e}")
|
||||
self.metrics_service.increment_errors(type(e).__name__, "http_server")
|
||||
raise
|
||||
|
||||
async def stop(self, runner: web.AppRunner):
|
||||
"""Остановка HTTP сервера"""
|
||||
try:
|
||||
await runner.cleanup()
|
||||
logger.info("HTTP server stopped")
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping HTTP server: {e}")
|
||||
self.metrics_service.increment_errors(type(e).__name__, "http_server")
|
||||
|
||||
|
||||
# Глобальный экземпляр HTTP сервера
|
||||
_http_server: Optional[HTTPServer] = None
|
||||
|
||||
|
||||
def get_http_server(host: str = DEFAULT_HTTP_HOST, port: int = DEFAULT_HTTP_PORT) -> HTTPServer:
|
||||
"""Получить экземпляр HTTP сервера"""
|
||||
global _http_server
|
||||
if _http_server is None:
|
||||
_http_server = HTTPServer(host, port)
|
||||
return _http_server
|
||||
|
||||
|
||||
async def start_http_server(host: str = DEFAULT_HTTP_HOST, port: int = DEFAULT_HTTP_PORT) -> web.AppRunner:
|
||||
"""Запустить HTTP сервер"""
|
||||
server = get_http_server(host, port)
|
||||
return await server.start()
|
||||
|
||||
|
||||
async def stop_http_server(runner: web.AppRunner):
|
||||
"""Остановить HTTP сервер"""
|
||||
server = get_http_server()
|
||||
await server.stop(runner)
|
||||
Reference in New Issue
Block a user