feat: configure static and dynamic alert rules

This commit is contained in:
zxr
2026-07-20 21:59:57 +08:00
parent 947fa2f9b9
commit 9cbacf5998
5 changed files with 209 additions and 57 deletions

View File

@@ -68,10 +68,11 @@ export const createRule = (data: {
severity_id: number
enabled?: boolean
metric_name?: string
query_expr?: string
data_source?: string
threshold?: number
compare_op?: string
duration?: number
eval_interval?: number
baseline_config?: string
labels?: string
annotations?: string
@@ -86,10 +87,11 @@ export const updateRule = (data: {
severity_id?: number
enabled?: boolean
metric_name?: string
query_expr?: string
data_source?: string
threshold?: number
compare_op?: string
duration?: number
eval_interval?: number
baseline_config?: string
labels?: string
annotations?: string

View File

@@ -20,12 +20,13 @@ export interface AlertRule {
name: string
data_source: string
metric_name: string
rule_type: 'static' | 'dynamic' | 'promql'
rule_type: 'static' | 'dynamic'
compare_op: '>' | '>=' | '<' | '<=' | '==' | '!='
threshold: string | number
duration: number
eval_interval: number
severity_code: string
baseline_config?: string
labels?: Record<string, string>
annotations?: Record<string, string>
}
@@ -105,12 +106,11 @@ export const TEMPLATE_CATEGORIES = [
// 数据源选项
export const DATA_SOURCES = [
{ value: 'dc-host', label: '主机/操作系统指标' },
{ value: 'dc-hardware', label: '服务器硬件指标' },
{ value: 'dc-network', label: '网络设备指标' },
{ value: 'dc-database', label: '数据库指标' },
{ value: 'dc-middleware', label: '中间件指标' },
{ value: 'dc-virtualization', label: '虚拟化指标' },
{ value: 'dc-env', label: '动力/环境/安防指标' },
{ value: 'dc-security', label: '安全设备指标' },
{ value: 'dc-storage', label: '存储设备指标' },
{ value: 'dc-room-device', label: '机房设备指标' },
]
// 比较运算符选项
@@ -127,7 +127,6 @@ export const COMPARE_OPERATORS = [
export const RULE_TYPES = [
{ value: 'static', label: '静态阈值' },
{ value: 'dynamic', label: '动态阈值' },
{ value: 'promql', label: 'PromQL 表达式' },
]
// ==================== 告警模板接口 ====================

View File

@@ -16,8 +16,9 @@
</a-col>
<a-col :span="12">
<a-form-item field="rule_type" label="规则类型" required>
<a-select v-model="formData.rule_type" placeholder="请选择规则类型">
<a-select v-model="formData.rule_type" placeholder="请选择规则类型" @change="handleRuleTypeChange">
<a-option value="static">静态规则</a-option>
<a-option value="dynamic">动态基线</a-option>
</a-select>
</a-form-item>
</a-col>
@@ -50,13 +51,12 @@
</a-col>
</a-row>
<!-- 指标/表达式 -->
<a-form-item v-if="formData.rule_type === 'static'" field="metric_name" label="指标名称" required>
<a-form-item field="metric_name" label="指标名称" required>
<a-input v-model="formData.metric_name" placeholder="请输入指标名称cpu_usage" allow-clear />
</a-form-item>
<!-- 阈值条件 -->
<a-row v-if="formData.rule_type !== 'promql'" :gutter="16">
<a-row :gutter="16">
<a-col :span="6">
<a-form-item field="compare_op" label="比较操作符" required>
<a-select v-model="formData.compare_op" placeholder="请选择">
@@ -70,8 +70,15 @@
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item field="threshold" label="阈值" required>
<a-input-number v-model="formData.threshold" placeholder="请输入阈值" :precision="2" allow-clear :style="{ width: '100%' }" />
<a-form-item field="threshold" :label="formData.rule_type === 'dynamic' ? '偏离比例(%' : '阈值'" required>
<a-input-number
v-model="formData.threshold"
:placeholder="formData.rule_type === 'dynamic' ? '请输入偏离比例' : '请输入阈值'"
:min="formData.rule_type === 'dynamic' ? 0 : undefined"
:precision="2"
allow-clear
:style="{ width: '100%' }"
/>
</a-form-item>
</a-col>
<a-col :span="12">
@@ -89,6 +96,45 @@
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="eval_interval" label="评估间隔(秒)" required>
<a-input-number v-model="formData.eval_interval" :min="1" :precision="0" :style="{ width: '100%' }" />
</a-form-item>
</a-col>
</a-row>
<a-row v-if="formData.rule_type === 'dynamic'" :gutter="16">
<a-col :span="6">
<a-form-item field="lookback_seconds" label="基线窗口(秒)" required>
<a-input-number v-model="formData.lookback_seconds" :min="formData.eval_interval" :precision="0" :style="{ width: '100%' }" />
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item field="baseline_aggregation" label="基线聚合" required>
<a-select v-model="formData.baseline_aggregation">
<a-option value="avg">平均值</a-option>
<a-option value="max">最大值</a-option>
<a-option value="min">最小值</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item field="current_aggregation" label="当前值聚合" required>
<a-select v-model="formData.current_aggregation">
<a-option value="avg">平均值</a-option>
<a-option value="max">最大值</a-option>
<a-option value="min">最小值</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item field="min_samples" label="最少样本数" required>
<a-input-number v-model="formData.min_samples" :min="1" :precision="0" :style="{ width: '100%' }" />
</a-form-item>
</a-col>
</a-row>
<a-form-item field="description" label="规则描述">
<a-textarea v-model="formData.description" placeholder="请输入规则描述" :auto-size="{ minRows: 2, maxRows: 4 }" allow-clear />
</a-form-item>
@@ -134,10 +180,15 @@ const formData = reactive({
enabled: true,
severity_id: 0 as number,
metric_name: '',
query_expr: '',
data_source: 'dc-control',
threshold: undefined as number | undefined,
compare_op: '>',
duration: 60,
eval_interval: 60,
lookback_seconds: 3600,
baseline_aggregation: 'avg',
current_aggregation: 'avg',
min_samples: 1,
description: '',
})
@@ -149,7 +200,7 @@ const formRules = {
metric_name: [
{
validator: (value: string, callback: any) => {
if (formData.rule_type !== 'promql' && !value) {
if (!value) {
callback('请输入指标名称')
return
}
@@ -157,21 +208,10 @@ const formRules = {
},
},
],
query_expr: [
{
validator: (value: string, callback: any) => {
if (formData.rule_type === 'promql' && !value) {
callback('请输入 PromQL 表达式')
return
}
callback()
},
},
],
compare_op: [
{
validator: (value: string, callback: any) => {
if (formData.rule_type !== 'promql' && !value) {
if (!value) {
callback('请选择比较操作符')
return
}
@@ -182,7 +222,7 @@ const formRules = {
threshold: [
{
validator: (value: number, callback: any) => {
if (formData.rule_type !== 'promql' && value === undefined) {
if (value === undefined) {
callback('请输入阈值')
return
}
@@ -246,16 +286,8 @@ const handleSeveritySearch = (keyword: string) => {
// 规则类型变化处理
const handleRuleTypeChange = (value: string) => {
// 根据规则类型清空相关字段
if (value === 'promql') {
formData.metric_name = ''
formData.compare_op = ''
formData.threshold = undefined
} else {
formData.query_expr = ''
if (!formData.compare_op) {
formData.compare_op = '>'
}
if (!formData.compare_op || (value === 'dynamic' && ['==', '!='].includes(formData.compare_op))) {
formData.compare_op = '>'
}
}
@@ -269,15 +301,29 @@ const loadRuleDetail = async () => {
const rule = res.details
formData.name = rule.name
// 当前产品只保留静态规则:强制按 static 渲染
formData.rule_type = 'static'
formData.rule_type = rule.rule_type === 'dynamic' ? 'dynamic' : 'static'
formData.enabled = rule.enabled
formData.severity_id = rule.severity_id
formData.metric_name = rule.metric_name || ''
formData.query_expr = rule.query_expr || ''
formData.data_source = rule.data_source || 'dc-control'
formData.threshold = rule.threshold
formData.compare_op = rule.compare_op || '>'
formData.duration = rule.duration || 60
formData.duration = rule.duration ?? 60
formData.eval_interval = rule.eval_interval ?? 60
if (rule.rule_type === 'dynamic') {
try {
const baseline = JSON.parse(rule.baseline_config || '{}')
formData.lookback_seconds = baseline.lookback_seconds || 3600
formData.baseline_aggregation = baseline.aggregation || 'avg'
formData.current_aggregation = baseline.current_aggregation || 'avg'
formData.min_samples = baseline.min_samples || 1
} catch {
formData.lookback_seconds = 3600
formData.baseline_aggregation = 'avg'
formData.current_aggregation = 'avg'
formData.min_samples = 1
}
}
formData.description = rule.description || ''
}
} catch (error) {
@@ -293,10 +339,15 @@ const resetForm = () => {
formData.enabled = true
formData.severity_id = 0
formData.metric_name = ''
formData.query_expr = ''
formData.data_source = 'dc-control'
formData.threshold = undefined
formData.compare_op = '>'
formData.duration = 60
formData.eval_interval = 60
formData.lookback_seconds = 3600
formData.baseline_aggregation = 'avg'
formData.current_aggregation = 'avg'
formData.min_samples = 1
formData.description = ''
}
@@ -327,10 +378,20 @@ const handleSubmit = async () => {
submitLoading.value = true
try {
const { lookback_seconds, baseline_aggregation, current_aggregation, min_samples, ...ruleFields } = formData
const data = {
...formData,
...ruleFields,
policy_id: props.policyId,
severity_id: formData.severity_id, // 确保 severity_id 是 number 类型
severity_id: formData.severity_id,
baseline_config:
formData.rule_type === 'dynamic'
? JSON.stringify({
lookback_seconds,
aggregation: baseline_aggregation,
current_aggregation,
min_samples,
})
: '{}',
}
// 按契约rule/create 不做 jsonb 默认值归一化。
@@ -338,7 +399,6 @@ const handleSubmit = async () => {
const createPayload = !isEdit.value
? {
...data,
baseline_config: '{}',
labels: '{}',
annotations: '{}',
}

View File

@@ -44,8 +44,8 @@
</a-table-column>
<a-table-column title="指标/表达式" data-index="metric_expr" :width="200">
<template #cell="{ record }">
<span :title="record.metric_name || record.query_expr">
{{ record.metric_name || record.query_expr || '-' }}
<span :title="record.metric_name">
{{ record.metric_name || '-' }}
</span>
</template>
</a-table-column>
@@ -134,7 +134,6 @@ const currentRuleId = ref<number | undefined>(undefined)
const ruleTypeMap: Record<string, string> = {
static: '静态规则',
dynamic: '动态规则',
promql: 'PromQL',
}
// 比较操作符映射

View File

@@ -75,7 +75,7 @@
<a-row :gutter="16">
<a-col :xs="24" :sm="12" :md="6">
<a-form-item label="规则类型" :field="`rules[${index}].rule_type`" :rules="getRuleFieldRules('rule_type')">
<a-select v-model="rule.rule_type" placeholder="请选择" @change="syncUpdate(index)">
<a-select v-model="rule.rule_type" placeholder="请选择" @change="handleRuleTypeChange(index)">
<a-option v-for="item in RULE_TYPES" :key="item.value" :value="item.value">
{{ item.label }}
</a-option>
@@ -92,8 +92,18 @@
</a-form-item>
</a-col>
<a-col :xs="24" :sm="12" :md="6">
<a-form-item label="阈值" :field="`rules[${index}].threshold`" :rules="getRuleFieldRules('threshold')">
<a-input-number v-model="rule.threshold" placeholder="请输入阈值" style="width: 100%" @change="syncUpdate(index)" />
<a-form-item
:label="rule.rule_type === 'dynamic' ? '偏离比例(%' : '阈值'"
:field="`rules[${index}].threshold`"
:rules="getRuleFieldRules('threshold')"
>
<a-input-number
v-model="rule.threshold"
:placeholder="rule.rule_type === 'dynamic' ? '请输入偏离比例' : '请输入阈值'"
:min="rule.rule_type === 'dynamic' ? 0 : undefined"
style="width: 100%"
@change="syncUpdate(index)"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :sm="12" :md="6">
@@ -115,7 +125,7 @@
<a-input-number
v-model="rule.eval_interval"
placeholder="请输入"
:min="0"
:min="1"
style="width: 100%"
@change="syncUpdate(index)"
>
@@ -123,6 +133,43 @@
</a-input-number>
</a-form-item>
</a-col>
<template v-if="rule.rule_type === 'dynamic' && rule._baseline">
<a-col :xs="24" :sm="12" :md="6">
<a-form-item label="基线回看时长" :field="`rules[${index}]._baseline.lookback_seconds`" required>
<a-input-number
v-model="rule._baseline.lookback_seconds"
:min="Math.max(rule.eval_interval || 1, 1)"
style="width: 100%"
@change="syncUpdate(index)"
>
<template #suffix></template>
</a-input-number>
</a-form-item>
</a-col>
<a-col :xs="24" :sm="12" :md="6">
<a-form-item label="历史基线聚合" :field="`rules[${index}]._baseline.aggregation`" required>
<a-select v-model="rule._baseline.aggregation" @change="syncUpdate(index)">
<a-option value="avg">平均值</a-option>
<a-option value="max">最大值</a-option>
<a-option value="min">最小值</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :xs="24" :sm="12" :md="6">
<a-form-item label="当前窗口聚合" :field="`rules[${index}]._baseline.current_aggregation`" required>
<a-select v-model="rule._baseline.current_aggregation" @change="syncUpdate(index)">
<a-option value="avg">平均值</a-option>
<a-option value="max">最大值</a-option>
<a-option value="min">最小值</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :xs="24" :sm="12" :md="6">
<a-form-item label="基线最小样本数" :field="`rules[${index}]._baseline.min_samples`" required>
<a-input-number v-model="rule._baseline.min_samples" :min="1" style="width: 100%" @change="syncUpdate(index)" />
</a-form-item>
</a-col>
</template>
</a-row>
</div>
</a-card>
@@ -148,6 +195,14 @@ import {
interface RuleItem extends AlertRule {
_metrics?: MetricMeta[]
_metricsLoading?: boolean
_baseline?: BaselineForm
}
interface BaselineForm {
lookback_seconds: number
aggregation: 'avg' | 'max' | 'min'
current_aggregation: 'avg' | 'max' | 'min'
min_samples: number
}
const props = defineProps<{
@@ -171,11 +226,36 @@ const baseSeverityOptions = ref<AlertSeverity[]>([])
const localSeverityOptions = ref<AlertSeverity[]>([])
const severityLoading = ref(false)
const defaultBaselineConfig = (): BaselineForm => ({
lookback_seconds: 3600,
aggregation: 'avg',
current_aggregation: 'avg',
min_samples: 1,
})
const parseBaselineConfig = (raw?: string): BaselineForm => {
try {
const parsed = JSON.parse(raw || '{}')
return {
lookback_seconds: Number(parsed.lookback_seconds) > 0 ? Number(parsed.lookback_seconds) : 3600,
aggregation: ['avg', 'max', 'min'].includes(parsed.aggregation) ? parsed.aggregation : 'avg',
current_aggregation: ['avg', 'max', 'min'].includes(parsed.current_aggregation) ? parsed.current_aggregation : 'avg',
min_samples: Number(parsed.min_samples) > 0 ? Number(parsed.min_samples) : 1,
}
} catch {
return defaultBaselineConfig()
}
}
// 监听 props 变化,深拷贝到本地
watch(
() => props.rules,
(val) => {
localRules.value = JSON.parse(JSON.stringify(val))
const copied: RuleItem[] = JSON.parse(JSON.stringify(val))
localRules.value = copied.map((rule) => ({
...rule,
_baseline: parseBaselineConfig(rule.baseline_config),
}))
},
{ immediate: true, deep: true }
)
@@ -268,8 +348,9 @@ defineExpose({
// 同步更新到父组件
const syncUpdate = (index: number) => {
const newRules = [...props.rules]
const { _metrics, _metricsLoading, ...rest } = localRules.value[index]
newRules[index] = { ...rest, _metrics, _metricsLoading } as RuleItem
const { _metrics, _metricsLoading, _baseline, ...rest } = localRules.value[index]
const baseline_config = rest.rule_type === 'dynamic' ? JSON.stringify(_baseline || defaultBaselineConfig()) : '{}'
newRules[index] = { ...rest, baseline_config, _metrics, _metricsLoading } as RuleItem
emit('update:rules', newRules)
}
@@ -287,6 +368,17 @@ const handleRuleDataSourceChange = (index: number) => {
handleLoadMetrics(index)
}
const handleRuleTypeChange = (index: number) => {
const rule = localRules.value[index]
if (rule.rule_type === 'dynamic') {
if (rule.compare_op === '==' || rule.compare_op === '!=') {
rule.compare_op = '>'
}
rule._baseline ||= parseBaselineConfig(rule.baseline_config)
}
syncUpdate(index)
}
// 下拉框展开时加载指标(备用触发)
const handleMetricsDropdownChange = (visible: boolean, index: number) => {
if (visible) {