Merge pull request #5630 from zexi/automated-cherry-pick-of-#5629-upstream-release-3.1

Automated cherry pick of #5629: monitor: add alert config helper
This commit is contained in:
Zexi Li
2020-03-31 16:45:32 +08:00
committed by GitHub
24 changed files with 1317 additions and 50 deletions
+31
View File
@@ -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
})
}
@@ -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
})
}
+48
View File
@@ -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"`
}
+26
View File
@@ -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"`
}
-8
View File
@@ -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"`
}
+77
View File
@@ -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"`
}
+7
View File
@@ -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),
+68 -7
View File
@@ -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
}
+473
View File
@@ -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
}
+156
View File
@@ -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: "",
})
})
}
+103
View File
@@ -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 <measurement>.<field>, 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
}
+40 -11
View File
@@ -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,
})
+3 -3
View File
@@ -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,
}
}
+4 -2
View File
@@ -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"},
}})
+2 -1
View File
@@ -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.
-14
View File
@@ -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"`
}
+40
View File
@@ -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)
+85
View File
@@ -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
}
+3 -1
View File
@@ -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
+49
View File
@@ -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
+25
View File
@@ -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)
}
@@ -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{
+1 -1
View File
@@ -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 == "" {
+16
View File
@@ -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
}