feat: hostwatch 模块 — 从 FOMS 单体拆分出的独立模块

This commit is contained in:
yumoqing 2026-08-26 15:28:18 +08:00
commit 6f440bf11b
20 changed files with 584 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
__pycache__/
*.pyc
*.pyo
*.egg-info/
dist/
build/
py3/
models/mysql.ddl.sql
.DS_Store

37
README.md Normal file
View File

@ -0,0 +1,37 @@
# hostwatch — 日志监控与分析 + 主机指标模块
FOMS 平台拆出的独立模块之一。松耦合:表 `node_id` 仅存字符串 ID不建外键
采集由 Agent 完成,管理中心负责入库、规则匹配、告警、归类分析。
## 职责
| 功能 | 表 | 说明 |
|---|---|---|
| 节点日志 | `hostwatch_logs` | 日志采集,含 category 归类字段,原 `node_logs` |
| 节点指标 | `hostwatch_metrics` | CPU/内存/磁盘/负载/网络,原 `node_metrics` |
| 监控规则 | `hostwatch_rules` | 关键词/正则 + 级别 + 频次阈值 + 通知 |
| 告警事件 | `hostwatch_events` | 命中事件,含状态/通知标记 |
## 端点
| 端点 | 说明 |
|---|---|
| `/hostwatch/api/agent_report_metrics.dspy` | Agent 上报指标 |
| `/hostwatch/api/agent_push_logs.dspy` | Agent 推送日志(入库+归类+规则匹配) |
| `/hostwatch/api/node_metrics_history.dspy` | 指标历史(图表) |
| `/hostwatch/api/node_logs.dspy` | 节点日志查询 |
| `/hostwatch/api/analyze_logs.dspy` | 日志分析(级别/归类聚合 + Top 错误) |
| `/hostwatch/api/acknowledge_event.dspy` | 确认/关闭告警 |
## 日志分析
- 归类关键词规则database/network/disk/system/security/application/general
- 告警:规则匹配命中 → 窗口内累计频次 → 超阈值生成 open 事件
- 分析:按级别/归类聚合统计 + Top 错误信息
## 加载
```python
from hostwatch.init import load_hostwatch
load_hostwatch()
```

1
__init__.py Normal file
View File

@ -0,0 +1 @@
from .init import load_hostwatch # noqa: F401

18
build.sh Normal file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env bash
# hostwatch 模块 build生成 DDL + CRUD 页面xls2ui
set -e
cdir=$(pwd)
if [ -d models ]; then
json2ddl mysql models/ > "$cdir/models/mysql.ddl.sql" 2>/dev/null || echo " skip ddl"
fi
if [ -d json ]; then
cd "$cdir/json"
for f in *.json; do
echo " xls2ui $f"
xls2ui -m ../models -o ../wwwroot hostwatch "$f" 2>/dev/null || echo " skipped"
done
fi
echo "=== hostwatch Build Complete ==="

1
hostwatch/__init__.py Normal file
View File

@ -0,0 +1 @@
# hostwatch 模块 — 日志监控与分析 + 主机指标

318
hostwatch/init.py Normal file
View File

