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 severity_id: number
enabled?: boolean enabled?: boolean
metric_name?: string metric_name?: string
query_expr?: string data_source?: string
threshold?: number threshold?: number
compare_op?: string compare_op?: string
duration?: number duration?: number
eval_interval?: number
baseline_config?: string baseline_config?: string
labels?: string labels?: string
annotations?: string annotations?: string
@@ -86,10 +87,11 @@ export const updateRule = (data: {
severity_id?: number severity_id?: number
enabled?: boolean enabled?: boolean
metric_name?: string metric_name?: string
query_expr?: string data_source?: string
threshold?: number threshold?: number
compare_op?: string compare_op?: string
duration?: number duration?: number
eval_interval?: number
baseline_config?: string baseline_config?: string
labels?: string labels?: string
annotations?: string annotations?: string

View File

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

View File

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

View File

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

View File

@@ -75,7 +75,7 @@
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :xs="24" :sm="12" :md="6"> <a-col :xs="24" :sm="12" :md="6">
<a-form-item label="规则类型" :field="`rules[${index}].rule_type`" :rules="getRuleFieldRules('rule_type')"> <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"> <a-option v-for="item in RULE_TYPES" :key="item.value" :value="item.value">
{{ item.label }} {{ item.label }}
</a-option> </a-option>
@@ -92,8 +92,18 @@
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :xs="24" :sm="12" :md="6"> <a-col :xs="24" :sm="12" :md="6">
<a-form-item label="阈值" :field="`rules[${index}].threshold`" :rules="getRuleFieldRules('threshold')"> <a-form-item
<a-input-number v-model="rule.threshold" placeholder="请输入阈值" style="width: 100%" @change="syncUpdate(index)" /> :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-form-item>
</a-col> </a-col>
<a-col :xs="24" :sm="12" :md="6"> <a-col :xs="24" :sm="12" :md="6">
@@ -115,7 +125,7 @@
<a-input-number <a-input-number
v-model="rule.eval_interval" v-model="rule.eval_interval"
placeholder="请输入" placeholder="请输入"
:min="0" :min="1"
style="width: 100%" style="width: 100%"
@change="syncUpdate(index)" @change="syncUpdate(index)"
> >
@@ -123,6 +133,43 @@
</a-input-number> </a-input-number>
</a-form-item> </a-form-item>
</a-col> </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> </a-row>
</div> </div>
</a-card> </a-card>
@@ -148,6 +195,14 @@ import {
interface RuleItem extends AlertRule { interface RuleItem extends AlertRule {
_metrics?: MetricMeta[] _metrics?: MetricMeta[]
_metricsLoading?: boolean _metricsLoading?: boolean
_baseline?: BaselineForm
}
interface BaselineForm {
lookback_seconds: number
aggregation: 'avg' | 'max' | 'min'
current_aggregation: 'avg' | 'max' | 'min'
min_samples: number
} }
const props = defineProps<{ const props = defineProps<{
@@ -171,11 +226,36 @@ const baseSeverityOptions = ref<AlertSeverity[]>([])
const localSeverityOptions = ref<AlertSeverity[]>([]) const localSeverityOptions = ref<AlertSeverity[]>([])
const severityLoading = ref(false) 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 变化,深拷贝到本地 // 监听 props 变化,深拷贝到本地
watch( watch(
() => props.rules, () => props.rules,
(val) => { (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 } { immediate: true, deep: true }
) )
@@ -268,8 +348,9 @@ defineExpose({
// 同步更新到父组件 // 同步更新到父组件
const syncUpdate = (index: number) => { const syncUpdate = (index: number) => {
const newRules = [...props.rules] const newRules = [...props.rules]
const { _metrics, _metricsLoading, ...rest } = localRules.value[index] const { _metrics, _metricsLoading, _baseline, ...rest } = localRules.value[index]
newRules[index] = { ...rest, _metrics, _metricsLoading } as RuleItem const baseline_config = rest.rule_type === 'dynamic' ? JSON.stringify(_baseline || defaultBaselineConfig()) : '{}'
newRules[index] = { ...rest, baseline_config, _metrics, _metricsLoading } as RuleItem
emit('update:rules', newRules) emit('update:rules', newRules)
} }
@@ -287,6 +368,17 @@ const handleRuleDataSourceChange = (index: number) => {
handleLoadMetrics(index) 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) => { const handleMetricsDropdownChange = (visible: boolean, index: number) => {
if (visible) { if (visible) {