feat: configure static and dynamic alert rules
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 表达式' },
|
||||
]
|
||||
|
||||
// ==================== 告警模板接口 ====================
|
||||
|
||||
@@ -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: '{}',
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
// 比较操作符映射
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user