Files
course-agent-od/backend/scripts/service_monitor.py
T
pengxiao ddbb79b9f6 Initial commit: 国土空间规划课程智能体 v1.0
单容器 Docker 架构的国土空间规划课程智能问答系统,集成 FastAPI 后端与 Next.js 前端。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 09:40:18 +08:00

373 lines
13 KiB
Python

#!/usr/bin/env python3
"""
服务监控脚本 - 监控多服务架构的健康状态
"""
import os
import sys
import time
import json
import logging
import requests
from datetime import datetime, timedelta
from typing import Dict, List, Optional
from pathlib import Path
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('service_monitor.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
class ServiceMonitor:
"""服务监控器"""
def __init__(self):
self.services = {
"backend": {
"name": "FastAPI后端服务",
"url": "http://localhost:8000/health",
"timeout": 5,
"required": True
},
"frontend": {
"name": "Next.js前端服务",
"url": "http://localhost:8001",
"timeout": 5,
"required": True
},
"database": {
"name": "PostgreSQL数据库",
"type": "internal", # 内部服务,通过后端检查
"required": True
},
"vectorizer": {
"name": "向量化服务",
"type": "process", # 进程检查
"process_name": "vectorizer_service.py",
"required": False
}
}
self.check_interval = 30 # 检查间隔(秒)
self.alert_threshold = 3 # 报警阈值(连续失败次数)
self.status_history = []
self.max_history = 100
def check_service(self, service_id: str, service_config: Dict) -> Dict:
"""检查单个服务"""
service_status = {
"service_id": service_id,
"name": service_config["name"],
"timestamp": datetime.now().isoformat(),
"status": "unknown",
"response_time": None,
"error": None
}
try:
start_time = time.time()
if service_config.get("type") == "internal":
# 内部服务,通过后端检查
backend_status = self._check_backend_service(service_id)
service_status.update(backend_status)
elif service_config.get("type") == "process":
# 进程检查
process_status = self._check_process_service(service_config)
service_status.update(process_status)
else:
# HTTP服务检查
response = requests.get(
service_config["url"],
timeout=service_config.get("timeout", 5)
)
response_time = (time.time() - start_time) * 1000 # 毫秒
if response.status_code == 200:
service_status["status"] = "healthy"
service_status["response_time"] = response_time
# 解析响应内容
try:
data = response.json()
service_status["details"] = data
except:
pass
else:
service_status["status"] = "unhealthy"
service_status["error"] = f"HTTP {response.status_code}"
except requests.exceptions.Timeout:
service_status["status"] = "timeout"
service_status["error"] = "请求超时"
except requests.exceptions.ConnectionError:
service_status["status"] = "unreachable"
service_status["error"] = "连接失败"
except Exception as e:
service_status["status"] = "error"
service_status["error"] = str(e)
return service_status
def _check_backend_service(self, service_id: str) -> Dict:
"""检查后端内部服务"""
try:
# 通过后端API检查数据库状态
response = requests.get(
"http://localhost:8000/system/status",
timeout=5
)
if response.status_code == 200:
data = response.json()
if service_id == "database":
db_status = data.get("database", {})
db_type = db_status.get("type", "unknown")
return {
"status": "healthy" if db_type != "unknown" else "unhealthy",
"details": db_status
}
return {"status": "unhealthy", "error": "后端检查失败"}
except Exception as e:
return {"status": "error", "error": str(e)}
def _check_process_service(self, service_config: Dict) -> Dict:
"""检查进程服务"""
process_name = service_config.get("process_name")
if not process_name:
return {"status": "unknown", "error": "未配置进程名"}
try:
# 检查进程是否存在(Linux/Mac)
if sys.platform != "win32":
import subprocess
result = subprocess.run(
["pgrep", "-f", process_name],
capture_output=True,
text=True
)
if result.returncode == 0:
return {"status": "healthy", "details": {"processes": result.stdout.strip().split()}}
else:
return {"status": "stopped", "error": "进程未运行"}
else:
# Windows平台检查
import psutil
process_count = 0
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
try:
cmdline = proc.info.get('cmdline')
if cmdline and process_name in ' '.join(cmdline):
process_count += 1
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
if process_count > 0:
return {"status": "healthy", "details": {"process_count": process_count}}
else:
return {"status": "stopped", "error": "进程未运行"}
except Exception as e:
return {"status": "error", "error": str(e)}
def check_all_services(self) -> Dict:
"""检查所有服务"""
logger.info("开始检查所有服务...")
results = {}
overall_status = "healthy"
failed_services = []
for service_id, service_config in self.services.items():
status = self.check_service(service_id, service_config)
results[service_id] = status
if status["status"] != "healthy":
if service_config.get("required", False):
overall_status = "unhealthy"
failed_services.append(service_id)
logger.warning(f"{service_config['name']}: {status['status']} - {status.get('error', '')}")
else:
logger.info(f"{service_config['name']}: 健康")
# 保存到历史记录
check_result = {
"timestamp": datetime.now().isoformat(),
"overall_status": overall_status,
"services": results,
"failed_services": failed_services
}
self.status_history.append(check_result)
if len(self.status_history) > self.max_history:
self.status_history = self.status_history[-self.max_history:]
# 保存到文件
self._save_status_history()
logger.info(f"检查完成: 总体状态 - {overall_status}")
return check_result
def _save_status_history(self):
"""保存状态历史"""
try:
history_file = Path("service_status_history.json")
# 只保存最近24小时的数据
cutoff_time = datetime.now() - timedelta(hours=24)
recent_history = [
h for h in self.status_history
if datetime.fromisoformat(h["timestamp"].replace("Z", "+00:00")) > cutoff_time
]
with open(history_file, "w", encoding="utf-8") as f:
json.dump(recent_history, f, ensure_ascii=False, indent=2)
except Exception as e:
logger.error(f"保存状态历史失败: {e}")
def load_status_history(self) -> List[Dict]:
"""加载状态历史"""
try:
history_file = Path("service_status_history.json")
if history_file.exists():
with open(history_file, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
logger.error(f"加载状态历史失败: {e}")
return []
def get_service_stats(self, hours: int = 24) -> Dict:
"""获取服务统计信息"""
cutoff_time = datetime.now() - timedelta(hours=hours)
relevant_history = [
h for h in self.status_history
if datetime.fromisoformat(h["timestamp"].replace("Z", "+00:00")) > cutoff_time
]
if not relevant_history:
return {}
stats = {
"total_checks": len(relevant_history),
"healthy_checks": sum(1 for h in relevant_history if h["overall_status"] == "healthy"),
"uptime_percentage": 0,
"service_stats": {}
}
if stats["total_checks"] > 0:
stats["uptime_percentage"] = (stats["healthy_checks"] / stats["total_checks"]) * 100
# 计算每个服务的统计
for service_id in self.services.keys():
service_checks = []
for check in relevant_history:
if service_id in check["services"]:
service_checks.append(check["services"][service_id])
if service_checks:
healthy_count = sum(1 for s in service_checks if s["status"] == "healthy")
total_count = len(service_checks)
stats["service_stats"][service_id] = {
"name": self.services[service_id]["name"],
"total_checks": total_count,
"healthy_checks": healthy_count,
"availability": (healthy_count / total_count * 100) if total_count > 0 else 0,
"last_status": service_checks[-1]["status"] if service_checks else "unknown"
}
return stats
def run_monitor(self, duration_minutes: Optional[int] = None):
"""运行监控器"""
logger.info("=" * 60)
logger.info("服务监控器启动")
logger.info("=" * 60)
start_time = time.time()
try:
while True:
# 检查是否达到运行时长限制
if duration_minutes:
elapsed_minutes = (time.time() - start_time) / 60
if elapsed_minutes >= duration_minutes:
logger.info(f"达到运行时长限制 ({duration_minutes} 分钟),停止监控")
break
# 执行检查
self.check_all_services()
# 显示统计信息
stats = self.get_service_stats(hours=1)
if stats:
logger.info(f"最近1小时可用性: {stats.get('uptime_percentage', 0):.1f}%")
# 等待下一次检查
logger.info(f"等待 {self.check_interval} 秒...")
time.sleep(self.check_interval)
except KeyboardInterrupt:
logger.info("收到中断信号,停止监控")
except Exception as e:
logger.error(f"监控器运行异常: {e}")
finally:
logger.info("服务监控器停止")
def main():
"""主函数"""
import argparse
parser = argparse.ArgumentParser(description="服务监控工具")
parser.add_argument("action", choices=["check", "monitor", "stats"],
help="执行的操作")
parser.add_argument("--duration", type=int, default=60,
help="监控持续时间(分钟),仅用于monitor模式")
parser.add_argument("--hours", type=int, default=24,
help="统计小时数,用于stats模式")
args = parser.parse_args()
monitor = ServiceMonitor()
if args.action == "check":
# 单次检查
result = monitor.check_all_services()
print(json.dumps(result, ensure_ascii=False, indent=2))
elif args.action == "monitor":
# 持续监控
monitor.run_monitor(args.duration)
elif args.action == "stats":
# 显示统计信息
stats = monitor.get_service_stats(args.hours)
print(json.dumps(stats, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()