feat: hostwatch 模块 — 从 FOMS 单体拆分出的独立模块
This commit is contained in:
commit
6f440bf11b
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
py3/
|
||||
models/mysql.ddl.sql
|
||||
.DS_Store
|
||||
37
README.md
Normal file
37
README.md
Normal 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
1
__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .init import load_hostwatch # noqa: F401
|
||||
18
build.sh
Normal file
18
build.sh
Normal 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
1
hostwatch/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# hostwatch 模块 — 日志监控与分析 + 主机指标
|
||||
318
hostwatch/init.py
Normal file
318
hostwatch/init.py
Normal 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
|
||||
15
json/hostwatch_events_list.json
Normal file
15
json/hostwatch_events_list.json
Normal 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": "已关闭"}]}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
json/hostwatch_logs_list.json
Normal file
15
json/hostwatch_logs_list.json
Normal 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"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
json/hostwatch_rules_list.json
Normal file
16
json/hostwatch_rules_list.json
Normal 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": "禁用"}]}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
models/hostwatch_events.json
Normal file
30
models/hostwatch_events.json
Normal 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'"}
|
||||
]
|
||||
}
|
||||
27
models/hostwatch_logs.json
Normal file
27
models/hostwatch_logs.json
Normal 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'"}
|
||||
]
|
||||
}
|
||||
32
models/hostwatch_metrics.json
Normal file
32
models/hostwatch_metrics.json
Normal 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"]}
|
||||
]
|
||||
}
|
||||
31
models/hostwatch_rules.json
Normal file
31
models/hostwatch_rules.json
Normal 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
16
pyproject.toml
Normal 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*"]
|
||||
3
wwwroot/api/acknowledge_event.dspy
Normal file
3
wwwroot/api/acknowledge_event.dspy
Normal file
@ -0,0 +1,3 @@
|
||||
# 确认/关闭告警事件
|
||||
result = await acknowledge_event(request, params_kw)
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
3
wwwroot/api/agent_push_logs.dspy
Normal file
3
wwwroot/api/agent_push_logs.dspy
Normal file
@ -0,0 +1,3 @@
|
||||
# Agent 推送日志(入库 + 归类 + 规则匹配)
|
||||
result = await agent_push_logs(request, params_kw)
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
3
wwwroot/api/agent_report_metrics.dspy
Normal file
3
wwwroot/api/agent_report_metrics.dspy
Normal file
@ -0,0 +1,3 @@
|
||||
# Agent 上报系统指标
|
||||
result = await agent_report_metrics(request, params_kw)
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
3
wwwroot/api/analyze_logs.dspy
Normal file
3
wwwroot/api/analyze_logs.dspy
Normal file
@ -0,0 +1,3 @@
|
||||
# 日志分析(聚合统计)
|
||||
result = await analyze_logs(request, params_kw)
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
3
wwwroot/api/node_logs.dspy
Normal file
3
wwwroot/api/node_logs.dspy
Normal file
@ -0,0 +1,3 @@
|
||||
# 节点最近日志
|
||||
result = await get_node_logs(request, params_kw)
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
3
wwwroot/api/node_metrics_history.dspy
Normal file
3
wwwroot/api/node_metrics_history.dspy
Normal file
@ -0,0 +1,3 @@
|
||||
# 节点历史指标(图表用)
|
||||
result = await get_node_metrics_history(request, params_kw)
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
Loading…
x
Reference in New Issue
Block a user