@ -0,0 +1,318 @@
"""hostwatch 模块 — 日志监控 + 日志分析 + 主机指标。
独立模块松耦合设计
· node_id 仅存字符串 ID不建外键引用 foms 核心的 nodes
· import foms 核心代码可独立部署独立测试
· 采集由 Agent 完成管理中心负责入库规则匹配告警归类分析
hostwatch_logs 节点日志 node_logs新增 category 归类字段
hostwatch_metrics 节点系统指标 node_metrics
hostwatch_rules 日志监控规则关键词/正则 + 级别 + 频次阈值 + 通知
hostwatch_events 命中事件告警含状态与通知标记
"""
import json
import re
from appPublic.uniqueID import getID
from appPublic.timeUtils import curDateString
from sqlor.dbpools import DBPools, get_sor_context
from ahserver.serverenv import ServerEnv
DBNAME = 'foms'
def _get(params_kw, key, default=''):
return params_kw.get(key, default) if hasattr(params_kw, 'get') else default
def _ns(params_kw):
ns = dict(params_kw) if hasattr(params_kw, 'items') else {}
for k in list(ns.keys()):
if k.endswith('_text'):
ns.pop(k)
return ns
# ═══════════════════════════════════════════
# 日志归类(关键词 → category
# ═══════════════════════════════════════════
CATEGORY_PATTERNS = [
('database', re.compile(r'(mysql|postgres|redis|slave|replication|sql|deadlock|连接|复制)', re.I)),
('network', re.compile(r'(network|socket|tcp|udp|connect|timeout|dns|网络|连接超时)', re.I)),
('disk', re.compile(r'(disk|filesystem|fsck|i/o error|磁盘|空间不足|no space)', re.I)),
('system', re.compile(r'(kernel|systemd|oom|out of memory|panic|reboot|内核|内存不足)', re.I)),
('security', re.compile(r'(auth|login|password|permission|denied|unauthorized|sudo|登录|认证)', re.I)),
('application', re.compile(r'(error|exception|traceback|crash|失败|错误)', re.I)),
]
def classify_log(message):
"""按关键词归类日志,返回 category 名(无匹配 → 'general'"""
for cat, pat in CATEGORY_PATTERNS:
if pat.search(message or ''):
return cat
return 'general'
# ═══════════════════════════════════════════
# 指标上报
# ═══════════════════════════════════════════
async def agent_report_metrics(request, params_kw):
"""Agent 上报系统指标CPU/内存/磁盘/负载/网络)"""
env = ServerEnv()
node_id = _get(params_kw, 'node_id')
metrics = _get(params_kw, 'metrics', {})
if isinstance(metrics, str):
metrics = json.loads(metrics)
async with get_sor_context(env, DBNAME) as sor:
await sor.C('hostwatch_metrics', {
'id': getID(),
'node_id': node_id,
'cpu_percent': metrics.get('cpu_percent'),
'mem_percent': metrics.get('mem_percent'),
'mem_used_mb': metrics.get('mem_used_mb'),
'mem_total_mb': metrics.get('mem_total_mb'),
'disk_percent': metrics.get('disk_percent'),
'disk_used_gb': metrics.get('disk_used_gb'),
'disk_path': metrics.get('disk_path', '/'),
'load_1m': metrics.get('load_1m'),
'load_5m': metrics.get('load_5m'),
'load_15m': metrics.get('load_15m'),
'net_rx_mb': metrics.get('net_rx_mb'),
'net_tx_mb': metrics.get('net_tx_mb'),
'process_count': metrics.get('process_count'),
'collected_at': curDateString(),
})
return {'status': 'ok'}
# ═══════════════════════════════════════════
# 日志推送(入库 + 归类 + 规则匹配告警)
# ═══════════════════════════════════════════
async def _match_rules(sor, node_id, log_entry):
"""对单条日志跑启用的规则,命中则累计事件;超频次阈值生成 open 事件。"""
rules = await sor.R('hostwatch_rules', {'enabled': '1'})
for rule in (rules or []):
pattern = getattr(rule, 'pattern', '') or ''
match_type = getattr(rule, 'match_type', 'keyword') or 'keyword'
msg = log_entry.get('message', '')
if not pattern:
continue
hit = False
if match_type == 'regex':
try:
hit = bool(re.search(pattern, msg, re.I))
except re.error:
hit = False
else: # keyword
hit = pattern.lower() in (msg or '').lower()
if not hit:
continue
# 限定日志级别
rule_level = getattr(rule, 'log_level', '') or ''
if rule_level and log_entry.get('level', '') != rule_level:
continue
# 命中:累计窗口内事件
rid = getattr(rule, 'id', '')
threshold = int(getattr(rule, 'threshold_count', '1') or 1)
events = await sor.sqlExe(
"SELECT id, match_count, first_seen_at FROM hostwatch_events "
"WHERE rule_id=${r}$ AND node_id=${n}$ AND status='open' "
"ORDER BY last_seen_at DESC LIMIT 1", {'r': rid, 'n': node_id})
now = curDateString()
if events:
ev = events[0]
await sor.U('hostwatch_events', {
'id': getattr(ev, 'id', ''),
'match_count': int(getattr(ev, 'match_count', 0)) + 1,
'last_seen_at': now,
})
else:
await sor.C('hostwatch_events', {
'id': getID(),
'rule_id': rid,
'node_id': node_id,
'severity': getattr(rule, 'severity', 'warning'),
'match_count': 1,
'sample_message': msg[:2000],
'status': 'open',
'notified': '0',
'first_seen_at': now,
'last_seen_at': now,
})
async def agent_push_logs(request, params_kw):
"""Agent 推送日志:入库 + 归类 + 规则匹配"""
env = ServerEnv()
node_id = _get(params_kw, 'node_id')
logs = _get(params_kw, 'logs', [])
if isinstance(logs, str):
logs = json.loads(logs)
if not logs:
return {'status': 'ok', 'count': 0}
async with get_sor_context(env, DBNAME) as sor:
count = 0
for entry in logs[:100]:
msg = (entry.get('message') or '')[:2000]
await sor.C('hostwatch_logs', {
'id': getID(),
'node_id': node_id,
'log_source': entry.get('source', 'syslog'),
'log_level': entry.get('level', 'INFO'),
'message': msg,
'category': classify_log(msg),
'logged_at': entry.get('time', curDateString()),
})
await _match_rules(sor, node_id, {
'level': entry.get('level', 'INFO'),
'message': msg,
})
count += 1
return {'status': 'ok', 'count': count}
# ═══════════════════════════════════════════
# 查询
# ═══════════════════════════════════════════
async def get_node_metrics_history(request, params_kw):
"""节点历史指标(图表用)"""
env = ServerEnv()
node_id = _get(params_kw, 'node_id')
hours = int(_get(params_kw, 'hours', 1) or 1)
async with get_sor_context(env, DBNAME) as sor:
rows = await sor.sqlExe(
'SELECT cpu_percent, mem_percent, disk_percent, load_1m, collected_at '
'FROM hostwatch_metrics WHERE node_id=${nid}$ AND collected_at > DATE_SUB(NOW(), INTERVAL ${h}$ HOUR) '
'ORDER BY collected_at ASC LIMIT 200',
{'nid': node_id, 'h': hours})
return {'metrics': [{
'cpu': getattr(r, 'cpu_percent', 0),
'mem': getattr(r, 'mem_percent', 0),
'disk': getattr(r, 'disk_percent', 0),
'load': getattr(r, 'load_1m', 0),
'time': str(getattr(r, 'collected_at', '')),
} for r in (rows or [])]}
async def get_node_logs(request, params_kw):
"""节点最近日志(按级别/归类过滤)"""
env = ServerEnv()
node_id = _get(params_kw, 'node_id')
level = _get(params_kw, 'level')
category = _get(params_kw, 'category')
sql = "SELECT log_source, log_level, category, message, logged_at FROM hostwatch_logs WHERE node_id=${nid}$"
p = {'nid': node_id}
if level:
sql += " AND log_level=${lv}$"
p['lv'] = level
if category:
sql += " AND category=${ct}$"
p['ct'] = category
sql += " ORDER BY logged_at DESC LIMIT 200"
async with get_sor_context(env, DBNAME) as sor:
rows = await sor.sqlExe(sql, p)
return {'logs': [{
'source': getattr(r, 'log_source', ''),
'level': getattr(r, 'log_level', ''),
'category': getattr(r, 'category', ''),
'message': getattr(r, 'message', ''),
'time': str(getattr(r, 'logged_at', '')),
} for r in (rows or [])]}
async def analyze_logs(request, params_kw):
"""日志分析:按时间窗口聚合统计(各级别/各归类计数 + Top 错误信息)"""
env = ServerEnv()
node_id = _get(params_kw, 'node_id')
hours = int(_get(params_kw, 'hours', 24) or 24)
async with get_sor_context(env, DBNAME) as sor:
levels = await sor.sqlExe(
"SELECT log_level, COUNT(*) c FROM hostwatch_logs "
"WHERE node_id=${nid}$ AND logged_at > DATE_SUB(NOW(), INTERVAL ${h}$ HOUR) "
"GROUP BY log_level", {'nid': node_id, 'h': hours})
cats = await sor.sqlExe(
"SELECT category, COUNT(*) c FROM hostwatch_logs "
"WHERE node_id=${nid}$ AND logged_at > DATE_SUB(NOW(), INTERVAL ${h}$ HOUR) "
"GROUP BY category ORDER BY c DESC", {'nid': node_id, 'h': hours})
errors = await sor.sqlExe(
"SELECT message, COUNT(*) c FROM hostwatch_logs "
"WHERE node_id=${nid}$ AND log_level IN ('ERROR','WARN','CRITICAL') "
"AND logged_at > DATE_SUB(NOW(), INTERVAL ${h}$ HOUR) "
"GROUP BY message ORDER BY c DESC LIMIT 10", {'nid': node_id, 'h': hours})
return {
'by_level': {getattr(r, 'log_level', ''): getattr(r, 'c', 0) for r in (levels or [])},
'by_category': [{'category': getattr(r, 'category', ''), 'count': getattr(r, 'c', 0)} for r in (cats or [])],
'top_errors': [{'message': getattr(r, 'message', '')[:200], 'count': getattr(r, 'c', 0)} for r in (errors or [])],
'window_hours': hours,
}
# ═══════════════════════════════════════════
# 监控规则 CRUD
# ═══════════════════════════════════════════
async def create_rule(request, params_kw):
env = ServerEnv()
async with get_sor_context(env, DBNAME) as sor:
ns = _ns(params_kw)
ns['id'] = getID()
ns['created_at'] = curDateString()
ns['updated_at'] = curDateString()
await sor.C('hostwatch_rules', ns)
return {'widgettype': 'Message', 'options': {'title': '成功', 'message': '监控规则创建成功', 'type': 'success'}}
async def update_rule(request, params_kw):
env = ServerEnv()
async with get_sor_context(env, DBNAME) as sor:
ns = _ns(params_kw)
ns['updated_at'] = curDateString()
await sor.U('hostwatch_rules', ns)
return {'widgettype': 'Message', 'options': {'title': '成功', 'message': '监控规则更新成功', 'type': 'success'}}
async def delete_rule(request, params_kw):
env = ServerEnv()
async with get_sor_context(env, DBNAME) as sor:
await sor.D('hostwatch_rules', {'id': _get(params_kw, 'id')})
return {'widgettype': 'Message', 'options': {'title': '成功', 'message': '监控规则已删除', 'type': 'success'}}
async def acknowledge_event(request, params_kw):
"""确认/关闭告警事件"""
env = ServerEnv()
async with get_sor_context(env, DBNAME) as sor:
await sor.U('hostwatch_events', {
'id': _get(params_kw, 'id'),
'status': _get(params_kw, 'status', 'closed'),
})
return {'status': 'ok'}
# ═══════════════════════════════════════════
# 注册
# ═══════════════════════════════════════════
def load_hostwatch():
env = ServerEnv()
# 指标 / 日志
env.agent_report_metrics = agent_report_metrics
env.agent_push_logs = agent_push_logs
env.get_node_metrics_history = get_node_metrics_history
env.get_node_logs = get_node_logs
env.analyze_logs = analyze_logs
# 规则 CRUD
env.create_rule = create_rule
env.create_hostwatch_rules = create_rule
env.update_rule = update_rule
env.update_hostwatch_rules = update_rule
env.delete_rule = delete_rule
env.delete_hostwatch_rules = delete_rule
# 事件
env.acknowledge_event = acknowledge_event
return True

