From 375865bee5889d55d123a39e33010fac51d1a806 Mon Sep 17 00:00:00 2001 From: Myrat92 Date: Mon, 27 Apr 2026 11:46:38 +0800 Subject: [PATCH 1/3] feat: custom thresholds --- src/pages/alert/rule/create.jsx | 163 +++++++++++++++++++++++++++++++- 1 file changed, 161 insertions(+), 2 deletions(-) diff --git a/src/pages/alert/rule/create.jsx b/src/pages/alert/rule/create.jsx index f3bd8f5..d51290a 100644 --- a/src/pages/alert/rule/create.jsx +++ b/src/pages/alert/rule/create.jsx @@ -50,6 +50,8 @@ import {SearchViewMetrics} from "../preview/searchViewMetrics.tsx"; import {Breadcrumb} from "../../../components/Breadcrumb" const format = 'HH:mm'; +const THRESHOLD_MODE_GLOBAL = 'global'; +const THRESHOLD_MODE_NODE_OVERRIDE = 'node_override'; const MyFormItemContext = React.createContext([]) const { Option } = Select; @@ -75,6 +77,7 @@ export const AlertRule = ({ type }) => { const searchParams = new URLSearchParams(window.location.search); const { appState } = useAppContext(); const [form] = Form.useForm() + const thresholdMode = Form.useWatch(['prometheusConfig', 'thresholdMode'], form); const { id,ruleId } = useParams() const [selectedRow,setSelectedRow] = useState({}) const [enabled, setEnabled] = useState(true) // 设置初始状态为 true @@ -211,7 +214,11 @@ export const AlertRule = ({ type }) => { if (type !== "edit" && searchParams.get("isClone") !== "1") { // 设置执行频率默认值为5 form.setFieldsValue({ - evalInterval: 5 + evalInterval: 5, + prometheusConfig: { + thresholdMode: THRESHOLD_MODE_GLOBAL, + thresholdOverrides: [], + } }); } } @@ -252,6 +259,8 @@ export const AlertRule = ({ type }) => { annotations: selectedRow?.prometheusConfig?.annotations, forDuration: selectedRow?.prometheusConfig?.forDuration, rules: selectedRow?.prometheusConfig?.rules, + thresholdMode: selectedRow?.prometheusConfig?.thresholdMode || THRESHOLD_MODE_GLOBAL, + thresholdOverrides: selectedRow?.prometheusConfig?.thresholdOverrides || [], callbakPromQLs: selectedRow?.prometheusConfig?.callbakPromQLs || [], }, alicloudSLSConfig: { @@ -524,6 +533,7 @@ export const AlertRule = ({ type }) => { ...values, externalLabels: formattedLabels, evalInterval: Number(values.evalInterval), + prometheusConfig: normalizeThresholdOverrides(values.prometheusConfig), elasticSearchConfig: newEsConfig, effectiveTime: { week: week, @@ -627,6 +637,41 @@ export const AlertRule = ({ type }) => { setExprRule(updatedExprRule) } + const getDefaultOverrideSeverity = () => { + const rules = form.getFieldValue(['prometheusConfig', 'rules']) || exprRule || [] + return rules?.[0]?.severity || 'P0' + } + + const getDefaultOverrideDuration = () => { + const rules = form.getFieldValue(['prometheusConfig', 'rules']) || exprRule || [] + return rules?.[0]?.forDuration || 300 + } + + const normalizeThresholdOverrides = (prometheusConfig = {}) => { + if (prometheusConfig.thresholdMode !== THRESHOLD_MODE_NODE_OVERRIDE) { + return { + ...prometheusConfig, + thresholdMode: THRESHOLD_MODE_GLOBAL, + thresholdOverrides: [], + } + } + + const thresholdOverrides = (prometheusConfig.thresholdOverrides || []) + .map((item) => ({ + matchLabels: Object.fromEntries( + Object.entries(item?.matchLabels || {}).filter((entry) => entry[1] !== undefined && entry[1] !== '') + ), + rules: (item?.rules || []).filter((rule) => rule?.severity && rule?.expr), + })) + .filter((item) => Object.keys(item.matchLabels).length > 0 && item.rules.length > 0) + + return { + ...prometheusConfig, + thresholdMode: thresholdOverrides.length > 0 ? THRESHOLD_MODE_NODE_OVERRIDE : THRESHOLD_MODE_GLOBAL, + thresholdOverrides, + } + } + const handleChange = (value) => { setWeek(value) }; @@ -1175,6 +1220,120 @@ export const AlertRule = ({ type }) => { +
+ + +
+ 节点阈值覆盖 + { + form.setFieldsValue({ + prometheusConfig: { + thresholdMode: checked ? THRESHOLD_MODE_NODE_OVERRIDE : THRESHOLD_MODE_GLOBAL, + thresholdOverrides: checked ? form.getFieldValue(['prometheusConfig', 'thresholdOverrides']) || [] : [], + }, + }) + }} + /> +
+ + {thresholdMode === THRESHOLD_MODE_NODE_OVERRIDE && ( + + {(fields, { add, remove }) => ( + <> + {fields.map(({ key, name, ...restField }) => ( +
+ + + + + + + + + { + if (!value || validateExpr(value)) { + return Promise.resolve() + } + return Promise.reject(new Error('请输入有效的告警条件,例如:> 98')) + }, + }, + ]} + > + + + + + + + + +
+ ))} + + + + )} +
+ )} +
+
{
) -} \ No newline at end of file +} From c23837342f70daa2c7f07b40a8aa5bfd01b9b239 Mon Sep 17 00:00:00 2001 From: Myrat92 Date: Mon, 27 Apr 2026 13:58:55 +0800 Subject: [PATCH 2/3] feat: auto load instance --- src/pages/alert/rule/create.jsx | 72 +++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/src/pages/alert/rule/create.jsx b/src/pages/alert/rule/create.jsx index d51290a..5cfb731 100644 --- a/src/pages/alert/rule/create.jsx +++ b/src/pages/alert/rule/create.jsx @@ -10,13 +10,13 @@ import { InputNumber, Card, TimePicker, - Typography, Modal, message, Checkbox + Typography, Modal, message, Checkbox, AutoComplete } from 'antd' import React, { useState, useEffect } from 'react' import {MinusCircleOutlined, PlusOutlined, RedoOutlined} from '@ant-design/icons' import {createRule, searchRuleInfo, updateRule} from '../../../api/rule' import {getDatasource, getDatasourceList} from '../../../api/datasource' -import {getJaegerService} from '../../../api/other' +import {getJaegerService, queryPromMetrics} from '../../../api/other' import {useParams} from 'react-router-dom' import dayjs from 'dayjs'; import './index.css' @@ -149,6 +149,8 @@ export const AlertRule = ({ type }) => { const [filterCondition,setFilterCondition] = useState('') // 匹配关系 const [queryWildcard,setQueryWildcard] = useState(0) // 匹配模式 const [metricAddress,setMetricAddress] = useState("") + const [nodeInstanceOptions, setNodeInstanceOptions] = useState([]) + const [loadingNodeInstances, setLoadingNodeInstances] = useState(false) const [viewLogsModalKey, setViewLogsModalKey] = useState(0); const [viewMetricsModalKey, setViewMetricsModalKey] = useState(0); const [openSearchContentModal, setOpenSearchContentModal] = useState(false) @@ -887,6 +889,55 @@ export const AlertRule = ({ type }) => { setOpenMetricQueryModel(true) }; + const handleQueryNodeInstances = async () => { + const currentPromQL = handleGetPromQL() + if (!selectedItems || selectedItems.length === 0) { + message.error("请先选择数据源") + return + } + if (!currentPromQL) { + message.error("请先填写 PromQL") + return + } + + try { + setLoadingNodeInstances(true) + const res = await queryPromMetrics({ + datasourceIds: selectedItems.join(","), + query: currentPromQL, + }) + + if (res.code !== 200) { + message.error(res.msg || "查询节点失败") + return + } + + const instances = Array.from(new Set( + (res.data || []) + .flatMap((item) => item?.data?.result || []) + .map((item) => item?.metric?.instance) + .filter(Boolean) + )).sort() + + setNodeInstanceOptions(instances.map((instance) => ({ + label: instance, + value: instance, + }))) + + if (instances.length === 0) { + message.warning("当前 PromQL 未返回 instance 标签") + return + } + + message.success(`已加载 ${instances.length} 个节点`) + } catch (error) { + console.error(error) + message.error("查询节点失败") + } finally { + setLoadingNodeInstances(false) + } + } + const handleGetKubernetesEventTypes = async() =>{ try{ const res = await getKubernetesResourceList() @@ -1238,6 +1289,15 @@ export const AlertRule = ({ type }) => { }) }} /> + {thresholdMode === THRESHOLD_MODE_NODE_OVERRIDE && ( + + )} {thresholdMode === THRESHOLD_MODE_NODE_OVERRIDE && ( @@ -1260,7 +1320,13 @@ export const AlertRule = ({ type }) => { name={[name, 'matchLabels', 'instance']} rules={[{ required: true, message: '请输入节点 instance' }]} > - + + option?.value?.toUpperCase().includes(inputValue.toUpperCase()) + } + /> Date: Mon, 27 Apr 2026 14:42:21 +0800 Subject: [PATCH 3/3] feat: support label-based threshold overrides --- src/pages/alert/rule/create.jsx | 188 +++++++++++++++++++++++++------- 1 file changed, 148 insertions(+), 40 deletions(-) diff --git a/src/pages/alert/rule/create.jsx b/src/pages/alert/rule/create.jsx index 5cfb731..88080c8 100644 --- a/src/pages/alert/rule/create.jsx +++ b/src/pages/alert/rule/create.jsx @@ -149,8 +149,9 @@ export const AlertRule = ({ type }) => { const [filterCondition,setFilterCondition] = useState('') // 匹配关系 const [queryWildcard,setQueryWildcard] = useState(0) // 匹配模式 const [metricAddress,setMetricAddress] = useState("") - const [nodeInstanceOptions, setNodeInstanceOptions] = useState([]) - const [loadingNodeInstances, setLoadingNodeInstances] = useState(false) + const [metricLabelOptions, setMetricLabelOptions] = useState([]) + const [metricLabelValueOptions, setMetricLabelValueOptions] = useState({}) + const [loadingMetricLabels, setLoadingMetricLabels] = useState(false) const [viewLogsModalKey, setViewLogsModalKey] = useState(0); const [viewMetricsModalKey, setViewMetricsModalKey] = useState(0); const [openSearchContentModal, setOpenSearchContentModal] = useState(false) @@ -649,6 +650,49 @@ export const AlertRule = ({ type }) => { return rules?.[0]?.forDuration || 300 } + const getOverrideMatchLabelKey = (overrideIndex) => { + const matchLabels = form.getFieldValue(['prometheusConfig', 'thresholdOverrides', overrideIndex, 'matchLabels']) || {} + return Object.keys(matchLabels).find((key) => matchLabels[key] !== undefined) || '' + } + + const updateOverrideMatchLabelKey = (overrideIndex, nextKey) => { + const thresholdOverrides = form.getFieldValue(['prometheusConfig', 'thresholdOverrides']) || [] + const nextOverrides = [...thresholdOverrides] + const currentOverride = nextOverrides[overrideIndex] || {} + const currentLabels = currentOverride.matchLabels || {} + const currentValue = currentLabels[getOverrideMatchLabelKey(overrideIndex)] || '' + const nextMatchLabels = nextKey ? { [nextKey]: currentValue } : {} + + nextOverrides[overrideIndex] = { + ...currentOverride, + matchLabels: nextMatchLabels, + } + + form.setFieldsValue({ + prometheusConfig: { + thresholdOverrides: nextOverrides, + }, + }) + } + + const updateOverrideMatchLabelValue = (overrideIndex, labelKey, nextValue) => { + const thresholdOverrides = form.getFieldValue(['prometheusConfig', 'thresholdOverrides']) || [] + const nextOverrides = [...thresholdOverrides] + const currentOverride = nextOverrides[overrideIndex] || {} + const nextMatchLabels = labelKey ? { [labelKey]: nextValue } : {} + + nextOverrides[overrideIndex] = { + ...currentOverride, + matchLabels: nextMatchLabels, + } + + form.setFieldsValue({ + prometheusConfig: { + thresholdOverrides: nextOverrides, + }, + }) + } + const normalizeThresholdOverrides = (prometheusConfig = {}) => { if (prometheusConfig.thresholdMode !== THRESHOLD_MODE_NODE_OVERRIDE) { return { @@ -889,55 +933,103 @@ export const AlertRule = ({ type }) => { setOpenMetricQueryModel(true) }; - const handleQueryNodeInstances = async () => { + const handleQueryMetricLabels = async ({ silent = false } = {}) => { const currentPromQL = handleGetPromQL() if (!selectedItems || selectedItems.length === 0) { - message.error("请先选择数据源") + if (!silent) { + message.error("请先选择数据源") + } return } if (!currentPromQL) { - message.error("请先填写 PromQL") + if (!silent) { + message.error("请先填写 PromQL") + } return } try { - setLoadingNodeInstances(true) + setLoadingMetricLabels(true) const res = await queryPromMetrics({ datasourceIds: selectedItems.join(","), query: currentPromQL, }) if (res.code !== 200) { - message.error(res.msg || "查询节点失败") + if (!silent) { + message.error(res.msg || "查询标签失败") + } return } - const instances = Array.from(new Set( - (res.data || []) - .flatMap((item) => item?.data?.result || []) - .map((item) => item?.metric?.instance) - .filter(Boolean) - )).sort() - - setNodeInstanceOptions(instances.map((instance) => ({ - label: instance, - value: instance, + const labelValues = {} + ;(res.data || []) + .flatMap((item) => item?.data?.result || []) + .forEach((item) => { + Object.entries(item?.metric || {}).forEach(([key, value]) => { + if (!key || key === '__name__' || value === undefined || value === '') { + return + } + + if (!labelValues[key]) { + labelValues[key] = new Set() + } + labelValues[key].add(value) + }) + }) + + const nextLabelValueOptions = Object.fromEntries( + Object.entries(labelValues).map(([key, values]) => [ + key, + Array.from(values).sort().map((value) => ({ + label: value, + value, + })), + ]) + ) + const labelKeys = Object.keys(nextLabelValueOptions).sort() + + setMetricLabelOptions(labelKeys.map((key) => ({ + label: key, + value: key, }))) + setMetricLabelValueOptions(nextLabelValueOptions) - if (instances.length === 0) { - message.warning("当前 PromQL 未返回 instance 标签") + if (labelKeys.length === 0) { + if (!silent) { + message.warning("当前 PromQL 未返回可用于匹配的标签") + } return } - message.success(`已加载 ${instances.length} 个节点`) + if (!silent) { + message.success(`已加载 ${labelKeys.length} 个标签`) + } } catch (error) { console.error(error) - message.error("查询节点失败") + if (!silent) { + message.error("查询标签失败") + } } finally { - setLoadingNodeInstances(false) + setLoadingMetricLabels(false) } } + useEffect(() => { + if (selectedType !== 0 || thresholdMode !== THRESHOLD_MODE_NODE_OVERRIDE) { + return + } + if (!selectedItems || selectedItems.length === 0 || !handleGetPromQL()) { + return + } + + const timer = setTimeout(() => { + handleQueryMetricLabels({ silent: true }) + }, 600) + + return () => clearTimeout(timer) + }, [selectedType, thresholdMode, selectedItems, promQL]) + const handleGetKubernetesEventTypes = async() =>{ try{ const res = await getKubernetesResourceList() @@ -1277,26 +1369,19 @@ export const AlertRule = ({ type }) => {
- 节点阈值覆盖 + 标签阈值覆盖 { form.setFieldsValue({ prometheusConfig: { thresholdMode: checked ? THRESHOLD_MODE_NODE_OVERRIDE : THRESHOLD_MODE_GLOBAL, - thresholdOverrides: checked ? form.getFieldValue(['prometheusConfig', 'thresholdOverrides']) || [] : [], }, }) }} /> - {thresholdMode === THRESHOLD_MODE_NODE_OVERRIDE && ( - + {thresholdMode === THRESHOLD_MODE_NODE_OVERRIDE && loadingMetricLabels && ( + 标签加载中... )}
@@ -1309,26 +1394,49 @@ export const AlertRule = ({ type }) => { key={key} style={{ display: 'grid', - gridTemplateColumns: 'minmax(220px, 1.5fr) 120px minmax(180px, 1fr) 180px 48px', + gridTemplateColumns: 'minmax(160px, 0.9fr) minmax(220px, 1.3fr) 120px minmax(180px, 1fr) 180px 48px', gap: '10px', alignItems: 'start', marginBottom: '8px', }} > updateOverrideMatchLabelKey(name, value)} filterOption={(inputValue, option) => option?.value?.toUpperCase().includes(inputValue.toUpperCase()) } /> + + {() => { + const labelKey = getOverrideMatchLabelKey(name) + const labelValue = form.getFieldValue(['prometheusConfig', 'thresholdOverrides', name, 'matchLabels', labelKey]) || '' + + return ( + updateOverrideMatchLabelValue(name, labelKey, value)} + disabled={!labelKey} + filterOption={(inputValue, option) => + option?.value?.toUpperCase().includes(inputValue.toUpperCase()) + } + /> + ) + }} + + { block icon={} onClick={() => add({ - matchLabels: { instance: '' }, + matchLabels: {}, rules: [{ severity: getDefaultOverrideSeverity(), expr: '', @@ -1392,7 +1500,7 @@ export const AlertRule = ({ type }) => { }], })} > - 添加节点阈值 + 添加标签阈值 )}