272 lines
12 KiB
PHP
Executable File
272 lines
12 KiB
PHP
Executable File
<?php
|
|
require_once 'cong.php';
|
|
session_start();
|
|
|
|
if (empty($_SESSION['password']) || $_SESSION['password'] != LOGIN_PASSWORD) {
|
|
unset($_SESSION['password']);
|
|
unset($_SESSION['check_result']);
|
|
echo("<script language=javascript>alert('密码错误!');window.location='ip_check_edit.php'; </script>");
|
|
exit;
|
|
}
|
|
|
|
$defaultFrom = date('Y-m-d', strtotime('-6 days'));
|
|
$defaultTo = date('Y-m-d');
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>日志数据分析</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<link href="//cdn.staticfile.org/layui/2.9.2/css/layui.css" rel="stylesheet">
|
|
<script src="//cdn.staticfile.org/Chart.js/4.4.1/chart.umd.min.js"></script>
|
|
<style>
|
|
:root {
|
|
--log-primary: #16baaa;
|
|
--log-primary-dark: #0e8d81;
|
|
--log-bg: #f0f2f5;
|
|
--log-card: #ffffff;
|
|
--log-border: #e8ecf1;
|
|
--log-text: #2c3e50;
|
|
--log-muted: #8c98a4;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body { margin: 0; background: var(--log-bg); color: var(--log-text); font-family: 'Segoe UI', system-ui, sans-serif; }
|
|
.log-page { max-width: 1680px; margin: 0 auto; padding: 24px 28px 40px; }
|
|
.log-page-header { display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; gap: 12px; margin-bottom: 20px; }
|
|
.log-page-header h1 { margin: 0; font-size: 1.35rem; font-weight: 600; }
|
|
.log-page-header p { margin: 4px 0 0; font-size: .85rem; color: var(--log-muted); }
|
|
.log-nav { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
.log-nav a {
|
|
display: inline-block; padding: 6px 14px; border-radius: 6px; font-size: .85rem;
|
|
text-decoration: none; border: 1px solid var(--log-border); background: #fff; color: var(--log-text);
|
|
}
|
|
.log-nav a.active { background: var(--log-primary); color: #fff; border-color: var(--log-primary); }
|
|
.log-card {
|
|
background: var(--log-card); border: 1px solid var(--log-border); border-radius: 10px;
|
|
box-shadow: 0 2px 12px rgba(15, 23, 42, .04); margin-bottom: 16px; overflow: hidden;
|
|
}
|
|
.log-card-title { padding: 14px 20px; font-weight: 600; border-bottom: 1px solid var(--log-border); background: #fafbfc; }
|
|
.log-card-body { padding: 18px 20px; }
|
|
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; }
|
|
.stat-item {
|
|
background: linear-gradient(135deg, #f8fafc 0%, #eef3f9 100%);
|
|
border: 1px solid var(--log-border); border-radius: 8px; padding: 14px 16px; text-align: center;
|
|
}
|
|
.stat-item .val { font-size: 1.5rem; font-weight: 700; color: var(--log-primary-dark); }
|
|
.stat-item .lbl { font-size: .78rem; color: var(--log-muted); margin-top: 4px; }
|
|
.chart-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
|
|
.chart-box { position: relative; height: 280px; }
|
|
.chart-box-wide { grid-column: 1 / -1; height: 300px; }
|
|
.conclusion-list { margin: 0; padding: 0; list-style: none; }
|
|
.conclusion-list li {
|
|
padding: 10px 14px; margin-bottom: 8px; border-radius: 8px; font-size: .9rem; line-height: 1.5;
|
|
background: #f0faf9; border-left: 3px solid var(--log-primary);
|
|
}
|
|
.conclusion-list li.alert { background: #fff8e6; border-left-color: #e6a23c; }
|
|
.notice-bar {
|
|
padding: 10px 16px; margin-bottom: 16px; border-radius: 8px; font-size: .85rem;
|
|
background: #e8f4fd; color: #0c5460; border: 1px solid #bee5eb;
|
|
}
|
|
.loading-mask { text-align: center; padding: 60px; color: var(--log-muted); }
|
|
@media (max-width: 900px) { .chart-grid { grid-template-columns: 1fr; } .chart-box-wide { grid-column: auto; } }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="log-page">
|
|
<div class="log-page-header">
|
|
<div>
|
|
<h1>日志数据分析</h1>
|
|
<p>判定结果、理由、国家与时序分布 · 自动分析结论</p>
|
|
</div>
|
|
<div class="log-nav">
|
|
<a href="table.php">访问日志</a>
|
|
<a href="log_analytics.php" class="active">数据分析</a>
|
|
<a href="dashboard.php">配置中心</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="notice-bar">数据来源于 visitor_logs 表;若已开启异步写入,日志可能有短暂延迟(取决于队列 Worker 落库进度)。</div>
|
|
|
|
<div class="log-card">
|
|
<div class="log-card-body">
|
|
<form class="layui-form layui-row layui-col-space16" id="filter-form">
|
|
<div class="layui-col-md3">
|
|
<input type="text" name="date_from" id="date_from" placeholder="开始日期" class="layui-input" value="<?php echo htmlspecialchars($defaultFrom); ?>">
|
|
</div>
|
|
<div class="layui-col-md3">
|
|
<input type="text" name="date_to" id="date_to" placeholder="结束日期" class="layui-input" value="<?php echo htmlspecialchars($defaultTo); ?>">
|
|
</div>
|
|
<div class="layui-col-md3">
|
|
<input type="text" name="domain" placeholder="域名(可选)" class="layui-input">
|
|
</div>
|
|
<div class="layui-col-md3">
|
|
<input type="text" name="campagin_id" placeholder="策略 ID(可选)" class="layui-input">
|
|
</div>
|
|
<div class="layui-col-md12" style="margin-top:8px;">
|
|
<button type="button" class="layui-btn" id="btn-query" style="background:var(--log-primary);">查询分析</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="content-area">
|
|
<div class="loading-mask">加载中…</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="//cdn.staticfile.org/layui/2.9.2/layui.js"></script>
|
|
<script>
|
|
layui.use(['laydate'], function(){
|
|
var laydate = layui.laydate;
|
|
laydate.render({ elem: '#date_from', type: 'date' });
|
|
laydate.render({ elem: '#date_to', type: 'date' });
|
|
|
|
var charts = [];
|
|
|
|
function destroyCharts() {
|
|
charts.forEach(function(c){ try { c.destroy(); } catch(e){} });
|
|
charts = [];
|
|
}
|
|
|
|
function renderStats(summary) {
|
|
return '<div class="log-card"><div class="log-card-title">概览</div><div class="log-card-body"><div class="stat-grid">' +
|
|
statBox(summary.total, '总访问') +
|
|
statBox(summary.pass, '通过') +
|
|
statBox(summary.block, '拦截') +
|
|
statBox(summary.wait, '检测中') +
|
|
statBox(summary.pass_rate + '%', '通过率') +
|
|
statBox(summary.block_rate + '%', '拦截率') +
|
|
'</div></div></div>';
|
|
}
|
|
|
|
function statBox(val, lbl) {
|
|
return '<div class="stat-item"><div class="val">' + val + '</div><div class="lbl">' + lbl + '</div></div>';
|
|
}
|
|
|
|
function renderConclusions(list) {
|
|
var html = '<div class="log-card"><div class="log-card-title">分析结论</div><div class="log-card-body"><ul class="conclusion-list">';
|
|
(list || []).forEach(function(line) {
|
|
var cls = (line.indexOf('告警') !== -1 || line.indexOf('关注') !== -1) ? 'alert' : '';
|
|
html += '<li class="' + cls + '">' + escapeHtml(line) + '</li>';
|
|
});
|
|
html += '</ul></div></div>';
|
|
return html;
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
var d = document.createElement('div');
|
|
d.textContent = s;
|
|
return d.innerHTML;
|
|
}
|
|
|
|
function renderChartArea(data) {
|
|
var hasTiming = data.timing && data.timing.stages && data.timing.stages.length > 0;
|
|
return '<div class="log-card"><div class="log-card-title">可视化图表</div><div class="log-card-body chart-grid">' +
|
|
'<div class="chart-box"><canvas id="chart-result"></canvas></div>' +
|
|
'<div class="chart-box"><canvas id="chart-country"></canvas></div>' +
|
|
'<div class="chart-box chart-box-wide"><canvas id="chart-hour"></canvas></div>' +
|
|
'<div class="chart-box chart-box-wide"><canvas id="chart-reason"></canvas></div>' +
|
|
(hasTiming ? '<div class="chart-box chart-box-wide"><canvas id="chart-timing"></canvas></div>' : '') +
|
|
'</div></div>';
|
|
}
|
|
|
|
function makeChart(id, config) {
|
|
var el = document.getElementById(id);
|
|
if (!el) return;
|
|
charts.push(new Chart(el, config));
|
|
}
|
|
|
|
function buildCharts(data) {
|
|
var colors = ['#16baaa','#e74c3c','#f39c12','#3498db','#9b59b6','#1abc9c','#95a5a6'];
|
|
|
|
if (data.by_result && data.by_result.length) {
|
|
makeChart('chart-result', {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: data.by_result.map(function(r){ return r.label; }),
|
|
datasets: [{ data: data.by_result.map(function(r){ return r.count; }), backgroundColor: colors }]
|
|
},
|
|
options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: '判定结果分布' } } }
|
|
});
|
|
}
|
|
|
|
if (data.by_country && data.by_country.length) {
|
|
makeChart('chart-country', {
|
|
type: 'bar',
|
|
data: {
|
|
labels: data.by_country.map(function(r){ return r.country; }),
|
|
datasets: [{ label: '访问量', data: data.by_country.map(function(r){ return r.count; }), backgroundColor: '#16baaa' }]
|
|
},
|
|
options: { responsive: true, maintainAspectRatio: false, indexAxis: 'y', plugins: { title: { display: true, text: '国家 Top10' } } }
|
|
});
|
|
}
|
|
|
|
if (data.by_hour && data.by_hour.length) {
|
|
makeChart('chart-hour', {
|
|
type: 'line',
|
|
data: {
|
|
labels: data.by_hour.map(function(r){ return r.hour; }),
|
|
datasets: [{ label: '访问量', data: data.by_hour.map(function(r){ return r.count; }), borderColor: '#3498db', backgroundColor: 'rgba(52,152,219,.1)', fill: true, tension: 0.3 }]
|
|
},
|
|
options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: '24 小时访问分布' } } }
|
|
});
|
|
}
|
|
|
|
if (data.by_reason && data.by_reason.length) {
|
|
makeChart('chart-reason', {
|
|
type: 'bar',
|
|
data: {
|
|
labels: data.by_reason.map(function(r){ return r.reason.length > 24 ? r.reason.slice(0,24)+'…' : r.reason; }),
|
|
datasets: [{ label: '次数', data: data.by_reason.map(function(r){ return r.count; }), backgroundColor: '#e74c3c' }]
|
|
},
|
|
options: {
|
|
responsive: true, maintainAspectRatio: false, indexAxis: 'y',
|
|
plugins: { title: { display: true, text: '拦截/待判定理由 Top15' }, tooltip: { callbacks: { title: function(items){ return data.by_reason[items[0].dataIndex].reason; } } } }
|
|
}
|
|
});
|
|
}
|
|
|
|
if (data.timing && data.timing.stages && data.timing.stages.length) {
|
|
var topStages = data.timing.stages.slice(0, 10);
|
|
makeChart('chart-timing', {
|
|
type: 'bar',
|
|
data: {
|
|
labels: topStages.map(function(s){ return s.stage; }),
|
|
datasets: [{ label: '平均 ms', data: topStages.map(function(s){ return s.avg_ms; }), backgroundColor: '#9b59b6' }]
|
|
},
|
|
options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: '判定阶段平均耗时(抽样 ' + data.timing.sample_size + ' 条)' } } }
|
|
});
|
|
}
|
|
}
|
|
|
|
function loadData() {
|
|
var form = document.getElementById('filter-form');
|
|
var params = new URLSearchParams(new FormData(form));
|
|
destroyCharts();
|
|
document.getElementById('content-area').innerHTML = '<div class="loading-mask">分析中…</div>';
|
|
|
|
fetch('datatable/log_analytics.php?' + params.toString())
|
|
.then(function(r){ return r.json(); })
|
|
.then(function(res) {
|
|
if (res.code !== 0) {
|
|
document.getElementById('content-area').innerHTML = '<div class="log-card"><div class="log-card-body">' + escapeHtml(res.msg || '加载失败') + '</div></div>';
|
|
return;
|
|
}
|
|
var data = res.data;
|
|
document.getElementById('content-area').innerHTML =
|
|
renderStats(data.summary) + renderConclusions(data.conclusions) + renderChartArea(data);
|
|
buildCharts(data);
|
|
})
|
|
.catch(function(err) {
|
|
document.getElementById('content-area').innerHTML = '<div class="log-card"><div class="log-card-body">请求失败:' + escapeHtml(String(err)) + '</div></div>';
|
|
});
|
|
}
|
|
|
|
document.getElementById('btn-query').addEventListener('click', loadData);
|
|
loadData();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|