Add heartbeat monitor

This commit is contained in:
dimitrievgs 2025-11-16 23:49:12 +03:00
parent 064ae9f6bf
commit 7f4c004032
2 changed files with 140 additions and 9 deletions

115
app/heartbeat_monitor.py Normal file
View File

@ -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

30
run.py
View File

@ -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
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")
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 запущен.")
api.run(
debug=True,
port=5000,
host='localhost',
threaded=True # Включаем многопоточность для обработки параллельных запросов
threaded=True
)