341 lines
18 KiB
PHP
341 lines
18 KiB
PHP
<?php
|
|
session_start();
|
|
header("Cache-Control: no-store, no-cache, must-revalidate");
|
|
header("Pragma: no-cache");
|
|
$expire = 1800;
|
|
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
|
|
session_destroy();
|
|
header("Location: login.php");
|
|
exit;
|
|
}
|
|
$userName = $_SESSION['user_name'] ?? '';
|
|
$today = date("Y-m-d");
|
|
$grafanaUrl = "http://10.150.117.190:3000";
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>告警时序监控面板</title>
|
|
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
|
|
<script src="/static/js/jquery.min.js"></script>
|
|
<script src="/static/js/chart.js"></script>
|
|
<style>
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
body{background:#F5F7FA;font-family:"Microsoft YaHei",system-ui,sans-serif;color:#1D2129;font-size:14px;}
|
|
.container{max-width:1400px;margin:0 auto;padding:20px;}
|
|
.header-bar{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;}
|
|
.nav-group{display:flex;gap:12px;align-items:center;}
|
|
.back-link{color:#165DFF;text-decoration:none;}
|
|
.user-info{display:flex;gap:12px;align-items:center;}
|
|
.card{background:#FFFFFF;border-radius:8px;padding:20px;box-shadow:0 1px 2px rgba(0,0,0,0.06);margin-bottom:16px;}
|
|
.card-title{font-size:15px;font-weight:500;color:#1D2129;margin-bottom:14px;}
|
|
.filter-row{display:flex;flex-wrap:wrap;gap:16px;align-items:flex-end;margin-bottom:16px;}
|
|
.filter-full{width:100%;}
|
|
.filter-item{display:flex;flex-direction:column;gap:6px;min-width:160px;}
|
|
label{font-size:13px;color:#4E5969;}
|
|
input[type="date"],input[type="text"]{height:36px;padding:0 12px;border:1px solid #DCDFE6;border-radius:6px;font-size:14px;color:#1D2129;}
|
|
.check-group{display:flex;gap:12px;flex-wrap:wrap;}
|
|
.btn{height:36px;padding:0 16px;border:none;border-radius:6px;cursor:pointer;font-size:14px;display:flex;align-items:center;gap:6px;transition:0.15s;}
|
|
.btn-primary{background:#165DFF;color:#fff;}
|
|
.btn-primary:hover{background:#0E4BDB;}
|
|
.btn-warning{background:#FF7D00;color:#fff;}
|
|
.btn-outline{background:#fff;color:#4E5969;border:1px solid #DCDFE6;}
|
|
.btn-outline:hover{border-color:#165DFF;color:#165DFF;}
|
|
.btn-sm{height:28px;padding:0 8px;font-size:13px;}
|
|
.stat-row{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:16px;}
|
|
.stat-card{padding:16px;border-radius:8px;text-align:center;}
|
|
.stat-1{background:#E8F3FF;}
|
|
.stat-2{background:#FFECE8;}
|
|
.stat-3{background:#E6FFEA;}
|
|
.stat-4{background:#FFF7E6;}
|
|
.stat-label{font-size:13px;color:#4E5969;}
|
|
.stat-num{font-size:26px;font-weight:600;margin:8px 0;}
|
|
.chart-row{display:grid;grid-template-columns:2fr 1fr;gap:16px;margin-bottom:16px;}
|
|
.chart-box{width:100%;height:260px;position:relative;overflow:hidden;}
|
|
canvas{width:100% !important;height:100% !important;}
|
|
.table-wrap{overflow-x:auto;}
|
|
table{width:100%;border-collapse:collapse;font-size:14px;}
|
|
th{background:#F5F7FA;padding:12px 10px;text-align:left;border-bottom:1px solid #E5E6EB;color:#4E5969;font-weight:500;white-space:nowrap;}
|
|
td{padding:12px 10px;border-bottom:1px solid #F2F3F5;color:#1D2129;}
|
|
.row-firing{background:#FFF7F7;}
|
|
.row-resolved{background:#F7FFF9;}
|
|
.storm{color:#F53F3F;font-weight:500;}
|
|
.pagination{display:flex;justify-content:space-between;align-items:center;margin-top:16px;flex-wrap:wrap;gap:12px;}
|
|
.pag-btn{height:32px;padding:0 12px;border:1px solid #DCDFE6;background:#fff;border-radius:6px;cursor:pointer;font-size:14px;}
|
|
.empty-box{padding:60px;text-align:center;color:#86909C;}
|
|
.tip-text{font-size:13px;color:#86909C;margin-left:8px;}
|
|
.shortcut-tip{text-align:center;color:#86909C;font-size:13px;margin-top:12px;}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header-bar">
|
|
<div class="nav-group">
|
|
<a href="dashboard.php" class="back-link"><i class="fa fa-arrow-left"></i> 返回监控大盘</a>
|
|
<a href="alert_subscribe.php" class="btn btn-outline"><i class="fa fa-bell"></i> 告警订阅管理</a>
|
|
</div>
|
|
<div class="user-info"><i class="fa fa-user"></i> <?php echo $userName; ?></div>
|
|
</div>
|
|
|
|
<!-- 筛选区域 -->
|
|
<div class="card">
|
|
<div class="filter-row filter-full">
|
|
<input type="text" id="globalSearch" placeholder="全局检索(告警/实例/级别)" style="width:100%;">
|
|
</div>
|
|
<div class="filter-row">
|
|
<div class="filter-item">
|
|
<label>起始日期</label>
|
|
<input type="date" id="startDate" value="<?php echo $today; ?>">
|
|
</div>
|
|
<div class="filter-item">
|
|
<label>结束日期</label>
|
|
<input type="date" id="endDate" value="<?php echo $today; ?>">
|
|
</div>
|
|
<div class="filter-item">
|
|
<label>实例模糊匹配</label>
|
|
<input type="text" id="filterInstance" placeholder="10.150.10.82">
|
|
</div>
|
|
<div class="filter-item">
|
|
<label>告警级别</label>
|
|
<div class="check-group">
|
|
<label><input type="checkbox" name="sev" value="critical"> Critical</label>
|
|
<label><input type="checkbox" name="sev" value="warning"> Warning</label>
|
|
<label><input type="checkbox" name="sev" value="info"> Info</label>
|
|
</div>
|
|
</div>
|
|
<div class="filter-item">
|
|
<label>告警状态</label>
|
|
<div class="check-group">
|
|
<label><input type="checkbox" name="sta" value="firing"> Firing</label>
|
|
<label><input type="checkbox" name="sta" value="resolved"> Resolved</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="filter-row">
|
|
<button class="btn btn-primary" id="btnSearch"><i class="fa fa-search"></i> 查询</button>
|
|
<button class="btn btn-outline" id="btnReset"><i class="fa fa-refresh"></i> 重置筛选</button>
|
|
<button class="btn btn-warning" id="btnToggleMode"><i class="fa fa-list"></i> 切换详细模式</button>
|
|
<span class="tip-text" id="modeTip">当前:去重汇总模式</span>
|
|
<button class="btn btn-outline" id="btnExport"><i class="fa fa-file-excel-o"></i> 导出CSV报表</button>
|
|
<label style="display:flex;align-items:center;gap:6px;">
|
|
<input type="checkbox" id="autoRefresh"> 60秒自动刷新
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 统计图表 -->
|
|
<div class="card" id="statCard" style="display:none;">
|
|
<div class="stat-row">
|
|
<div class="stat-card stat-1">
|
|
<div class="stat-label">告警种类</div>
|
|
<div class="stat-num" id="statTotal">0</div>
|
|
</div>
|
|
<div class="stat-card stat-2">
|
|
<div class="stat-label">触发中</div>
|
|
<div class="stat-num" id="statFiring">0</div>
|
|
</div>
|
|
<div class="stat-card stat-3">
|
|
<div class="stat-label">已恢复</div>
|
|
<div class="stat-num" id="statResolved">0</div>
|
|
</div>
|
|
<div class="stat-card stat-4">
|
|
<div class="stat-label">严重告警</div>
|
|
<div class="stat-num" id="statCritical">0</div>
|
|
</div>
|
|
</div>
|
|
<div class="chart-row">
|
|
<div>
|
|
<h4 class="card-title">24小时告警触发趋势</h4>
|
|
<div class="chart-box">
|
|
<canvas id="hourChart"></canvas>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h4 class="card-title">告警级别分布占比</h4>
|
|
<div class="chart-box">
|
|
<canvas id="pieChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 告警表格 -->
|
|
<div class="card">
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead id="tableHead"></thead>
|
|
<tbody id="tableBody">
|
|
<tr><td colspan="6" class="empty-box">数据加载中...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="pagination">
|
|
<div class="pag-info">总告警条数:<span id="pageTotal">0</span></div>
|
|
<div style="display:flex;gap:8px;align-items:center;">
|
|
<span>每页展示</span>
|
|
<select id="pageSizeSel" class="pag-btn">
|
|
<option value="10">10</option>
|
|
<option value="20" selected>20</option>
|
|
<option value="50">50</option>
|
|
</select>
|
|
<button class="pag-btn" id="pageFirst">首页</button>
|
|
<button class="pag-btn" id="pagePrev">上一页</button>
|
|
<span id="pageInfo">1 / 1</span>
|
|
<button class="pag-btn" id="pageNext">下一页</button>
|
|
<button class="pag-btn" id="pageLast">末页</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="shortcut-tip">快捷键:Enter快速查询 | ESC关闭弹窗 | Ctrl+F全局搜索</div>
|
|
</div>
|
|
<script>
|
|
// 原有告警查询、图表、分页、导出逻辑不变
|
|
const grafanaBase = "<?php echo $grafanaUrl; ?>";
|
|
let page = 1;
|
|
let pageSize = 20;
|
|
let total = 0;
|
|
let distinctMode = 1;
|
|
let autoRefreshTimer = null;
|
|
let hourChart = null;
|
|
let pieChart = null;
|
|
const stormThreshold = 1000;
|
|
const t = {
|
|
col_state:"状态",col_alert:"告警名称",col_instance:"实例",
|
|
col_severity:"级别",col_count:"触发次数",col_time:"最新时间",
|
|
empty_tip:"暂无匹配告警数据,请调整筛选条件",storm_tip:"风暴告警"
|
|
};
|
|
function saveFilter(){
|
|
const d={start:$("#startDate").val(),end:$("#endDate").val(),instance:$("#filterInstance").val(),global:$("#globalSearch").val(),sev:$("input[name=sev]:checked").map((i,e)=>e.value).get(),sta:$("input[name=sta]:checked").map((i,e)=>e.value).get(),distinct:distinctMode,pageSize:pageSize};
|
|
localStorage.setItem("alertFilter",JSON.stringify(d));
|
|
}
|
|
function loadFilter(){
|
|
const s=localStorage.getItem("alertFilter");if(!s)return;
|
|
const d=JSON.parse(s);
|
|
$("#startDate").val(d.start);$("#endDate").val(d.end);$("#filterInstance").val(d.instance);$("#globalSearch").val(d.global);
|
|
pageSize=d.pageSize;$("#pageSizeSel").val(pageSize);distinctMode=d.distinct;
|
|
$("#modeTip").text(distinctMode===1?"当前:去重汇总模式":"当前:逐条时序模式");
|
|
$("input[name=sev]").prop("checked",false);d.sev.forEach(v=>$(`input[name=sev][value="${v}"]`).prop("checked",true));
|
|
$("input[name=sta]").prop("checked",false);d.sta.forEach(v=>$(`input[name=sta][value="${v}"]`).prop("checked",true));
|
|
}
|
|
function renderHead(){
|
|
let h="";
|
|
if(distinctMode===1){
|
|
h=`<tr><th>${t.col_state}</th><th>${t.col_alert}</th><th>${t.col_instance}</th><th>${t.col_severity}</th><th>${t.col_count}</th><th>${t.col_time}</th><th>操作</th></tr>`;
|
|
}else{
|
|
h=`<tr><th>${t.col_state}</th><th>${t.col_alert}</th><th>${t.col_instance}</th><th>${t.col_severity}</th><th>${t.col_time}</th><th>操作</th></tr>`;
|
|
}
|
|
$("#tableHead").html(h);
|
|
}
|
|
function drawChart(stat){
|
|
$("#statCard").show();
|
|
$("#statTotal").text(stat.total_all);$("#statFiring").text(stat.total_firing);
|
|
$("#statResolved").text(stat.total_resolved);$("#statCritical").text(stat.total_critical);
|
|
const hourData=stat.hour_data,hourLabels=[],hourVals=[];
|
|
for(let i=0;i<24;i++){hourLabels.push(i+":00");hourVals.push(hourData[i]);}
|
|
if(hourChart) hourChart.destroy();
|
|
const hCtx=document.getElementById("hourChart").getContext("2d");
|
|
hourChart=new Chart(hCtx,{type:"line",data:{labels:hourLabels,datasets:[{label:"触发量",data:hourVals,borderColor:"#165DFF",backgroundColor:"rgba(22,93,255,0.08)",fill:true,tension:0.2,pointRadius:2}]},
|
|
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:"top",labels:{boxWidth:12,font:{size:12}}}},scales:{x:{grid:{display:false}},y:{grid:{color:"#F2F3F5"}}}}});
|
|
if(pieChart) pieChart.destroy();
|
|
const pCtx=document.getElementById("pieChart").getContext("2d");
|
|
let c=0,w=0,i=0;$("#tableBody tr").each((idx,tr)=>{const s=$(tr).find("td:nth-child(4)").text();if(s.includes("critical"))c++;else if(s.includes("warning"))w++;else i++;});
|
|
pieChart=new Chart(pCtx,{type:"doughnut",data:{labels:["Critical","Warning","Info"],datasets:[{data:[c,w,i],backgroundColor:["#F53F3F","#FF7D00","#165DFF"],borderWidth:0}]},
|
|
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:"bottom"}},cutout:"60%"}});
|
|
}
|
|
function loadData(){
|
|
saveFilter();renderHead();
|
|
$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box">数据加载中...</td></tr>`);
|
|
const sevArr=$("input[name=sev]:checked").map((i,e)=>e.value).get();
|
|
const staArr=$("input[name=sta]:checked").map((i,e)=>e.value).get();
|
|
const params=new URLSearchParams();
|
|
params.append("page",page);params.append("page_size",pageSize);
|
|
params.append("start_date",$("#startDate").val());params.append("end_date",$("#endDate").val());
|
|
params.append("instance",$("#filterInstance").val());params.append("global",$("#globalSearch").val());
|
|
params.append("distinct",distinctMode);
|
|
sevArr.forEach(v=>params.append("severity[]",v));staArr.forEach(v=>params.append("status[]",v));
|
|
fetch(`./api/api_alert_list.php?${params.toString()}`).then(r=>r.json()).then(res=>{
|
|
if(res.code!==0){$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box" style="color:#F53F3F;">${res.msg}</td></tr>`);$("#statCard").hide();return;}
|
|
total=res.total;$("#pageTotal").text(total);const tp=Math.ceil(total/pageSize)||1;$("#pageInfo").text(`${page} / ${tp}`);
|
|
if(res.stat.total_all>0) drawChart(res.stat);else $("#statCard").hide();
|
|
const list=res.list;if(list.length===0){$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box">${t.empty_tip}</td></tr>`);return;}
|
|
let html="";
|
|
list.forEach(row=>{
|
|
const cls=row.state==="firing"?"row-firing":"row-resolved";
|
|
const tagText=row.state==="firing"?"Firing":"Resolved";
|
|
let sevCls="";if(row.severity==="critical")sevCls="color:#F53F3F";else if(row.severity==="warning")sevCls="color:#FF7D00";else sevCls="color:#165DFF";
|
|
let storm="";if(distinctMode===1&&row.total_count>stormThreshold)storm=`<span class="storm">${t.storm_tip}</span>`;
|
|
if(distinctMode===1){
|
|
html+=`<tr class="${cls}" data-row='${JSON.stringify(row)}'>
|
|
<td>${tagText}</td><td>${row.metric}</td><td>${row.instance}</td>
|
|
<td style="${sevCls}">${row.severity}</td><td>${row.total_count} ${storm}</td><td>${row.time}</td>
|
|
<td><button class="btn btn-sm btn-outline copy-name" data-name="${row.metric}">复制告警</button></td>
|
|
</tr>`;
|
|
}else{
|
|
html+=`<tr class="${cls}" data-row='${JSON.stringify(row)}'>
|
|
<td>${tagText}</td><td>${row.metric}</td><td>${row.instance}</td>
|
|
<td style="${sevCls}">${row.severity}</td><td>${row.time}</td>
|
|
<td><button class="btn btn-sm btn-outline copy-name" data-name="${row.metric}">复制告警</button></td>
|
|
</tr>`;
|
|
}
|
|
});
|
|
$("#tableBody").html(html);
|
|
}).catch(()=>{$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box" style="color:#F53F3F;">接口请求失败</td></tr>`);$("#statCard").hide();});
|
|
}
|
|
function exportExcel(){
|
|
const sevArr=$("input[name=sev]:checked").map((i,e)=>e.value).get();
|
|
const staArr=$("input[name=sta]").map((i,e)=>e.value).get();
|
|
const params=new URLSearchParams();
|
|
params.append("export",1);params.append("page_size",9999);
|
|
params.append("start_date",$("#startDate").val());params.append("end_date",$("#endDate").val());
|
|
params.append("instance",$("#filterInstance").val());params.append("global",$("#globalSearch").val());
|
|
params.append("distinct",distinctMode);
|
|
sevArr.forEach(v=>params.append("severity[]",v));staArr.forEach(v=>params.append("status[]",v));
|
|
fetch(`./api/api_alert_list.php?${params.toString()}`).then(r=>r.json()).then(res=>{
|
|
if(res.code!==0) return alert(res.msg);
|
|
let csv="";
|
|
if(distinctMode===1) csv="状态,告警名称,实例,级别,触发次数,最新时间\n";
|
|
else csv="状态,告警名称,实例,级别,触发时间\n";
|
|
res.list.forEach(r=>{
|
|
if(distinctMode===1) csv+=`${r.state},${r.metric},${r.instance},${r.severity},${r.total_count},${r.time}\n`;
|
|
else csv+=`${r.state},${r.metric},${r.instance},${r.severity},${r.time}\n`;
|
|
});
|
|
const blob=new Blob([csv],{type:"text/csv;charset=utf-8"});
|
|
const a=document.createElement("a");a.href=URL.createObjectURL(blob);a.download=`告警报表_${new Date().getTime()}.csv`;a.click();
|
|
});
|
|
}
|
|
function toggleAutoRefresh(){
|
|
if($("#autoRefresh").is(":checked")) autoRefreshTimer=setInterval(()=>loadData(),60000);
|
|
else clearInterval(autoRefreshTimer);
|
|
}
|
|
$(function(){
|
|
loadFilter();loadData();
|
|
$("#btnSearch").click(()=>{page=1;loadData();});
|
|
$("#btnReset").click(()=>{
|
|
const today=new Date().toISOString().split('T')[0];
|
|
$("#startDate").val(today);$("#endDate").val(today);$("#filterInstance").val("");$("#globalSearch").val("");
|
|
$("input[name=sev]").prop("checked",false);$("input[name=sta]").prop("checked",false);
|
|
distinctMode=1;$("#modeTip").text("当前:去重汇总模式");page=1;loadData();
|
|
});
|
|
$("#btnToggleMode").click(()=>{distinctMode=distinctMode===1?0:1;$("#modeTip").text(distinctMode===1?"当前:去重汇总模式":"当前:逐条时序模式");page=1;loadData();});
|
|
$("#btnExport").click(exportExcel);
|
|
$("#autoRefresh").change(toggleAutoRefresh);
|
|
$("#pageSizeSel").change(function(){pageSize=parseInt($(this).val());page=1;loadData();});
|
|
$("#pageFirst").click(()=>{page=1;loadData();});
|
|
$("#pagePrev").click(()=>{if(page>1){page--;loadData();}});
|
|
$("#pageNext").click(()=>{const tp=Math.ceil(total/pageSize);if(page<tp){page++;loadData();}});
|
|
$("#pageLast").click(()=>{page=Math.ceil(total/pageSize);loadData();});
|
|
$(document).on("click",".copy-name",function(){
|
|
const name=$(this).data("name");
|
|
navigator.clipboard.writeText(name).then(()=>alert("已复制告警名称:"+name));
|
|
});
|
|
$(document).keydown(function(e){
|
|
if(e.key==="Enter"){e.preventDefault();page=1;loadData();}
|
|
if(e.ctrlKey&&e.key.toLowerCase()==="f"){e.preventDefault();$("#globalSearch").focus();}
|
|
});
|
|
})
|
|
</script>
|
|
</body>
|
|
</html>
|