From 6f440bf11bf612e78a2311f614f0b553bb4b734c Mon Sep 17 00:00:00 2001 From: yumoqing Date: Wed, 26 Aug 2026 15:28:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20hostwatch=20=E6=A8=A1=E5=9D=97=20?= =?UTF-8?q?=E2=80=94=20=E4=BB=8E=20FOMS=20=E5=8D=95=E4=BD=93=E6=8B=86?= =?UTF-8?q?=E5=88=86=E5=87=BA=E7=9A=84=E7=8B=AC=E7=AB=8B=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 9 + README.md | 37 +++ __init__.py | 1 + build.sh | 18 ++ hostwatch/__init__.py | 1 + hostwatch/init.py | 318 ++++++++++++++++++++++++++ json/hostwatch_events_list.json | 15 ++ json/hostwatch_logs_list.json | 15 ++ json/hostwatch_rules_list.json | 16 ++ models/hostwatch_events.json | 30 +++ models/hostwatch_logs.json | 27 +++ models/hostwatch_metrics.json | 32 +++ models/hostwatch_rules.json | 31 +++ pyproject.toml | 16 ++ wwwroot/api/acknowledge_event.dspy | 3 + wwwroot/api/agent_push_logs.dspy | 3 + wwwroot/api/agent_report_metrics.dspy | 3 + wwwroot/api/analyze_logs.dspy | 3 + wwwroot/api/node_logs.dspy | 3 + wwwroot/api/node_metrics_history.dspy | 3 + 20 files changed, 584 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 __init__.py create mode 100644 build.sh create mode 100644 hostwatch/__init__.py create mode 100644 hostwatch/init.py create mode 100644 json/hostwatch_events_list.json create mode 100644 json/hostwatch_logs_list.json create mode 100644 json/hostwatch_rules_list.json create mode 100644 models/hostwatch_events.json create mode 100644 models/hostwatch_logs.json create mode 100644 models/hostwatch_metrics.json create mode 100644 models/hostwatch_rules.json create mode 100644 pyproject.toml create mode 100644 wwwroot/api/acknowledge_event.dspy create mode 100644 wwwroot/api/agent_push_logs.dspy create mode 100644 wwwroot/api/agent_report_metrics.dspy create mode 100644 wwwroot/api/analyze_logs.dspy create mode 100644 wwwroot/api/node_logs.dspy create mode 100644 wwwroot/api/node_metrics_history.dspy diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d674a41 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +__pycache__/ +*.pyc +*.pyo +*.egg-info/ +dist/ +build/ +py3/ +models/mysql.ddl.sql +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..5785a36 --- /dev/null +++ b/README.md @@ -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() +``` diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..801a39d --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +from .init import load_hostwatch # noqa: F401 diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..8d6f8b2 --- /dev/null +++ b/build.sh @@ -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 ===" diff --git a/hostwatch/__init__.py b/hostwatch/__init__.py new file mode 100644 index 0000000..0b5daf5 --- /dev/null +++ b/hostwatch/__init__.py @@ -0,0 +1 @@ +# hostwatch 模块 — 日志监控与分析 + 主机指标 diff --git a/hostwatch/init.py b/hostwatch/init.py new file mode 100644 index 0000000..10a6523 --- /dev/null +++ b/hostwatch/init.py @@ -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 diff --git a/json/hostwatch_events_list.json b/json/hostwatch_events_list.json new file mode 100644 index 0000000..4ebdb2b --- /dev/null +++ b/json/hostwatch_events_list.json @@ -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": "已关闭"}]} + } + } + } +} diff --git a/json/hostwatch_logs_list.json b/json/hostwatch_logs_list.json new file mode 100644 index 0000000..0a07944 --- /dev/null +++ b/json/hostwatch_logs_list.json @@ -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"} + } + } + } +} diff --git a/json/hostwatch_rules_list.json b/json/hostwatch_rules_list.json new file mode 100644 index 0000000..546a13a --- /dev/null +++ b/json/hostwatch_rules_list.json @@ -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": "禁用"}]} + } + } + } +} diff --git a/models/hostwatch_events.json b/models/hostwatch_events.json new file mode 100644 index 0000000..8ec93ab --- /dev/null +++ b/models/hostwatch_events.json @@ -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'"} + ] +} diff --git a/models/hostwatch_logs.json b/models/hostwatch_logs.json new file mode 100644 index 0000000..46d2da2 --- /dev/null +++ b/models/hostwatch_logs.json @@ -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'"} + ] +} diff --git a/models/hostwatch_metrics.json b/models/hostwatch_metrics.json new file mode 100644 index 0000000..3f88aa5 --- /dev/null +++ b/models/hostwatch_metrics.json @@ -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"]} + ] +} diff --git a/models/hostwatch_rules.json b/models/hostwatch_rules.json new file mode 100644 index 0000000..ad5f885 --- /dev/null +++ b/models/hostwatch_rules.json @@ -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'"} + ] +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fe176c3 --- /dev/null +++ b/pyproject.toml @@ -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*"] diff --git a/wwwroot/api/acknowledge_event.dspy b/wwwroot/api/acknowledge_event.dspy new file mode 100644 index 0000000..4efc3a0 --- /dev/null +++ b/wwwroot/api/acknowledge_event.dspy @@ -0,0 +1,3 @@ +# 确认/关闭告警事件 +result = await acknowledge_event(request, params_kw) +return json.dumps(result, ensure_ascii=False) diff --git a/wwwroot/api/agent_push_logs.dspy b/wwwroot/api/agent_push_logs.dspy new file mode 100644 index 0000000..69b8d51 --- /dev/null +++ b/wwwroot/api/agent_push_logs.dspy @@ -0,0 +1,3 @@ +# Agent 推送日志(入库 + 归类 + 规则匹配) +result = await agent_push_logs(request, params_kw) +return json.dumps(result, ensure_ascii=False) diff --git a/wwwroot/api/agent_report_metrics.dspy b/wwwroot/api/agent_report_metrics.dspy new file mode 100644 index 0000000..58030cc --- /dev/null +++ b/wwwroot/api/agent_report_metrics.dspy @@ -0,0 +1,3 @@ +# Agent 上报系统指标 +result = await agent_report_metrics(request, params_kw) +return json.dumps(result, ensure_ascii=False) diff --git a/wwwroot/api/analyze_logs.dspy b/wwwroot/api/analyze_logs.dspy new file mode 100644 index 0000000..a2a245f --- /dev/null +++ b/wwwroot/api/analyze_logs.dspy @@ -0,0 +1,3 @@ +# 日志分析(聚合统计) +result = await analyze_logs(request, params_kw) +return json.dumps(result, ensure_ascii=False) diff --git a/wwwroot/api/node_logs.dspy b/wwwroot/api/node_logs.dspy new file mode 100644 index 0000000..97d54f6 --- /dev/null +++ b/wwwroot/api/node_logs.dspy @@ -0,0 +1,3 @@ +# 节点最近日志 +result = await get_node_logs(request, params_kw) +return json.dumps(result, ensure_ascii=False) diff --git a/wwwroot/api/node_metrics_history.dspy b/wwwroot/api/node_metrics_history.dspy new file mode 100644 index 0000000..d004c87 --- /dev/null +++ b/wwwroot/api/node_metrics_history.dspy @@ -0,0 +1,3 @@ +# 节点历史指标(图表用) +result = await get_node_metrics_history(request, params_kw) +return json.dumps(result, ensure_ascii=False)