Files
aler_dashboard_git/alert_list.php
T
2026-07-09 16:31:30 +08:00

370 lines
20 KiB
PHP
Raw 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.
<?php
session_start();
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Pragma: no-cache");
require_once 'auth.php';
$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");
$grafanaBase = "http://10.150.117.190:3000/d/linux-server-alert-fix-startsat/linux-fu-wu-qi-gao-jing-zhong-xin?orgId=1&refresh=10s";
?>
<!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-left{display:flex;gap:12px;align-items:center;}
.nav-right{display:flex;gap:10px;align-items:center;}
.back-link{color:#165DFF;text-decoration:none;}
.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-danger{background:#F53F3F;color:#fff;}
.btn-danger:hover{background:#D83636;}
.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-left">
<a href="dashboard.php" class="back-link"><i class="fa fa-arrow-left"></i> 返回监控大盘</a>
</div>
<div class="nav-right">
<a href="alert_subscribe.php" class="btn btn-outline"><i class="fa fa-bell"></i> 告警订阅管理</a>
<span style="color:#4E5969;"><i class="fa fa-user"></i> <?php echo $userName; ?></span>
<span class="user-info"><i class="fa fa-user-circle"></i><?php echo $t['current_user']; ?><?php echo $userName; ?><?php echo $roleRealName; ?></span>
<button class="btn btn-danger" id="logoutBtn"><i class="fa fa-sign-out"></i> 退出登录</button>
</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 grafanaFullBase = "<?php echo $grafanaBase; ?>";
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:"风暴告警"
};
// 时区回滚:原生浏览器本地时区,删除手动+8偏移
function utcTsToCst(ts) {
const date = new Date(ts * 1000);
const Y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
const H = String(date.getHours()).padStart(2, '0');
const i = String(date.getMinutes()).padStart(2, '0');
const s = String(date.getSeconds()).padStart(2, '0');
return `${Y}-${m}-${d} ${H}:${i}:${s}`;
}
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]").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>`;
const cstTimeStr = utcTsToCst(row.timestamp);
const grafanaUrl = `${grafanaFullBase}&var-instance=${encodeURIComponent(row.instance)}&var-alertname=${encodeURIComponent(row.metric)}`;
// 统一btn-sm btn-outline,无蓝色主按钮
const btnHtml = `
<button class="btn btn-sm btn-outline copy-name" data-name="${row.metric}">复制告警</button>
<a class="btn btn-sm btn-outline" target="_blank" href="${grafanaUrl}">查看图表</a>
`;
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>${cstTimeStr}</td>
<td>${btnHtml}</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>${cstTimeStr}</td>
<td>${btnHtml}</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},${utcTsToCst(r.timestamp)}\n`;
else csv+=`${r.state},${r.metric},${r.instance},${r.severity},${utcTsToCst(r.timestamp)}\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();
$("#logoutBtn").click(function(){
if(confirm("确定退出当前账号?")){
location.href="login.php";
}
});
$("#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>