From d3c3a201b88ff77d02d2c4c4fa51a5967f947ba6 Mon Sep 17 00:00:00 2001 From: Zexi Li Date: Mon, 30 Mar 2020 21:01:32 +0800 Subject: [PATCH] monitor: add alert config helper --- cmd/climc/shell/monitor/alert.go | 31 ++ cmd/climc/shell/monitor/alertnotification.go | 60 +++ pkg/apis/monitor/alert.go | 48 ++ pkg/apis/monitor/alertnotification.go | 26 + pkg/apis/monitor/template.go | 8 - pkg/apis/monitor/zz_generated.model.go | 77 +++ pkg/mcclient/modules/managers.go | 7 + pkg/mcclient/modules/monitor/alert.go | 75 ++- pkg/mcclient/modules/monitor/helper.go | 473 +++++++++++++++++++ pkg/mcclient/modules/monitor/helper_test.go | 156 ++++++ pkg/mcclient/options/monitor/alert.go | 103 ++++ pkg/monitor/alerting/conditions/query.go | 51 +- pkg/monitor/alerting/eval_context.go | 6 +- pkg/monitor/alerting/eval_handler_test.go | 6 +- pkg/monitor/alerting/interfaces.go | 3 +- pkg/monitor/alerting/job.go | 14 - pkg/monitor/alerting/rule.go | 40 ++ pkg/monitor/models/alert.go | 85 ++++ pkg/monitor/models/alertjoint.go | 4 +- pkg/monitor/models/alertnotification.go | 49 ++ pkg/monitor/models/notification.go | 25 + pkg/monitor/tsdb/driver/influxdb/influxdb.go | 2 - pkg/monitor/tsdb/interval.go | 2 +- pkg/monitor/validators/validators.go | 16 + 24 files changed, 1317 insertions(+), 50 deletions(-) create mode 100644 cmd/climc/shell/monitor/alertnotification.go create mode 100644 pkg/apis/monitor/alertnotification.go create mode 100644 pkg/apis/monitor/zz_generated.model.go create mode 100644 pkg/mcclient/modules/monitor/helper.go create mode 100644 pkg/mcclient/modules/monitor/helper_test.go diff --git a/cmd/climc/shell/monitor/alert.go b/cmd/climc/shell/monitor/alert.go index d37147dfa4..cde0861205 100644 --- a/cmd/climc/shell/monitor/alert.go +++ b/cmd/climc/shell/monitor/alert.go @@ -15,6 +15,9 @@ package monitor import ( + "fmt" + + monitorapi "yunion.io/x/onecloud/pkg/apis/monitor" "yunion.io/x/onecloud/pkg/mcclient" "yunion.io/x/onecloud/pkg/mcclient/modules/monitor" options "yunion.io/x/onecloud/pkg/mcclient/options/monitor" @@ -36,6 +39,20 @@ func init() { return nil }) + R(&options.AlertCreateOptions{}, aN("create"), "Create alert rule", + func(s *mcclient.ClientSession, args *options.AlertCreateOptions) error { + params, err := args.Params() + if err != nil { + return err + } + ret, err := monitor.Alerts.DoCreate(s, params) + if err != nil { + return err + } + printObject(ret) + return nil + }) + R(&options.AlertShowOptions{}, aN("show"), "Show details of a alert rule", func(s *mcclient.ClientSession, args *options.AlertShowOptions) error { ret, err := monitor.Alerts.Get(s, args.ID, nil) @@ -66,4 +83,18 @@ func init() { printBatchResults(ret, monitor.Alerts.GetColumns(s)) return nil }) + + R(&options.AlertTestRunOptions{}, aN("test-run"), "Test run alert", + func(s *mcclient.ClientSession, args *options.AlertTestRunOptions) error { + data := new(monitorapi.AlertTestRunInput) + if args.Debug { + data.IsDebug = true + } + ret, err := monitor.Alerts.DoTestRun(s, args.ID, data) + if err != nil { + return err + } + fmt.Println(ret.JSON(ret).YAMLString()) + return nil + }) } diff --git a/cmd/climc/shell/monitor/alertnotification.go b/cmd/climc/shell/monitor/alertnotification.go new file mode 100644 index 0000000000..5046b64f4e --- /dev/null +++ b/cmd/climc/shell/monitor/alertnotification.go @@ -0,0 +1,60 @@ +package monitor + +import ( + monitorapi "yunion.io/x/onecloud/pkg/apis/monitor" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/mcclient/modulebase" + "yunion.io/x/onecloud/pkg/mcclient/modules/monitor" + options "yunion.io/x/onecloud/pkg/mcclient/options/monitor" +) + +func init() { + initAlertNotification() +} + +func initAlertNotification() { + aN := cmdN("alert-notification") + R(&options.AlertNotificationListOptions{}, aN("list"), "List alert notification pairs", + func(s *mcclient.ClientSession, args *options.AlertNotificationListOptions) error { + params, err := args.Params() + if err != nil { + return err + } + var result *modulebase.ListResult + if len(args.Alert) > 0 { + result, err = monitor.Alertnotification.ListDescendent(s, args.Alert, params) + } else if len(args.Notification) > 0 { + result, err = monitor.Alertnotification.ListDescendent2(s, args.Notification, params) + } else { + result, err = monitor.Alertnotification.List(s, params) + } + if err != nil { + return err + } + printList(result, monitor.Alertnotification.GetColumns(s)) + return nil + }) + + R(&options.AlertNotificationAttachOptions{}, aN("attach"), "Attach a notification to a alert", + func(s *mcclient.ClientSession, args *options.AlertNotificationAttachOptions) error { + input := &monitorapi.AlertnotificationCreateInput{ + UsedBy: args.UsedBy, + } + ret, err := monitor.Alertnotification.Attach(s, args.ALERT, args.NOTIFICATION, input.JSON(input)) + if err != nil { + return err + } + printObject(ret) + return nil + }) + + R(&options.AlertNotificationAttachOptions{}, aN("detach"), "Detach a notification to a alert", + func(s *mcclient.ClientSession, args *options.AlertNotificationAttachOptions) error { + ret, err := monitor.Alertnotification.Detach(s, args.ALERT, args.NOTIFICATION, nil) + if err != nil { + return err + } + printObject(ret) + return nil + }) +} diff --git a/pkg/apis/monitor/alert.go b/pkg/apis/monitor/alert.go index 1291fb2eb7..db378d6479 100644 --- a/pkg/apis/monitor/alert.go +++ b/pkg/apis/monitor/alert.go @@ -15,6 +15,8 @@ package monitor import ( + "time" + "yunion.io/x/pkg/errors" "yunion.io/x/onecloud/pkg/apis" @@ -135,3 +137,49 @@ type AlertListInput struct { // 以报警是否启用/禁用过滤列表 Enabled *bool `json:"enabled"` } + +type AlertTestRunInput struct { + apis.Meta + + IsDebug bool `json:"is_debug"` +} + +// ResultLogEntry represents log data for the alert evaluation. +type ResultLogEntry struct { + Message string `json:"message"` + Data interface{} `json:"data"` +} + +// EvalMatch represents the series violating the threshold. +type EvalMatch struct { + Condition string `json:"condition"` + Value *float64 `json:"value"` + Metric string `json:"metric"` + Tags map[string]string `json:"tags"` +} + +type AlertTestRunOutput struct { + apis.Meta + + Firing bool `json:"firing"` + EvalMatches []*EvalMatch `json:"eval_matches"` + Logs []*ResultLogEntry `json:"logs"` + Error error `json:"error"` + ConditionEvals string `json:"condition_evals"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` +} + +type AlertAttachNotificationInput struct { + apis.Meta + + NotificationId string `json:"notification_id"` + UsedBy string `json:"used_by"` +} + +type AlertAttachNotificationOutput struct { + apis.Meta + + NotificationId string `json:"notification_id"` + UsedBy string `json:"used_by"` +} diff --git a/pkg/apis/monitor/alertnotification.go b/pkg/apis/monitor/alertnotification.go new file mode 100644 index 0000000000..43542c985b --- /dev/null +++ b/pkg/apis/monitor/alertnotification.go @@ -0,0 +1,26 @@ +package monitor + +import "yunion.io/x/onecloud/pkg/apis" + +type AlertJointResourceBaseDetails struct { + SAlertJointsBase + Alert string `json:"alert"` +} + +type AlertnotificationDetails struct { + AlertJointResourceBaseDetails + Notification string `json:"notification"` +} + +type AlertJointCreateInput struct { + apis.Meta + + AlertId string `json:"alert_id"` +} + +type AlertnotificationCreateInput struct { + AlertJointCreateInput + + NotificationId string `json:"notification_id"` + UsedBy string `json:"used_by"` +} diff --git a/pkg/apis/monitor/template.go b/pkg/apis/monitor/template.go index 2d13763156..f085b2d3e9 100644 --- a/pkg/apis/monitor/template.go +++ b/pkg/apis/monitor/template.go @@ -32,11 +32,3 @@ type NotificationTemplateConfig struct { Level string `json:"level"` IsRecovery bool `json:"is_recovery"` } - -// EvalMatch represents the series violating the threshold. -type EvalMatch struct { - Condition string `json:"condition"` - Value *float64 `json:"value"` - Metric string `json:"metric"` - Tags map[string]string `json:"tags"` -} diff --git a/pkg/apis/monitor/zz_generated.model.go b/pkg/apis/monitor/zz_generated.model.go new file mode 100644 index 0000000000..e95f369163 --- /dev/null +++ b/pkg/apis/monitor/zz_generated.model.go @@ -0,0 +1,77 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by model-api-gen. DO NOT EDIT. + +package monitor + +import ( + time "time" + + "yunion.io/x/onecloud/pkg/apis" +) + +// SAlert is an autogenerated struct via yunion.io/x/onecloud/pkg/monitor/models.SAlert. +type SAlert struct { + apis.SVirtualResourceBase + Frequency int64 `json:"frequency"` + Settings interface{} `json:"settings"` + Level string `json:"level"` + Message string `json:"message"` + UsedBy string `json:"used_by"` + // Silenced bool + ExecutionError string `json:"execution_error"` + For int64 `json:"for"` + EvalData interface{} `json:"eval_data"` + State string `json:"state"` + NoDataState string `json:"no_data_state"` + ExecutionErrorState string `json:"execution_error_state"` + LastStateChange time.Time `json:"last_state_change"` + StateChanges int `json:"state_changes"` +} + +// SAlertJointsBase is an autogenerated struct via yunion.io/x/onecloud/pkg/monitor/models.SAlertJointsBase. +type SAlertJointsBase struct { + apis.SVirtualJointResourceBase + AlertId string `json:"alert_id"` +} + +// SAlertnotification is an autogenerated struct via yunion.io/x/onecloud/pkg/monitor/models.SAlertnotification. +type SAlertnotification struct { + SAlertJointsBase + NotificationId string `json:"notification_id"` + State string `json:"state"` + Index byte `json:"index"` + UsedBy string `json:"used_by"` +} + +// SDataSource is an autogenerated struct via yunion.io/x/onecloud/pkg/monitor/models.SDataSource. +type SDataSource struct { + apis.SStandaloneResourceBase + Type string `json:"type"` + Url string `json:"url"` + User string `json:"user"` + Password string `json:"password"` + Database string `json:"database"` + IsDefault *bool `json:"is_default,omitempty"` +} + +// SNotification is an autogenerated struct via yunion.io/x/onecloud/pkg/monitor/models.SNotification. +type SNotification struct { + apis.SVirtualResourceBase + Type string `json:"type"` + IsDefault bool `json:"is_default"` + SendReminder bool `json:"send_reminder"` + DisableResolveMessage bool `json:"disable_resolve_message"` + Frequency int64 `json:"frequency"` + Settings interface{} `json:"settings"` +} diff --git a/pkg/mcclient/modules/managers.go b/pkg/mcclient/modules/managers.go index 5cec2d777e..f8753d62f6 100644 --- a/pkg/mcclient/modules/managers.go +++ b/pkg/mcclient/modules/managers.go @@ -54,6 +54,13 @@ func NewMonitorV2Manager(keyword, keywordPlural string, columns, adminColumns [] Keyword: keyword, KeywordPlural: keywordPlural} } +func NewJointMonitorV2Manager(keyword, keywordPlural string, columns, adminColumns []string, master, slave modulebase.Manager) modulebase.JointResourceManager { + return modulebase.JointResourceManager{ + ResourceManager: NewMonitorV2Manager(keyword, keywordPlural, columns, adminColumns), + Master: master, + Slave: slave} +} + func NewCloudwatcherManager(keyword, keywordPlural string, columns, adminColumns []string) modulebase.ResourceManager { return modulebase.ResourceManager{ BaseManager: *modulebase.NewBaseManager("cloudwatcher", "", "v1", columns, adminColumns), diff --git a/pkg/mcclient/modules/monitor/alert.go b/pkg/mcclient/modules/monitor/alert.go index 070a1a6b44..9880369d26 100644 --- a/pkg/mcclient/modules/monitor/alert.go +++ b/pkg/mcclient/modules/monitor/alert.go @@ -15,27 +15,88 @@ package monitor import ( + "yunion.io/x/jsonutils" + + "yunion.io/x/onecloud/pkg/apis/monitor" + "yunion.io/x/onecloud/pkg/mcclient" "yunion.io/x/onecloud/pkg/mcclient/modulebase" "yunion.io/x/onecloud/pkg/mcclient/modules" ) var ( - Alerts modulebase.ResourceManager - Notifications modulebase.ResourceManager + Alerts *SAlertManager + Notifications *SNotificationManager + Alertnotification *SAlertnotificationManager ) -func init() { - Alerts = modules.NewMonitorV2Manager("alert", "alerts", +type SAlertManager struct { + *modulebase.ResourceManager +} + +func NewAlertManager() *SAlertManager { + man := modules.NewMonitorV2Manager("alert", "alerts", []string{"id", "name", "state", "frequency", "enabled", "settings"}, []string{}) - Notifications = modules.NewMonitorV2Manager( + return &SAlertManager{ + ResourceManager: &man, + } +} + +type SNotificationManager struct { + *modulebase.ResourceManager +} + +func NewNotificationManager() *SNotificationManager { + man := modules.NewMonitorV2Manager( "alert_notification", "alert_notifications", []string{"id", "name", "type", "is_default", "disable_resolve_message", "send_reminder", "settings"}, []string{}) - for _, m := range []modulebase.ResourceManager{ + return &SNotificationManager{ + ResourceManager: &man, + } +} + +type SAlertnotificationManager struct { + *modulebase.JointResourceManager +} + +func NewAlertnotificationManager() *SAlertnotificationManager { + man := modules.NewJointMonitorV2Manager("alertnotification", "alertnotifications", + []string{"Alert_ID", "Alert", "Notification_ID", "Notification", "Used_by", "State"}, + []string{}, + Alerts, Notifications) + return &SAlertnotificationManager{&man} +} + +func init() { + Alerts = NewAlertManager() + Notifications = NewNotificationManager() + for _, m := range []modulebase.IBaseManager{ Alerts, Notifications, } { - modules.Register(&m) + modules.Register(m) + } + + Alertnotification = NewAlertnotificationManager() + for _, m := range []modulebase.IBaseManager{ + Alertnotification, + } { + modules.Register(m) } } + +func (m *SAlertManager) DoCreate(s *mcclient.ClientSession, config *AlertConfig) (jsonutils.JSONObject, error) { + input := config.ToAlertCreateInput() + return m.Create(s, input.JSON(input)) +} + +func (m *SAlertManager) DoTestRun(s *mcclient.ClientSession, id string, input *monitor.AlertTestRunInput) (*monitor.AlertTestRunOutput, error) { + ret, err := m.PerformAction(s, id, "test-run", input.JSON(input)) + if err != nil { + return nil, err + } + out := new(monitor.AlertTestRunOutput) + err = ret.Unmarshal(out) + return out, err +} diff --git a/pkg/mcclient/modules/monitor/helper.go b/pkg/mcclient/modules/monitor/helper.go new file mode 100644 index 0000000000..edc26fc578 --- /dev/null +++ b/pkg/mcclient/modules/monitor/helper.go @@ -0,0 +1,473 @@ +package monitor + +import ( + "fmt" + "time" + + "yunion.io/x/onecloud/pkg/apis/monitor" + "yunion.io/x/onecloud/pkg/monitor/tsdb" +) + +// AlertConfig is a helper to generate monitor service alert related api input +type AlertConfig struct { + name string + frequency int64 + level string + enabled bool + conditions []*AlertCondition +} + +func NewAlertConfig(name string, frequency string, enabled bool) (*AlertConfig, error) { + freq, err := time.ParseDuration(frequency) + if err != nil { + return nil, err + } + input := &AlertConfig{ + name: name, + frequency: int64(freq / time.Second), + level: "", + enabled: enabled, + conditions: make([]*AlertCondition, 0), + } + return input, nil +} + +func (c *AlertConfig) Level(l string) *AlertConfig { + c.level = l + return c +} + +func (c *AlertConfig) Enable(e bool) *AlertConfig { + c.enabled = e + return c +} + +func (c *AlertConfig) ToAlertCreateInput() monitor.AlertCreateInput { + return monitor.AlertCreateInput{ + Name: c.name, + Frequency: c.frequency, + Settings: c.ToAlertSetting(), + Enabled: &c.enabled, + Level: c.level, + } +} + +func (c *AlertConfig) ToAlertSetting() monitor.AlertSetting { + conds := make([]monitor.AlertCondition, len(c.conditions)) + for i, cc := range c.conditions { + conds[i] = cc.ToCondition() + } + return monitor.AlertSetting{ + Conditions: conds, + } +} + +func (c *AlertConfig) Condition(database string, measurement string) *AlertCondition { + cc := NewAlertCondition(database, measurement) + c.conditions = append(c.conditions, cc) + return cc +} + +func (c *AlertConfig) AND(cs ...*AlertCondition) *AlertConfig { + if len(cs) == 0 { + return c + } + for _, cond := range cs { + cond.setOperator("AND") + } + return c +} + +func (c *AlertConfig) OR(cs ...*AlertCondition) *AlertConfig { + if len(cs) == 0 { + return c + } + for _, cond := range cs { + cond.setOperator("OR") + } + return c +} + +type AlertCondition struct { + operator string + reducer *monitor.Condition + evaluator *monitor.Condition + query *AlertQuery +} + +func NewAlertCondition( + database string, + measurement string, +) *AlertCondition { + c := &AlertCondition{ + query: NewAlertQuery(database, measurement), + } + // set default avg reducer + c.Avg() + return c +} + +func (c *AlertCondition) ToCondition() monitor.AlertCondition { + return monitor.AlertCondition{ + Type: "query", + Query: c.query.ToAlertQuery(), + Reducer: *c.reducer, + Evaluator: *c.evaluator, + Operator: c.operator, + } +} + +func (c *AlertCondition) setOperator(op string) *AlertCondition { + c.operator = op + return c +} + +func (c *AlertCondition) setReducer(typ string, params ...float64) *AlertCondition { + c.reducer = &monitor.Condition{ + Type: typ, + } + c.reducer.Params = params + return c +} + +func (c *AlertCondition) Avg() *AlertCondition { + return c.setReducer("avg") +} + +func (c *AlertCondition) Sum() *AlertCondition { + return c.setReducer("sum") +} + +func (c *AlertCondition) Min() *AlertCondition { + return c.setReducer("min") +} + +func (c *AlertCondition) Max() *AlertCondition { + return c.setReducer("max") +} + +func (c *AlertCondition) Count() *AlertCondition { + return c.setReducer("count") +} + +func (c *AlertCondition) Last() *AlertCondition { + return c.setReducer("last") +} + +func (c *AlertCondition) Median() *AlertCondition { + return c.setReducer("median") +} + +func (c *AlertCondition) setEvaluator(typ string, threshold float64) *AlertCondition { + c.evaluator = &monitor.Condition{ + Type: typ, + Params: []float64{threshold}, + } + return c +} + +// LessThan is evaluator part +func (c *AlertCondition) LT(threshold float64) *AlertCondition { + return c.setEvaluator("lt", threshold) +} + +// GreaterThan is evaluator part +func (c *AlertCondition) GT(threshold float64) *AlertCondition { + return c.setEvaluator("gt", threshold) +} + +func (c *AlertCondition) Query() *AlertQuery { + return c.query +} + +type AlertQuery struct { + from string + to string + alias string + tz string + database string + measurement string + interval string + policy string + resultFormat string + + selects *AlertQuerySelects + where *AlertQueryWhere + groupBy *AlertQueryGroupBy +} + +func NewAlertQuery(database string, measurement string) *AlertQuery { + q := &AlertQuery{ + selects: new(AlertQuerySelects), + where: new(AlertQueryWhere), + groupBy: new(AlertQueryGroupBy), + } + q = q.Database(database).Measurement(measurement) + return q +} + +func (q *AlertQuery) ToAlertQuery() monitor.AlertQuery { + return monitor.AlertQuery{ + Model: q.ToMetricQuery(), + From: q.from, + To: q.to, + } +} + +func (q *AlertQuery) ToMetricQuery() monitor.MetricQuery { + return monitor.MetricQuery{ + Alias: q.alias, + Tz: q.tz, + Database: q.database, + Measurement: q.measurement, + Tags: q.where.ToTags(), + GroupBy: q.groupBy.ToGroupBy(), + Selects: q.selects.ToSelects(), + Interval: q.interval, + Policy: q.policy, + ResultFormat: q.resultFormat, + } +} + +func (q *AlertQuery) ToTsdbQuery() *tsdb.TsdbQuery { + timeRange := tsdb.NewTimeRange(q.from, q.to) + tsdbQ := &tsdb.TsdbQuery{ + TimeRange: timeRange, + Queries: []*tsdb.Query{ + { + MetricQuery: q.ToMetricQuery(), + }, + }, + } + return tsdbQ +} + +func (q *AlertQuery) From(from string) *AlertQuery { + q.from = from + return q +} + +func (q *AlertQuery) To(to string) *AlertQuery { + q.to = to + return q +} + +func (q *AlertQuery) Alias(alias string) *AlertQuery { + q.alias = alias + return q +} + +func (q *AlertQuery) Tz(tz string) *AlertQuery { + q.tz = tz + return q +} + +func (q *AlertQuery) Database(db string) *AlertQuery { + q.database = db + return q +} + +func (q *AlertQuery) Measurement(m string) *AlertQuery { + q.measurement = m + return q +} + +func (q *AlertQuery) Interval(i string) *AlertQuery { + q.interval = i + return q +} + +func (q *AlertQuery) Policy(p string) *AlertQuery { + q.policy = p + return q +} + +func (q *AlertQuery) Selects() *AlertQuerySelects { + q.selects = &AlertQuerySelects{ + parts: make([]*AlertQuerySelect, 0), + } + return q.selects +} + +func (q *AlertQuery) Where() *AlertQueryWhere { + w := &AlertQueryWhere{ + parts: make([]monitor.MetricQueryTag, 0), + } + w.AND() + q.where = w + return w +} + +func (q *AlertQuery) GroupBy() *AlertQueryGroupBy { + g := &AlertQueryGroupBy{parts: make([]monitor.MetricQueryPart, 0)} + q.groupBy = g + return g +} + +type AlertQuerySelects struct { + parts []*AlertQuerySelect +} + +type AlertQuerySelect struct { + monitor.MetricQuerySelect +} + +func (s *AlertQuerySelects) Select(fieldName string) *AlertQuerySelect { + if s.parts == nil { + s.parts = make([]*AlertQuerySelect, 0) + } + sel := make([]monitor.MetricQueryPart, 0) + sel = append(sel, monitor.MetricQueryPart{ + Type: "field", + Params: []string{fieldName}, + }) + part := &AlertQuerySelect{sel} + s.parts = append(s.parts, part) + return part +} + +func (s *AlertQuerySelects) ToSelects() []monitor.MetricQuerySelect { + ret := make([]monitor.MetricQuerySelect, len(s.parts)) + for i, p := range s.parts { + ret[i] = p.MetricQuerySelect + } + return ret +} + +func (s *AlertQuerySelect) addFunc(funcName string) *AlertQuerySelect { + s.MetricQuerySelect = append(s.MetricQuerySelect, monitor.MetricQueryPart{ + Type: funcName, + }) + return s +} + +// Aggregations method +func (s *AlertQuerySelect) MEAN() *AlertQuerySelect { + return s.addFunc("mean") +} + +func (s *AlertQuerySelect) COUNT() *AlertQuerySelect { + return s.addFunc("count") +} + +func (s *AlertQuerySelect) DISTINCT() *AlertQuerySelect { + return s.addFunc("distinct") +} + +func (s *AlertQuerySelect) SUM() *AlertQuerySelect { + return s.addFunc("sum") +} + +// AS is alias method +func (s *AlertQuerySelect) AS(alias string) *AlertQuerySelect { + s.MetricQuerySelect = append(s.MetricQuerySelect, monitor.MetricQueryPart{ + Type: "alias", + Params: []string{alias}, + }) + return s +} + +// MATH method +func (s *AlertQuerySelect) MATH(op string, val string) *AlertQuerySelect { + s.MetricQuerySelect = append(s.MetricQuerySelect, monitor.MetricQueryPart{ + Type: "math", + Params: []string{fmt.Sprintf("%s %s", op, val)}, + }) + return s +} + +type AlertQueryWhere struct { + parts []monitor.MetricQueryTag + cond string +} + +func (w *AlertQueryWhere) add(tags ...monitor.MetricQueryTag) *AlertQueryWhere { + w.parts = append(w.parts, tags...) + return w +} + +func (w *AlertQueryWhere) condAdd(cond string, tags ...monitor.MetricQueryTag) *AlertQueryWhere { + for i := range tags { + tags[i].Condition = cond + } + w.add(tags...) + return w +} + +func (w *AlertQueryWhere) filter(op string, key string, value string) *AlertQueryWhere { + if len(w.parts) == 0 { + w.add(w.newTag(op, key, value)) + return w + } + return w.condAdd(w.cond, w.newTag(op, key, value)) +} + +func (w *AlertQueryWhere) Equal(key string, value string) *AlertQueryWhere { + return w.filter("=", key, value) +} + +func (w *AlertQueryWhere) NotEqual(key string, value string) *AlertQueryWhere { + return w.filter("!=", key, value) +} + +func (w *AlertQueryWhere) LT(key string, value string) *AlertQueryWhere { + return w.filter("<", key, value) +} + +func (w *AlertQueryWhere) GT(key string, value string) *AlertQueryWhere { + return w.filter(">", key, value) +} + +func (w *AlertQueryWhere) AND() *AlertQueryWhere { + w.cond = "AND" + return w +} + +func (w *AlertQueryWhere) OR() *AlertQueryWhere { + w.cond = "OR" + return w +} + +func (w *AlertQueryWhere) newTag(op string, key string, value string) monitor.MetricQueryTag { + return monitor.MetricQueryTag{ + Key: key, + Operator: op, + Value: value, + } +} + +func (w *AlertQueryWhere) ToTags() []monitor.MetricQueryTag { + return w.parts +} + +type AlertQueryGroupBy struct { + parts []monitor.MetricQueryPart +} + +func (g *AlertQueryGroupBy) addPart(typ string, params ...string) *AlertQueryGroupBy { + g.parts = append(g.parts, monitor.MetricQueryPart{ + Type: typ, + Params: params, + }) + return g +} + +func (g *AlertQueryGroupBy) TIME(val string) *AlertQueryGroupBy { + return g.addPart("time", val) +} + +func (g *AlertQueryGroupBy) TAG(val string) *AlertQueryGroupBy { + return g.addPart("tag", val) +} + +func (g *AlertQueryGroupBy) FILL_NULL() *AlertQueryGroupBy { + return g.FILL("null") +} + +func (g *AlertQueryGroupBy) FILL(val string) *AlertQueryGroupBy { + return g.addPart("fill", val) +} + +func (g *AlertQueryGroupBy) ToGroupBy() []monitor.MetricQueryPart { + return g.parts +} diff --git a/pkg/mcclient/modules/monitor/helper_test.go b/pkg/mcclient/modules/monitor/helper_test.go new file mode 100644 index 0000000000..35cb4792eb --- /dev/null +++ b/pkg/mcclient/modules/monitor/helper_test.go @@ -0,0 +1,156 @@ +package monitor + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" + + "yunion.io/x/onecloud/pkg/apis/monitor" + "yunion.io/x/onecloud/pkg/monitor/tsdb/driver/influxdb" +) + +func TestHelperWhere(t *testing.T) { + Convey("Alert query where", t, func() { + q := NewAlertCondition("", "").Query() + w1 := q.Where() + w1.Equal("hostname", "server1").NotEqual("hypervisor", "kvm"). + OR().GT("key", "val").LT("key1", "val2") + So(w1.ToTags(), ShouldResemble, []monitor.MetricQueryTag{ + { + Operator: "=", + Key: "hostname", + Value: "server1", + }, + { + Condition: "AND", + Operator: "!=", + Key: "hypervisor", + Value: "kvm", + }, + { + Condition: "OR", + Operator: ">", + Key: "key", + Value: "val", + }, + { + Condition: "OR", + Operator: "<", + Key: "key1", + Value: "val2", + }, + }) + }) +} + +func TestHelperSelects(t *testing.T) { + Convey("Alert query selects", t, func() { + sels := NewAlertCondition("", "").Query().Selects() + sels.Select("name").COUNT().MATH("/", "100") + sels.Select("io_util") + + So(sels.parts[0].MetricQuerySelect, ShouldResemble, monitor.MetricQuerySelect{ + { + Type: "field", + Params: []string{"name"}, + }, + { + Type: "count", + }, + { + Type: "math", + Params: []string{"/ 100"}, + }, + }) + + So(sels.parts[1].MetricQuerySelect, ShouldResemble, monitor.MetricQuerySelect{ + { + Type: "field", + Params: []string{"io_util"}, + }, + }) + }) +} + +func TestAlertQuery(t *testing.T) { + Convey("Alert query test", t, func() { + parser := new(influxdb.InfluxdbQueryParser) + q := NewAlertQuery("telegraf", "diskio").From("5m").To("now") + q.Selects().Select("await").MEAN() + q.Where().Equal("hostname", "host1").Equal("provider", "kvm") + q.GroupBy().TAG("*").FILL_NULL() + qCtx := q.ToTsdbQuery() + influxdbQ, err := parser.Parse(qCtx.Queries[0], nil) + So(err, ShouldBeNil) + rawQuery, err := influxdbQ.Build(qCtx) + So(err, ShouldBeNil) + So(rawQuery, ShouldEqual, `SELECT mean("await") FROM "diskio" WHERE ("hostname" = 'host1' AND "provider" = 'kvm') AND time > now() - 5m GROUP BY * fill(null)`) + }) +} + +func TestAlertConfig(t *testing.T) { + Convey("Alert config test", t, func() { + enabled := true + // disabled := false + conf, err := NewAlertConfig("alert1", "5s", true) + So(err, ShouldBeNil) + q := conf.Condition("telegraf", "cpu").Avg().LT(50).Query() + sels := q.Selects() + sels.Select("usage_active").MEAN() + sels.Select("usage_irq").COUNT() + q.Where().Equal("host_ip", "10.168.222.231") + So(conf.ToAlertCreateInput(), ShouldResemble, monitor.AlertCreateInput{ + Name: "alert1", + Frequency: 5, + Settings: monitor.AlertSetting{ + Conditions: []monitor.AlertCondition{ + { + Type: "query", + Query: monitor.AlertQuery{ + Model: monitor.MetricQuery{ + Database: "telegraf", + Measurement: "cpu", + Selects: []monitor.MetricQuerySelect{ + { + { + Type: "field", + Params: []string{"usage_active"}, + }, + { + Type: "mean", + }, + }, + { + { + Type: "field", + Params: []string{"usage_irq"}, + }, + { + Type: "count", + }, + }, + }, + Tags: []monitor.MetricQueryTag{ + { + Key: "host_ip", + Operator: "=", + Value: "10.168.222.231", + }, + }, + }, + }, + Reducer: monitor.Condition{ + Type: "avg", + }, + Evaluator: monitor.Condition{ + Type: "lt", + Params: []float64{50}, + }, + }, + }, + }, + Enabled: &enabled, + Level: "", + }) + }) +} diff --git a/pkg/mcclient/options/monitor/alert.go b/pkg/mcclient/options/monitor/alert.go index 9ca1e508cb..830f9057a4 100644 --- a/pkg/mcclient/options/monitor/alert.go +++ b/pkg/mcclient/options/monitor/alert.go @@ -16,9 +16,13 @@ package monitor import ( "fmt" + "strings" "time" + "yunion.io/x/jsonutils" + "yunion.io/x/onecloud/pkg/apis/monitor" + monitor2 "yunion.io/x/onecloud/pkg/mcclient/modules/monitor" "yunion.io/x/onecloud/pkg/mcclient/options" ) @@ -34,6 +38,85 @@ type AlertDeleteOptions struct { ID []string `help:"ID of alert to delete"` } +type AlertTestRunOptions struct { + ID string `help:"ID of alert to delete"` + Debug bool `help:"Show more debug info"` +} + +type AlertConditionOptions struct { + REDUCER string `help:"Metric query reducer, e.g. 'avg'" choices:"avg|sum|min|max|count|last|median"` + DATABASE string `help:"Metric database, e.g. 'telegraf'"` + METRIC string `help:"Query metric format ., e.g. 'cpu.cpu_usage'"` + COMPARATOR string `help:"Evaluator compare" choices:"gt|lt"` + THRESHOLD float64 `help:"Alert threshold"` + Period string `help:"Query metric period e.g. '5m', '1h'" default:"5m"` + Tag []string `help:"Query tag, e.g. 'zone=zon0,name=vmname'"` +} + +func (opt AlertConditionOptions) Params(conf *monitor2.AlertConfig) (*monitor2.AlertCondition, error) { + parts := strings.Split(opt.METRIC, ".") + if len(parts) != 2 { + return nil, fmt.Errorf("metric %s is invalid format", opt.METRIC) + } + cond := conf.Condition(opt.DATABASE, parts[0]) + if opt.COMPARATOR == "gt" { + cond.GT(opt.THRESHOLD) + } + if opt.COMPARATOR == "lt" { + cond.LT(opt.THRESHOLD) + } + switch opt.REDUCER { + case "avg": + cond.Avg() + case "sum": + cond.Sum() + case "min": + cond.Min() + case "max": + cond.Max() + case "count": + cond.Count() + case "last": + cond.Last() + case "median": + cond.Median() + } + + q := cond.Query().From(opt.Period) + q.Selects().Select(parts[1]) + + for _, tag := range opt.Tag { + parts := strings.Split(tag, "=") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid tag format: %s", tag) + } + q.Where().Equal(parts[0], parts[1]) + } + + return cond, nil +} + +type AlertCreateOptions struct { + AlertConditionOptions + NAME string `help:"Name of the alert"` + Frequency string `help:"Alert execute frequency, e.g. '5m', '1h'"` + Enabled bool `help:"Enable alert"` + Level string `help:"Alert level"` +} + +func (opt AlertCreateOptions) Params() (*monitor2.AlertConfig, error) { + input, err := monitor2.NewAlertConfig(opt.NAME, opt.Frequency, opt.Enabled) + if err != nil { + return nil, err + } + _, err = opt.AlertConditionOptions.Params(input) + if err != nil { + return nil, err + } + + return input, nil +} + type AlertUpdateOptions struct { ID string `help:"ID or name of the alert"` Name string `help:"Update alert name"` @@ -55,3 +138,23 @@ func (opt AlertUpdateOptions) Params() (*monitor.AlertUpdateInput, error) { } return input, nil } + +type AlertNotificationAttachOptions struct { + ALERT string `help:"ID or name of alert"` + NOTIFICATION string `help:"ID or name of alert notification"` + UsedBy string `help:"UsedBy annotation"` +} + +type AlertNotificationListOptions struct { + options.BaseListOptions + Alert string `help:"ID or name of alert" short-token:"a"` + Notification string `help:"ID or name of notification" short-token:"n"` +} + +func (o AlertNotificationListOptions) Params() (*jsonutils.JSONDict, error) { + params, err := o.BaseListOptions.Params() + if err != nil { + return nil, err + } + return params, nil +} diff --git a/pkg/monitor/alerting/conditions/query.go b/pkg/monitor/alerting/conditions/query.go index db191c2307..3d7bbbb17b 100644 --- a/pkg/monitor/alerting/conditions/query.go +++ b/pkg/monitor/alerting/conditions/query.go @@ -19,6 +19,7 @@ import ( "fmt" "strings" + "yunion.io/x/jsonutils" "yunion.io/x/pkg/errors" "yunion.io/x/onecloud/pkg/apis/monitor" @@ -98,7 +99,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio emptySeriesCount := 0 evalMatchCount := 0 - var matches []*alerting.EvalMatch + var matches []*monitor.EvalMatch for idx, series := range seriesList { reducedValue := c.Reducer.Reduce(series) @@ -109,7 +110,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio } if context.IsTestRun { - context.Logs = append(context.Logs, &alerting.ResultLogEntry{ + context.Logs = append(context.Logs, &monitor.ResultLogEntry{ Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %v", c.Index, evalMatch, series.Name, reducedValue), }) } @@ -118,7 +119,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio evalMatchCount++ } tags := c.filterTags(series.Tags) - matches = append(matches, &alerting.EvalMatch{ + matches = append(matches, &monitor.EvalMatch{ Condition: c.GenerateFormatCond(&metas[idx]).String(), Metric: series.Name, Value: reducedValue, @@ -132,14 +133,14 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio evalMatch := c.Evaluator.Eval(nil) if context.IsTestRun { - context.Logs = append(context.Logs, &alerting.ResultLogEntry{ + context.Logs = append(context.Logs, &monitor.ResultLogEntry{ Message: fmt.Sprintf("Condition: Eval: %v, Query returned No Series (reduced to null/no value)", evalMatch), }) } if evalMatch { evalMatchCount++ - matches = append(matches, &alerting.EvalMatch{ + matches = append(matches, &monitor.EvalMatch{ Metric: "NoData", Value: nil, }) @@ -170,7 +171,37 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange * metas := make([]tsdb.QueryResultMeta, 0) if context.IsDebug { - // TODO: record info when is debug mode + data := jsonutils.NewDict() + if req.TimeRange != nil { + data.Set("from", jsonutils.NewInt(req.TimeRange.GetFromAsMsEpoch())) + data.Set("to", jsonutils.NewInt(req.TimeRange.GetToAsMsEpoch())) + } + + type queryDto struct { + RefId string `json:"refId"` + Model monitor.MetricQuery `json:"model"` + Datasource tsdb.DataSource `json:"datasource"` + MaxDataPoints int64 `json:"maxDataPoints"` + IntervalMs int64 `json:"intervalMs"` + } + + queries := []*queryDto{} + for _, q := range req.Queries { + queries = append(queries, &queryDto{ + RefId: q.RefId, + Model: q.MetricQuery, + Datasource: q.DataSource, + MaxDataPoints: q.MaxDataPoints, + IntervalMs: q.IntervalMs, + }) + } + + data.Set("queries", jsonutils.Marshal(queries)) + + context.Logs = append(context.Logs, &monitor.ResultLogEntry{ + Message: fmt.Sprintf("Condition[%d]: Query", c.Index), + Data: data, + }) } resp, err := c.HandleRequest(context.Ctx, ds.ToTSDBDataSource(""), req) @@ -182,8 +213,6 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange * return nil, errors.Wrap(err, "tsdb.HandleRequest() error") } - // log.Errorf("===query resp %s", jsonutils.Marshal(resp).PrettyString()) - for _, v := range resp.Results { if v.Error != nil { return nil, errors.Wrap(err, "tsdb.HandleResult() response") @@ -198,12 +227,12 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange * queryResultData["series"] = v.Series } - /*if context.IsDebug && v.Meta != nil { + if context.IsDebug { queryResultData["meta"] = v.Meta - }*/ + } if context.IsTestRun || context.IsDebug { - context.Logs = append(context.Logs, &alerting.ResultLogEntry{ + context.Logs = append(context.Logs, &monitor.ResultLogEntry{ Message: fmt.Sprintf("Condition[%d]: Query Result", c.Index), Data: queryResultData, }) diff --git a/pkg/monitor/alerting/eval_context.go b/pkg/monitor/alerting/eval_context.go index dbcbef5538..def133da38 100644 --- a/pkg/monitor/alerting/eval_context.go +++ b/pkg/monitor/alerting/eval_context.go @@ -30,8 +30,8 @@ type EvalContext struct { Firing bool IsTestRun bool IsDebug bool - EvalMatches []*EvalMatch - Logs []*ResultLogEntry + EvalMatches []*monitor.EvalMatch + Logs []*monitor.ResultLogEntry Error error ConditionEvals string StartTime time.Time @@ -52,7 +52,7 @@ func NewEvalContext(alertCtx context.Context, userCred mcclient.TokenCredential, UserCred: userCred, StartTime: time.Now(), Rule: rule, - EvalMatches: make([]*EvalMatch, 0), + EvalMatches: make([]*monitor.EvalMatch, 0), PrevAlertState: rule.State, } } diff --git a/pkg/monitor/alerting/eval_handler_test.go b/pkg/monitor/alerting/eval_handler_test.go index c0a94784a6..4b4884fe45 100644 --- a/pkg/monitor/alerting/eval_handler_test.go +++ b/pkg/monitor/alerting/eval_handler_test.go @@ -19,12 +19,14 @@ import ( "testing" . "github.com/smartystreets/goconvey/convey" + + "yunion.io/x/onecloud/pkg/apis/monitor" ) type conditionStub struct { firing bool operator string - matches []*EvalMatch + matches []*monitor.EvalMatch noData bool } @@ -59,7 +61,7 @@ func TestAlertingEvaluationHandler(t *testing.T) { Convey("Show return false with not passing asdf", func() { ctx := NewEvalContext(context.TODO(), nil, &Rule{ Conditions: []Condition{ - &conditionStub{firing: true, operator: "and", matches: []*EvalMatch{{}, {}}}, + &conditionStub{firing: true, operator: "and", matches: []*monitor.EvalMatch{{}, {}}}, &conditionStub{firing: false, operator: "and"}, }}) diff --git a/pkg/monitor/alerting/interfaces.go b/pkg/monitor/alerting/interfaces.go index 82445c5edd..9a32873996 100644 --- a/pkg/monitor/alerting/interfaces.go +++ b/pkg/monitor/alerting/interfaces.go @@ -18,6 +18,7 @@ import ( "context" "time" + "yunion.io/x/onecloud/pkg/apis/monitor" "yunion.io/x/onecloud/pkg/monitor/models" "yunion.io/x/onecloud/pkg/monitor/notifydrivers" ) @@ -36,7 +37,7 @@ type ConditionResult struct { Firing bool NoDataFound bool Operator string - EvalMatches []*EvalMatch + EvalMatches []*monitor.EvalMatch } // Condition is responsible for evaluating an alert condition. diff --git a/pkg/monitor/alerting/job.go b/pkg/monitor/alerting/job.go index db6d52f079..8af24ff6f1 100644 --- a/pkg/monitor/alerting/job.go +++ b/pkg/monitor/alerting/job.go @@ -41,17 +41,3 @@ func (j *Job) SetRunning(b bool) { j.running = b j.runningLock.Unlock() } - -// ResultLogEntry represents log data for the alert evaluation. -type ResultLogEntry struct { - Message string - Data interface{} -} - -// EvalMatch represents the series violating the threshold. -type EvalMatch struct { - Condition string `json:“condition` - Value *float64 `json:"value"` - Metric string `json:"metric"` - Tags map[string]string `json:"tags"` -} diff --git a/pkg/monitor/alerting/rule.go b/pkg/monitor/alerting/rule.go index ca858bd780..08341c5728 100644 --- a/pkg/monitor/alerting/rule.go +++ b/pkg/monitor/alerting/rule.go @@ -15,6 +15,7 @@ package alerting import ( + "context" "regexp" "strconv" "time" @@ -23,6 +24,7 @@ import ( "yunion.io/x/pkg/errors" "yunion.io/x/onecloud/pkg/apis/monitor" + "yunion.io/x/onecloud/pkg/mcclient" "yunion.io/x/onecloud/pkg/monitor/models" "yunion.io/x/onecloud/pkg/monitor/validators" ) @@ -35,6 +37,10 @@ var ( ErrFrequencyCouldNotBeParsed = errors.Error(`"evaluate every" field could not be parsed`) ) +func init() { + models.AlertManager.SetTester(NewAlertRuleTester()) +} + // Rule is the in-memory version of an alert rule. type Rule struct { Id string @@ -152,6 +158,40 @@ func NewRuleFromDBAlert(ruleDef *models.SAlert) (*Rule, error) { return model, nil } +type AlertRuleTester struct{} + +func NewAlertRuleTester() models.AlertTestRunner { + return new(AlertRuleTester) +} + +func (_ AlertRuleTester) DoTest(ruleDef *models.SAlert, userCred mcclient.TokenCredential, input monitor.AlertTestRunInput) (*monitor.AlertTestRunOutput, error) { + rule, err := NewRuleFromDBAlert(ruleDef) + if err != nil { + return nil, err + } + handler := NewEvalHandler() + + ctx := NewEvalContext(context.Background(), userCred, rule) + ctx.IsTestRun = true + ctx.IsDebug = input.IsDebug + + handler.Eval(ctx) + + return ctx.ToTestRunResult(), nil +} + +func (ctx *EvalContext) ToTestRunResult() *monitor.AlertTestRunOutput { + return &monitor.AlertTestRunOutput{ + Firing: ctx.Firing, + EvalMatches: ctx.EvalMatches, + Logs: ctx.Logs, + Error: ctx.Error, + ConditionEvals: ctx.ConditionEvals, + StartTime: ctx.StartTime, + EndTime: ctx.EndTime, + } +} + // ConditionFactory is the function signature for creating `Conditions` type ConditionFactory func(model *monitor.AlertCondition, index int) (Condition, error) diff --git a/pkg/monitor/models/alert.go b/pkg/monitor/models/alert.go index c21aa9ecad..e6bc2edaa8 100644 --- a/pkg/monitor/models/alert.go +++ b/pkg/monitor/models/alert.go @@ -45,8 +45,14 @@ func init() { AlertManager = NewAlertManager(SAlert{}, "alert", "alerts") } +type AlertTestRunner interface { + DoTest(ruleDef *SAlert, userCred mcclient.TokenCredential, input monitor.AlertTestRunInput) (*monitor.AlertTestRunOutput, error) +} + type SAlertManager struct { db.SVirtualResourceBaseManager + + tester AlertTestRunner } func NewAlertManager(dt interface{}, keyword, keywordPlural string) *SAlertManager { @@ -61,6 +67,14 @@ func NewAlertManager(dt interface{}, keyword, keywordPlural string) *SAlertManag return man } +func (man *SAlertManager) SetTester(tester AlertTestRunner) { + man.tester = tester +} + +func (man *SAlertManager) GetTester() AlertTestRunner { + return man.tester +} + func (man *SAlertManager) FetchAllAlerts() ([]SAlert, error) { objs := make([]SAlert, 0) q := man.Query() @@ -391,3 +405,74 @@ func (alert *SAlert) SetFor(forTime time.Duration) error { }) return err } + +func (alert *SAlert) AllowPerformTestRun( + ctx context.Context, + userCred mcclient.TokenCredential, + query jsonutils.JSONObject, + data jsonutils.JSONObject, +) bool { + return db.IsProjectAllowPerform(userCred, alert, "test-run") +} + +func (alert *SAlert) PerformTestRun( + ctx context.Context, + userCred mcclient.TokenCredential, + query jsonutils.JSONObject, + input monitor.AlertTestRunInput, +) (*monitor.AlertTestRunOutput, error) { + return alert.testRunAlert(userCred, input) +} + +func (alert *SAlert) testRunAlert(userCred mcclient.TokenCredential, input monitor.AlertTestRunInput) (*monitor.AlertTestRunOutput, error) { + return AlertManager.GetTester().DoTest(alert, userCred, input) +} + +/*func (alert *SAlert) AllowPerformAttachNotification( + ctx context.Context, + userCred mcclient.TokenCredential, + query jsonutils.JSONObject, + data jsonutils.JSONObject, +) bool { + return db.IsProjectAllowPerform(userCred, alert, "attach-notification") +} + +func (alert *SAlert) PerformAttachNotification( + ctx context.Context, + userCred mcclient.TokenCredential, + query jsonutils.JSONObject, + input monitor.AlertAttachNotificationInput, +) (*monitor.AlertAttachNotificationOutput, error) { + notiObj, err := NotificationManager.FetchByIdOrName(userCred, input.NotificationId) + if err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return nil, httperrors.NewResourceNotFoundError("Alert notification %s not found", input.NotificationId) + } + return nil, err + } + noti := notiObj.(*SNotification) + ret, err := alert.AttachNotification(ctx, userCred, noti, monitor.AlertNotificationStateUnknown, input.UsedBy) + if err != nil { + return nil, err + } + return &monitor.AlertAttachNotificationOutput{ + NotificationId: ret.NotificationId, + UsedBy: ret.UsedBy, + }, nil +}*/ + +func (alert *SAlert) CustomizeDelete( + ctx context.Context, userCred mcclient.TokenCredential, + query jsonutils.JSONObject, data jsonutils.JSONObject, +) error { + notis, err := alert.GetNotifications() + if err != nil { + return err + } + for _, noti := range notis { + if err := noti.Detach(ctx, userCred); err != nil { + return err + } + } + return nil +} diff --git a/pkg/monitor/models/alertjoint.go b/pkg/monitor/models/alertjoint.go index 87e6612664..e9fcb2bbed 100644 --- a/pkg/monitor/models/alertjoint.go +++ b/pkg/monitor/models/alertjoint.go @@ -14,7 +14,9 @@ package models -import "yunion.io/x/onecloud/pkg/cloudcommon/db" +import ( + "yunion.io/x/onecloud/pkg/cloudcommon/db" +) type SAlertJointsManager struct { db.SVirtualJointResourceBaseManager diff --git a/pkg/monitor/models/alertnotification.go b/pkg/monitor/models/alertnotification.go index 6b458ef5cd..bbdb3654b2 100644 --- a/pkg/monitor/models/alertnotification.go +++ b/pkg/monitor/models/alertnotification.go @@ -16,11 +16,14 @@ package models import ( "context" + "database/sql" "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" "yunion.io/x/onecloud/pkg/apis/monitor" "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/httperrors" "yunion.io/x/onecloud/pkg/mcclient" ) @@ -78,6 +81,52 @@ func (man *SAlertNotificationManager) Get(alertId string, notiId string) (*SAler return obj, err } +func (man *SAlertNotificationManager) ValidateCreateData( + ctx context.Context, + userCred mcclient.TokenCredential, + ownerId mcclient.IIdentityProvider, + query jsonutils.JSONObject, + input monitor.AlertnotificationCreateInput, +) (*jsonutils.JSONDict, error) { + if input.AlertId == "" { + return nil, httperrors.NewMissingParameterError("alert_id") + } + if input.NotificationId == "" { + return nil, httperrors.NewMissingParameterError("notification_id") + } + _, err := AlertManager.FetchById(input.AlertId) + if err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return nil, httperrors.NewResourceNotFoundError("not find alert %s", input.AlertId) + } + return nil, err + } + _, err = NotificationManager.FetchById(input.NotificationId) + if err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return nil, httperrors.NewResourceNotFoundError("not find notification %s", input.NotificationId) + } + return nil, err + } + ret := input.JSON(input) + ret.Add(jsonutils.NewString(string(monitor.AlertNotificationStateUnknown)), "state") + return ret, nil +} + +func (joint *SAlertnotification) getExtraDetails(noti SNotification, out monitor.AlertnotificationDetails) monitor.AlertnotificationDetails { + out.Notification = noti.GetName() + return out +} + +func (joint *SAlertnotification) GetExtraDetails( + ctx context.Context, + userCred mcclient.TokenCredential, + query jsonutils.JSONObject, + isList bool, +) (monitor.AlertnotificationDetails, error) { + return monitor.AlertnotificationDetails{}, nil +} + func (joint *SAlertnotification) DoSave(ctx context.Context, userCred mcclient.TokenCredential) error { if err := AlertNotificationManager.TableSpec().Insert(joint); err != nil { return err diff --git a/pkg/monitor/models/notification.go b/pkg/monitor/models/notification.go index 23551e9001..9b7f483def 100644 --- a/pkg/monitor/models/notification.go +++ b/pkg/monitor/models/notification.go @@ -54,6 +54,7 @@ func NewNotificationManager() *SNotificationManager { "alert_notifications", ), } + man.SetAlias("notification", "notifications") man.SetVirtualObject(man) return man } @@ -176,3 +177,27 @@ func (n *SNotification) AttachToAlert( } return alert.AttachNotification(ctx, userCred, n, monitor.AlertNotificationStateUnknown, "") } + +func (n *SNotification) GetAlertNotificationCount() (int, error) { + alertNotis := AlertNotificationManager.Query() + return alertNotis.Equals("notification_id", n.Id).CountWithError() +} + +func (n *SNotification) IsAttached() (bool, error) { + cnt, err := n.GetAlertNotificationCount() + if err != nil { + return false, err + } + return cnt > 0, nil +} + +func (n *SNotification) ValidateDeleteCondition(ctx context.Context) error { + cnt, err := n.GetAlertNotificationCount() + if err != nil { + return err + } + if cnt > 0 { + return httperrors.NewNotEmptyError("Alert notification used by %d alert", cnt) + } + return n.SVirtualResourceBase.ValidateDeleteCondition(ctx) +} diff --git a/pkg/monitor/tsdb/driver/influxdb/influxdb.go b/pkg/monitor/tsdb/driver/influxdb/influxdb.go index 15e2f58e9f..f12ef8eefd 100644 --- a/pkg/monitor/tsdb/driver/influxdb/influxdb.go +++ b/pkg/monitor/tsdb/driver/influxdb/influxdb.go @@ -102,8 +102,6 @@ func (e *InfluxdbExecutor) Query(ctx context.Context, dsInfo *tsdb.DataSource, t return nil, response.Err } - // log.Errorf("==influxdb response: %s", jsonutils.Marshal(response).PrettyString()) - result.Results = make(map[string]*tsdb.QueryResult) ret := e.ResponseParser.Parse(&response, query) ret.Meta = tsdb.QueryResultMeta{ diff --git a/pkg/monitor/tsdb/interval.go b/pkg/monitor/tsdb/interval.go index addaa4dd34..cb732d3fe1 100644 --- a/pkg/monitor/tsdb/interval.go +++ b/pkg/monitor/tsdb/interval.go @@ -79,7 +79,7 @@ func (ic *intervalCalculator) Calculate(timerange *TimeRange, minInterval time.D func GetIntervalFrom(dsInfo *DataSource, queryModel *Query, defaultInterval time.Duration) (time.Duration, error) { interval := queryModel.Interval - if interval == "" && dsInfo.TimeInterval != "" { + if interval == "" && (dsInfo != nil && dsInfo.TimeInterval != "") { interval = dsInfo.TimeInterval } if interval == "" { diff --git a/pkg/monitor/validators/validators.go b/pkg/monitor/validators/validators.go index e21accd2b1..b937e7db5f 100644 --- a/pkg/monitor/validators/validators.go +++ b/pkg/monitor/validators/validators.go @@ -80,6 +80,22 @@ func ValidateAlertConditionQuery(input monitor.AlertQuery) error { if err := ValidateToValue(input.To); err != nil { return err } + if err := ValidateAlertQueryModel(input.Model); err != nil { + return err + } + return nil +} + +func ValidateAlertQueryModel(input monitor.MetricQuery) error { + if len(input.Selects) == 0 { + return httperrors.NewInputParameterError("no select data in query") + } + if len(input.Database) == 0 { + return httperrors.NewInputParameterError("no database in query") + } + if len(input.Measurement) == 0 { + return httperrors.NewInputParameterError("no measurement in query") + } return nil }