View File

@ -0,0 +1,15 @@
{
"tblname": "hostwatch_events",
"alias": "hostwatch_events_list",
"title": "日志监控事件",
"params": {
"sortby": ["last_seen_at desc"],
"browserfields": {
"exclouded": ["id", "sample_message"],
"alters": {
"severity": {"uitype": "code", "data": [{"value": "info", "text": "提示"}, {"value": "warning", "text": "警告"}, {"value": "critical", "text": "严重"}]},
"status": {"uitype": "code", "data": [{"value": "open", "text": "未处理"}, {"value": "closed", "text": "已关闭"}]}
}
}
}
}

View File

@ -0,0 +1,15 @@
{
"tblname": "hostwatch_logs",
"alias": "hostwatch_logs_list",
"title": "节点日志",
"params": {
"sortby": ["logged_at desc"],
"browserfields": {
"exclouded": ["id", "message"],
"alters": {
"log_level": {"uitype": "code"},
"category": {"uitype": "code"}
}
}
}
}

View File

@ -0,0 +1,16 @@
{
"tblname": "hostwatch_rules",
"alias": "hostwatch_rules_list",
"title": "日志监控规则",
"params": {
"sortby": ["updated_at desc"],
"browserfields": {
"exclouded": ["id"],
"alters": {
"match_type": {"uitype": "code", "data": [{"value": "keyword", "text": "关键词"}, {"value": "regex", "text": "正则"}]},
"severity": {"uitype": "code", "data": [{"value": "info", "text": "提示"}, {"value": "warning", "text": "警告"}, {"value": "critical", "text": "严重"}]},
"enabled": {"uitype": "code", "data": [{"value": "1", "text": "启用"}, {"value": "0", "text": "禁用"}]}
}
}
}
}

