""" 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)