4a1d949e41
完整 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
149 lines
6.0 KiB
HTML
149 lines
6.0 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}日志导入 - Squid Manager{% endblock %}
|
|
{% block head %}
|
|
{% if imported %}
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
{% endif %}
|
|
{% endblock %}
|
|
{% block content %}
|
|
<div class="page-header">
|
|
<h1>📥 日志导入分析</h1>
|
|
</div>
|
|
|
|
{% if not imported %}
|
|
<div class="card">
|
|
<p class="card-desc">将 Squid access.log 内容粘贴到下方文本框,系统将解析并展示完整分析结果。
|
|
适用于离线日志分析或一次性诊断。</p>
|
|
<form method="post">
|
|
<div class="form-group">
|
|
<label>日志内容</label>
|
|
<textarea name="log_text" rows="20" class="form-input monospace"
|
|
placeholder="1783841223.438 2488 172.16.12.232 TCP_MISS_ABORTED/000 0 GET http://169.254.169.254/metadata/instance/compute - HIER_DIRECT/169.254.169.254 -
|
|
1783841225.683 4564 172.16.237.9 TCP_TUNNEL/200 10197 CONNECT default.exp-tas.com:443 - HIER_DIRECT/13.107.5.93 -
|
|
..."></textarea>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="submit" class="btn btn-primary">📊 开始分析</button>
|
|
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary">取消</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
{% else %}
|
|
|
|
<div class="page-header">
|
|
<h2>分析结果</h2>
|
|
<div class="page-actions">
|
|
<span class="meta-text">共解析 {{ entries | length }} 条 / {{ raw_lines | length }} 行</span>
|
|
<a href="{{ url_for('logs_import') }}" class="btn btn-small btn-secondary">📥 重新导入</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="kpi-grid">
|
|
<div class="kpi-card kpi-blue">
|
|
<div class="kpi-label">总请求数</div>
|
|
<div class="kpi-value">{{ stats.total_requests | thousands }}</div>
|
|
</div>
|
|
<div class="kpi-card kpi-green">
|
|
<div class="kpi-label">缓存命中率</div>
|
|
<div class="kpi-value">{{ '%.1f' % (stats.hit_ratio * 100) }}%</div>
|
|
</div>
|
|
<div class="kpi-card kpi-purple">
|
|
<div class="kpi-label">总流量</div>
|
|
<div class="kpi-value">{{ format_bytes(stats.total_bytes) }}</div>
|
|
</div>
|
|
<div class="kpi-card kpi-orange">
|
|
<div class="kpi-label">平均响应</div>
|
|
<div class="kpi-value">{{ format_duration(stats.avg_elapsed_ms) }}</div>
|
|
</div>
|
|
<div class="kpi-card kpi-red">
|
|
<div class="kpi-label">拒绝率</div>
|
|
<div class="kpi-value">{{ '%.2f' % (stats.denied_rate * 100) }}%</div>
|
|
</div>
|
|
<div class="kpi-card kpi-yellow">
|
|
<div class="kpi-label">中断率</div>
|
|
<div class="kpi-value">{{ '%.2f' % (stats.aborted_rate * 100) }}%</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid-2">
|
|
<div class="card chart-card">
|
|
<h3 class="card-title">🏷️ 结果码分布</h3>
|
|
<div class="chart-container"><canvas id="resultChart" height="200"></canvas></div>
|
|
</div>
|
|
<div class="card chart-card">
|
|
<h3 class="card-title">🚀 方法分布</h3>
|
|
<div class="chart-container"><canvas id="methodChart" height="200"></canvas></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid-2">
|
|
<div class="card">
|
|
<h3 class="card-title">👥 Top 客户端</h3>
|
|
<table class="data-table">
|
|
<thead><tr><th>#</th><th>IP</th><th>请求数</th><th>流量</th></tr></thead>
|
|
<tbody>
|
|
{% for c in stats.top_clients[:15] %}
|
|
<tr><td>{{ loop.index }}</td><td>{{ c.client }}</td><td>{{ c.count }}</td><td>{{ format_bytes(c.bytes) }}</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="card">
|
|
<h3 class="card-title">🌍 Top 目标主机</h3>
|
|
<table class="data-table">
|
|
<thead><tr><th>#</th><th>主机</th><th>请求数</th><th>流量</th></tr></thead>
|
|
<tbody>
|
|
{% for h in stats.top_hosts[:15] %}
|
|
<tr><td>{{ loop.index }}</td><td>{{ h.host }}</td><td>{{ h.count }}</td><td>{{ format_bytes(h.bytes) }}</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3 class="card-title">📋 原始日志 (前 200 条)</h3>
|
|
<div class="table-scroll">
|
|
<table class="data-table log-table">
|
|
<thead><tr><th>时间</th><th>客户端</th><th>结果</th><th>HTTP</th><th>方法</th><th>大小</th><th>URL</th><th>层级</th></tr></thead>
|
|
<tbody>
|
|
{% for e in entries[:200] %}
|
|
<tr>
|
|
<td class="time-cell">{{ e.timestamp.strftime('%H:%M:%S') if e.timestamp else e.time }}</td>
|
|
<td>{{ e.client }}</td>
|
|
<td><span class="badge badge-{% if e.category == 'Hit' %}green{% elif e.category == 'Miss' %}blue{% elif e.category == 'Tunnel' %}cyan{% elif e.category == 'Denied' %}red{% elif e.category == 'Aborted' %}orange{% else %}gray{% endif %}">{{ e.result_code }}</span></td>
|
|
<td>{{ e.http_code or '-' }}</td>
|
|
<td>{{ e.method }}</td>
|
|
<td>{{ format_bytes(e.size) }}</td>
|
|
<td class="url-cell" title="{{ e.url }}">{{ e.url }}</td>
|
|
<td><code>{{ e.hier_code }}</code></td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{% endif %}
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
{% if imported %}
|
|
<script>
|
|
const COLORS = ['#3b82f6','#ef4444','#10b981','#f59e0b','#8b5cf6','#ec4899','#06b6d4','#84cc16','#f97316','#6366f1','#14b8a6','#a855f7','#eab308','#22c55e','#64748b','#0ea5e9'];
|
|
function makeDoughnut(id, data) {
|
|
const ctx = document.getElementById(id).getContext('2d');
|
|
new Chart(ctx, {
|
|
type: 'doughnut',
|
|
data: { labels: data.map(d=>d[0]), datasets: [{ data: data.map(d=>d[1]), backgroundColor: COLORS, borderWidth:1, borderColor:'#1e293b' }] },
|
|
options: { responsive:true, maintainAspectRatio:false, plugins: { legend: { position:'right', labels:{color:'#cbd5e1',font:{size:11}} } } }
|
|
});
|
|
}
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
makeDoughnut('resultChart', {{ stats.by_result | tojson }});
|
|
makeDoughnut('methodChart', {{ stats.by_method | tojson }});
|
|
});
|
|
</script>
|
|
{% endif %}
|
|
{% endblock %}
|