Files
Hermes Agent 4a1d949e41 feat: 完整 Squid Web Manager 平台 - P0-P3 全功能
完整 Web 管理平台,涵盖:

P0 生产加固:
- 会话超时(空闲+绝对) + CSRF 防护 + 登录失败限流
- HTTPS/TLS 证书管理(自签生成/上传/删除/过期提醒)
- SSL Bump (HTTPS 透明代理) 可视化配置
- 配置文件 diff 预览 + 二次确认才写入
- 服务控制二次确认 + 操作期间按钮锁定

P1 运维能力:
- 日志轮转管理(logrotate) + 日志状态监控
- 告警规则(5xx/命中率/磁盘/客户端流量)
- 多 Squid 实例管理 + 一键切换
- squidclient mgr 实时性能指标
- 流量异常检测(突增/大文件/高频小包/新客户端)

P2 日志分析增强:
- 日志持久化到 SQLite + 历史时间范围查询
- 导出 CSV/JSON(日志/KPI/异常/审计)
- GeoIP 地图(Leaflet + ip-api.com 离线可选 GeoLite2)
- 每客户端 24h 趋势图
- 自定义 Squid logformat 解析器

P3 体验锦上添花:
- 暗/亮主题切换(cookie 持久化)
- WebSSH 终端(xterm.js + paramiko)
- 完整审计日志 + 用户管理

技术栈: Flask 3.0 + SQLAlchemy 2.0 + SQLite + Chart.js + Leaflet
总代码量: ~16500 行 (15 Python 模块 + 34 模板 + CSS)
路由数: 73
2026-07-15 10:19:21 +08:00