View File

@ -0,0 +1,30 @@
{
"summary": [
{
"name": "hostwatch_events",
"title": "日志监控事件",
"primary": ["id"],
"catelog": "entity"
}
],
"fields": [
{"name": "id", "title": "事件ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "rule_id", "title": "触发规则", "type": "str", "length": 32},
{"name": "node_id", "title": "节点ID", "type": "str", "length": 32},
{"name": "severity", "title": "告警级别", "type": "str", "length": 16},
{"name": "match_count", "title": "命中次数", "type": "int", "default": "1"},
{"name": "sample_message", "title": "样例日志", "type": "text"},
{"name": "status", "title": "状态", "type": "str", "length": 16, "nullable": "no", "default": "open"},
{"name": "notified", "title": "已通知", "type": "str", "length": 1, "default": "0"},
{"name": "first_seen_at", "title": "首次命中", "type": "datetime"},
{"name": "last_seen_at", "title": "最近命中", "type": "datetime"}
],
"indexes": [
{"name": "idx_hw_event_node", "idxtype": "index", "idxfields": ["node_id"]},
{"name": "idx_hw_event_status", "idxtype": "index", "idxfields": ["status"]}
],
"codes": [
{"field": "severity", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='severity'"},
{"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='event_status'"}
]
}

View File

@ -0,0 +1,27 @@
{
"summary": [
{
"name": "hostwatch_logs",
"title": "节点日志",
"primary": ["id"],
"catelog": "entity"
}
],
"fields": [
{"name": "id", "title": "日志ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "node_id", "title": "节点ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "log_source", "title": "日志来源", "type": "str", "length": 64},
{"name": "log_level", "title": "日志级别", "type": "str", "length": 16},
{"name": "message", "title": "日志内容", "type": "text"},
{"name": "category", "title": "归类", "type": "str", "length": 64},
{"name": "logged_at", "title": "日志时间", "type": "datetime", "nullable": "no"}
],
"indexes": [
{"name": "idx_hw_log_node", "idxtype": "index", "idxfields": ["node_id"]},
{"name": "idx_hw_log_time", "idxtype": "index", "idxfields": ["logged_at"]},
{"name": "idx_hw_log_level", "idxtype": "index", "idxfields": ["log_level"]}
],
"codes": [
{"field": "log_level", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='log_level'"}
]
}

View File

@ -0,0 +1,32 @@
{
"summary": [
{
"name": "hostwatch_metrics",
"title": "节点系统指标",
"primary": ["id"],
"catelog": "indication"
}
],
"fields": [
{"name": "id", "title": "记录ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "node_id", "title": "节点ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "cpu_percent", "title": "CPU使用率(%)", "type": "float", "length": 5, "dec": 1},
{"name": "mem_percent", "title": "内存使用率(%)", "type": "float", "length": 5, "dec": 1},
{"name": "mem_used_mb", "title": "已用内存(MB)", "type": "int"},
{"name": "mem_total_mb", "title": "总内存(MB)", "type": "int"},
{"name": "disk_percent", "title": "磁盘使用率(%)", "type": "float", "length": 5, "dec": 1},
{"name": "disk_used_gb", "title": "已用磁盘(GB)", "type": "float", "length": 8, "dec": 1},
{"name": "disk_path", "title": "磁盘挂载点", "type": "str", "length": 128},
{"name": "load_1m", "title": "1分钟负载", "type": "float", "length": 5, "dec": 2},
{"name": "load_5m", "title": "5分钟负载", "type": "float", "length": 5, "dec": 2},
{"name": "load_15m", "title": "15分钟负载", "type": "float", "length": 5, "dec": 2},
{"name": "net_rx_mb", "title": "网络接收(MB)", "type": "float", "length": 10, "dec": 1},
{"name": "net_tx_mb", "title": "网络发送(MB)", "type": "float", "length": 10, "dec": 1},
{"name": "process_count", "title": "进程数", "type": "int"},
{"name": "collected_at", "title": "采集时间", "type": "datetime", "nullable": "no"}
],
"indexes": [
{"name": "idx_hw_metric_node", "idxtype": "index", "idxfields": ["node_id"]},
{"name": "idx_hw_metric_time", "idxtype": "index", "idxfields": ["collected_at"]}
]
}

View File

@ -0,0 +1,31 @@
{
"summary": [
{
"name": "hostwatch_rules",
"title": "日志监控规则",
"primary": ["id"],
"catelog": "entity"
}
],
"fields": [
{"name": "id", "title": "规则ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "rule_name", "title": "规则名称", "type": "str", "length": 128, "nullable": "no"},
{"name": "match_type", "title": "匹配类型", "type": "str", "length": 16, "nullable": "no", "default": "keyword"},
{"name": "pattern", "title": "匹配模式(关键词/正则)", "type": "str", "length": 512, "nullable": "no"},
{"name": "log_level", "title": "限定日志级别", "type": "str", "length": 16},
{"name": "severity", "title": "告警级别", "type": "str", "length": 16, "nullable": "no", "default": "warning"},
{"name": "threshold_count", "title": "频次阈值(次/窗口)", "type": "int", "default": "1"},
{"name": "window_sec", "title": "窗口(秒)", "type": "int", "default": "60"},
{"name": "notify", "title": "通知方式", "type": "str", "length": 128},
{"name": "enabled", "title": "启用", "type": "str", "length": 1, "nullable": "no", "default": "1"},
{"name": "created_at", "title": "创建时间", "type": "datetime"},
{"name": "updated_at", "title": "更新时间", "type": "datetime"}
],
"indexes": [
{"name": "idx_hw_rule_name", "idxtype": "index", "idxfields": ["rule_name"]}
],
"codes": [
{"field": "match_type", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='match_type'"},
{"field": "severity", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='severity'"}
]
}

16
pyproject.toml Normal file
View File

@ -0,0 +1,16 @@
[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "hostwatch"
version = "1.0.0"
description = "日志监控与分析 + 主机指标模块 — 日志采集、规则告警、归类分析、指标存储查询"
requires-python = ">=3.8"
dependencies = [
"sqlor",
]
[tool.setuptools.packages.find]
where = ["."]
include = ["hostwatch*"]

View File

@ -0,0 +1,3 @@
# 确认/关闭告警事件
result = await acknowledge_event(request, params_kw)
return json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,3 @@
# Agent 推送日志(入库 + 归类 + 规则匹配)
result = await agent_push_logs(request, params_kw)
return json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,3 @@
# Agent 上报系统指标
result = await agent_report_metrics(request, params_kw)
return json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,3 @@
# 日志分析(聚合统计)
result = await analyze_logs(request, params_kw)
return json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,3 @@
# 节点最近日志
result = await get_node_logs(request, params_kw)
return json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,3 @@
# 节点历史指标(图表用)
result = await get_node_metrics_history(request, params_kw)
return json.dumps(result, ensure_ascii=False)