中文+README
All checks were successful
CI / lint-and-build (push) Successful in 8s

This commit is contained in:
2026-06-14 12:08:27 +08:00
parent f9b32208e4
commit 26a5f99587
16 changed files with 376 additions and 80 deletions

View File

@@ -0,0 +1,3 @@
# web 包Web 服务模块
# 基于 Flask 提供可视化界面与接口,用于展示和管理系统调用监控数据
# 显式声明是常规包,不包含任何代码,仅用于组织模块结构

View File

@@ -1,4 +1,10 @@
"""Flask web layer for the syscall monitor."""
"""系统调用监控器的 Flask Web 服务层。
提供两个页面与一个 JSON 接口:
- 实时监控页:展示各系统调用的累计调用次数
- 配置页:增删需要追踪的系统调用名称
- /api/counts返回当前计数供前端轮询
"""
import json
import threading
@@ -9,12 +15,16 @@ from flask import Flask, jsonify, redirect, render_template, request, url_for
from collector.syscall_tracer import get_tracer
# 项目根目录与配置文件路径
BASE_DIR = Path(__file__).resolve().parent.parent
CONFIG_PATH = BASE_DIR / "config" / "monitors.json"
# 配置读写锁:防止多个请求并发修改 monitors.json 时互相覆盖
_config_lock = threading.Lock()
def _read_config() -> dict:
"""读取监控配置;文件不存在时返回空配置。"""
if not CONFIG_PATH.exists():
return {"syscalls": []}
with CONFIG_PATH.open("r", encoding="utf-8") as f:
@@ -22,33 +32,42 @@ def _read_config() -> dict:
def _write_config(data: dict) -> None:
"""原子化写入配置:先写临时文件再替换,避免写入过程中被读到半个文件。"""
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
tmp = CONFIG_PATH.with_suffix(".json.tmp")
with tmp.open("w", encoding="utf-8") as f:
# ensure_ascii=False 保留中文注释字段的原始字符
json.dump(data, f, indent=2, ensure_ascii=False)
tmp.replace(CONFIG_PATH)
def create_app() -> Flask:
"""Flask 应用工厂:创建并返回配置好的 Flask 实例。"""
app = Flask(__name__, template_folder="templates", static_folder="static")
# 初始化系统调用追踪器,由其负责挂载 eBPF 探针并维护计数
tracer = get_tracer(CONFIG_PATH)
@app.get("/")
def index():
# 实时监控首页:渲染当前监控项列表,前端会定时轮询 /api/counts 刷新数据
cfg = _read_config()
return render_template("index.html", syscalls=cfg.get("syscalls", []))
@app.get("/api/counts")
def api_counts():
# 返回各系统调用的累计调用次数dictsyscall 名 -> 次数)
return jsonify(tracer.get_counts())
@app.route("/config", methods=["GET", "POST"])
def config_page():
# 加锁保护配置文件的读-改-写流程,避免并发请求导致更新丢失
with _config_lock:
cfg = _read_config()
syscalls = list(cfg.get("syscalls", []))
if request.method == "POST":
# 处理「添加」/「移除」表单:根据 action 字段决定操作
action = request.form.get("action", "")
name = (request.form.get("name") or "").strip()
if action == "add" and name and name not in syscalls:
@@ -57,6 +76,7 @@ def create_app() -> Flask:
syscalls.remove(name)
cfg["syscalls"] = syscalls
_write_config(cfg)
# PRG 模式:提交后重定向到配置页,避免刷新重复提交
return redirect(url_for("config_page"))
return render_template("config.html", syscalls=syscalls)

View File

@@ -7,7 +7,7 @@
</head>
<body>
<header>
<h1>Syscall Monitor</h1>
<h1>系统调用监控器</h1>
<nav>
<a href="{{ url_for('index') }}">实时监控</a>
<a href="{{ url_for('config_page') }}">配置</a>
@@ -18,14 +18,14 @@
<h3 style="margin-top:0">添加监控项</h3>
<form method="post" action="{{ url_for('config_page') }}">
<input type="hidden" name="action" value="add">
<input type="text" name="name" placeholder="syscall 名称,例如 openat" required>
<input type="text" name="name" placeholder="系统调用名称例如 openat" required>
<button type="submit" class="primary">添加</button>
</form>
<p class="muted">名称需为内核 syscall 名(可参考 <code>man syscalls</code>)。新增后会立即出现在监控页面。</p>
<p class="muted">名称需为内核系统调用名(可参考 <code>man syscalls</code>。新增后会立即出现在监控页面。</p>
</div>
<div class="card">
<h3 style="margin-top:0">当前监控项 ({{ syscalls|length }})</h3>
<h3 style="margin-top:0">当前监控项(共 {{ syscalls|length }} 项)</h3>
{% if syscalls %}
<table>
<thead><tr><th>名称</th><th style="text-align:right">操作</th></tr></thead>

View File

@@ -2,12 +2,12 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>Syscall Monitor</title>
<title>系统调用监控器</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<header>
<h1>Syscall Monitor</h1>
<h1>系统调用监控器</h1>
<nav>
<a href="{{ url_for('index') }}">实时监控</a>
<a href="{{ url_for('config_page') }}">配置</a>
@@ -15,10 +15,10 @@
</header>
<main>
<div class="card">
<p class="muted">当前监控 {{ syscalls|length }} 项,每秒刷新。计数自 Agent 启动起累计。</p>
<p class="muted">当前监控 {{ syscalls|length }} 项每秒刷新一次。计数自采集器启动起累计。</p>
{% if syscalls %}
<table>
<thead><tr><th>System Call</th><th style="text-align:right">调用次数</th></tr></thead>
<thead><tr><th>系统调用</th><th style="text-align:right">调用次数</th></tr></thead>
<tbody id="rows">
{% for name in syscalls %}
<tr><td><span class="tag">{{ name }}</span></td><td class="count" data-name="{{ name }}"></td></tr>
@@ -26,7 +26,7 @@
</tbody>
</table>
{% else %}
<p>尚未配置监控项,前往 <a href="{{ url_for('config_page') }}">配置页面</a> 添加。</p>
<p>尚未配置监控项前往 <a href="{{ url_for('config_page') }}">配置页面</a> 添加。</p>
{% endif %}
</div>
</main>
@@ -39,7 +39,9 @@
const n = td.dataset.name;
td.textContent = (data[n] ?? 0).toLocaleString();
});
} catch (e) { /* keep last value on error */ }
} catch (e) {
console.error('刷新失败', e);
}
}
refresh();
setInterval(refresh, 1000);