224 lines
11 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}告警规则 - Squid Manager{% endblock %}
{% block content %}
<div class="page-header">
<h1>🚨 告警规则</h1>
<div class="page-actions">
<form method="post" action="{{ url_for('alerts_evaluate') }}" style="display:inline;">
<input type="hidden" name="_csrf" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-primary btn-small">⚡ 立即评估</button>
</form>
<a href="{{ url_for('alerts_history') }}" class="btn btn-secondary btn-small">📜 触发历史</a>
<button onclick="document.getElementById('add-form').style.display='block'" class="btn btn-primary btn-small"> 添加规则</button>
</div>
</div>
<p class="meta-text">
基于 access.log 解析指标与磁盘使用情况,按阈值触发告警。
同一规则在 cooldown 时间窗内只记录一次,避免告警风暴。
严重度: <span class="badge badge-blue">info</span>
<span class="badge badge-yellow">warning</span>
<span class="badge badge-red">critical</span>
</p>
{# active alerts banner #}
{% set active = [] %}
{% for status in current_status %}
{% if status.triggered %}{% set _ = active.append(status) %}{% endif %}
{% endfor %}
{% if active %}
<div class="card" style="border-left: 4px solid #ef4444;">
<h3 class="card-title">⚠️ 当前活跃告警 ({{ active | length }})</h3>
<ul class="rule-messages">
{% for s in active %}
<li>
<span class="badge badge-{% if s.severity == 'critical' %}red{% elif s.severity == 'warning' %}yellow{% else %}blue{% endif %}">{{ s.severity }}</span>
{{ s.message }}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{# add rule form #}
<div class="card" id="add-form" style="display:none;">
<h3 class="card-title">添加新规则</h3>
<form method="post" action="{{ url_for('alerts_add') }}" class="inline-form">
<input type="hidden" name="_csrf" value="{{ csrf_token() }}">
<div class="filter-grid">
<div class="form-group" style="grid-column: span 2;">
<label>规则名称 <span class="hint">(简短描述,便于审计追溯)</span></label>
<input type="text" name="name" class="form-input" required placeholder="例如: 高 5xx 错误率">
</div>
<div class="form-group">
<label>指标</label>
<select name="metric" class="form-input" required>
{% for m, desc in metrics %}
<option value="{{ m }}">{{ m }} — {{ desc }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label>条件</label>
<select name="operator" class="form-input" required>
{% for op, sym in operators %}
<option value="{{ op }}">{{ sym }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label>阈值</label>
<input type="number" name="threshold" class="form-input" step="0.0001" required value="0.05">
</div>
<div class="form-group">
<label>窗口 (分钟)</label>
<input type="number" name="window_minutes" class="form-input" min="1" max="1440" value="5">
</div>
<div class="form-group">
<label>冷却 (分钟)</label>
<input type="number" name="cooldown_minutes" class="form-input" min="1" max="10080" value="30">
</div>
<div class="form-group">
<label>严重度</label>
<select name="severity" class="form-input">
<option value="info">info</option>
<option value="warning" selected>warning</option>
<option value="critical">critical</option>
</select>
</div>
<div class="form-group">
<label>启用</label>
<select name="enabled" class="form-input">
<option value="1" selected></option>
<option value="0"></option>
</select>
</div>
<div class="form-group" style="grid-column: span 2;">
<label>Webhook URL <span class="hint">(可选。留空仅记审计,非空则 POST JSON)</span></label>
<input type="text" name="notify_webhook" class="form-input" placeholder="https://hooks.example.com/squid">
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">添加</button>
<button type="button" onclick="document.getElementById('add-form').style.display='none'" class="btn btn-secondary">取消</button>
</div>
</form>
</div>
<div class="card">
<h3 class="card-title">规则列表</h3>
<p class="card-desc">共 {{ rules | length }} 条规则。当前值是最近一次评估的快照。
勾选/取消勾选"启用"列后点击"💾 保存所有规则"即可生效。</p>
<form method="post" action="{{ url_for('alerts_save') }}">
<input type="hidden" name="_csrf" value="{{ csrf_token() }}">
<div class="table-scroll">
<table class="data-table">
<thead>
<tr>
<th style="width:40px;">#</th>
<th>名称</th>
<th style="width:130px;">指标</th>
<th style="width:80px;">条件</th>
<th style="width:110px;">阈值</th>
<th style="width:70px;">窗口</th>
<th style="width:80px;">冷却</th>
<th style="width:90px;">严重度</th>
<th style="width:160px;">当前值</th>
<th style="width:140px;">状态</th>
<th style="width:80px;">启用</th>
<th style="width:80px;">删除</th>
</tr>
</thead>
<tbody>
{% for r in rules %}
{% set s = status_map.get(loop.index0) %}
<tr>
<td>{{ loop.index }}<input type="hidden" name="name[]" value="{{ r.name }}"></td>
<td>
{{ r.name }}
{% if r.notify_webhook %}<br><span class="meta-text">🔗 webhook</span>{% endif %}
</td>
<td>
<select name="metric[]" class="form-input">
{% for m, desc in metrics %}
<option value="{{ m }}" {% if r.metric == m %}selected{% endif %}>{{ m }}</option>
{% endfor %}
</select>
</td>
<td>
<select name="operator[]" class="form-input">
{% for op, sym in operators %}
<option value="{{ op }}" {% if r.operator == op %}selected{% endif %}>{{ sym }}</option>
{% endfor %}
</select>
</td>
<td><input type="number" step="0.0001" name="threshold[]" value="{{ r.threshold }}" class="form-input"></td>
<td><input type="number" name="window_minutes[]" value="{{ r.window_minutes }}" min="1" class="form-input"></td>
<td><input type="number" name="cooldown_minutes[]" value="{{ r.cooldown_minutes }}" min="1" class="form-input"></td>
<td>
<select name="severity[]" class="form-input">
{% for sv in severities %}
<option value="{{ sv }}" {% if r.severity == sv %}selected{% endif %}>{{ sv }}</option>
{% endfor %}
</select>
</td>
<td>
{% if s %}
<code>{{ '%.4f' % s.value }}</code>
{% else %}
<span class="meta-text"></span>
{% endif %}
</td>
<td>
{% if not r.enabled %}
<span class="badge badge-gray">已禁用</span>
{% elif s %}
{% if s.triggered %}
<span class="badge badge-red">触发</span>
{% elif s.in_cooldown %}
<span class="badge badge-yellow">冷却</span>
{% else %}
<span class="badge badge-green">正常</span>
{% endif %}
{% else %}
<span class="badge badge-gray">未评估</span>
{% endif %}
</td>
<td>
<input type="checkbox" name="enabled[]" value="1" {% if r.enabled %}checked{% endif %}>
</td>
<td>
<button type="button" class="btn btn-danger btn-tiny" onclick="this.closest('tr').remove()">删除</button>
</td>
</tr>
{% endfor %}
{% if not rules %}
<tr><td colspan="12" class="empty-row">暂无规则。点击上方"添加规则"按钮开始,或点击 "立即评估" 先运行默认规则。</td></tr>
{% endif %}
</tbody>
</table>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">💾 保存所有规则</button>
</div>
</form>
</div>
<div class="card">
<h3 class="card-title">📖 规则说明</h3>
<ul>
<li><strong>触发逻辑</strong>: 计算当前指标值,与阈值按指定运算符 ( &gt; &lt; &ge; &le; ) 比较,满足则记录并 (可选) POST webhook。</li>
<li><strong>冷却 (cooldown)</strong>: 同一规则两次触发之间的最小间隔,避免告警风暴。</li>
<li><strong>窗口 (window_minutes)</strong>: 用于按窗口归一化的指标 (如 <code>client_bytes_per_min</code>);其他指标仅用于备注。</li>
<li><strong>评估方式</strong>: 点击"立即评估"使用当前 access.log 快照立即跑一遍;要持续监控,配合系统的 cron 调用 <code>/alerts/evaluate</code> 接口即可。</li>
<li><strong>触发历史</strong>: 持久化记录会写入 <code>audit_log</code>,可在"<a href="{{ url_for('alerts_history') }}">触发历史</a>"查看 (取最近 200 条)。</li>
</ul>
</div>
<style>
.rule-messages { list-style: none; padding: 0; margin: 0; }
.rule-messages li { padding: 6px 0; border-bottom: 1px dashed rgba(148, 163, 184, 0.18); }
.rule-messages li:last-child { border-bottom: none; }
</style>
{% endblock %}