From 7f4c004032d4d1c74b867877faaec3dd5acedb58 Mon Sep 17 00:00:00 2001 From: dimitrievgs Date: Sun, 16 Nov 2025 23:49:12 +0300 Subject: [PATCH] Add heartbeat monitor --- app/heartbeat_monitor.py | 115 +++++++++++++++++++++++++++++++++++++++ run.py | 34 +++++++++--- 2 files changed, 140 insertions(+), 9 deletions(-) create mode 100644 app/heartbeat_monitor.py diff --git a/app/heartbeat_monitor.py b/app/heartbeat_monitor.py new file mode 100644 index 0000000..e993127 --- /dev/null +++ b/app/heartbeat_monitor.py @@ -0,0 +1,115 @@ +# app/heartbeat_monitor.py +import threading +import time +import requests +import datetime +import logging +# import traceback + +# Настройка логирования для HeartbeatMonitor +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +# Добавляем обработчик, если его еще нет (чтобы логи выводились в консоль) +if not logger.handlers: + handler = logging.StreamHandler() + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + handler.setFormatter(formatter) + logger.addHandler(handler) + +class HeartbeatMonitor: + """ + Мониторит доступность заданных внешних эндпоинтов путем периодических HTTP-запросов. + Работает в отдельном фоновом потоке, логируя результаты. + """ + + def __init__(self, interval_minutes: int = 5): + self._interval_seconds = interval_minutes * 60 + self._target_urls = [ + "https://render-service-gsu7.onrender.com/heartbeat", + # Добавьте сюда другие URL для мониторинга, если они появятся + ] + self._running = False + self._thread = None + self._last_check_results = {} + logger.info(f"HeartbeatMonitor инициализирован с интервалом {interval_minutes} минут.") + logger.info(f"Мониторинг URL: {', '.join(self._target_urls)}") + + # Вывод call stack + # stack_trace = "".join(traceback.format_stack()) + # logger.info(f"Call stack при инициализации HeartbeatMonitor:\n{stack_trace}") + + def _run_heartbeat_loop(self): + """ + Основной цикл мониторинга, выполняющийся в отдельном потоке. + """ + while self._running: + logger.info("Начинается выполнение Heartbeat-проверок...") + current_results = {} + overall_healthy = True + + for url in self._target_urls: + start_time = time.time() + try: + response = requests.get(url, timeout=10) # Таймаут 10 секунд + if 200 <= response.status_code < 300: + status = {"healthy": True, "status_code": response.status_code, "message": "OK"} + logger.info(f"✅ Heartbeat {url}: OK (HTTP {response.status_code})") + else: + status = {"healthy": False, "status_code": response.status_code, "message": f"Non-2xx status code"} + logger.warning(f"⚠️ Heartbeat {url}: Ошибка (HTTP {response.status_code})") + overall_healthy = False + except requests.exceptions.RequestException as e: + status = {"healthy": False, "status_code": None, "message": f"Request failed: {e}"} + logger.error(f"❌ Heartbeat {url}: Ошибка запроса: {e}") + overall_healthy = False + except Exception as e: + status = {"healthy": False, "status_code": None, "message": f"Unexpected error: {e}"} + logger.critical(f"🔥 Heartbeat {url}: Неожиданная ошибка: {e}") + overall_healthy = False + finally: + status["timestamp"] = datetime.datetime.now().isoformat() + status["response_time_ms"] = int((time.time() - start_time) * 1000) + current_results[url] = status + + self._last_check_results = current_results + if overall_healthy: + logger.info("✔ Все Heartbeat-проверки успешно пройдены.") + else: + logger.warning("✖ Обнаружены проблемы в Heartbeat-проверках.") + + time.sleep(self._interval_seconds) + + def start(self): + """ + Запускает мониторинг в отдельном потоке. + """ + if not self._running: + self._running = True + # daemon=True позволяет приложению завершиться, даже если этот поток еще работает. + # Python автоматически завершит daemon-потоки при выходе из основной программы. + self._thread = threading.Thread(target=self._run_heartbeat_loop, daemon=True) + self._thread.start() + logger.info("HeartbeatMonitor запущен.") + else: + logger.warning("HeartbeatMonitor уже запущен.") + + def stop(self): + """ + Останавливает поток мониторинга. + """ + if self._running: + logger.info("Остановка HeartbeatMonitor...") + self._running = False + if self._thread and self._thread.is_alive(): + self._thread.join(timeout=5) # Даем потоку 5 секунд на завершение + if self._thread.is_alive(): + logger.warning("HeartbeatMonitor поток не завершился в течение таймаута.") + logger.info("HeartbeatMonitor остановлен.") + else: + logger.warning("HeartbeatMonitor не был запущен.") + + def get_last_check_results(self): + """ + Возвращает результаты последней проверки (для возможного использования внутри приложения). + """ + return self._last_check_results diff --git a/run.py b/run.py index 64c39ff..20da55a 100644 --- a/run.py +++ b/run.py @@ -1,22 +1,38 @@ # ----------------------------------------------- Imports ------------------------------------------------------------ import os import sys +import atexit +import logging + +# ----------------------------------------------- Logging Setup ------------------------------------------------------------ +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) # ----------------------------------------------- Main ------------------------------------------------------------ if __name__ == "__main__": - # Добавляем папку app в Python path sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'app')) from app.api import api + from app.heartbeat_monitor import HeartbeatMonitor + + logger.info("🚀 Запуск LLM Agent Backend сервера...") + logger.info("📍 API будет доступно на: http://localhost:5000/api") + logger.info("📊 Список графов: http://localhost:5000/api/graphs") + logger.info("💬 Чат эндпоинт: http://localhost:5000/api/chat") + logger.info("\n⚠️ Для остановки нажмите Ctrl+C\n") + + # TODO: Есть проблема с двойным запуском, скорее всего, всего приложения + # Если api.debug == True, то проверяем FLASK_RUN_FROM_RELOADER. + # Если api.debug == False, то FLASK_RUN_FROM_RELOADER, скорее всего, не будет установлен, + # и условие `not api.debug` будет True, что приведет к запуску HeartbeatMonitor. + if not api.debug or os.environ.get("FLASK_RUN_FROM_RELOADER") != "true": + heartbeat_monitor = HeartbeatMonitor(interval_minutes=5) + heartbeat_monitor.start() + atexit.register(heartbeat_monitor.stop) + logger.info("HeartbeatMonitor запущен.") - print("🚀 Запуск LLM Agent Backend сервера...") - print("📍 API будет доступно на: http://localhost:5000/api") - print("📊 Список графов: http://localhost:5000/api/graphs") - print("💬 Чат эндпоинт: http://localhost:5000/api/chat") - print("\n⚠️ Для остановки нажмите Ctrl+C\n") - api.run( - debug=True, + debug=True, port=5000, host='localhost', - threaded=True # Включаем многопоточность для обработки параллельных запросов + threaded=True ) \ No newline at end of file