119 lines
5.8 KiB
Go
119 lines
5.8 KiB
Go
package handler
|
|
|
|
import (
|
|
"html/template"
|
|
"net/http"
|
|
)
|
|
|
|
// DashboardHandler 是前端页面路由处理器
|
|
type DashboardHandler struct {
|
|
templates *template.Template
|
|
}
|
|
|
|
func NewDashboardHandler() *DashboardHandler {
|
|
tmpl := template.Must(template.New("dashboard").Parse(dashboardHTML))
|
|
return &DashboardHandler{templates: tmpl}
|
|
}
|
|
|
|
// RegisterRoutes 注册页面路由
|
|
func (h *DashboardHandler) RegisterRoutes(mux *http.ServeMux) {
|
|
mux.HandleFunc("GET /ops/dashboard", h.Dashboard)
|
|
mux.HandleFunc("GET /ops/dashboard/logs", h.Dashboard)
|
|
mux.HandleFunc("GET /ops/dashboard/rules", h.Dashboard)
|
|
mux.HandleFunc("GET /ops/dashboard/alerts", h.Dashboard)
|
|
mux.HandleFunc("GET /ops/dashboard/channels", h.Dashboard)
|
|
}
|
|
|
|
// Dashboard 首页
|
|
func (h *DashboardHandler) Dashboard(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
_ = h.templates.ExecuteTemplate(w, "dashboard", nil)
|
|
}
|
|
|
|
const dashboardHTML = `
|
|
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>AI-Ops 运维看板</title>
|
|
<style>
|
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 0; background: #0f172a; color: #e2e8f0; }
|
|
header { padding: 18px 28px; background: #111827; border-bottom: 1px solid #334155; display:flex; justify-content:space-between; align-items:center; }
|
|
main { padding: 24px; display: grid; gap: 18px; }
|
|
.grid { display: grid; grid-template-columns: repeat(4, minmax(140px, 1fr)); gap: 14px; }
|
|
.card { background:#111827; border:1px solid #334155; border-radius:12px; padding:16px; box-shadow:0 10px 24px rgba(0,0,0,.18); }
|
|
.metric { font-size: 28px; font-weight: 700; color:#38bdf8; margin-top:8px; }
|
|
button, input { border-radius:8px; border:1px solid #475569; background:#0b1220; color:#e2e8f0; padding:8px 10px; }
|
|
button { cursor:pointer; background:#2563eb; border-color:#2563eb; }
|
|
table { width:100%; border-collapse: collapse; font-size: 14px; }
|
|
th, td { border-bottom: 1px solid #334155; padding: 8px; text-align:left; vertical-align: top; }
|
|
th { color:#93c5fd; }
|
|
.muted { color:#94a3b8; }
|
|
.row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; }
|
|
.error { color:#fca5a5; white-space:pre-wrap; }
|
|
code { color:#bae6fd; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<div><strong>AI-Ops 运维看板</strong><span class="muted"> · 规则 / 事件 / 渠道 / 日志</span></div>
|
|
<div class="row"><input id="username" placeholder="admin" value="admin"><input id="password" type="password" placeholder="admin" value="admin"><button onclick="login()">登录</button><button onclick="loadAll()">刷新</button></div>
|
|
</header>
|
|
<main>
|
|
<section class="grid">
|
|
<div class="card">QPS<div id="qps" class="metric">-</div></div>
|
|
<div class="card">平均延迟<div id="avg" class="metric">-</div></div>
|
|
<div class="card">P99<div id="p99" class="metric">-</div></div>
|
|
<div class="card">错误率<div id="err" class="metric">-</div></div>
|
|
</section>
|
|
<section class="card"><h3>告警事件</h3><div id="alerts"></div></section>
|
|
<section class="card"><h3>告警规则</h3><div id="rules"></div></section>
|
|
<section class="card"><h3>通知渠道</h3><div id="channels"></div></section>
|
|
<section class="card"><h3>日志</h3><div id="logs"></div></section>
|
|
<section class="card error" id="error"></section>
|
|
</main>
|
|
<script>
|
|
const api = '/api/v1/ai-ops';
|
|
function token(){ return localStorage.getItem('ai_ops_token') || ''; }
|
|
function setError(e){ document.getElementById('error').textContent = e ? String(e) : ''; }
|
|
async function login(){
|
|
setError('');
|
|
const res = await fetch(api + '/login', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({username: username.value, password: password.value})});
|
|
const data = await res.json();
|
|
const t = data?.data?.token || data?.token;
|
|
if(!res.ok || !t){ setError(JSON.stringify(data)); return; }
|
|
localStorage.setItem('ai_ops_token', t);
|
|
await loadAll();
|
|
}
|
|
async function get(path){
|
|
const res = await fetch(api + path, {headers:{Authorization:'Bearer ' + token()}});
|
|
const data = await res.json();
|
|
if(!res.ok) throw new Error(path + ' ' + JSON.stringify(data));
|
|
return data.data ?? data;
|
|
}
|
|
function table(rows, cols){
|
|
if(!Array.isArray(rows) || rows.length === 0) return '<p class="muted">暂无数据</p>';
|
|
return '<table><thead><tr>'+cols.map(c=>'<th>'+c[0]+'</th>').join('')+'</tr></thead><tbody>'+rows.map(r=>'<tr>'+cols.map(c=>'<td>'+escapeHtml(String(r[c[1]] ?? ''))+'</td>').join('')+'</tr>').join('')+'</tbody></table>';
|
|
}
|
|
function escapeHtml(s){ return s.replace(/[&<>"]/g, m=>({'&':'&','<':'<','>':'>','"':'"'}[m])); }
|
|
async function loadAll(){
|
|
try{
|
|
setError('');
|
|
const m = await get('/metrics/realtime');
|
|
qps.textContent = m.qps ?? '-'; avg.textContent = (m.avg_latency_ms ?? '-') + 'ms'; p99.textContent = (m.p99_latency_ms ?? '-') + 'ms'; err.textContent = m.error_rate ?? '-';
|
|
const ev = await get('/alerts?page=1&page_size=20');
|
|
alerts.innerHTML = table(ev.items || [], [['级别','level'],['资源','resource_id'],['状态','status'],['聚合','is_aggregated'],['数量','aggregated_count'],['开始时间','started_at']]);
|
|
const rs = await get('/rules');
|
|
rules.innerHTML = table(rs || [], [['名称','name'],['指标','metric_name'],['条件','threshold_type'],['阈值','threshold_value'],['级别','level'],['启用','enabled']]);
|
|
const cs = await get('/channels');
|
|
channels.innerHTML = table(cs || [], [['名称','name'],['类型','channel_type'],['优先级','priority'],['启用','enabled']]);
|
|
const lg = await get('/logs?page=1&page_size=20');
|
|
logs.innerHTML = table(lg.items || [], [['服务','service'],['级别','level'],['消息','message'],['时间','timestamp']]);
|
|
}catch(e){ setError(e); }
|
|
}
|
|
if(token()) loadAll();
|
|
</script>
|
|
</body>
|
|
</html>
|
|
`
|