hy-app-monitor/monitor/host_metrics.py
2026-04-22 14:39:40 +08:00

165 lines
5.6 KiB
Python

from __future__ import annotations
import json
from typing import Any
from .cache import TTLCache
from .config import HOST_METRIC_CACHE_TTL_SECONDS, TAT_CPU_SAMPLE_SECONDS, TAT_METRIC_TIMEOUT_SECONDS, utc_now
from .tencent_tat import run_shell_command
# 主机指标单独缓存,避免页面频繁刷新时不断调用 TAT。
HOST_METRIC_CACHE = TTLCache[dict[str, dict[str, Any]]](HOST_METRIC_CACHE_TTL_SECONDS)
def build_metrics_script() -> str:
# 远端脚本只依赖 Python 标准库,避免每台机器额外装 agent。
return f"""set -euo pipefail
python3 - <<'PY'
import json
import os
import shutil
import time
# 读取 /proc/stat 的 CPU 统计值。
def read_cpu():
with open('/proc/stat', 'r', encoding='utf-8') as handle:
parts = handle.readline().split()[1:8]
values = [int(item) for item in parts]
idle = values[3] + values[4]
total = sum(values)
return idle, total
# 第一次采样。
idle_1, total_1 = read_cpu()
# 等待一个很短的采样窗口。
time.sleep({TAT_CPU_SAMPLE_SECONDS})
# 第二次采样。
idle_2, total_2 = read_cpu()
# 计算 CPU 使用率。
total_diff = max(total_2 - total_1, 1)
idle_diff = max(idle_2 - idle_1, 0)
cpu_percent = round((1 - idle_diff / total_diff) * 100, 2)
# 读取内存信息。
memory_info = {{}}
with open('/proc/meminfo', 'r', encoding='utf-8') as handle:
for line in handle:
key, value = line.split(':', 1)
memory_info[key] = int(value.strip().split()[0])
# 计算内存用量。
memory_total_gb = round(memory_info.get('MemTotal', 0) / 1024 / 1024, 2)
memory_available_gb = round(memory_info.get('MemAvailable', 0) / 1024 / 1024, 2)
memory_used_gb = round(max(memory_total_gb - memory_available_gb, 0), 2)
memory_percent = round((memory_used_gb / memory_total_gb) * 100, 2) if memory_total_gb else 0.0
# 读取根分区磁盘占用。
disk = shutil.disk_usage('/')
disk_total_gb = round(disk.total / 1024 / 1024 / 1024, 2)
disk_used_gb = round(disk.used / 1024 / 1024 / 1024, 2)
disk_percent = round((disk.used / disk.total) * 100, 2) if disk.total else 0.0
# 通过 /proc 估算当前进程数。
process_count = len([name for name in os.listdir('/proc') if name.isdigit()])
# 输出统一 JSON。
print(json.dumps({{
'ok': True,
'hostname': os.uname().nodename,
'cpuPercent': cpu_percent,
'memoryPercent': memory_percent,
'memoryUsedGb': memory_used_gb,
'memoryTotalGb': memory_total_gb,
'diskPercent': disk_percent,
'diskUsedGb': disk_used_gb,
'diskTotalGb': disk_total_gb,
'processCount': process_count
}}))
PY
"""
def metrics_error_payload(error: str, instance_id: str = "") -> dict[str, Any]:
# 返回统一的主机指标错误结构。
return {
"ok": False,
"error": error,
"updatedAt": utc_now(),
"instanceId": instance_id,
}
def collect_host_metrics(hosts: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
# 建立实例 ID 到 host 的映射。
host_by_instance = {host["instanceId"]: host for host in hosts if host.get("instanceId")}
# 去重后的实例 ID 列表。
instance_ids = list(host_by_instance.keys())
# 没有实例 ID 时直接返回错误。
if not instance_ids:
return {
host["host"]: metrics_error_payload("missing instanceId in config/targets.json")
for host in hosts
}
# 通过 TAT 批量执行主机指标采集脚本。
task_results = run_shell_command(
instance_ids,
build_metrics_script(),
command_name="hy-app-monitor-host-metrics",
timeout_seconds=TAT_METRIC_TIMEOUT_SECONDS,
)
# 这里按 host name 回填最终结果。
payload_by_host: dict[str, dict[str, Any]] = {}
# 逐台主机解析结果。
for host in hosts:
# 读取实例 ID。
instance_id = host.get("instanceId", "")
# 读取该实例的远端执行结果。
task = task_results.get(instance_id, {})
# 读取执行状态。
status = task.get("status", "")
# 读取远端输出。
output = task.get("output", "")
# 成功时尝试解析 JSON。
if status == "SUCCESS":
try:
# 把 JSON 输出反序列化成 Python 结构。
payload = json.loads(output or "{}")
# 补齐统一字段。
payload["updatedAt"] = utc_now()
payload["instanceId"] = instance_id
payload_by_host[host["host"]] = payload
continue
except Exception as exc: # noqa: BLE001
payload_by_host[host["host"]] = metrics_error_payload(f"metrics parse failed: {exc}", instance_id)
continue
# 失败时保留错误文本或状态码。
payload_by_host[host["host"]] = metrics_error_payload(output.strip() or status or "metrics failed", instance_id)
# 返回按 host 索引的主机指标。
return payload_by_host
def get_host_metrics(hosts: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
# 定义真正的采集构建函数。
def builder() -> dict[str, dict[str, Any]]:
try:
# 优先返回真实采集结果。
return collect_host_metrics(hosts)
except Exception as exc: # noqa: BLE001
# 任意异常都降级成错误占位,避免拖垮整个页面。
return {
host["host"]: metrics_error_payload(f"metrics collection failed: {exc}", host.get("instanceId", ""))
for host in hosts
}
# 通过缓存保护 TAT 调用。
return HOST_METRIC_CACHE.get_or_build(builder)