feat(monitor): support threashold range (#23854)

This commit is contained in:
Zexi Li
2025-11-27 15:11:28 +08:00
committed by GitHub
parent 859384643f
commit 92be2b6d11
10 changed files with 196 additions and 83 deletions
+2 -2
View File
@@ -34,7 +34,7 @@ type CommonAlertTerm struct {
Comparator string
Threshold float64
Filters []monitorapi.MetricQueryTag
FieldOpt string
FieldOpt monitorapi.CommonAlertFieldOpt
Name string
ConditionType string
From string
@@ -94,7 +94,7 @@ func newCommonAlertQuery(tem *CommonAlertTerm) *monitorapi.CommonAlertQuery {
Operator: tem.Operator,
}
if tem.FieldOpt != "" {
commonAlert.FieldOpt = monitorapi.CommonAlertFieldOpt_Division
commonAlert.FieldOpt = monitorapi.CommonAlertFieldOptDivision
}
if len(tem.ConditionType) != 0 {
commonAlert.ConditionType = tem.ConditionType
+10
View File
@@ -238,3 +238,13 @@ func init() {
return &AlertSetting{}
})
}
type EvaluatorType string
const (
EvaluatorTypeGT EvaluatorType = "gt"
EvaluatorTypeLT EvaluatorType = "lt"
EvaluatorTypeEQ EvaluatorType = "eq"
EvaluatorTypeWithinRange EvaluatorType = "within_range"
EvaluatorTypeOutsideRange EvaluatorType = "outside_range"
)
+6 -4
View File
@@ -77,10 +77,12 @@ type AlertRecordRule struct {
// 比较运算符, 比如: >, <, >=, <=
Comparator string `json:"comparator"`
// 报警阀值
Threshold string `json:"threshold"`
Period string `json:"period"`
AlertDuration int64 `json:"alert_duration"`
ConditionType string `json:"condition_type"`
Threshold string `json:"threshold"`
ThresholdRange []float64 `json:"threshold_range"`
Unit string `json:"unit"`
Period string `json:"period"`
AlertDuration int64 `json:"alert_duration"`
ConditionType string `json:"condition_type"`
// 静默期
SilentPeriod string `json:"silent_period"`
Reducer string `json:"reducer"`
+20 -14
View File
@@ -34,9 +34,6 @@ const (
CommonAlertDefaultRecipient = "commonalert-default"
//metirc fields 之间的运算
CommonAlertFieldOpt_Division = "/"
DEFAULT_SEND_NOTIFY_CHANNEL = "users"
METRIC_QUERY_TYPE_NO_DATA = "nodata_query"
@@ -47,6 +44,13 @@ const (
CommonAlertLevelFatal = "fatal"
)
type CommonAlertFieldOpt string
const (
//metirc fields 之间的运算
CommonAlertFieldOptDivision CommonAlertFieldOpt = "/"
)
var CommonAlertLevels = []string{CommonAlertLevelNormal, CommonAlertLevelImportant, CommonAlertLevelFatal}
type CommonAlertCreateBaseInput struct {
@@ -96,13 +100,15 @@ type CommonAlertQuery struct {
*AlertQuery
// metric points'value的运算方式
Reduce string `json:"reduce"`
// 比较运算符, 比如: >, <, >=, <=
// 比较运算符, 比如: >, <, >=, <=, within_range, outside_range
Comparator string `json:"comparator"`
// 报警阀值
// 报警阀值 (用于 gt, lt, eq)
Threshold float64 `json:"threshold"`
//field yunsuan
FieldOpt string `json:"field_opt"`
ConditionType string `json:"condition_type"`
// 范围参数 (用于 within_range, outside_range)
ThresholdRange []float64 `json:"threshold_range"`
// field 运算
FieldOpt CommonAlertFieldOpt `json:"field_opt"`
ConditionType string `json:"condition_type"`
// Operator should be chosen from 'and | or'
Operator string `json:"operator"`
}
@@ -161,12 +167,12 @@ type CommonAlertDetails struct {
}
type CommonAlertMetricDetails struct {
Operator string `json:"operator"`
Comparator string `json:"comparator"`
Threshold float64 `json:"threshold"`
WithinRange []float64 `json:"within_range"`
ConditionType string `json:"condition_type"`
ThresholdStr string `json:"threshold_str"`
Operator string `json:"operator"`
Comparator string `json:"comparator"`
Threshold float64 `json:"threshold"`
ThresholdRange []float64 `json:"threshold_range"`
ConditionType string `json:"condition_type"`
ThresholdStr string `json:"threshold_str"`
// metric points'value的运算方式
Reduce string `json:"reduce"`
DB string `json:"db"`
+4 -4
View File
@@ -168,15 +168,15 @@ const (
)
func GetNodeAlertEvaluator(comparator string, threshold float64) Condition {
typ := ConditionGreaterThan
typ := EvaluatorType(ConditionGreaterThan)
switch comparator {
case ">=", ">":
typ = ConditionGreaterThan
typ = EvaluatorType(ConditionGreaterThan)
case "<=", "<":
typ = ConditionLessThan
typ = EvaluatorType(ConditionLessThan)
}
return Condition{
Type: typ,
Type: string(typ),
Params: []float64{threshold},
}
}
+1 -1
View File
@@ -184,7 +184,7 @@ func (c *AlertCondition) ToCondition() monitor.AlertCondition {
func (c *AlertCondition) ToCommonAlertQuery() monitor.CommonAlertQuery {
aq := c.query.ToAlertQuery()
eval := c.evaluator.Type
eval := string(c.evaluator.Type)
if !utils.IsInStringArray(eval, []string{"lt", "gt", "eq"}) {
panic(fmt.Sprintf("Invalid evaluator %q", eval))
}
+2 -2
View File
@@ -134,10 +134,10 @@ func NewAlertEvaluator(cond *monitor.Condition) (AlertEvaluator, error) {
return nil, validators.ErrMissingParameterType
}
if utils.IsInStringArray(typ, validators.EvaluatorDefaultTypes) {
if utils.IsInStringArray(string(typ), validators.EvaluatorDefaultTypes) {
return newThresholdEvaluator(cond)
}
if utils.IsInStringArray(typ, validators.EvaluatorRangedTypes) {
if utils.IsInStringArray(string(typ), validators.EvaluatorRangedTypes) {
return newRangedEvaluator(cond)
}
+2 -2
View File
@@ -115,7 +115,7 @@ func (man *SAlertPanelManager) ValidateCreateData(
} else {
for _, query := range data.CommonMetricInputQuery.MetricQuery {
if len(query.Comparator) != 0 {
if !utils.IsInStringArray(getQueryEvalType(query.Comparator), validators.EvaluatorDefaultTypes) {
if !utils.IsInStringArray(string(getQueryEvalType(query.Comparator)), validators.EvaluatorDefaultTypes) {
return data, httperrors.NewInputParameterError("the Comparator is illegal: %s", query.Comparator)
}
}
@@ -274,7 +274,7 @@ func (dash *SAlertPanel) ValidateUpdateData(
return data, errors.Wrap(err, "metric_query Unmarshal error")
}
if len(query.Comparator) != 0 {
if !utils.IsInStringArray(getQueryEvalType(query.Comparator), validators.EvaluatorDefaultTypes) {
if !utils.IsInStringArray(string(getQueryEvalType(query.Comparator)), validators.EvaluatorDefaultTypes) {
return data, httperrors.NewInputParameterError("the Comparator is illegal: %s", query.Comparator)
}
}
+136 -43
View File
@@ -218,11 +218,8 @@ func (man *SCommonAlertManager) ValidateCreateData(
return data, merrors.NewArgIsEmptyErr("metric_query")
} else {
for _, query := range data.CommonMetricInputQuery.MetricQuery {
if query.ConditionType == monitor.METRIC_QUERY_TYPE_NO_DATA {
query.Comparator = "=="
}
if !utils.IsInStringArray(getQueryEvalType(query.Comparator), validators.EvaluatorDefaultTypes) {
return data, httperrors.NewInputParameterError("the Comparator is illegal: %s", query.Comparator)
if err := validateCommonAlertQuery(query); err != nil {
return data, err
}
if _, ok := monitor.AlertReduceFunc[query.Reduce]; !ok {
return data, httperrors.NewInputParameterError("the reduce is illegal: %s", query.Reduce)
@@ -453,7 +450,7 @@ func (alert *SCommonAlert) PostCreate(ctx context.Context,
for i, metricQ := range input.CommonMetricInputQuery.MetricQuery {
if metricQ.FieldOpt != "" {
if i == 0 {
fieldOpt = metricQ.FieldOpt
fieldOpt = string(metricQ.FieldOpt)
continue
}
fieldOpt = fmt.Sprintf("%s+%s", fieldOpt, metricQ.FieldOpt)
@@ -804,11 +801,23 @@ func getCommonAlertMetricDetailsFromCondition(
cmp = "=="
case "lt":
cmp = "<="
case "within_range":
cmp = "within_range"
case "outside_range":
cmp = "outside_range"
}
metricDetails.Comparator = cmp
if len(cond.Evaluator.Params) != 0 {
metricDetails.Threshold = cond.Evaluator.Params[0]
// 处理 ranged types
if utils.IsInStringArray(cond.Evaluator.Type, validators.EvaluatorRangedTypes) {
if len(cond.Evaluator.Params) >= 2 {
metricDetails.ThresholdRange = []float64{cond.Evaluator.Params[0], cond.Evaluator.Params[1]}
}
} else {
// 处理默认 types
if len(cond.Evaluator.Params) != 0 {
metricDetails.Threshold = cond.Evaluator.Params[0]
}
}
metricDetails.Reduce = cond.Reducer.Type
@@ -910,25 +919,94 @@ func getMetricDescriptionDetails(metricDetails *monitor.CommonAlertMetricDetails
}
func getExtraFieldDetails(metricDetails *monitor.CommonAlertMetricDetails) {
if metricDetails.FieldOpt == monitor.CommonAlertFieldOpt_Division && metricDetails.Threshold < float64(1) {
if metricDetails.FieldOpt == string(monitor.CommonAlertFieldOptDivision) && metricDetails.Threshold < float64(1) {
metricDetails.Threshold = metricDetails.Threshold * float64(100)
metricDetails.FieldDescription.Unit = "%"
}
}
func getQueryEvalType(evalType string) string {
typ := ""
func getQueryEvalType(evalType string) monitor.EvaluatorType {
var typ monitor.EvaluatorType
switch evalType {
case ">=", ">":
typ = "gt"
typ = monitor.EvaluatorTypeGT
case "<=", "<":
typ = "lt"
typ = monitor.EvaluatorTypeLT
case "==":
typ = "eq"
typ = monitor.EvaluatorTypeEQ
case "within_range":
typ = monitor.EvaluatorTypeWithinRange
case "outside_range":
typ = monitor.EvaluatorTypeOutsideRange
}
return typ
}
// validateCommonAlertQuery 校验 CommonAlertQuery 的 comparator 和 threshold_range
func validateCommonAlertQuery(query *monitor.CommonAlertQuery) error {
if query.ConditionType == monitor.METRIC_QUERY_TYPE_NO_DATA {
query.Comparator = "=="
}
evalType := getQueryEvalType(query.Comparator)
if !sets.NewString(append(
validators.EvaluatorDefaultTypes,
validators.EvaluatorRangedTypes...)...).Has(string(evalType)) {
return httperrors.NewInputParameterError("the Comparator is illegal: %s", query.Comparator)
}
// 验证 ranged types 的参数
if utils.IsInStringArray(string(evalType), validators.EvaluatorRangedTypes) {
if len(query.ThresholdRange) < 2 {
return httperrors.NewInputParameterError("threshold_range or outside_range requires 2 parameters, got %d", len(query.ThresholdRange))
}
// 确保第一项小于等于第二项
if query.ThresholdRange[0] > query.ThresholdRange[1] {
return httperrors.NewInputParameterError("threshold_range first value (%v) must be less than or equal to second value (%v)", query.ThresholdRange[0], query.ThresholdRange[1])
}
}
return nil
}
// validateComparatorAndThreshold 校验字符串形式的 comparator, threshold 和 threshold_range
func validateComparatorAndThreshold(comparator string, threshold string, thresholdRange []jsonutils.JSONObject) error {
var evalType monitor.EvaluatorType
if len(comparator) != 0 {
evalType = getQueryEvalType(comparator)
if !utils.IsInStringArray(string(evalType), append(validators.EvaluatorDefaultTypes, validators.EvaluatorRangedTypes...)) {
return httperrors.NewInputParameterError("the Comparator is illegal: %s", comparator)
}
// 验证 ranged types 的参数
if utils.IsInStringArray(string(evalType), validators.EvaluatorRangedTypes) {
if len(thresholdRange) < 2 {
return httperrors.NewInputParameterError("threshold_range or outside_range requires 2 parameters, got %d", len(thresholdRange))
}
}
}
if len(threshold) != 0 {
_, err := strconv.ParseFloat(threshold, 64)
if err != nil {
return httperrors.NewInputParameterError("threshold:%s should be number type", threshold)
}
}
if len(thresholdRange) > 0 {
if len(thresholdRange) < 2 {
return httperrors.NewInputParameterError("threshold_range requires 2 parameters, got %d", len(thresholdRange))
}
vals := make([]float64, len(thresholdRange))
for i, val := range thresholdRange {
parsedVal, err := strconv.ParseFloat(val.String(), 64)
if err != nil {
return httperrors.NewInputParameterError("threshold_range[%d]: %s should be number type", i, val.String())
}
vals[i] = parsedVal
}
// 确保第一项小于等于第二项
if vals[0] > vals[1] {
return httperrors.NewInputParameterError("threshold_range first value (%v) must be less than or equal to second value (%v)", vals[0], vals[1])
}
}
return nil
}
func (man *SCommonAlertManager) toAlertCreatInput(input monitor.CommonAlertCreateInput) (monitor.AlertCreateInput, error) {
freq, _ := time.ParseDuration(input.Period)
ret := new(monitor.AlertCreateInput)
@@ -944,13 +1022,29 @@ func (man *SCommonAlertManager) toAlertCreatInput(input monitor.CommonAlertCreat
if len(metricquery.ConditionType) != 0 {
conditionType = metricquery.ConditionType
}
evalType := getQueryEvalType(metricquery.Comparator)
var evaluatorParams []float64
// 处理 ranged types (within_range, outside_range)
if utils.IsInStringArray(string(evalType), validators.EvaluatorRangedTypes) {
if len(metricquery.ThresholdRange) < 2 {
return *ret, httperrors.NewInputParameterError("threshold_range or outside_range requires 2 parameters, got %d", len(metricquery.ThresholdRange))
}
fieldOpt := monitor.CommonAlertFieldOpt(metricquery.FieldOpt)
evaluatorParams = []float64{
fieldOperatorThreshold(fieldOpt, metricquery.ThresholdRange[0]),
fieldOperatorThreshold(fieldOpt, metricquery.ThresholdRange[1]),
}
} else {
// 处理默认 types (gt, lt, eq)
fieldOpt := monitor.CommonAlertFieldOpt(metricquery.FieldOpt)
evaluatorParams = []float64{fieldOperatorThreshold(fieldOpt, metricquery.Threshold)}
}
condition := monitor.AlertCondition{
Type: conditionType,
Query: *metricquery.AlertQuery,
Reducer: monitor.Condition{Type: metricquery.Reduce},
Evaluator: monitor.Condition{Type: getQueryEvalType(metricquery.Comparator),
Params: []float64{fieldOperatorThreshold(metricquery.FieldOpt, metricquery.Threshold)}},
Operator: "and",
Type: conditionType,
Query: *metricquery.AlertQuery,
Reducer: monitor.Condition{Type: metricquery.Reduce},
Evaluator: monitor.Condition{Type: string(evalType), Params: evaluatorParams},
Operator: "and",
}
if metricquery.Operator != "" {
if !sets.NewString("and", "or").Has(metricquery.Operator) {
@@ -959,15 +1053,15 @@ func (man *SCommonAlertManager) toAlertCreatInput(input monitor.CommonAlertCreat
condition.Operator = metricquery.Operator
}
if metricquery.FieldOpt != "" {
condition.Reducer.Operators = []string{metricquery.FieldOpt}
condition.Reducer.Operators = []string{string(metricquery.FieldOpt)}
}
ret.Settings.Conditions = append(ret.Settings.Conditions, condition)
}
return *ret, nil
}
func fieldOperatorThreshold(opt string, threshold float64) float64 {
if opt == monitor.CommonAlertFieldOpt_Division && threshold > 1 {
func fieldOperatorThreshold(opt monitor.CommonAlertFieldOpt, threshold float64) float64 {
if opt == monitor.CommonAlertFieldOptDivision && threshold > 1 {
return threshold / float64(100)
}
return threshold
@@ -1023,11 +1117,8 @@ func (alert *SCommonAlert) ValidateUpdateData(
if err != nil {
return data, errors.Wrap(err, "metric_query Unmarshal error")
}
if query.ConditionType == monitor.METRIC_QUERY_TYPE_NO_DATA {
query.Comparator = "=="
}
if !utils.IsInStringArray(getQueryEvalType(query.Comparator), validators.EvaluatorDefaultTypes) {
return data, httperrors.NewInputParameterError("the Comparator is illegal: %s", query.Comparator)
if err := validateCommonAlertQuery(query); err != nil {
return data, err
}
if _, ok := monitor.AlertReduceFunc[query.Reduce]; !ok {
return data, httperrors.NewInputParameterError("the reduce is illegal: %s", query.Reduce)
@@ -1276,21 +1367,14 @@ func (alert *SCommonAlert) PerformConfig(ctx context.Context, userCred mcclient.
period, _ := data.GetString("period")
comparator, _ := data.GetString("comparator")
threshold, _ := data.GetString("threshold")
thresholdRange, _ := data.GetArray("threshold_range")
if len(period) != 0 {
if _, err := time.ParseDuration(period); err != nil {
return data, httperrors.NewInputParameterError("Invalid period format: %s", period)
}
}
if len(comparator) != 0 {
if !utils.IsInStringArray(getQueryEvalType(comparator), validators.EvaluatorDefaultTypes) {
return data, httperrors.NewInputParameterError("the Comparator is illegal: %s", comparator)
}
}
if len(threshold) != 0 {
_, err := strconv.ParseFloat(threshold, 64)
if err != nil {
return data, httperrors.NewInputParameterError("threshold:%s should be number type", threshold)
}
if err := validateComparatorAndThreshold(comparator, threshold, thresholdRange); err != nil {
return data, err
}
_, err := db.Update(alert, func() error {
if len(period) != 0 {
@@ -1299,12 +1383,19 @@ func (alert *SCommonAlert) PerformConfig(ctx context.Context, userCred mcclient.
}
setting, _ := alert.GetSettings()
if len(comparator) != 0 {
setting.Conditions[0].Evaluator.Type = getQueryEvalType(comparator)
evalType := getQueryEvalType(comparator)
setting.Conditions[0].Evaluator.Type = string(evalType)
}
if len(threshold) != 0 {
// 处理 ranged types
if len(thresholdRange) >= 2 {
vals := make([]float64, 2)
for i := 0; i < 2 && i < len(thresholdRange); i++ {
val, _ := strconv.ParseFloat(thresholdRange[i].String(), 64)
vals[i] = fieldOperatorThreshold("", val)
}
setting.Conditions[0].Evaluator.Params = vals
} else if len(threshold) != 0 {
val, _ := strconv.ParseFloat(threshold, 64)
fmt.Println(threshold)
setting.Conditions[0].Evaluator.Params = []float64{fieldOperatorThreshold("", val)}
}
alert.Settings = jsonutils.Marshal(setting)
@@ -1509,7 +1600,9 @@ func (alert *SCommonAlert) GetAlertRule(settings *monitor.AlertSetting, index in
Field: alertDetails.Field,
FieldDesc: alertDetails.FieldDescription.DisplayName,
Comparator: alertDetails.Comparator,
Unit: alertDetails.FieldDescription.Unit,
Threshold: RationalizeValueFromUnit(alertDetails.Threshold, alertDetails.FieldDescription.Unit, ""),
ThresholdRange: alertDetails.ThresholdRange,
ConditionType: alertDetails.ConditionType,
Reducer: alertDetails.Reduce,
}
@@ -1557,7 +1650,7 @@ func RationalizeValueFromUnit(value float64, unit string, opt string) string {
}
return FormatFileSize(value, unit, float64(1000))
}
if unit == "%" && monitor.CommonAlertFieldOpt_Division == opt {
if unit == "%" && monitor.CommonAlertFieldOptDivision == monitor.CommonAlertFieldOpt(opt) {
return fmt.Sprintf("%0.2f%s", value*100, unit)
}
return fmt.Sprintf("%0.2f%s", value, unit)
+13 -11
View File
@@ -36,8 +36,8 @@ const (
)
var (
EvaluatorDefaultTypes = []string{"gt", "lt", "eq"}
EvaluatorRangedTypes = []string{"within_range", "outside_range"}
EvaluatorDefaultTypes = []string{string(monitor.EvaluatorTypeGT), string(monitor.EvaluatorTypeLT), string(monitor.EvaluatorTypeEQ)}
EvaluatorRangedTypes = []string{string(monitor.EvaluatorTypeWithinRange), string(monitor.EvaluatorTypeOutsideRange)}
CommonAlertType = []string{
monitor.CommonAlertNomalAlertType,
@@ -152,10 +152,10 @@ func ValidateAlertConditionEvaluator(input monitor.Condition) error {
if typ == "" {
return ErrMissingParameterType
}
if utils.IsInStringArray(typ, EvaluatorDefaultTypes) {
if utils.IsInStringArray(string(typ), EvaluatorDefaultTypes) {
return ValidateAlertConditionThresholdEvaluator(input)
}
if utils.IsInStringArray(typ, EvaluatorRangedTypes) {
if utils.IsInStringArray(string(typ), EvaluatorRangedTypes) {
return ValidateAlertConditionRangedEvaluator(input)
}
if typ != "no_value" {
@@ -176,14 +176,14 @@ func ValidateAlertConditionType(typ string) error {
func ValidateAlertConditionThresholdEvaluator(input monitor.Condition) error {
if len(input.Params) == 0 {
return errors.Wrapf(ErrMissingParameterThreshold, "Evaluator %s", HumanThresholdType(input.Type))
return errors.Wrapf(ErrMissingParameterThreshold, "Evaluator %s", HumanThresholdType(monitor.EvaluatorType(input.Type)))
}
return nil
}
func ValidateAlertConditionRangedEvaluator(input monitor.Condition) error {
if len(input.Params) == 0 {
return errors.Wrapf(ErrMissingParameterThreshold, "Evaluator %s", HumanThresholdType(input.Type))
return errors.Wrapf(ErrMissingParameterThreshold, "Evaluator %s", HumanThresholdType(monitor.EvaluatorType(input.Type)))
}
if len(input.Params) == 1 {
return errors.Wrap(ErrMissingParameterThreshold, "RangedEvaluator parameter second parameter is missing")
@@ -193,16 +193,18 @@ func ValidateAlertConditionRangedEvaluator(input monitor.Condition) error {
// HumanThresholdType converts a threshold "type" string to a string that matches the UI
// so errors are less confusing.
func HumanThresholdType(typ string) string {
func HumanThresholdType(typ monitor.EvaluatorType) string {
switch typ {
case "gt":
case monitor.EvaluatorTypeGT:
return "IS ABOVE"
case "lt":
case monitor.EvaluatorTypeLT:
return "IS BELOW"
case "within_range":
case monitor.EvaluatorTypeWithinRange:
return "IS WITHIN RANGE"
case "outside_range":
case monitor.EvaluatorTypeOutsideRange:
return "IS OUTSIDE RANGE"
case monitor.EvaluatorTypeEQ:
return "IS EQUAL TO"
}
return ""
}