功能:完善通用云管工作台、审计与诊断能力
This commit is contained in:
1 parent
5ac692724d
commit
9f05aa20ea
225 files changed
+14266
-141
No files matched your search
@@ -6,7 +6,7 @@ import { useAuth } from '../stores/auth'
|
||||
import { useAction } from '../composables/useAction'
|
||||
import ResourceTrend from '../components/ResourceTrend.vue'
|
||||
const data = usePlatform(), auth = useAuth(), { run, busy: saving } = useAction()
|
||||
const metricLabels: Record<string, string> = { node_cpu: '节点 CPU', node_memory: '节点内存', node_storage: '节点存储', node_unavailable: '节点不可用 / 采集异常', terminal_offline: '终端离线' }
|
||||
const metricLabels: Record<string, string> = { node_cpu: '节点 CPU', node_memory: '节点内存', node_storage: '节点存储', node_unavailable: '节点不可用 / 采集异常', terminal_offline: '终端离线', terminal_cpu: '终端 CPU', terminal_memory: '终端内存', terminal_temperature: '终端 SoC 温度' }
|
||||
const rules = ref<AlertRule[]>([]), alerts = ref<AlertRecord[]>([]), points = ref<ResourcePoint[]>([])
|
||||
const resourceId = ref(''), hours = ref(24), bucket = ref(300), from = ref(Date.now() - 86400000), to = ref(Date.now())
|
||||
const status = ref(''), keyword = ref(''), page = ref(1), total = ref(0), firing = ref(0), unacknowledged = ref(0), loading = ref(false), error = ref(''), chartError = ref('')
|
||||
@@ -14,6 +14,9 @@ const nodes = computed(() => data.resources.filter(r => r.provider === 'proxmox'
|
||||
const tab = ref('alerts'), dialog = ref(false), editId = ref('')
|
||||
const form = reactive<AlertRuleInput>({ name: '', metric: 'node_cpu', threshold: 90, holdSeconds: 120, enabled: true })
|
||||
const offline = computed(() => ['node_unavailable', 'terminal_offline'].includes(form.metric))
|
||||
const temperature = computed(() => form.metric === 'terminal_temperature')
|
||||
const terminalMetric = computed(() => ['terminal_cpu', 'terminal_memory', 'terminal_temperature'].includes(form.metric))
|
||||
const unit = (metric: string) => ['node_unavailable', 'terminal_offline'].includes(metric) ? '秒' : metric === 'terminal_temperature' ? '°C' : '%'
|
||||
const stamp = (v?: string | null) => v ? new Date(v).toLocaleString('zh-CN', { hour12: false }) : '—'
|
||||
const reasons: Record<string, string> = { condition_cleared: '指标恢复', rule_changed: '规则变更', target_unavailable: '目标移除或停用' }
|
||||
async function chart() {
|
||||
@@ -42,7 +45,7 @@ function edit(rule?: AlertRule) {
|
||||
Object.assign(form, rule ? { name: rule.name, metric: rule.metric, threshold: rule.threshold, holdSeconds: rule.holdSeconds, enabled: rule.enabled } : { name: '', metric: 'node_cpu', threshold: 90, holdSeconds: 120, enabled: true })
|
||||
dialog.value = true
|
||||
}
|
||||
function metricChanged() { form.threshold = offline.value ? 180 : 90; form.holdSeconds = offline.value ? 0 : 120 }
|
||||
function metricChanged() { form.threshold = offline.value ? 180 : temperature.value ? 80 : 90; form.holdSeconds = offline.value ? 0 : 120 }
|
||||
async function save() {
|
||||
if (!form.name.trim()) return
|
||||
const ok = await run(() => api.saveAlertRule(editId.value, { ...form, name: form.name.trim() }), '告警规则已保存')
|
||||
@@ -54,7 +57,7 @@ onMounted(() => { void load(); timer = setInterval(() => { if (!document.hidden
|
||||
onUnmounted(() => clearInterval(timer))
|
||||
</script>
|
||||
<template>
|
||||
<div class="page-heading"><div><h1>监控告警</h1><p class="subtext">节点历史趋势与终端离线告警 · 每 15 秒刷新</p></div><el-button :loading="loading" @click="load()">刷新监控</el-button></div>
|
||||
<div class="page-heading"><div><h1>监控告警</h1><p class="subtext">节点与终端指标告警 · 每 15 秒刷新</p></div><el-button :loading="loading" @click="load()">刷新监控</el-button></div>
|
||||
<el-alert v-if="error" type="error" :title="error" :closable="false" class="notice" />
|
||||
<div class="monitor-summary"><article class="panel"><span>正在告警</span><strong :class="firing ? 'tone-danger' : 'tone-ok'">{{ firing }}</strong></article><article class="panel"><span>等待确认</span><strong>{{ unacknowledged }}</strong></article><article class="panel"><span>启用规则</span><strong>{{ rules.filter(r => r.enabled).length }}</strong></article></div>
|
||||
<section class="panel">
|
||||
@@ -65,15 +68,15 @@ onUnmounted(() => clearInterval(timer))
|
||||
<el-tabs v-model="tab"><el-tab-pane label="告警事件" name="alerts" /><el-tab-pane label="告警规则" name="rules" /></el-tabs>
|
||||
<template v-if="tab === 'alerts'">
|
||||
<div class="alert-filters"><el-input v-model="keyword" aria-label="搜索告警" placeholder="规则或目标名称" clearable @keyup.enter="load(true)" /><el-select v-model="status" aria-label="告警状态"><el-option value="" label="全部状态" /><el-option value="firing" label="正在告警" /><el-option value="resolved" label="已恢复" /></el-select><el-button :loading="loading" @click="load(true)">查询告警</el-button></div>
|
||||
<el-table :data="alerts" empty-text="暂无告警事件"><el-table-column label="规则 / 目标" min-width="190"><template #default="{ row }"><strong>{{ row.ruleName }}</strong><div class="subtext">{{ row.targetName }}</div></template></el-table-column><el-table-column label="状态" min-width="110"><template #default="{ row }"><el-tag :type="row.status === 'firing' ? 'danger' : 'success'">{{ row.status === 'firing' ? '正在告警' : '已恢复' }}</el-tag></template></el-table-column><el-table-column label="观测值 / 阈值" min-width="160"><template #default="{ row }">{{ row.value.toFixed(1) }} / {{ row.threshold }} {{ row.metric.endsWith('offline') || row.metric === 'node_unavailable' ? '秒' : '%' }}<div v-if="row.metric === 'node_unavailable'" class="subtext">含节点离线状态检测</div></template></el-table-column><el-table-column label="发生 / 恢复" min-width="185"><template #default="{ row }"><div>{{ stamp(row.openedAt) }}</div><div class="subtext">{{ row.resolvedAt ? stamp(row.resolvedAt) : '持续中' }}</div><div class="subtext">{{ reasons[row.resolveReason] }}</div></template></el-table-column><el-table-column label="确认" min-width="150"><template #default="{ row }"><span v-if="row.acknowledgedAt">{{ row.acknowledgedBy }}<div class="subtext">{{ stamp(row.acknowledgedAt) }}</div></span><el-button v-else-if="auth.canOperate && row.status === 'firing'" link type="primary" :disabled="saving" @click="acknowledge(row)">确认告警</el-button><span v-else class="secondary">未确认</span></template></el-table-column></el-table>
|
||||
<el-table :data="alerts" empty-text="暂无告警事件"><el-table-column label="规则 / 目标" min-width="190"><template #default="{ row }"><strong>{{ row.ruleName }}</strong><div class="subtext">{{ row.targetName }}</div></template></el-table-column><el-table-column label="状态" min-width="110"><template #default="{ row }"><el-tag :type="row.status === 'firing' ? 'danger' : 'success'">{{ row.status === 'firing' ? '正在告警' : '已恢复' }}</el-tag></template></el-table-column><el-table-column label="观测值 / 阈值" min-width="160"><template #default="{ row }">{{ row.value.toFixed(1) }} / {{ row.threshold }} {{ unit(row.metric) }}<div v-if="row.status === 'firing' && row.dataFresh === false" class="data-unknown" role="status">数据已过期或缺失;显示最近告警值,等待有效采样</div><div v-if="row.metric === 'node_unavailable'" class="subtext">含节点离线状态检测</div></template></el-table-column><el-table-column label="发生 / 恢复" min-width="185"><template #default="{ row }"><div>{{ stamp(row.openedAt) }}</div><div class="subtext">{{ row.resolvedAt ? stamp(row.resolvedAt) : '持续中' }}</div><div class="subtext">{{ reasons[row.resolveReason] }}</div></template></el-table-column><el-table-column label="确认" min-width="150"><template #default="{ row }"><span v-if="row.acknowledgedAt">{{ row.acknowledgedBy }}<div class="subtext">{{ stamp(row.acknowledgedAt) }}</div></span><el-button v-else-if="auth.canOperate && row.status === 'firing'" link type="primary" :disabled="saving" @click="acknowledge(row)">确认告警</el-button><span v-else class="secondary">未确认</span></template></el-table-column></el-table>
|
||||
<el-pagination v-model:current-page="page" :page-size="20" :total="total" layout="prev, pager, next, total" @current-change="load()" /><p class="subtext retention-note">确认表示已知悉,指标恢复后自动结束告警。已恢复事件保留 90 天。</p>
|
||||
</template>
|
||||
<template v-else><div class="panel-heading"><p class="subtext">水位按持续时间触发;离线按秒数触发。修改规则后重新计时。</p><el-button v-if="auth.canAdmin" type="primary" @click="edit()">新增规则</el-button></div><el-table :data="rules" empty-text="暂无规则"><el-table-column prop="name" label="规则名称" min-width="190" /><el-table-column label="指标" min-width="190"><template #default="{ row }">{{ metricLabels[row.metric] }}</template></el-table-column><el-table-column label="阈值 / 持续" min-width="155"><template #default="{ row }">{{ row.threshold }}{{ ['node_unavailable', 'terminal_offline'].includes(row.metric) ? ' 秒离线' : `% / ${row.holdSeconds} 秒` }}</template></el-table-column><el-table-column label="状态" width="100"><template #default="{ row }"><el-tag :type="row.enabled ? 'success' : 'info'">{{ row.enabled ? '启用' : '停用' }}</el-tag></template></el-table-column><el-table-column v-if="auth.canAdmin" label="操作" width="90"><template #default="{ row }"><el-button link type="primary" @click="edit(row)">编辑规则</el-button></template></el-table-column></el-table></template>
|
||||
<template v-else><div class="panel-heading"><p class="subtext">水位按持续时间触发;离线按秒数触发。修改规则后重新计时。</p><el-button v-if="auth.canAdmin" type="primary" @click="edit()">新增规则</el-button></div><el-table :data="rules" empty-text="暂无规则"><el-table-column prop="name" label="规则名称" min-width="190" /><el-table-column label="指标" min-width="190"><template #default="{ row }">{{ metricLabels[row.metric] }}</template></el-table-column><el-table-column label="阈值 / 持续" min-width="155"><template #default="{ row }">{{ row.threshold }}{{ ['node_unavailable', 'terminal_offline'].includes(row.metric) ? ' 秒离线' : `${unit(row.metric)} / ${row.holdSeconds} 秒` }}</template></el-table-column><el-table-column label="状态" width="100"><template #default="{ row }"><el-tag :type="row.enabled ? 'success' : 'info'">{{ row.enabled ? '启用' : '停用' }}</el-tag></template></el-table-column><el-table-column v-if="auth.canAdmin" label="操作" width="90"><template #default="{ row }"><el-button link type="primary" @click="edit(row)">编辑规则</el-button></template></el-table-column></el-table></template>
|
||||
</section>
|
||||
<el-dialog v-model="dialog" :title="editId ? '编辑告警规则' : '新增告警规则'" width="540px" :close-on-click-modal="false" :close-on-press-escape="!saving" :show-close="!saving"><el-form label-position="top"><el-form-item label="规则名称"><el-input v-model="form.name" maxlength="80" aria-label="规则名称" :disabled="saving" /></el-form-item><el-form-item label="监控指标"><el-select v-model="form.metric" aria-label="规则监控指标" :disabled="saving" @change="metricChanged"><el-option v-for="(label, value) in metricLabels" :key="value" :value="value" :label="label" /></el-select></el-form-item><el-form-item :label="offline ? '离线阈值(秒)' : '使用率阈值(%)'"><el-input-number v-model="form.threshold" :min="offline ? 90 : 1" :max="offline ? 86400 : 100" :precision="offline ? 0 : 1" aria-label="告警阈值" :disabled="saving" /></el-form-item><el-form-item v-if="!offline" label="持续时间(秒)"><el-input-number v-model="form.holdSeconds" :min="0" :max="3600" :precision="0" aria-label="告警持续时间" :disabled="saving" /></el-form-item><el-form-item label="启用规则"><el-switch v-model="form.enabled" aria-label="启用告警规则" :disabled="saving" /></el-form-item></el-form><el-alert v-if="editId" title="保存会结束本规则当前告警,并按新规则重新计时。" type="info" :closable="false" /><template #footer><el-button :disabled="saving" @click="dialog = false">取消</el-button><el-button type="primary" :loading="saving" :disabled="!form.name.trim()" @click="save">保存规则</el-button></template></el-dialog>
|
||||
<el-dialog v-model="dialog" :title="editId ? '编辑告警规则' : '新增告警规则'" width="540px" :close-on-click-modal="false" :close-on-press-escape="!saving" :show-close="!saving"><el-form label-position="top"><el-form-item label="规则名称"><el-input v-model="form.name" maxlength="80" aria-label="规则名称" :disabled="saving" /></el-form-item><el-form-item label="监控指标"><el-select v-model="form.metric" aria-label="规则监控指标" :disabled="saving" @change="metricChanged"><el-option v-for="(label, value) in metricLabels" :key="value" :value="value" :label="label" /></el-select></el-form-item><el-form-item :label="offline ? '离线阈值(秒)' : temperature ? 'SoC 温度阈值(°C)' : '使用率阈值(%)'"><el-input-number v-model="form.threshold" :min="offline ? 90 : 1" :max="offline ? 86400 : temperature ? 150 : 100" :precision="offline ? 0 : 1" aria-label="告警阈值" :disabled="saving" /></el-form-item><el-form-item v-if="!offline" label="持续时间(秒)"><el-input-number v-model="form.holdSeconds" :min="0" :max="3600" :precision="0" aria-label="告警持续时间" :disabled="saving" /></el-form-item><el-form-item label="启用规则"><el-switch v-model="form.enabled" aria-label="启用告警规则" :disabled="saving" /></el-form-item></el-form><p v-if="terminalMetric" class="subtext terminal-alert-note">对已激活且启用的终端生效;SoC 温度只使用 soc-thermal 传感器。持续时间按新采样累计,重复读取不推进;采样超过 90 秒或缺失会重新计时,已有告警等待有效正常采样才恢复。此规则只在控制台告警,不发送外部通知。</p><el-alert v-if="editId" title="保存会结束本规则当前告警,并按新规则重新计时。" type="info" :closable="false" /><template #footer><el-button :disabled="saving" @click="dialog = false">取消</el-button><el-button type="primary" :loading="saving" :disabled="!form.name.trim()" @click="save">保存规则</el-button></template></el-dialog>
|
||||
</template>
|
||||
<style scoped>
|
||||
.monitor-summary{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:18px}.monitor-summary .panel{display:flex;justify-content:space-between;align-items:center;gap:12px}.monitor-summary span{font-size:12px;color:var(--vdi-muted)}.monitor-summary strong{font-size:28px}.trend-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;margin-top:20px}.chart-filters,.alert-filters{display:flex;gap:12px;flex-wrap:wrap}.chart-filters .el-select{width:160px}.alert-filters{margin:18px 0}.alert-filters .el-input{width:230px}.alert-filters .el-select{width:150px}.el-pagination{margin-top:20px;flex-wrap:wrap}.retention-note{margin-top:15px}.subtext{line-height:1.7}
|
||||
.monitor-summary{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:18px}.monitor-summary .panel{display:flex;justify-content:space-between;align-items:center;gap:12px}.monitor-summary span{font-size:12px;color:var(--vdi-muted)}.monitor-summary strong{font-size:28px}.trend-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;margin-top:20px}.chart-filters,.alert-filters{display:flex;gap:12px;flex-wrap:wrap}.chart-filters .el-select{width:160px}.alert-filters{margin:18px 0}.alert-filters .el-input{width:230px}.alert-filters .el-select{width:150px}.el-pagination{margin-top:20px;flex-wrap:wrap}.retention-note{margin-top:15px}.subtext{line-height:1.7}.data-unknown{color:var(--el-color-warning);font-size:12px;line-height:1.7;margin-top:6px}.terminal-alert-note{margin:14px 0;font-size:12px}
|
||||
@media(max-width:1200px){.trend-grid{grid-template-columns:1fr}.panel-heading{flex-wrap:wrap;gap:16px}}
|
||||
@media(max-width:600px){.monitor-summary{gap:8px}.monitor-summary .panel{padding:14px;flex-direction:column;align-items:flex-start}.monitor-summary strong{font-size:24px}.chart-filters,.alert-filters{width:100%}.chart-filters .el-select,.alert-filters .el-select{flex:1;min-width:120px}.alert-filters .el-input{width:100%}}
|
||||
</style>
|
||||
Reference in new issue
Block a user