add monitor service

This commit is contained in:
Zexi Li
2020-01-07 12:04:34 +08:00
parent bbf23e2a55
commit 5645e4e15a
192 changed files with 18327 additions and 49 deletions
+4
View File
@@ -0,0 +1,4 @@
FROM registry.cn-beijing.aliyuncs.com/yunionio/onecloud-base:latest
ADD ./_output/bin/monitor /opt/yunion/bin/monitor
+1
View File
@@ -35,6 +35,7 @@ import (
_ "yunion.io/x/onecloud/cmd/climc/shell/cloudnet"
_ "yunion.io/x/onecloud/cmd/climc/shell/etcd"
_ "yunion.io/x/onecloud/cmd/climc/shell/k8s"
_ "yunion.io/x/onecloud/cmd/climc/shell/monitor"
"yunion.io/x/onecloud/pkg/mcclient"
)
+55
View File
@@ -0,0 +1,55 @@
// Copyright 2019 Yunion
//
// 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.
package monitor
import (
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/modules/monitor"
options "yunion.io/x/onecloud/pkg/mcclient/options/monitor"
)
func init() {
aN := cmdN("alert")
R(&options.AlertListOptions{}, aN("list"), "List all alerts",
func(s *mcclient.ClientSession, args *options.AlertListOptions) error {
params, err := args.Params()
if err != nil {
return err
}
ret, err := monitor.Alerts.List(s, params)
if err != nil {
return err
}
printList(ret, monitor.Alerts.GetColumns(s))
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)
if err != nil {
return err
}
printObject(ret)
return nil
})
R(&options.AlertDeleteOptions{}, aN("delete"), "Delete alerts",
func(s *mcclient.ClientSession, args *options.AlertDeleteOptions) error {
ret := monitor.Alerts.BatchDelete(s, args.ID, nil)
printBatchResults(ret, monitor.Alerts.GetColumns(s))
return nil
})
}
+27
View File
@@ -0,0 +1,27 @@
// Copyright 2019 Yunion
//
// 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.
package monitor
import (
"yunion.io/x/onecloud/cmd/climc/shell"
"yunion.io/x/onecloud/pkg/util/printutils"
)
var (
R = shell.R
printList = printutils.PrintJSONList
printObject = printutils.PrintJSONObject
printBatchResults = printutils.PrintJSONBatchResults
)
+56
View File
@@ -0,0 +1,56 @@
// Copyright 2019 Yunion
//
// 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.
package monitor
import (
"fmt"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/modules/monitor"
options "yunion.io/x/onecloud/pkg/mcclient/options/monitor"
)
func cmdN(suffix string) func(action string) string {
return func(action string) string {
return fmt.Sprintf("monitor-%s-%s", suffix, action)
}
}
func init() {
dsN := cmdN("datasource")
R(&options.DataSourceListOptions{}, dsN("list"), "List all monitor data source",
func(s *mcclient.ClientSession, args *options.DataSourceListOptions) error {
params, err := args.Params()
if err != nil {
return err
}
ret, err := monitor.DataSources.List(s, params)
if err != nil {
return err
}
printList(ret, monitor.DataSources.GetColumns(s))
return nil
})
R(&options.DataSourceDeleteOptions{}, dsN("delete"), "Delete monitor data source",
func(s *mcclient.ClientSession, args *options.DataSourceDeleteOptions) error {
ret, err := monitor.DataSources.Delete(s, args.ID, nil)
if err != nil {
return err
}
printObject(ret)
return nil
})
}
+102
View File
@@ -0,0 +1,102 @@
// Copyright 2019 Yunion
//
// 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.
package monitor
import (
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/modules/monitor"
options "yunion.io/x/onecloud/pkg/mcclient/options/monitor"
)
func init() {
nN := cmdN("notification")
R(&options.NotificationListOptions{}, nN("list"), "List all alert notification",
func(s *mcclient.ClientSession, args *options.NotificationListOptions) error {
params, err := args.Params()
if err != nil {
return err
}
ret, err := monitor.AlertNotifications.List(s, params)
if err != nil {
return err
}
printList(ret, monitor.AlertNotifications.GetColumns(s))
return nil
})
R(&options.NotificationDingDingCreateOptions{}, nN("create-dingding"),
"Create dingding alert notification",
func(s *mcclient.ClientSession, args *options.NotificationDingDingCreateOptions) error {
params, err := args.Params()
if err != nil {
return err
}
ret, err := monitor.AlertNotifications.Create(s, params.JSON(params))
if err != nil {
return err
}
printObject(ret)
return nil
})
R(&options.NotificationFeishuCreateOptions{}, nN("create-feishu"),
"Create feishu alert notification",
func(s *mcclient.ClientSession, args *options.NotificationFeishuCreateOptions) error {
params, err := args.Params()
if err != nil {
return err
}
ret, err := monitor.AlertNotifications.Create(s, params.JSON(params))
if err != nil {
return err
}
printObject(ret)
return nil
})
R(&options.NotificationShowOptions{}, nN("show"), "Show alert notification",
func(s *mcclient.ClientSession, args *options.NotificationShowOptions) error {
ret, err := monitor.AlertNotifications.Get(s, args.ID, nil)
if err != nil {
return err
}
printObject(ret)
return nil
})
R(&options.NotificationUpdateOptions{}, nN("update"), "Update alert notification",
func(s *mcclient.ClientSession, args *options.NotificationUpdateOptions) error {
params, err := args.Params()
if err != nil {
return err
}
ret, err := monitor.AlertNotifications.Update(s, args.ID, params.JSON(params))
if err != nil {
return err
}
printObject(ret)
return nil
})
R(&options.NotificationShowOptions{}, nN("delete"), "Show delete notification",
func(s *mcclient.ClientSession, args *options.NotificationShowOptions) error {
ret, err := monitor.AlertNotifications.Delete(s, args.ID, nil)
if err != nil {
return err
}
printObject(ret)
return nil
})
}
+15 -18
View File
@@ -59,18 +59,18 @@ func init() {
* 修改指定的报警规则
*/
type NodealertUpdateOptions struct {
ID string `help:"ID of the alert rule" required:"true" positional:"true"`
Type string `help:"Alert rule type" choices:"guest|host"`
Metric string `help:"Metric name, include measurement and field, such as vm_cpu.usage_active"`
NodeName string `help:"Name of the guest or host"`
NodeID string `help:"ID of the guest or host"`
Period string `help:"Specify the query time period for the data"`
Window string `help:"Specify the query interval for the data"`
Threshold float64 `help:"Threshold value of the metric"`
Comparator string `help:"Comparison operator for join expressions" choices:">|<|>=|<=|=|!="`
Recipients string `help:"Comma separated recipient ID"`
Level string `help:"Alert level" choices:"normal|important|fatal"`
Channel string `help:"Ways to send an alarm" choices:"email|mobile"`
ID string `help:"ID of the alert rule" required:"true" positional:"true"`
Type string `help:"Alert rule type" choices:"guest|host"`
Metric string `help:"Metric name, include measurement and field, such as vm_cpu.usage_active"`
NodeName string `help:"Name of the guest or host"`
NodeID string `help:"ID of the guest or host"`
Period string `help:"Specify the query time period for the data"`
Window string `help:"Specify the query interval for the data"`
Threshold *float64 `help:"Threshold value of the metric"`
Comparator string `help:"Comparison operator for join expressions" choices:">|<|>=|<=|=|!="`
Recipients string `help:"Comma separated recipient ID"`
Level string `help:"Alert level" choices:"normal|important|fatal"`
Channel string `help:"Ways to send an alarm" choices:"email|mobile"`
}
R(&NodealertUpdateOptions{}, "nodealert-update", "Update the node alert rule", func(s *mcclient.ClientSession, args *NodealertUpdateOptions) error {
params, err := options.StructToParams(args)
@@ -92,14 +92,11 @@ func init() {
* 删除指定ID的报警规则
*/
type NodealertDeleteOptions struct {
ID string `help:"ID of node alert" required:"true" positional:"true"`
ID []string `help:"ID of node alert" required:"true" positional:"true"`
}
R(&NodealertDeleteOptions{}, "nodealert-delete", "Delete a node alert", func(s *mcclient.ClientSession, args *NodealertDeleteOptions) error {
alarm, err := modules.NodeAlert.Delete(s, args.ID, nil)
if err != nil {
return err
}
printObject(alarm)
ret := modules.NodeAlert.BatchDelete(s, args.ID, nil)
printBatchResults(ret, modules.NodeAlert.GetColumns(s))
return nil
})
+23
View File
@@ -0,0 +1,23 @@
// Copyright 2019 Yunion
//
// 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.
package main
import (
"yunion.io/x/onecloud/pkg/monitor/service"
)
func main() {
service.StartService()
}
+4 -1
View File
@@ -23,6 +23,7 @@ require (
github.com/aokoli/goutils v1.0.1
github.com/aws/aws-sdk-go v1.21.4
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect
github.com/benbjohnson/clock v1.0.0
github.com/bitly/go-simplejson v0.5.0
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/c-bata/go-prompt v0.2.1
@@ -101,6 +102,7 @@ require (
github.com/shirou/gopsutil v2.18.10+incompatible
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect
github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9
github.com/smartystreets/goconvey v1.6.4
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/pflag v1.0.3 // indirect
github.com/stretchr/testify v1.4.0
@@ -121,6 +123,7 @@ require (
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
golang.org/x/sync v0.0.0-20190423024810-112230192c58
golang.org/x/sys v0.0.0-20191008105621-543471e840be
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20191008142428-8d021180e987
google.golang.org/api v0.13.0 // indirect
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873
@@ -140,7 +143,7 @@ require (
yunion.io/x/executor v0.0.0-20200227030256-a18417815e74
yunion.io/x/jsonutils v0.0.0-20200113074440-9297fd00ba07
yunion.io/x/log v0.0.0-20190629062853-9f6483a7103d
yunion.io/x/pkg v0.0.0-20200227105015-b0738bd1ffe9
yunion.io/x/pkg v0.0.0-20200302034534-fdf44d54b070
yunion.io/x/s3cli v0.0.0-20190917004522-13ac36d8687e
yunion.io/x/sqlchemy v0.0.0-20200221103553-6a98f7f8ab92
yunion.io/x/structarg v0.0.0-20190809075558-115bed041de3
+9 -2
View File
@@ -80,6 +80,8 @@ github.com/aws/aws-sdk-go v1.21.4 h1:1xB+x6Dzev8ETmeHEiSfUVbIzmC/0EyFfXMkJpzKPCE
github.com/aws/aws-sdk-go v1.21.4/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f h1:ZNv7On9kyUzm7fvRZumSyy/IUiSC7AzL0I1jKKtwooA=
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=
github.com/benbjohnson/clock v1.0.0 h1:78Jk/r6m4wCi6sndMpty7A//t4dw/RW5fV4ZgDVfX1w=
github.com/benbjohnson/clock v1.0.0/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
@@ -219,6 +221,7 @@ github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gopacket v1.1.17 h1:rMrlX2ZY2UbvT+sdz3+6J+pp2z+msCq9MxTU6ymxbBY=
github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM=
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -449,6 +452,8 @@ github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a h1:JSvGDIbm
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/gunit v0.0.0-20180314194857-6f0d6275bdcd h1:p5kvxG4NHogJX1brTLtvUSGdW0/aBvIyqDSW7tmnsmQ=
github.com/smartystreets/gunit v0.0.0-20180314194857-6f0d6275bdcd/go.mod h1:XUKj4gbqj2QvJk/OdLWzyZ3FYli0f+MdpngyryX0gcw=
github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E=
@@ -618,6 +623,8 @@ golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135 h1:5Beo0mZN8dRzgrMMkDp0jc8YXQKx9DiJ2k1dkvGsn5A=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wireguard v0.0.20190908 h1:SUoXDdwSMtomLdvke+zz83/u9tNvl4hHmcTIWp38tow=
golang.zx2c4.com/wireguard v0.0.20190908/go.mod h1:LhfXh5z6bLC2lW2ve6BzYZFwnnsXK3OQjySR0Yh2dO8=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20191008142428-8d021180e987 h1:26OAgqBTufVr8WKonCEhhjO1oKsYhHv0iM5Dg92G1TM=
@@ -705,8 +712,8 @@ yunion.io/x/pkg v0.0.0-20190620104149-945c25821dbf/go.mod h1:t6rEGG2sQ4J7DhFxSZV
yunion.io/x/pkg v0.0.0-20190628082551-f4033ba2ea30/go.mod h1:t6rEGG2sQ4J7DhFxSZVOTjNd0YO/KlfWQyK1W4tog+E=
yunion.io/x/pkg v0.0.0-20200103043034-27c6f82160fa h1:+7zYi8MhaOW/53/7FOERnhQqAU4UhgaOVIS+AMzTKNU=
yunion.io/x/pkg v0.0.0-20200103043034-27c6f82160fa/go.mod h1:t6rEGG2sQ4J7DhFxSZVOTjNd0YO/KlfWQyK1W4tog+E=
yunion.io/x/pkg v0.0.0-20200227105015-b0738bd1ffe9 h1:O6/7+SUm2MDVC8TUEjn6GBeVFPvDprg0TFEl8A+aas8=
yunion.io/x/pkg v0.0.0-20200227105015-b0738bd1ffe9/go.mod h1:t6rEGG2sQ4J7DhFxSZVOTjNd0YO/KlfWQyK1W4tog+E=
yunion.io/x/pkg v0.0.0-20200302034534-fdf44d54b070 h1:rKnYgtvMHKmzPEUTkyNjyKOG7wzjpUvI7fcZwLNGQXw=
yunion.io/x/pkg v0.0.0-20200302034534-fdf44d54b070/go.mod h1:t6rEGG2sQ4J7DhFxSZVOTjNd0YO/KlfWQyK1W4tog+E=
yunion.io/x/s3cli v0.0.0-20190917004522-13ac36d8687e h1:v+EzIadodSwkdZ/7bremd7J8J50Cise/HCylsOJngmo=
yunion.io/x/s3cli v0.0.0-20190917004522-13ac36d8687e/go.mod h1:0iFKpOs1y4lbCxeOmq3Xx/0AcQoewVPwj62eRluioEo=
yunion.io/x/sqlchemy v0.0.0-20200221103553-6a98f7f8ab92 h1:Iz70/alKMAW3KeePhmExuhWsYw1MGTcMr5ewAL5lj1I=
+129
View File
@@ -0,0 +1,129 @@
// Copyright 2019 Yunion
//
// 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.
package monitor
import (
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/apis"
)
type AlertStateType string
type AlertSeverityType string
type NoDataOption string
type ExecutionErrorOption string
const (
AlertStateNoData AlertStateType = "no_data"
AlertStatePaused AlertStateType = "paused"
AlertStateAlerting AlertStateType = "alerting"
AlertStateOK AlertStateType = "ok"
AlertStatePending AlertStateType = "pending"
AlertStateUnknown AlertStateType = "unknown"
)
const (
NoDataSetOK NoDataOption = "ok"
NoDataSetNoData NoDataOption = "no_data"
NoDataKeepState NoDataOption = "keep_state"
NoDataSetAlerting NoDataOption = "alerting"
)
const (
ExecutionErrorSetAlerting ExecutionErrorOption = "alerting"
ExecutionErrorKeepState ExecutionErrorOption = "keep_state"
)
var (
ErrCannotChangeStateOnPausedAlert = errors.Error("Cannot change state on pause alert")
ErrRequiresNewState = errors.Error("update alert state requires a new state")
)
func (s AlertStateType) IsValid() bool {
return s == AlertStateOK ||
s == AlertStateNoData ||
s == AlertStatePaused ||
s == AlertStatePending ||
s == AlertStateAlerting ||
s == AlertStateUnknown
}
func (s NoDataOption) IsValid() bool {
return s == NoDataSetNoData || s == NoDataSetAlerting || s == NoDataKeepState || s == NoDataSetOK
}
func (s NoDataOption) ToAlertState() AlertStateType {
return AlertStateType(s)
}
func (s ExecutionErrorOption) IsValid() bool {
return s == ExecutionErrorSetAlerting || s == ExecutionErrorKeepState
}
func (s ExecutionErrorOption) ToAlertState() AlertStateType {
return AlertStateType(s)
}
// AlertSettings contains alert conditions
type AlertSetting struct {
Conditions []AlertCondition `json:"conditions"`
Notifications []string `json:"notifications"`
Level string `json:"level"`
}
type AlertCondition struct {
Type string `json:"type"`
Query AlertQuery `json:"query"`
Reducer Condition `json:"reducer"`
Evaluator Condition `json:"evaluator"`
Operator string `json:"operator"`
}
type AlertQuery struct {
Model MetricQuery `json:"model"`
DataSourceId string `json:"data_source_id"`
From string `json:"from"`
To string `json:"to"`
}
type AlertCreateInput struct {
apis.Meta
Name string `json:"name"`
Frequency int64 `json:"frequency"`
Settings AlertSetting `json:"settings"`
Enabled *bool `json:"enabled"`
}
type AlertUpdateInput struct {
apis.Meta
Name *string `json:"name"`
Frequency *int64 `json:"frequency"`
Settings *AlertSetting `json:"settings"`
ResourceId *string `json:"resource_id"`
ResourceType *string `json:"resource_type"`
Message *string `json:"message"`
Enabled *bool `json:"enabled"`
}
type AlertListInput struct {
apis.VirtualResourceListInput
// 监控指标名称
Metric string `json:"metric"`
// 以报警是否启用/禁用过滤列表
Enabled *bool `json:"enabled"`
}
+1
View File
@@ -0,0 +1 @@
package monitor // import "yunion.io/x/onecloud/pkg/apis/monitor"
+60
View File
@@ -0,0 +1,60 @@
// Copyright 2019 Yunion
//
// 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.
package monitor
import (
"yunion.io/x/onecloud/pkg/apis"
)
const (
MeterAlertTypeBalance = "balance"
MeterAlertTypeDailyResFee = "resFee"
MeterAlertTypeMonthResFee = "monthFee"
)
type MeterAlertCreateInput struct {
ResourceAlertV1CreateInput
// 监控资源类型, 比如: balance, resFree, monthFee
Type string `json:"type"`
// 云平台类型
Provider string `json:"provider"`
// 云账号 Id
AccountId string `json:"account_id"`
// 项目 Id string
ProjectId string `json:"project_id"`
}
type MeterAlertListInput struct {
apis.VirtualResourceListInput
// 监控资源类型, 比如: balance, resFree, monthFee
Type string `json:"type"`
// 云平台类型
Provider string `json:"provider"`
// 云账号 Id
AccountId string `json:"account_id"`
// 项目 Id string
ProjectId string `json:"project_id"`
}
type MeterAlertDetails struct {
AlertV1Details
Type string `json:"type"`
ProjectId string `json:"project_id"`
AccountId string `json:"account_id"`
Provider string `json:"provider"`
}
+213
View File
@@ -0,0 +1,213 @@
// Copyright 2019 Yunion
//
// 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.
package monitor
import (
"time"
"yunion.io/x/onecloud/pkg/apis"
)
const (
NodeAlertTypeGuest = "guest"
NodeAlertTypeHost = "host"
)
type ResourceAlertV1CreateInput struct {
*AlertCreateInput
// 查询指标周期
Period string `json:"period"`
// 每隔多久查询一次
Window string `json:"window"`
// 比较运算符, 比如: >, <, >=, <=
Comparator string `json:"comparator"`
// 报警阀值
Threshold float64 `json:"threshold"`
// 报警级别
Level string `json:"level"`
// 通知方式, 比如: email, mobile
Channel string `json:"channel"`
// 通知接受者
Recipients string `json:"recipients"`
}
type NodeAlertCreateInput struct {
ResourceAlertV1CreateInput
// 监控指标名称
Metric string `json:"metric"`
// 监控资源类型, 比如: guest, host
Type string `json:"type"`
// 监控资源名称
NodeName string `json:"node_name"`
// 监控资源 Id
NodeId string `json:"node_id"`
}
func (input NodeAlertCreateInput) ToAlertCreateInput(
name string,
field string,
measurement string,
db string,
notifications []string) AlertCreateInput {
freq, _ := time.ParseDuration(input.Window)
ret := AlertCreateInput{
Name: name,
Frequency: int64(freq / time.Second),
Settings: AlertSetting{
Level: input.Level,
Notifications: notifications,
Conditions: []AlertCondition{
{
Type: "query",
Operator: "and",
Query: AlertQuery{
Model: input.GetQuery(field, measurement, db),
From: input.Period,
To: "now",
},
Evaluator: input.GetEvaluator(),
Reducer: Condition{
Type: "avg",
},
},
},
},
}
return ret
}
func (input NodeAlertCreateInput) GetQuery(field, measurement, db string) MetricQuery {
return GetNodeAlertQuery(input.Type, field, measurement, db, input.NodeId)
}
func GetNodeAlertQuery(typ, field, measurement, db, nodeId string) MetricQuery {
var idField string
switch typ {
case NodeAlertTypeGuest:
idField = "vm_id"
case NodeAlertTypeHost:
idField = "host_id"
}
sels := make([]MetricQuerySelect, 0)
sels = append(sels, NewMetricQuerySelect(MetricQueryPart{Type: "field", Params: []string{field}}))
return MetricQuery{
Selects: sels,
Tags: []MetricQueryTag{
{
Key: idField,
Value: nodeId,
},
},
GroupBy: []MetricQueryPart{
{
Type: "field",
Params: []string{"*"},
},
},
Measurement: measurement,
Database: db,
}
}
func (input NodeAlertCreateInput) GetEvaluator() Condition {
return GetNodeAlertEvaluator(input.Comparator, input.Threshold)
}
func GetNodeAlertEvaluator(comparator string, threshold float64) Condition {
typ := "gt"
switch comparator {
case ">=", ">":
typ = "gt"
case "<=", "<":
typ = "lt"
}
return Condition{
Type: typ,
Params: []float64{threshold},
}
}
type NodeAlertListInput struct {
apis.VirtualResourceListInput
// 监控指标名称
Metric string `json:"metric"`
// 监控资源类型, 比如: guest, host
Type string `json:"type"`
// 监控资源名称
NodeName string `json:"node_name"`
// 监控资源 Id
NodeId string `json:"node_id"`
}
func (input NodeAlertListInput) ToAlertListInput() AlertListInput {
return AlertListInput{
VirtualResourceListInput: input.VirtualResourceListInput,
Metric: input.Metric,
}
}
type AlertV1Details struct {
apis.VirtualResourceDetails
Name string `json:"name"`
Period string `json:"period"`
Window string `json:"window"`
Comparator string `json:"comparator"`
Threshold float64 `json:"threshold"`
Recipients string `json:"recipients"`
Level string `json:"level"`
Channel string `json:"channel"`
DB string `json:"db"`
Measurement string `json:"measurement"`
Field string `json:"field"`
NotifierId string `json:"notifier_id"`
}
type NodeAlertDetails struct {
AlertV1Details
Type string `json:"type"`
Metric string `json:"metric"`
NodeId string `json:"node_id"`
NodeName string `json:"node_name"`
}
type NodeAlertUpdateInput struct {
// 监控指标名称
Metric *string `json:"metric"`
// 监控资源类型, 比如: guest, host
Type *string `json:"type"`
// 监控资源名称
NodeName *string `json:"node_name"`
// 监控资源 Id
NodeId *string `json:"node_id"`
// 查询指标周期
Period *string `json:"period"`
// 每隔多久查询一次
Window *string `json:"window"`
// 比较运算符, 比如: >, <, >=, <=
Comparator *string `json:"comparator"`
// 报警阀值
Threshold *float64 `json:"threshold"`
// 报警级别
Level *string `json:"level"`
// 通知方式, 比如: email, mobile
Channel *string `json:"channel"`
// 通知接受者
Recipients *string `json:"recipients"`
}
+106
View File
@@ -0,0 +1,106 @@
// Copyright 2019 Yunion
//
// 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.
package monitor
import (
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/onecloud/pkg/apis"
)
type AlertNotificationStateType string
var (
AlertNotificationStatePending = AlertNotificationStateType("pending")
AlertNotificationStateCompleted = AlertNotificationStateType("completed")
AlertNotificationStateUnknown = AlertNotificationStateType("unknown")
)
const (
AlertNotificationTypeOneCloud = "onecloud"
AlertNotificationTypeDingding = "dingding"
AlertNotificationTypeFeishu = "feishu"
)
type AlertNotificationCreateInput struct {
apis.Meta
// 报警通知名称
Name string `json:"name"`
// 类型
Type string `json:"type"`
// 是否为默认通知配置
IsDefault bool `json:"is_default"`
// 是否一直提醒
SendReminder *bool `json:"send_reminder"`
// 是否禁用报警恢复提醒
DisableResolveMessage *bool `json:"disable_resolve_message"`
// 发送频率
Frequency time.Duration `json:"frequency"`
// 通知配置
Settings jsonutils.JSONObject `json:"settings"`
}
type AlertNotificationUpdateInput struct {
apis.Meta
// 报警通知名称
Name string `json:"name"`
// 是否为默认通知配置
IsDefault *bool `json:"is_default"`
// 是否一直提醒
SendReminder *bool `json:"send_reminder"`
// 是否禁用报警恢复提醒
DisableResolveMessage *bool `json:"disable_resolve_message"`
// 发送频率
Frequency *time.Duration `json:"frequency"`
}
type NotificationSettingOneCloud struct {
Channel string `json:"channel"`
UserIds []string `json:"user_ids"`
}
type SendWebhookSync struct {
Url string
User string
Password string
Body string
HttpMethod string
HttpHeader map[string]string
ContentType string
}
type NotificationSettingDingding struct {
Url string `json:"url"`
MessageType string `json:"message_type"`
}
type NotificationSettingFeishu struct {
// Url string `json:"url"`
AppId string `json:"app_id"`
AppSecret string `json:"app_secret"`
}
type AlertNotificationStateCreateInput struct {
apis.Meta
Name string `json:"name"`
AlertId string `json:"alert_id"`
NotifierId string `json:"notifier_id"`
State AlertNotificationStateType `json:"state"`
}
+41
View File
@@ -0,0 +1,41 @@
// Copyright 2019 Yunion
//
// 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.
package monitor
type NotificationTemplateCreateInput struct {
Content string `json:"content"`
}
type NotificationTemplateConfig struct {
Title string `json:"title"`
Name string `json:"name"`
Matches []EvalMatch `json:"matches"`
// PrevAlertState AlertStateType `json:"prev_alert_state"`
// State AlertStateType `json:"state"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
Description string `json:"description"`
Priority string `json:"priority"`
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"`
}
+103
View File
@@ -0,0 +1,103 @@
// Copyright 2019 Yunion
//
// 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.
package monitor
const (
DataSourceTypeInfluxdb = "influxdb"
)
type DataSourceConfig struct {
Id string
Name string
Driver string
Config interface{}
}
type MetricResource struct {
// Type is the metric resource type. e.g: host, vm, lbinstance
Type string `json:"type"`
// ConfigId is the data source config id
ConfigId string `json:"config_id"`
}
type Metric struct {
Resource MetricResource `json:"resource"`
Measurement string `json:"measurement"`
Field string `json:"field"`
DisplayName string `json:"displayname"`
}
type TimeSeries struct {
Results []TimeSeriesResult `json:"results"`
}
type TimeSeriesResult struct {
Series []TimeSeriesRow `json:"series"`
}
type TimeSeriesRow struct {
Metric Metric `json:"metric"`
Tags map[string]string `json:"tags,omitempty"`
Columns []string `json:"columns,omitempty"`
// Value item is a point with timestamp and value
Values [][]interface{} `json:"values,omitempty"`
}
type MetricRequest struct {
// The start time for the query
From string `json:"from"`
// An end time for the query
To string `json:"to"`
Queries []*MetricQuery `json:"queries"`
Debug bool `json:"debug"`
}
type MetricQueryTag struct {
Key string `json:"key"`
Operator string `json:"operator"`
Value string `json:"value"`
Condition string `json:"condition"`
}
type MetricQueryPart struct {
Type string `json:"type"`
Params []string `json:"params"`
}
type MetricQuerySelect []MetricQueryPart
func NewMetricQuerySelect(parts ...MetricQueryPart) MetricQuerySelect {
return parts
}
type MetricQuery struct {
Alias string `json:"alias"`
Tz string `json:"tz"`
Database string `json:"database"`
Measurement string `json:"measurement"`
Tags []MetricQueryTag `json:"tags"`
GroupBy []MetricQueryPart `json:"group_by"`
Selects []MetricQuerySelect `json:"select"`
Interval string `json:"interval"`
Policy string `json:"policy"`
ResultFormat string `json:"result_format"`
}
type AlertConditionCombiner string
type Condition struct {
Type string `json:"type"`
Params []float64 `json:"params"`
}
+59 -14
View File
@@ -42,42 +42,83 @@ func NewEnabledStatusStandaloneResourceBaseManager(dt interface{}, tableName str
return SEnabledStatusStandaloneResourceBaseManager{SStatusStandaloneResourceBaseManager: NewStatusStandaloneResourceBaseManager(dt, tableName, keyword, keywordPlural)}
}
type IEnableModel interface {
IModel
IsEnable() bool
SetEnable() error
SetDisable() error
}
func (self *SEnabledStatusStandaloneResourceBase) IsEnable() bool {
return self.Enabled
}
func (self *SEnabledStatusStandaloneResourceBase) SetEnable() error {
self.Enabled = true
return nil
}
func (self *SEnabledStatusStandaloneResourceBase) SetDisable() error {
self.Enabled = false
return nil
}
func (self *SEnabledStatusStandaloneResourceBase) AllowPerformEnable(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool {
return IsAllowPerform(rbacutils.ScopeSystem, userCred, self, "enable")
return AllowPerformEnable(self, rbacutils.ScopeSystem, userCred)
}
func AllowPerformEnable(obj IEnableModel, scope rbacutils.TRbacScope, userCred mcclient.TokenCredential) bool {
return IsAllowPerform(scope, userCred, obj, "enable")
}
func (self *SEnabledStatusStandaloneResourceBase) PerformEnable(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) {
if !self.Enabled {
_, err := Update(self, func() error {
self.Enabled = true
return PerformEnable(self, userCred)
}
func PerformEnable(obj IEnableModel, userCred mcclient.TokenCredential) (jsonutils.JSONObject, error) {
if !obj.IsEnable() {
_, err := Update(obj, func() error {
if err := obj.SetEnable(); err != nil {
return err
}
return nil
})
if err != nil {
log.Errorf("PerformEnable save update fail %s", err)
return nil, err
}
OpsLog.LogEvent(self, ACT_ENABLE, "", userCred)
logclient.AddSimpleActionLog(self, logclient.ACT_ENABLE, nil, userCred, true)
OpsLog.LogEvent(obj, ACT_ENABLE, "", userCred)
logclient.AddSimpleActionLog(obj, logclient.ACT_ENABLE, nil, userCred, true)
}
return nil, nil
}
func (self *SEnabledStatusStandaloneResourceBase) AllowPerformDisable(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool {
return IsAllowPerform(rbacutils.ScopeSystem, userCred, self, "disable")
return AllowPerformDisable(self, rbacutils.ScopeSystem, userCred)
}
func AllowPerformDisable(obj IEnableModel, scope rbacutils.TRbacScope, userCred mcclient.TokenCredential) bool {
return IsAllowPerform(scope, userCred, obj, "disable")
}
func (self *SEnabledStatusStandaloneResourceBase) PerformDisable(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) {
if self.Enabled {
_, err := Update(self, func() error {
self.Enabled = false
return PerformDisable(self, userCred)
}
func PerformDisable(obj IEnableModel, userCred mcclient.TokenCredential) (jsonutils.JSONObject, error) {
if obj.IsEnable() {
_, err := Update(obj, func() error {
if err := obj.SetDisable(); err != nil {
return err
}
return nil
})
if err != nil {
log.Errorf("PerformDisable save update fail %s", err)
return nil, err
}
OpsLog.LogEvent(self, ACT_DISABLE, "", userCred)
logclient.AddSimpleActionLog(self, logclient.ACT_DISABLE, nil, userCred, true)
OpsLog.LogEvent(obj, ACT_DISABLE, "", userCred)
logclient.AddSimpleActionLog(obj, logclient.ACT_DISABLE, nil, userCred, true)
}
return nil, nil
}
@@ -96,8 +137,12 @@ func (manager *SEnabledStatusStandaloneResourceBaseManager) ListItemFilter(ctx c
if err != nil {
return nil, errors.Wrap(err, "SStatusStandaloneResourceBaseManager.ListItemFilter")
}
if query.Enabled != nil {
if *query.Enabled {
return ListEnableItemFilter(q, query.Enabled)
}
func ListEnableItemFilter(q *sqlchemy.SQuery, enabled *bool) (*sqlchemy.SQuery, error) {
if enabled != nil {
if *enabled {
q = q.IsTrue("enabled")
} else {
q = q.IsFalse("enabled")
+1 -1
View File
@@ -309,7 +309,7 @@ func localUserVerifyPassword(user *api.SUserExtended, passwd string) error {
if err == nil {
return nil
}
return errors.Error("invalid password")
return errors.Error(fmt.Sprintf("invalid password: %v", err))
}
// 用户列表
+6 -6
View File
@@ -48,6 +48,12 @@ func NewMonitorManager(keyword, keywordPlural string, columns, adminColumns []st
Keyword: keyword, KeywordPlural: keywordPlural}
}
func NewMonitorV2Manager(keyword, keywordPlural string, columns, adminColumns []string) modulebase.ResourceManager {
return modulebase.ResourceManager{
BaseManager: *modulebase.NewBaseManager("monitor", "", "", columns, adminColumns),
Keyword: keyword, KeywordPlural: keywordPlural}
}
func NewCloudwatcherManager(keyword, keywordPlural string, columns, adminColumns []string) modulebase.ResourceManager {
return modulebase.ResourceManager{
BaseManager: *modulebase.NewBaseManager("cloudwatcher", "", "v1", columns, adminColumns),
@@ -115,12 +121,6 @@ func NewMeterManager(keyword, keywordPlural string, columns, adminColumns []stri
Keyword: keyword, KeywordPlural: keywordPlural}
}
func NewMeterAlertManager(keyword, keywordPlural string, columns, adminColumns []string) modulebase.ResourceManager {
return modulebase.ResourceManager{
BaseManager: *modulebase.NewBaseManager("meteralert", "", "", columns, adminColumns),
Keyword: keyword, KeywordPlural: keywordPlural}
}
func NewYunionAgentManager(keyword, keywordPlural string, columns, adminColumns []string) modulebase.ResourceManager {
return modulebase.ResourceManager{
BaseManager: *modulebase.NewBaseManager("yunionagent", "", "", columns, adminColumns),
+2 -2
View File
@@ -21,8 +21,8 @@ var (
)
func init() {
MeterAlert = NewMeterAlertManager("meteralert", "meteralerts",
[]string{"id", "type", "provider", "account", "account_id", "comparator", "threshold", "recipients", "level", "channel", "status", "create_by", "update_by", "delete_by", "gmt_create", "gmt_modified", "gmt_delete", "is_deleted", "project_id", "remark"},
MeterAlert = NewMonitorV2Manager("meteralert", "meteralerts",
[]string{"id", "type", "provider", "account", "account_id", "comparator", "threshold", "recipients", "level", "channel", "state", "project_id"},
[]string{})
register(&MeterAlert)
+2 -2
View File
@@ -21,8 +21,8 @@ var (
)
func init() {
NodeAlert = NewMeterAlertManager("nodealert", "nodealerts",
[]string{"id", "type", "metric", "node_name", "node_id", "period", "window", "comparator", "threshold", "recipients", "level", "channel", "status", "create_by", "update_by", "delete_by", "gmt_create", "gmt_modified", "gmt_delete", "is_deleted", "project_id", "remark"},
NodeAlert = NewMonitorV2Manager("nodealert", "nodealerts",
[]string{"id", "type", "metric", "node_name", "node_id", "period", "window", "comparator", "threshold", "recipients", "level", "channel", "state", "project_id"},
[]string{})
register(&NodeAlert)
+41
View File
@@ -0,0 +1,41 @@
// Copyright 2019 Yunion
//
// 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.
package monitor
import (
"yunion.io/x/onecloud/pkg/mcclient/modulebase"
"yunion.io/x/onecloud/pkg/mcclient/modules"
)
var (
Alerts modulebase.ResourceManager
AlertNotifications modulebase.ResourceManager
)
func init() {
Alerts = modules.NewMonitorV2Manager("alert", "alerts",
[]string{"id", "name", "settings"},
[]string{})
AlertNotifications = modules.NewMonitorV2Manager(
"alert_notification", "alert_notifications",
[]string{"id", "name", "type", "is_default", "disable_resolve_message", "send_reminder", "settings"},
[]string{})
for _, m := range []modulebase.ResourceManager{
Alerts,
AlertNotifications,
} {
modules.Register(&m)
}
}
@@ -0,0 +1,31 @@
// Copyright 2019 Yunion
//
// 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.
package monitor
import (
"yunion.io/x/onecloud/pkg/mcclient/modulebase"
"yunion.io/x/onecloud/pkg/mcclient/modules"
)
var (
DataSources modulebase.ResourceManager
)
func init() {
DataSources = modules.NewMonitorV2Manager("datasource", "datasources",
[]string{"Id", "Name", "Type", "Url"},
[]string{})
modules.Register(&DataSources)
}
+1
View File
@@ -0,0 +1 @@
package monitor // import "yunion.io/x/onecloud/pkg/mcclient/modules/monitor"
+1
View File
@@ -0,0 +1 @@
package monitor // import "yunion.io/x/onecloud/pkg/mcclient/options/monitor"
+146
View File
@@ -0,0 +1,146 @@
// Copyright 2019 Yunion
//
// 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.
package monitor
import (
"yunion.io/x/jsonutils"
"yunion.io/x/onecloud/pkg/apis/monitor"
"yunion.io/x/onecloud/pkg/mcclient/options"
)
type DataSourceCreateOptions struct {
NAME string
}
type DataSourceListOptions struct {
options.BaseListOptions
}
type DataSourceDeleteOptions struct {
ID string `json:"-"`
}
type NotificationListOptions struct {
options.BaseListOptions
}
type NotificationShowOptions struct {
ID string `help:"ID or name of the alert notification config" json:"-"`
}
type NotificationFields struct {
Frequency string `help:"notify frequency, e.g. 5m, 1h"`
IsDefault *bool `help:"set as default notification"`
DisableResolveMessage *bool `help:"disable notify recover message"`
SendReminder *bool `help:"send reminder"`
}
type NotificationCreateOptions struct {
NAME string `help:"notification config name"`
NotificationFields
}
func (opt NotificationCreateOptions) Params() (*monitor.AlertNotificationCreateInput, error) {
ret := &monitor.AlertNotificationCreateInput{
Name: opt.NAME,
SendReminder: opt.SendReminder,
DisableResolveMessage: opt.DisableResolveMessage,
}
if opt.IsDefault != nil && *opt.IsDefault {
ret.IsDefault = true
}
return ret, nil
}
type NotificationDingDingCreateOptions struct {
NotificationCreateOptions
URL string `help:"dingding webhook url"`
MsgType string `help:"message type" choices:"markdown|actionCard" default:"markdown"`
}
func (opt NotificationDingDingCreateOptions) Params() (*monitor.AlertNotificationCreateInput, error) {
out, err := opt.NotificationCreateOptions.Params()
if err != nil {
return nil, err
}
out.Type = monitor.AlertNotificationTypeDingding
out.Settings = jsonutils.Marshal(monitor.NotificationSettingDingding{
Url: opt.URL,
MessageType: opt.MsgType,
})
return out, nil
}
type NotificationFeishuCreateOptions struct {
NotificationCreateOptions
APPID string `help:"feishu robot appId"`
APPSECRET string `help:"feishu robt appSecret"`
}
func (opt NotificationFeishuCreateOptions) Params() (*monitor.AlertNotificationCreateInput, error) {
out, err := opt.NotificationCreateOptions.Params()
if err != nil {
return nil, err
}
out.Type = monitor.AlertNotificationTypeFeishu
out.Settings = jsonutils.Marshal(monitor.NotificationSettingFeishu{
AppId: opt.APPID,
AppSecret: opt.APPSECRET,
})
return out, nil
}
type NotificationUpdateOptions struct {
NotificationFields
ID string `help:"ID or name of the alert notification config" json:"-"`
DisableDefault *bool `help:"disable as default notification" json:"-"`
ResolveMessage *bool `help:"enable notify recover message" json:"-"`
DisableSendReminder *bool `help:"disable send reminder" json:"-"`
}
func (opt NotificationUpdateOptions) Params() (*monitor.AlertNotificationUpdateInput, error) {
if opt.DisableDefault != nil && *opt.DisableDefault {
tmp := false
opt.IsDefault = &tmp
}
if opt.ResolveMessage != nil && *opt.ResolveMessage {
tmp := false
opt.DisableDefault = &tmp
}
if opt.DisableSendReminder != nil && *opt.DisableSendReminder {
tmp := false
opt.SendReminder = &tmp
}
ret := &monitor.AlertNotificationUpdateInput{
IsDefault: opt.IsDefault,
DisableResolveMessage: opt.DisableResolveMessage,
SendReminder: opt.SendReminder,
}
return ret, nil
}
type AlertListOptions struct {
options.BaseListOptions
}
type AlertShowOptions struct {
ID string `help:"ID or name of the alert" json:"-"`
}
type AlertDeleteOptions struct {
ID []string `help:"ID of alert to delete"`
}
+1
View File
@@ -0,0 +1 @@
package conditions // import "yunion.io/x/onecloud/pkg/monitor/alerting/conditions"
@@ -0,0 +1,145 @@
// Copyright 2019 Yunion
//
// 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.
package conditions
import (
"fmt"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/utils"
"yunion.io/x/onecloud/pkg/apis/monitor"
"yunion.io/x/onecloud/pkg/monitor/validators"
)
// AlertEvaluator evaluates the reduced value of a timeserie.
// Returning true if a timeseries is violating the condition
// ex: ThresholdEvaluator, NoValueEvaluator, RangeEvaluator
type AlertEvaluator interface {
Eval(reducedValue *float64) bool
String() string
}
type noValueEvaluator struct{}
func (e *noValueEvaluator) Eval(reducedValue *float64) bool {
return reducedValue == nil
}
func (e *noValueEvaluator) String() string {
return "no_data"
}
type thresholdEvaluator struct {
Type string
Threshold float64
}
func newThresholdEvaluator(cond *monitor.Condition) (*thresholdEvaluator, error) {
defaultEval := &thresholdEvaluator{
Type: cond.Type,
Threshold: cond.Params[0],
}
return defaultEval, nil
}
func (e *thresholdEvaluator) Eval(reducedValue *float64) bool {
if reducedValue == nil {
return false
}
val := *reducedValue
switch e.Type {
case "gt":
return val > e.Threshold
case "lt":
return val < e.Threshold
}
return false
}
func (e *thresholdEvaluator) String() string {
var op string
switch e.Type {
case "gt":
op = ">"
case "lt":
op = "<"
}
return fmt.Sprintf("%s %.2f", op, e.Threshold)
}
type rangedEvaluator struct {
Type string
Lower float64
Upper float64
}
func newRangedEvaluator(cond *monitor.Condition) (*rangedEvaluator, error) {
if len(cond.Params) == 0 {
return nil, errors.Wrap(validators.ErrMissingParameterThreshold, "RangedEvaluator parameter is empty")
}
if len(cond.Params) == 1 {
return nil, errors.Wrap(validators.ErrMissingParameterThreshold, "RangedEvaluator parameter second parameter is missing")
}
rangedEval := &rangedEvaluator{
Type: cond.Type,
Lower: cond.Params[0],
Upper: cond.Params[1],
}
return rangedEval, nil
}
func (e *rangedEvaluator) Eval(reducedValue *float64) bool {
if reducedValue == nil {
return false
}
val := *reducedValue
switch e.Type {
case "within_range":
return (e.Lower < val && e.Upper > val) || (e.Upper < val && e.Lower > val)
case "outside_range":
return (e.Upper < val && e.Lower < val) || (e.Upper > val && e.Lower > val)
}
return false
}
func (e *rangedEvaluator) String() string {
return fmt.Sprintf("%s [%.2f, %.2f]", e.Type, e.Lower, e.Upper)
}
// NewAlertEvaluator is a factory function for returning
// an `AlertEvaluator` depending on the input condition.
func NewAlertEvaluator(cond *monitor.Condition) (AlertEvaluator, error) {
typ := cond.Type
if typ == "" {
return nil, validators.ErrMissingParameterType
}
if utils.IsInStringArray(typ, validators.EvaluatorDefaultTypes) {
return newThresholdEvaluator(cond)
}
if utils.IsInStringArray(typ, validators.EvaluatorRangedTypes) {
return newRangedEvaluator(cond)
}
if typ == "no_value" {
return &noValueEvaluator{}, nil
}
return nil, errors.Wrapf(validators.ErrInvalidEvaluatorType, "type: %s", typ)
}
@@ -0,0 +1,68 @@
// Copyright 2019 Yunion
//
// 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.
package conditions
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
"yunion.io/x/onecloud/pkg/apis/monitor"
)
func evalutorScenario(typ string, params []float64, reducedValue float64) bool {
evaluator, err := NewAlertEvaluator(&monitor.Condition{Type: typ, Params: params})
So(err, ShouldBeNil)
return evaluator.Eval(&reducedValue)
}
func TestEvalutors(t *testing.T) {
Convey("greater than", t, func() {
So(evalutorScenario("gt", []float64{1}, 3), ShouldBeTrue)
So(evalutorScenario("gt", []float64{3}, 1), ShouldBeFalse)
})
Convey("less than", t, func() {
So(evalutorScenario("lt", []float64{1}, 3), ShouldBeFalse)
So(evalutorScenario("lt", []float64{3}, 1), ShouldBeTrue)
})
Convey("within_range", t, func() {
So(evalutorScenario("within_range", []float64{1, 100}, 3), ShouldBeTrue)
So(evalutorScenario("within_range", []float64{1, 100}, 300), ShouldBeFalse)
So(evalutorScenario("within_range", []float64{100, 1}, 3), ShouldBeTrue)
So(evalutorScenario("within_range", []float64{100, 1}, 300), ShouldBeFalse)
})
Convey("outside_range", t, func() {
So(evalutorScenario("outside_range", []float64{1, 100}, 1000), ShouldBeTrue)
So(evalutorScenario("outside_range", []float64{1, 100}, 50), ShouldBeFalse)
So(evalutorScenario("outside_range", []float64{100, 1}, 1000), ShouldBeTrue)
So(evalutorScenario("outside_range", []float64{100, 1}, 50), ShouldBeFalse)
})
Convey("no_value", t, func() {
Convey("should be false if series have values", func() {
So(evalutorScenario("no_value", nil, 50), ShouldBeFalse)
})
Convey("should be true when the series have no value", func() {
evaluator, err := NewAlertEvaluator(&monitor.Condition{Type: "no_value"})
So(err, ShouldBeNil)
So(evaluator.Eval(nil), ShouldBeTrue)
})
})
}
+268
View File
@@ -0,0 +1,268 @@
// Copyright 2019 Yunion
//
// 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.
package conditions
import (
gocontext "context"
"fmt"
"strings"
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/apis/monitor"
"yunion.io/x/onecloud/pkg/monitor/alerting"
"yunion.io/x/onecloud/pkg/monitor/models"
"yunion.io/x/onecloud/pkg/monitor/tsdb"
"yunion.io/x/onecloud/pkg/monitor/validators"
)
func init() {
alerting.RegisterCondition("query", func(model *monitor.AlertCondition, index int) (alerting.Condition, error) {
return newQueryCondition(model, index)
})
}
// QueryCondition is responsible for issue and query. reduce the
// timeseries into single values and evaluate if they are firing or not.
type QueryCondition struct {
Index int
Query AlertQuery
Reducer *queryReducer
Evaluator AlertEvaluator
Operator string
HandleRequest tsdb.HandleRequestFunc
}
// AlertQuery contains information about what datasource a query
// should be send to and the query object.
type AlertQuery struct {
Model monitor.MetricQuery
DataSourceId string
From string
To string
}
type FormatCond struct {
QueryMeta *tsdb.QueryResultMeta
Reducer string
Evaluator AlertEvaluator
}
func (c *QueryCondition) GenerateFormatCond(meta *tsdb.QueryResultMeta) *FormatCond {
return &FormatCond{
QueryMeta: meta,
Reducer: c.Reducer.Type,
Evaluator: c.Evaluator,
}
}
func (c FormatCond) String() string {
if c.QueryMeta != nil {
return fmt.Sprintf("%s(%q) %s", c.Reducer, c.QueryMeta.RawQuery, c.Evaluator.String())
}
return "no_data"
}
func (c *QueryCondition) filterTags(tags map[string]string) map[string]string {
ret := make(map[string]string)
for key, val := range tags {
if strings.HasSuffix(key, "_id") {
continue
}
ret[key] = val
}
return ret
}
// Eval evaluates te `QueryCondition`.
func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.ConditionResult, error) {
timeRange := tsdb.NewTimeRange(c.Query.From, c.Query.To)
ret, err := c.executeQuery(context, timeRange)
if err != nil {
return nil, err
}
seriesList := ret.series
metas := ret.metas
emptySeriesCount := 0
evalMatchCount := 0
var matches []*alerting.EvalMatch
for idx, series := range seriesList {
reducedValue := c.Reducer.Reduce(series)
evalMatch := c.Evaluator.Eval(reducedValue)
if reducedValue == nil {
emptySeriesCount++
}
if context.IsTestRun {
context.Logs = append(context.Logs, &alerting.ResultLogEntry{
Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %v", c.Index, evalMatch, series.Name, reducedValue),
})
}
if evalMatch {
evalMatchCount++
}
tags := c.filterTags(series.Tags)
matches = append(matches, &alerting.EvalMatch{
Condition: c.GenerateFormatCond(&metas[idx]).String(),
Metric: series.Name,
Value: reducedValue,
Tags: tags,
})
}
// handle no series special case
if len(seriesList) == 0 {
// eval condition for null value
evalMatch := c.Evaluator.Eval(nil)
if context.IsTestRun {
context.Logs = append(context.Logs, &alerting.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{
Metric: "NoData",
Value: nil,
})
}
}
return &alerting.ConditionResult{
Firing: evalMatchCount > 0,
NoDataFound: emptySeriesCount == len(seriesList),
Operator: c.Operator,
EvalMatches: matches,
}, nil
}
type queryResult struct {
series tsdb.TimeSeriesSlice
metas []tsdb.QueryResultMeta
}
func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange *tsdb.TimeRange) (*queryResult, error) {
ds, err := models.DataSourceManager.GetSource(c.Query.DataSourceId)
if err != nil {
return nil, errors.Wrapf(err, "Cound not find datasource %v", c.Query.DataSourceId)
}
req := c.getRequestForAlertRule(ds, timeRange, context.IsDebug)
result := make(tsdb.TimeSeriesSlice, 0)
metas := make([]tsdb.QueryResultMeta, 0)
if context.IsDebug {
// TODO: record info when is debug mode
}
resp, err := c.HandleRequest(context.Ctx, ds.ToTSDBDataSource(""), req)
if err != nil {
if err == gocontext.DeadlineExceeded {
return nil, errors.Error("Alert execution exceeded the timeout")
}
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")
}
result = append(result, v.Series...)
metas = append(metas, v.Meta)
queryResultData := map[string]interface{}{}
if context.IsTestRun {
queryResultData["series"] = v.Series
}
/*if context.IsDebug && v.Meta != nil {
queryResultData["meta"] = v.Meta
}*/
if context.IsTestRun || context.IsDebug {
context.Logs = append(context.Logs, &alerting.ResultLogEntry{
Message: fmt.Sprintf("Condition[%d]: Query Result", c.Index),
Data: queryResultData,
})
}
}
return &queryResult{
series: result,
metas: metas,
}, nil
}
func (c *QueryCondition) getRequestForAlertRule(ds *models.SDataSource, timeRange *tsdb.TimeRange, debug bool) *tsdb.TsdbQuery {
req := &tsdb.TsdbQuery{
TimeRange: timeRange,
Queries: []*tsdb.Query{
{
RefId: "A",
MetricQuery: c.Query.Model,
DataSource: *ds.ToTSDBDataSource(c.Query.Model.Database),
},
},
Debug: debug,
}
return req
}
func newQueryCondition(model *monitor.AlertCondition, index int) (*QueryCondition, error) {
cond := new(QueryCondition)
cond.Index = index
cond.HandleRequest = tsdb.HandleRequest
q := model.Query
cond.Query.Model = q.Model
cond.Query.From = q.From
cond.Query.To = q.To
if err := validators.ValidateFromValue(cond.Query.From); err != nil {
return nil, errors.Wrapf(err, "from value %q", cond.Query.From)
}
if err := validators.ValidateToValue(cond.Query.To); err != nil {
return nil, errors.Wrapf(err, "to value %q", cond.Query.To)
}
cond.Query.DataSourceId = q.DataSourceId
reducer := model.Reducer
cond.Reducer = newSimpleReducer(reducer.Type)
evaluator, err := NewAlertEvaluator(&model.Evaluator)
if err != nil {
return nil, fmt.Errorf("error in condition %v: %v", index, err)
}
cond.Evaluator = evaluator
operator := model.Operator
if operator == "" {
operator = "and"
}
cond.Operator = operator
return cond, nil
}
@@ -0,0 +1,15 @@
// Copyright 2019 Yunion
//
// 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.
package conditions
+169
View File
@@ -0,0 +1,169 @@
// Copyright 2019 Yunion
//
// 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.
package conditions
import (
"math"
"sort"
"yunion.io/x/onecloud/pkg/monitor/tsdb"
)
// queryReducer reduces an timeseries to a float
type queryReducer struct {
// Type is how the timeseries should be reduced.
// Ex: avg, sum, max, min, count
Type string
}
func (s *queryReducer) Reduce(series *tsdb.TimeSeries) *float64 {
if len(series.Points) == 0 {
return nil
}
value := float64(0)
allNull := true
switch s.Type {
case "avg":
validPointsCount := 0
for _, point := range series.Points {
if point.IsValid() {
value += point.Value()
validPointsCount++
allNull = false
}
}
if validPointsCount > 0 {
value = value / float64(validPointsCount)
}
case "sum":
for _, point := range series.Points {
if point.IsValid() {
value += point.Value()
allNull = false
}
}
case "min":
value = math.MaxFloat64
for _, point := range series.Points {
if point.IsValid() {
allNull = false
if value > point.Value() {
value = point.Value()
}
}
}
case "max":
value = -math.MaxFloat64
for _, point := range series.Points {
if point.IsValid() {
allNull = false
if value < point.Value() {
value = point.Value()
}
}
}
case "count":
value = float64(len(series.Points))
allNull = false
case "last":
points := series.Points
for i := len(points) - 1; i >= 0; i-- {
if points[i].IsValid() {
value = points[i].Value()
allNull = false
break
}
}
case "median":
var values []float64
for _, v := range series.Points {
if v.IsValid() {
allNull = false
values = append(values, v.Value())
}
}
if len(values) >= 1 {
sort.Float64s(values)
length := len(values)
if length%2 == 1 {
value = values[(length-1)/2]
} else {
value = (values[(length/2)-1] + values[length/2]) / 2
}
}
case "diff":
allNull, value = calculateDiff(series, allNull, value, diff)
case "percent_diff":
allNull, value = calculateDiff(series, allNull, value, percentDiff)
case "count_non_null":
for _, v := range series.Points {
if v.IsValid() {
value++
}
}
if value > 0 {
allNull = false
}
}
if allNull {
return nil
}
return &value
}
func newSimpleReducer(t string) *queryReducer {
return &queryReducer{Type: t}
}
func calculateDiff(series *tsdb.TimeSeries, allNull bool, value float64, fn func(float64, float64) float64) (bool, float64) {
var (
points = series.Points
first float64
i int
)
// get the newest point
for i = len(points) - 1; i >= 0; i-- {
if points[i].IsValid() {
allNull = false
first = points[i].Value()
break
}
}
if i >= 1 {
// get the oldest point
for i := 0; i < len(points); i++ {
if points[i].IsValid() {
allNull = false
val := fn(first, points[i].Value())
value = math.Abs(val)
break
}
}
}
return allNull, value
}
var diff = func(newest, oldest float64) float64 {
return newest - oldest
}
var percentDiff = func(newest, oldest float64) float64 {
return (newest - oldest) / oldest * 100
}
@@ -0,0 +1,202 @@
// Copyright 2019 Yunion
//
// 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.
package conditions
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
"yunion.io/x/onecloud/pkg/monitor/tsdb"
)
func TestSimpleReducer(t *testing.T) {
Convey("Test simple reducer by calculating", t, func() {
Convey("sum", func() {
result := testReducer("sum", 1, 2, 3)
So(result, ShouldEqual, float64(6))
})
Convey("min", func() {
result := testReducer("min", 3, 2, 1)
So(result, ShouldEqual, float64(1))
})
Convey("max", func() {
result := testReducer("max", 1, 2, 3)
So(result, ShouldEqual, float64(3))
})
Convey("count", func() {
result := testReducer("count", 1, 2, 3000)
So(result, ShouldEqual, float64(3))
})
Convey("last", func() {
result := testReducer("last", 1, 2, 3000)
So(result, ShouldEqual, float64(3000))
})
Convey("median odd amount of numbers", func() {
result := testReducer("median", 1, 2, 3000)
So(result, ShouldEqual, float64(2))
})
Convey("median even amount of numbers", func() {
result := testReducer("median", 1, 2, 4, 3000)
So(result, ShouldEqual, float64(3))
})
Convey("median with one values", func() {
result := testReducer("median", 1)
So(result, ShouldEqual, float64(1))
})
Convey("median should ignore null values", func() {
reducer := newSimpleReducer("median")
series := &tsdb.TimeSeries{
Name: "test time series",
}
series.Points = append(series.Points, tsdb.NewTimePoint(nil, 1))
series.Points = append(series.Points, tsdb.NewTimePoint(nil, 2))
series.Points = append(series.Points, tsdb.NewTimePoint(nil, 3))
series.Points = append(series.Points, tsdb.NewTimePointByVal(1, 4))
series.Points = append(series.Points, tsdb.NewTimePointByVal(2, 5))
series.Points = append(series.Points, tsdb.NewTimePointByVal(3, 6))
result := reducer.Reduce(series)
So(result, ShouldNotBeNil)
So(*result, ShouldEqual, 2)
})
Convey("avg", func() {
result := testReducer("avg", 1, 2, 3)
So(result, ShouldEqual, float64(2))
})
Convey("count_non_null", func() {
Convey("with null values and real values", func() {
reducer := newSimpleReducer("count_non_null")
series := &tsdb.TimeSeries{
Name: "test time series",
}
series.Points = append(series.Points, tsdb.NewTimePoint(nil, 1))
series.Points = append(series.Points, tsdb.NewTimePoint(nil, 2))
series.Points = append(series.Points, tsdb.NewTimePointByVal(3, 3))
series.Points = append(series.Points, tsdb.NewTimePointByVal(4, 4))
So(reducer.Reduce(series), ShouldNotBeNil)
So(*reducer.Reduce(series), ShouldEqual, 2)
})
Convey("with null values", func() {
reducer := newSimpleReducer("count_non_null")
series := &tsdb.TimeSeries{
Name: "test time series",
}
series.Points = append(series.Points, tsdb.NewTimePoint(nil, 1))
series.Points = append(series.Points, tsdb.NewTimePoint(nil, 2))
So(reducer.Reduce(series), ShouldBeNil)
})
})
Convey("avg of number values and null values should ignore nulls", func() {
reduer := newSimpleReducer("avg")
series := &tsdb.TimeSeries{
Name: "test time series",
}
series.Points = append(series.Points, tsdb.NewTimePoint(nil, 1))
series.Points = append(series.Points, tsdb.NewTimePoint(nil, 2))
series.Points = append(series.Points, tsdb.NewTimePoint(nil, 3))
series.Points = append(series.Points, tsdb.NewTimePointByVal(3, 4))
So(*reduer.Reduce(series), ShouldEqual, 3)
})
Convey("diff one point", func() {
result := testReducer("diff", 30)
So(result, ShouldEqual, float64(0))
})
Convey("diff two points", func() {
result := testReducer("diff", 30, 40)
So(result, ShouldEqual, float64(10))
})
Convey("diff three points", func() {
result := testReducer("diff", 30, 40, 40)
So(result, ShouldEqual, float64(10))
})
Convey("diff with only nulls", func() {
reducer := newSimpleReducer("diff")
series := &tsdb.TimeSeries{
Name: "test time serie",
}
series.Points = append(series.Points, tsdb.NewTimePoint(nil, 1))
series.Points = append(series.Points, tsdb.NewTimePoint(nil, 2))
So(reducer.Reduce(series), ShouldBeNil)
})
Convey("percent_diff one point", func() {
result := testReducer("percent_diff", 40)
So(result, ShouldEqual, float64(0))
})
Convey("percent_diff two points", func() {
result := testReducer("percent_diff", 30, 40)
So(result, ShouldEqual, float64(33.33333333333333))
})
Convey("percent_diff three points", func() {
result := testReducer("percent_diff", 30, 40, 40)
So(result, ShouldEqual, float64(33.33333333333333))
})
Convey("percent_diff with only nulls", func() {
reducer := newSimpleReducer("percent_diff")
series := &tsdb.TimeSeries{
Name: "test time serie",
}
series.Points = append(series.Points, tsdb.NewTimePoint(nil, 1))
series.Points = append(series.Points, tsdb.NewTimePoint(nil, 2))
So(reducer.Reduce(series), ShouldBeNil)
})
})
}
func testReducer(reducerType string, datapoints ...float64) float64 {
reducer := newSimpleReducer(reducerType)
serires := &tsdb.TimeSeries{
Name: "test time series",
}
for idx := range datapoints {
val := datapoints[idx]
serires.Points = append(serires.Points, tsdb.NewTimePoint(&val, 1234134))
}
return *reducer.Reduce(serires)
}
+1
View File
@@ -0,0 +1 @@
package alerting // import "yunion.io/x/onecloud/pkg/monitor/alerting"
+240
View File
@@ -0,0 +1,240 @@
// Copyright 2019 Yunion
//
// 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.
package alerting
import (
"context"
"runtime/debug"
"time"
"github.com/benbjohnson/clock"
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
"yunion.io/x/log"
"yunion.io/x/onecloud/pkg/mcclient/auth"
"yunion.io/x/onecloud/pkg/monitor/options"
"yunion.io/x/onecloud/pkg/monitor/registry"
)
// AlertEngine is the background process that
// schedules alert evaluations and makes sure notifications
// are sent.
type AlertEngine struct {
execQueue chan *Job
ticker *Ticker
scheduler scheduler
evalHandler evalHandler
ruleReader ruleReader
resultHandler resultHandler
}
func init() {
registry.RegisterService(&AlertEngine{})
}
// IsDisabled returns true if the alerting service is disable for this instance.
func (e *AlertEngine) IsDisabled() bool {
// TODO: read from config options
return false
}
// Init initalizes the AlertingService.
func (e *AlertEngine) Init() error {
e.ticker = NewTicker(time.Now(), time.Second*0, clock.New())
e.execQueue = make(chan *Job, 1000)
e.scheduler = newScheduler()
e.evalHandler = NewEvalHandler()
e.ruleReader = newRuleReader()
e.resultHandler = newResultHandler()
return nil
}
// Run starts the alerting service background process.
func (e *AlertEngine) Run(ctx context.Context) error {
alertGroup, ctx := errgroup.WithContext(ctx)
alertGroup.Go(func() error { return e.alertingTicker(ctx) })
alertGroup.Go(func() error { return e.runJobDispatcher(ctx) })
err := alertGroup.Wait()
return err
}
func (e *AlertEngine) alertingTicker(ctx context.Context) error {
defer func() {
if err := recover(); err != nil {
log.Errorf("Scheduler panic: stopping alertingTicker, error: %v", err)
debug.PrintStack()
}
}()
tickIndex := 0
for {
select {
case <-ctx.Done():
return ctx.Err()
case tick := <-e.ticker.C:
// TEMP SOLUTION update rules ever tenth tick
if tickIndex%10 == 0 {
e.scheduler.Update(e.ruleReader.fetch())
}
e.scheduler.Tick(tick, e.execQueue)
tickIndex++
}
}
}
func (e *AlertEngine) runJobDispatcher(ctx context.Context) error {
dispatcherGroup, alertCtx := errgroup.WithContext(ctx)
for {
select {
case <-ctx.Done():
return dispatcherGroup.Wait()
case job := <-e.execQueue:
dispatcherGroup.Go(func() error { return e.processJobWithRetry(alertCtx, job) })
}
}
}
var (
unfinishedWorkTimeout = time.Second * 5
)
func (e *AlertEngine) processJobWithRetry(ctx context.Context, job *Job) error {
defer func() {
if err := recover(); err != nil {
log.Errorf("Alert panic, error: %v", err)
}
}()
cancelChan := make(chan context.CancelFunc, options.Options.AlertingMaxAttempts*2)
attemptChan := make(chan int, 1)
// Initialize with first attemptID=1
attemptChan <- 1
job.SetRunning(true)
for {
select {
case <-ctx.Done():
// In case monitor server is cancel, let a chance to job processing
// to finish gracefully - by waiting a timeout duration -
unfinishedWorkTimer := time.NewTimer(unfinishedWorkTimeout)
select {
case <-unfinishedWorkTimer.C:
return e.endJob(ctx.Err(), cancelChan, job)
case <-attemptChan:
return e.endJob(nil, cancelChan, job)
}
case attemptId, more := <-attemptChan:
if !more {
return e.endJob(nil, cancelChan, job)
}
go e.processJob(attemptId, attemptChan, cancelChan, job)
}
}
}
func (e *AlertEngine) endJob(err error, cancelChan chan context.CancelFunc, job *Job) error {
job.SetRunning(false)
close(cancelChan)
for cancelFn := range cancelChan {
cancelFn()
}
return err
}
func (e *AlertEngine) processJob(attemptID int, attemptChan chan int, cancelChan chan context.CancelFunc, job *Job) {
defer func() {
if err := recover(); err != nil {
log.Errorf("Alert Panic: error: %v", err)
}
}()
alertCtx, cancelFn := context.WithTimeout(context.Background(), time.Duration(options.Options.AlertingEvaluationTimeoutSeconds)*time.Second)
cancelChan <- cancelFn
// span := opentracing.StartSpan("alert execution")
// alertCtx = opentracing.ContextWithSpan(alertCtx, span)
evalContext := NewEvalContext(alertCtx, auth.AdminCredential(), job.Rule)
evalContext.Ctx = alertCtx
go func() {
defer func() {
if err := recover(); err != nil {
log.Errorf("Alert panic, error: %v", err)
debug.PrintStack()
// ext.Error.Set(span, true)
// span.LogFields(
// tlog.Error(fmt.Errorf("%v", err)),
// tlog.String("message", "failed to execute alert rule. panic was recovered."),
//)
//span.Finish()
close(attemptChan)
}
}()
e.evalHandler.Eval(evalContext)
/*span.SetTag("alertId", evalContext.Rule.ID)
span.SetTag("dashboardId", evalContext.Rule.DashboardID)
span.SetTag("firing", evalContext.Firing)
span.SetTag("nodatapoints", evalContext.NoDataFound)
span.SetTag("attemptID", attemptID)*/
if evalContext.Error != nil {
/*ext.Error.Set(span, true)
span.LogFields(
tlog.Error(evalContext.Error),
tlog.String("message", "alerting execution attempt failed"),
)
*/
if attemptID < options.Options.AlertingMaxAttempts {
// span.Finish(
log.Debugf("Job Execution attempt triggered retry, timeMs: %v, alertId: %d", evalContext.GetDurationMs(), attemptID)
attemptChan <- (attemptID + 1)
return
}
}
// create new context with timeout for notifications
resultHandleCtx, resultHandleCancelFn := context.WithTimeout(context.Background(), time.Duration(options.Options.AlertingNotificationTimeoutSeconds)*time.Second)
cancelChan <- resultHandleCancelFn
// override the context used for evaluation with a new context for notifications.
// This makes it possible for notifiers to execute when datasources
// don't respond within the timeout limit. We should rewrite this so notifications
// don't reuse the evalContext and get its own context.
evalContext.Ctx = resultHandleCtx
evalContext.Rule.State = evalContext.GetNewState()
if err := e.resultHandler.handle(evalContext); err != nil {
if xerrors.Is(err, context.Canceled) {
log.Debugf("Result handler returned context.Canceled")
} else if xerrors.Is(err, context.DeadlineExceeded) {
log.Debugf("Result handler returned context.DeadlineExceeded")
} else {
log.Errorf("Failed to handle result: %v", err)
}
}
// span.Finish()
log.Debugf("Job execution completed, timeMs: %v, alertId: %s, attemptId: %d", evalContext.GetDurationMs(), evalContext.Rule.Id, attemptID)
close(attemptChan)
}()
}
+194
View File
@@ -0,0 +1,194 @@
// Copyright 2019 Yunion
//
// 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.
package alerting
import (
"context"
"time"
"yunion.io/x/log"
"yunion.io/x/onecloud/pkg/apis/monitor"
"yunion.io/x/onecloud/pkg/mcclient"
)
// EvalContext is the context object for an alert evaluation.
type EvalContext struct {
Firing bool
IsTestRun bool
IsDebug bool
EvalMatches []*EvalMatch
Logs []*ResultLogEntry
Error error
ConditionEvals string
StartTime time.Time
EndTime time.Time
Rule *Rule
NoDataFound bool
PrevAlertState monitor.AlertStateType
Ctx context.Context
UserCred mcclient.TokenCredential
}
// NewEvalContext is the EvalContext constructor.
func NewEvalContext(alertCtx context.Context, userCred mcclient.TokenCredential, rule *Rule) *EvalContext {
return &EvalContext{
Ctx: alertCtx,
UserCred: userCred,
StartTime: time.Now(),
Rule: rule,
EvalMatches: make([]*EvalMatch, 0),
PrevAlertState: rule.State,
}
}
// SateDescription contains visual information about the alert state.
type StateDescription struct {
//Color string
Text string
Data string
}
// GetStateModel returns the `StateDescription` based on current state.
func (c *EvalContext) GetStateModel() *StateDescription {
switch c.Rule.State {
case monitor.AlertStateOK:
return &StateDescription{
Text: "OK",
}
case monitor.AlertStateNoData:
return &StateDescription{
Text: "No Data",
}
case monitor.AlertStateAlerting:
return &StateDescription{
Text: "Alerting",
}
case monitor.AlertStateUnknown:
return &StateDescription{
Text: "Unknown",
}
default:
panic("Unknown rule state for alert " + c.Rule.State)
}
}
func (c *EvalContext) shouldUpdateAlertState() bool {
return c.Rule.State != c.PrevAlertState
}
// GetDurationMs returns the duration of the alert evaluation.
func (c *EvalContext) GetDurationMs() float64 {
return float64(c.EndTime.Nanosecond()-c.StartTime.Nanosecond()) / float64(1000000)
}
func (c *EvalContext) GetRuleTitle() string {
rule := c.Rule
if rule.Title != "" {
return rule.Title
}
return rule.Name
}
// GetNotificationTitle returns the title of the alert rule including alert state.
func (c *EvalContext) GetNotificationTitle() string {
return "[" + c.GetStateModel().Text + "] " + c.GetRuleTitle()
}
// GetNewState returns the new state from the alert rule evaluation.
func (c *EvalContext) GetNewState() monitor.AlertStateType {
ns := getNewStateInternal(c)
if ns != monitor.AlertStateAlerting || c.Rule.For == 0 {
return ns
}
since := time.Since(c.Rule.LastStateChange)
if c.PrevAlertState == monitor.AlertStatePending && since > c.Rule.For {
return monitor.AlertStateAlerting
}
if c.PrevAlertState == monitor.AlertStateAlerting {
return monitor.AlertStateAlerting
}
return monitor.AlertStatePending
}
func getNewStateInternal(c *EvalContext) monitor.AlertStateType {
if c.Error != nil {
log.Errorf("Alert Rule Result Error, ruleId: %s, name: %s, error: %v, changing state to %v",
c.Rule.Id,
c.Rule.Name,
c.Error,
c.Rule.ExecutionErrorState.ToAlertState())
if c.Rule.ExecutionErrorState == monitor.ExecutionErrorKeepState {
return c.PrevAlertState
}
return c.Rule.ExecutionErrorState.ToAlertState()
}
if c.Firing {
return monitor.AlertStateAlerting
}
if c.NoDataFound {
log.Infof("Alert Rule returned no data, ruleId: %s, name: %s, changing state to %v",
c.Rule.Id,
c.Rule.Name,
c.Rule.NoDataState.ToAlertState())
if c.Rule.NoDataState == monitor.NoDataKeepState {
return c.PrevAlertState
}
return c.Rule.NoDataState.ToAlertState()
}
return monitor.AlertStateOK
}
func (c *EvalContext) GetNotificationTemplateConfig() monitor.NotificationTemplateConfig {
desc := c.Rule.Message
if c.Error != nil {
if desc != "" {
desc += "\n"
}
desc += "Error: " + c.Error.Error()
}
return monitor.NotificationTemplateConfig{
Title: c.GetNotificationTitle(),
Name: c.Rule.Name,
Matches: c.GetEvalMatches(),
StartTime: c.StartTime.Format(time.RFC3339),
EndTime: c.EndTime.Format(time.RFC3339),
Description: desc,
Level: c.Rule.Level,
}
}
func (c *EvalContext) GetEvalMatches() []monitor.EvalMatch {
ret := make([]monitor.EvalMatch, 0)
for _, c := range c.EvalMatches {
ret = append(ret, monitor.EvalMatch{
Condition: c.Condition,
Value: c.Value,
Metric: c.Metric,
Tags: c.Tags,
})
}
return ret
}
+220
View File
@@ -0,0 +1,220 @@
// Copyright 2019 Yunion
//
// 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.
package alerting
import (
"context"
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"yunion.io/x/onecloud/pkg/apis/monitor"
)
func TestStateIsUpdatedWhenNeeded(t *testing.T) {
ctx := NewEvalContext(context.TODO(), nil, &Rule{Conditions: []Condition{&conditionStub{firing: true}}})
t.Run("ok -> alerting", func(t *testing.T) {
ctx.PrevAlertState = monitor.AlertStateOK
ctx.Rule.State = monitor.AlertStateAlerting
if !ctx.shouldUpdateAlertState() {
t.Fatalf("expected should updated to be true")
}
})
t.Run("ok -> ok", func(t *testing.T) {
ctx.PrevAlertState = monitor.AlertStateOK
ctx.Rule.State = monitor.AlertStateOK
if ctx.shouldUpdateAlertState() {
t.Fatalf("expected should updated to be false")
}
})
}
func TestGetStateFromEvalContext(t *testing.T) {
tcs := []struct {
name string
expected monitor.AlertStateType
applyFn func(ec *EvalContext)
}{
{
name: "ok -> alerting",
expected: monitor.AlertStateAlerting,
applyFn: func(ec *EvalContext) {
ec.Firing = true
ec.PrevAlertState = monitor.AlertStateOK
},
},
{
name: "ok -> error(alerting)",
expected: monitor.AlertStateAlerting,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = monitor.AlertStateOK
ec.Error = errors.New("test error")
ec.Rule.ExecutionErrorState = monitor.ExecutionErrorSetAlerting
},
},
{
name: "ok -> pending. since its been firing for less than FOR",
expected: monitor.AlertStatePending,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = monitor.AlertStateOK
ec.Firing = true
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2)
ec.Rule.For = time.Minute * 5
},
},
{
name: "ok -> pending. since it has to be pending longer than FOR and prev state is ok",
expected: monitor.AlertStatePending,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = monitor.AlertStateOK
ec.Firing = true
ec.Rule.LastStateChange = time.Now().Add(-(time.Hour * 5))
ec.Rule.For = time.Minute * 2
},
},
{
name: "pending -> alerting. since its been firing for more than FOR and prev state is pending",
expected: monitor.AlertStateAlerting,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = monitor.AlertStatePending
ec.Firing = true
ec.Rule.LastStateChange = time.Now().Add(-(time.Hour * 5))
ec.Rule.For = time.Minute * 2
},
},
{
name: "alerting -> alerting. should not update regardless of FOR",
expected: monitor.AlertStateAlerting,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = monitor.AlertStateAlerting
ec.Firing = true
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
ec.Rule.For = time.Minute * 2
},
},
{
name: "ok -> ok. should not update regardless of FOR",
expected: monitor.AlertStateOK,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = monitor.AlertStateOK
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
ec.Rule.For = time.Minute * 2
},
},
{
name: "ok -> error(keep_last)",
expected: monitor.AlertStateOK,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = monitor.AlertStateOK
ec.Error = errors.New("test error")
ec.Rule.ExecutionErrorState = monitor.ExecutionErrorKeepState
},
},
{
name: "pending -> error(keep_last)",
expected: monitor.AlertStatePending,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = monitor.AlertStatePending
ec.Error = errors.New("test error")
ec.Rule.ExecutionErrorState = monitor.ExecutionErrorKeepState
},
},
{
name: "ok -> no_data(alerting)",
expected: monitor.AlertStateAlerting,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = monitor.AlertStateOK
ec.Rule.NoDataState = monitor.NoDataSetAlerting
ec.NoDataFound = true
},
},
{
name: "ok -> no_data(keep_last)",
expected: monitor.AlertStateOK,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = monitor.AlertStateOK
ec.Rule.NoDataState = monitor.NoDataKeepState
ec.NoDataFound = true
},
},
{
name: "pending -> no_data(keep_last)",
expected: monitor.AlertStatePending,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = monitor.AlertStatePending
ec.Rule.NoDataState = monitor.NoDataKeepState
ec.NoDataFound = true
},
},
{
name: "pending -> no_data(alerting) with for duration have not passed",
expected: monitor.AlertStatePending,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = monitor.AlertStatePending
ec.Rule.NoDataState = monitor.NoDataSetAlerting
ec.NoDataFound = true
ec.Rule.For = time.Minute * 5
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2)
},
},
{
name: "pending -> no_data(alerting) should set alerting since time passed FOR",
expected: monitor.AlertStateAlerting,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = monitor.AlertStatePending
ec.Rule.NoDataState = monitor.NoDataSetAlerting
ec.NoDataFound = true
ec.Rule.For = time.Minute * 2
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
},
},
{
name: "pending -> error(alerting) with for duration have not passed ",
expected: monitor.AlertStatePending,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = monitor.AlertStatePending
ec.Rule.ExecutionErrorState = monitor.ExecutionErrorSetAlerting
ec.Error = errors.New("test error")
ec.Rule.For = time.Minute * 5
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2)
},
},
{
name: "pending -> error(alerting) should set alerting since time passed FOR",
expected: monitor.AlertStateAlerting,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = monitor.AlertStatePending
ec.Rule.ExecutionErrorState = monitor.ExecutionErrorSetAlerting
ec.Error = errors.New("test error")
ec.Rule.For = time.Minute * 2
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
},
},
}
for _, tc := range tcs {
evalContext := NewEvalContext(context.Background(), nil, &Rule{Conditions: []Condition{&conditionStub{firing: true}}})
tc.applyFn(evalContext)
newState := evalContext.GetNewState()
assert.Equal(t, tc.expected, newState, "failed: %s \n expected '%s' have '%s'\n", tc.name, tc.expected, string(newState))
}
}
+83
View File
@@ -0,0 +1,83 @@
// Copyright 2019 Yunion
//
// 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.
package alerting
import (
"strconv"
"strings"
"time"
)
// DefaultEvalHandler is responsible for evaluating the alert rule.
type DefaultEvalHandler struct {
alertJobTimeout time.Duration
}
// NewEvalHandler is the `DefaultEvalHandler` constructor.
func NewEvalHandler() *DefaultEvalHandler {
return &DefaultEvalHandler{
alertJobTimeout: time.Second * 5,
}
}
// Eval evaluated the alert rule.
func (e *DefaultEvalHandler) Eval(context *EvalContext) {
firing := true
noDataFound := true
conditionEvals := ""
for i := 0; i < len(context.Rule.Conditions); i++ {
condition := context.Rule.Conditions[i]
cr, err := condition.Eval(context)
if err != nil {
context.Error = err
}
// break if condition could not be evaluated
if context.Error != nil {
break
}
if i == 0 {
firing = cr.Firing
noDataFound = cr.NoDataFound
}
// calculating Firing based on operator
if cr.Operator == "or" {
firing = firing || cr.Firing
noDataFound = noDataFound || cr.NoDataFound
} else {
firing = firing && cr.Firing
noDataFound = noDataFound && cr.NoDataFound
}
if i > 0 {
conditionEvals = "[" + conditionEvals + " " + strings.ToUpper(cr.Operator) + " " + strconv.FormatBool(cr.Firing) + "]"
} else {
conditionEvals = strconv.FormatBool(firing)
}
context.EvalMatches = append(context.EvalMatches, cr.EvalMatches...)
}
context.ConditionEvals = conditionEvals + " = " + strconv.FormatBool(firing)
context.Firing = firing
context.NoDataFound = noDataFound
context.EndTime = time.Now()
// elapsedTime := ctx.EndTime.Sub(ctx.StartTime).Nanoseconds() / int64(time.Millisecond)
// metrics.MAlertingExecutionTime.Observe(float64(elapsedTime))
}
+216
View File
@@ -0,0 +1,216 @@
// Copyright 2019 Yunion
//
// 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.
package alerting
import (
"context"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
type conditionStub struct {
firing bool
operator string
matches []*EvalMatch
noData bool
}
func (c *conditionStub) Eval(context *EvalContext) (*ConditionResult, error) {
return &ConditionResult{Firing: c.firing, EvalMatches: c.matches, Operator: c.operator, NoDataFound: c.noData}, nil
}
func TestAlertingEvaluationHandler(t *testing.T) {
Convey("Test alert evaluation handler", t, func() {
handler := NewEvalHandler()
Convey("Show return triggered with single passing condition", func() {
ctx := NewEvalContext(context.TODO(), nil, &Rule{
Conditions: []Condition{&conditionStub{firing: true}},
})
handler.Eval(ctx)
So(ctx.Firing, ShouldEqual, true)
So(ctx.ConditionEvals, ShouldEqual, "true = true")
})
Convey("Show return triggered with single passing conditions2", func() {
ctx := NewEvalContext(context.TODO(), nil, &Rule{
Conditions: []Condition{&conditionStub{firing: true, operator: "and"}},
})
handler.Eval(ctx)
So(ctx.Firing, ShouldEqual, true)
So(ctx.ConditionEvals, ShouldEqual, "true = true")
})
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: false, operator: "and"},
}})
handler.Eval(ctx)
So(ctx.Firing, ShouldEqual, false)
So(ctx.ConditionEvals, ShouldEqual, "[true AND false] = false")
})
Convey("Show return true if any of condition is passing with OR operator", func() {
ctx := NewEvalContext(context.TODO(), nil, &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "or"},
},
})
handler.Eval(ctx)
So(ctx.Firing, ShouldEqual, true)
So(ctx.ConditionEvals, ShouldEqual, "[true OR false] = true")
})
Convey("Show return false if any of the condition is failing with AND operator", func() {
context := NewEvalContext(context.TODO(), nil, &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "and"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, false)
So(context.ConditionEvals, ShouldEqual, "[true AND false] = false")
})
Convey("Show return true if one condition is failing with nested OR operator", func() {
context := NewEvalContext(context.TODO(), nil, &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "or"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, true)
So(context.ConditionEvals, ShouldEqual, "[[true AND true] OR false] = true")
})
Convey("Show return false if one condition is passing with nested OR operator", func() {
context := NewEvalContext(context.TODO(), nil, &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "and"},
&conditionStub{firing: false, operator: "or"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, false)
So(context.ConditionEvals, ShouldEqual, "[[true AND false] OR false] = false")
})
Convey("Show return false if a condition is failing with nested AND operator", func() {
context := NewEvalContext(context.TODO(), nil, &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "and"},
&conditionStub{firing: true, operator: "and"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, false)
So(context.ConditionEvals, ShouldEqual, "[[true AND false] AND true] = false")
})
Convey("Show return true if a condition is passing with nested OR operator", func() {
context := NewEvalContext(context.TODO(), nil, &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "or"},
&conditionStub{firing: true, operator: "or"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, true)
So(context.ConditionEvals, ShouldEqual, "[[true OR false] OR true] = true")
})
Convey("Should return false if no condition is firing using OR operator", func() {
context := NewEvalContext(context.TODO(), nil, &Rule{
Conditions: []Condition{
&conditionStub{firing: false, operator: "or"},
&conditionStub{firing: false, operator: "or"},
&conditionStub{firing: false, operator: "or"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, false)
So(context.ConditionEvals, ShouldEqual, "[[false OR false] OR false] = false")
})
Convey("Should retuasdfrn no data if one condition has nodata", func() {
context := NewEvalContext(context.TODO(), nil, &Rule{
Conditions: []Condition{
&conditionStub{operator: "or", noData: false},
&conditionStub{operator: "or", noData: false},
&conditionStub{operator: "or", noData: false},
},
})
handler.Eval(context)
So(context.NoDataFound, ShouldBeFalse)
})
Convey("Should return no data if one condition has nodata", func() {
context := NewEvalContext(context.TODO(), nil, &Rule{
Conditions: []Condition{
&conditionStub{operator: "and", noData: true},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, false)
So(context.NoDataFound, ShouldBeTrue)
})
Convey("Should return no data if both conditions have no data and using AND", func() {
context := NewEvalContext(context.TODO(), nil, &Rule{
Conditions: []Condition{
&conditionStub{operator: "and", noData: true},
&conditionStub{operator: "and", noData: false},
},
})
handler.Eval(context)
So(context.NoDataFound, ShouldBeFalse)
})
Convey("Should not return no data if both conditions have no data and using OR", func() {
ctx := NewEvalContext(context.TODO(), nil, &Rule{
Conditions: []Condition{
&conditionStub{operator: "or", noData: true},
&conditionStub{operator: "or", noData: false},
},
})
handler.Eval(ctx)
So(ctx.NoDataFound, ShouldBeTrue)
})
})
}
+54
View File
@@ -0,0 +1,54 @@
// Copyright 2019 Yunion
//
// 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.
package alerting
import (
"context"
"time"
"yunion.io/x/onecloud/pkg/monitor/models"
"yunion.io/x/onecloud/pkg/monitor/notifydrivers"
)
type evalHandler interface {
Eval(ctx *EvalContext)
}
type scheduler interface {
Tick(time time.Time, execQueue chan *Job)
Update(rules []*Rule)
}
// ConditionResult is the result of a condition evaluation.
type ConditionResult struct {
Firing bool
NoDataFound bool
Operator string
EvalMatches []*EvalMatch
}
// Condition is responsible for evaluating an alert condition.
type Condition interface {
Eval(result *EvalContext) (*ConditionResult, error)
}
type Notifier interface {
notifydrivers.Notifier
Notify(evalContext *EvalContext) error
// ShouldNotify checks this evaluation should send an alert notification
ShouldNotify(ctx context.Context, evalContext *EvalContext, notificationState *models.SAlertNotificationState) bool
}
+57
View File
@@ -0,0 +1,57 @@
// Copyright 2019 Yunion
//
// 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.
package alerting
import (
"sync"
)
// Job holds state about when the alert rule should be evaluated.
type Job struct {
Offset int64
OffsetWait bool
Delay bool
running bool
Rule *Rule
runningLock sync.Mutex
}
// GetRunning returns true if the job is running. A lock is taken and released on the Job to ensure atomicity.
func (j *Job) GetRunning() bool {
defer j.runningLock.Unlock()
j.runningLock.Lock()
return j.running
}
// SetRunning sets the running property on the Job. A lock is taken and released on the Job to ensure atomicity.
func (j *Job) SetRunning(b bool) {
j.runningLock.Lock()
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"`
}
+164
View File
@@ -0,0 +1,164 @@
// Copyright 2019 Yunion
//
// 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.
package alerting
import (
"time"
"yunion.io/x/log"
"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/notifydrivers"
)
type notificationService struct {
}
func newNotificationService() *notificationService {
return &notificationService{}
}
func (n *notificationService) SendIfNeeded(evalCtx *EvalContext) error {
notifierStates, err := n.getNeededNotifiers(evalCtx.Rule.Notifications, evalCtx)
if err != nil {
return errors.Wrap(err, "failed to get alert notifiers")
}
if len(notifierStates) == 0 {
return nil
}
return n.sendNotifications(evalCtx, notifierStates)
}
type notifierState struct {
notifier Notifier
state *models.SAlertNotificationState
}
type notifierStateSlice []*notifierState
func (n *notificationService) sendNotification(evalCtx *EvalContext, state *notifierState) error {
if !evalCtx.IsTestRun {
if err := state.state.SetToPending(); err != nil {
return err
}
}
return n.sendAndMarkAsComplete(evalCtx, state)
}
func (n *notificationService) sendAndMarkAsComplete(evalCtx *EvalContext, state *notifierState) error {
notifier := state.notifier
log.Debugf("Sending notification, type %s, id %s", notifier.GetType(), notifier.GetNotifierId())
if err := notifier.Notify(evalCtx); err != nil {
log.Errorf("failed to send notification %s: %v", notifier.GetNotifierId(), err)
return err
}
if evalCtx.IsTestRun {
return nil
}
return state.state.SetToCompleted()
}
func (n *notificationService) sendNotifications(evalCtx *EvalContext, states notifierStateSlice) error {
for _, state := range states {
if err := n.sendNotification(evalCtx, state); err != nil {
log.Errorf("failed to send %s notification: %v", state.notifier.GetNotifierId(), err)
if evalCtx.IsTestRun {
return err
}
}
}
return nil
}
func (n *notificationService) getNeededNotifiers(nIds []string, evalCtx *EvalContext) (notifierStateSlice, error) {
notis, err := models.AlertNotificationManager.GetNotificationsWithDefault(nIds)
if err != nil {
return nil, err
}
var result notifierStateSlice
for _, obj := range notis {
not, err := InitNotifier(NotificationConfig{
Id: obj.GetId(),
Name: obj.GetName(),
Type: obj.Type,
Frequency: time.Duration(obj.Frequency),
SendReminder: obj.SendReminder,
DisableResolveMessage: obj.DisableResolveMessage,
Settings: obj.Settings,
})
if err != nil {
log.Errorf("Could not creat enotifier %s, error: %v", obj.GetId(), err)
continue
}
state, err := models.AlertNotificationStateManager.GetOrCreateState(evalCtx.Ctx, evalCtx.UserCred, evalCtx.Rule.Id, obj.GetId())
if err != nil {
log.Errorf("Get alert state: %v, alertId %s, notifierId: %s", err, evalCtx.Rule.Id, obj.GetId())
continue
}
if not.ShouldNotify(evalCtx.Ctx, evalCtx, state) {
result = append(result, &notifierState{
notifier: not,
state: state,
})
}
}
return result, nil
}
type NotifierPlugin struct {
Type string
Factory NotifierFactory
ValidateCreateData func(cred mcclient.IIdentityProvider, input monitor.AlertNotificationCreateInput) (monitor.AlertNotificationCreateInput, error)
}
type NotificationConfig notifydrivers.NotificationConfig
// NotifierFactory is a signature for creating notifiers
type NotifierFactory func(config NotificationConfig) (Notifier, error)
func RegisterNotifier(plug *NotifierPlugin) {
notifydrivers.RegisterNotifier(&notifydrivers.NotifierPlugin{
Type: plug.Type,
Factory: func(cfg notifydrivers.NotificationConfig) (notifydrivers.Notifier, error) {
ret, err := plug.Factory(NotificationConfig(cfg))
if err != nil {
return nil, err
}
return ret.(notifydrivers.Notifier), nil
},
ValidateCreateData: plug.ValidateCreateData,
})
}
// InitNotifier construct a new notifier
func InitNotifier(config NotificationConfig) (Notifier, error) {
plug, err := notifydrivers.InitNotifier(notifydrivers.NotificationConfig(config))
if err != nil {
return nil, err
}
return plug.(Notifier), nil
}
+143
View File
@@ -0,0 +1,143 @@
// Copyright 2019 Yunion
//
// 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.
package notifiers
import (
"context"
"time"
"yunion.io/x/onecloud/pkg/apis/monitor"
"yunion.io/x/onecloud/pkg/monitor/alerting"
"yunion.io/x/onecloud/pkg/monitor/models"
)
// NotifierBase is the base implentation of a notifier
type NotifierBase struct {
Name string
Type string
Id string
IsDefault bool
SendReminder bool
DisableResolveMessage bool
Frequency time.Duration
}
// NewNotifierBase returns a new NotifierBase
func NewNotifierBase(config alerting.NotificationConfig) NotifierBase {
return NotifierBase{
Id: config.Id,
Name: config.Name,
// IsDefault: config.IsDefault,
Type: config.Type,
SendReminder: config.SendReminder,
DisableResolveMessage: config.DisableResolveMessage,
Frequency: config.Frequency,
}
}
// ShouldNotify checks this evaluation should send an alert notification
func (n *NotifierBase) ShouldNotify(_ context.Context, evalCtx *alerting.EvalContext, state *models.SAlertNotificationState) bool {
prevState := evalCtx.PrevAlertState
newState := evalCtx.Rule.State
// Only notify on state change
if prevState == newState && !n.SendReminder {
return false
}
if prevState == newState && n.SendReminder {
// Do not notify if interval has not elapsed
lastNotify := state.UpdatedAt
// if state.UpdatedAt != 0 && lastNotify.Add(n.Frequency).After(time.Now()) {
if lastNotify.Add(n.Frequency).After(time.Now()) {
return false
}
// Do not notify if alert state is OK or pending even on repeated notify
if newState == monitor.AlertStateOK || newState == monitor.AlertStatePending {
return false
}
}
okOrPending := newState == monitor.AlertStatePending || newState == monitor.AlertStateOK
// Do not notify when new state is ok/pending when previous is unknown
if prevState == monitor.AlertStateUnknown && okOrPending {
return false
}
// Do not notify when we become Pending for the first
if prevState == monitor.AlertStatePending && newState == monitor.AlertStatePending {
return false
}
// Do not notify when we become OK from pending
if prevState == monitor.AlertStatePending && newState == monitor.AlertStateOK {
return false
}
// Do not notify when we OK -> Pending
if prevState == monitor.AlertStateOK && newState == monitor.AlertStatePending {
return false
}
// Do not notify if state pending and it have been updated last minute
if state.GetState() == monitor.AlertNotificationStatePending {
lastUpdated := state.UpdatedAt
if lastUpdated.Add(1 * time.Minute).After(time.Now()) {
return false
}
}
// Do not notify when state is OK if DisableResolveMessage is set to true
if newState == monitor.AlertStateOK && n.DisableResolveMessage {
return false
}
return true
}
// GetType returns the notifier type.
func (n *NotifierBase) GetType() string {
return n.Type
}
// GetNotifierId returns the notifier `uid`.
func (n *NotifierBase) GetNotifierId() string {
return n.Id
}
// GetIsDefault returns true if the notifiers should
// be used for all alerts.
/*func (n *NotifierBase) GetIsDefault() bool {
return n.IsDeault
}*/
// GetSendReminder returns true if reminders should be sent.
func (n *NotifierBase) GetSendReminder() bool {
return n.SendReminder
}
// GetDisableResolveMessage returns true if ok alert notifications
// should be skipped.
func (n *NotifierBase) GetDisableResolveMessage() bool {
return n.DisableResolveMessage
}
// GetFrequency returns the frequency for how often
// alerts should be evaluated.
func (n *NotifierBase) GetFrequency() time.Duration {
return n.Frequency
}
+154
View File
@@ -0,0 +1,154 @@
// Copyright 2019 Yunion
//
// 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.
package notifiers
import (
"encoding/json"
"net/url"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/utils"
"yunion.io/x/onecloud/pkg/apis/monitor"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/monitor/alerting"
"yunion.io/x/onecloud/pkg/monitor/alerting/notifiers/templates"
)
const (
defaultDingdingMsgType = DingdingMsgTypeMarkdown
DingdingMsgTypeLink = "link"
DingdingMsgTypeMarkdown = "markdown"
DingdingMsgTypeActionCard = "actionCard"
)
func init() {
alerting.RegisterNotifier(&alerting.NotifierPlugin{
Type: monitor.AlertNotificationTypeDingding,
Factory: newDingdingNotifier,
ValidateCreateData: func(cred mcclient.IIdentityProvider, input monitor.AlertNotificationCreateInput) (monitor.AlertNotificationCreateInput, error) {
settings := new(monitor.NotificationSettingDingding)
if err := input.Settings.Unmarshal(settings); err != nil {
return input, errors.Wrap(err, "unmarshal setting")
}
if settings.Url == "" {
return input, httperrors.NewInputParameterError("url is empty")
}
if _, err := url.Parse(settings.Url); err != nil {
return input, httperrors.NewInputParameterError("invalid url: %v", err)
}
if settings.MessageType == "" {
settings.MessageType = defaultDingdingMsgType
}
if !utils.IsInStringArray(settings.MessageType, []string{
DingdingMsgTypeMarkdown,
DingdingMsgTypeLink,
DingdingMsgTypeActionCard,
}) {
return input, httperrors.NewInputParameterError("unsupport type: %s", settings.MessageType)
}
input.Settings = jsonutils.Marshal(settings)
return input, nil
},
})
}
type DingDingNotifier struct {
NotifierBase
MsgType string
Url string
}
func newDingdingNotifier(config alerting.NotificationConfig) (alerting.Notifier, error) {
settings := new(monitor.NotificationSettingDingding)
if err := config.Settings.Unmarshal(settings); err != nil {
return nil, errors.Wrap(err, "unmarshal setting")
}
return &DingDingNotifier{
NotifierBase: NewNotifierBase(config),
Url: settings.Url,
MsgType: settings.MessageType,
}, nil
}
func (dd *DingDingNotifier) Notify(ctx *alerting.EvalContext) error {
log.Infof("Sending alert notification to dingding")
// msgUrl, err := ctx.GetRuleURL()
body, err := dd.genBody(ctx)
if err != nil {
return err
}
input := &monitor.SendWebhookSync{
Url: dd.Url,
Body: string(body),
}
return SendWebRequestSync(ctx.Ctx, input)
}
func (dd *DingDingNotifier) genBody(ctx *alerting.EvalContext) ([]byte, error) {
q := url.Values{
"pc_slide": {"false"},
// "url": {messageURL},
}
// Use special link to auto open the message url outside of Dingding
// Refer: https://open-doc.dingtalk.com/docs/doc.htm?treeId=385&articleId=104972&docType=1#s9
messageURL := "dingtalk://dingtalkclient/page/link?" + q.Encode()
log.Infof("messageUrl: " + messageURL)
config := GetNotifyTemplateConfig(ctx)
contentConfig := templates.NewTemplateConfig(config)
content, err := contentConfig.GenerateMarkdown()
if err != nil {
return nil, errors.Wrap(err, "build content")
}
var bodyMsg map[string]interface{}
switch dd.MsgType {
case DingdingMsgTypeMarkdown:
bodyMsg = map[string]interface{}{
"msgtype": DingdingMsgTypeMarkdown,
DingdingMsgTypeMarkdown: map[string]string{
"title": config.Title,
"text": content,
},
}
case DingdingMsgTypeActionCard:
bodyMsg = map[string]interface{}{
"msgtype": DingdingMsgTypeActionCard,
DingdingMsgTypeActionCard: map[string]string{
"text": content,
"title": config.Title,
// "singleTitle": "More",
// "singleURL": messageURL,
},
}
case DingdingMsgTypeLink:
bodyMsg = map[string]interface{}{
"msgtype": DingdingMsgTypeLink,
"link": map[string]string{
"text": content,
"title": config.Title,
// "messageUrl": messageURL,
},
}
}
return json.Marshal(bodyMsg)
}
+1
View File
@@ -0,0 +1 @@
package notifiers // import "yunion.io/x/onecloud/pkg/monitor/alerting/notifiers"
+218
View File
@@ -0,0 +1,218 @@
// Copyright 2019 Yunion
//
// 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.
package notifiers
import (
"fmt"
"golang.org/x/sync/errgroup"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/apis/monitor"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/monitor/alerting"
"yunion.io/x/onecloud/pkg/monitor/notifydrivers/feishu"
)
func init() {
alerting.RegisterNotifier(&alerting.NotifierPlugin{
Type: monitor.AlertNotificationTypeFeishu,
Factory: newFeishuNotifier,
ValidateCreateData: func(cred mcclient.IIdentityProvider, input monitor.AlertNotificationCreateInput) (monitor.AlertNotificationCreateInput, error) {
settings := new(monitor.NotificationSettingFeishu)
if err := input.Settings.Unmarshal(settings); err != nil {
return input, errors.Wrap(err, "unmarshal setting")
}
if settings.AppId == "" {
return input, httperrors.NewInputParameterError("app_id is empty")
}
if settings.AppSecret == "" {
return input, httperrors.NewInputParameterError("app_secret is empty")
}
_, err := feishu.NewTenant(settings.AppId, settings.AppSecret)
if err != nil {
return input, httperrors.NewGeneralError(errors.Wrap(err, "test connection"))
}
input.Settings = jsonutils.Marshal(settings)
return input, nil
},
})
}
type FeishuNotifier struct {
NotifierBase
// Settings *monitor.NotificationSettingFeishu
Client *feishu.Tenant
ChatIds []string
}
func newFeishuNotifier(config alerting.NotificationConfig) (alerting.Notifier, error) {
settings := new(monitor.NotificationSettingFeishu)
if err := config.Settings.Unmarshal(settings); err != nil {
return nil, errors.Wrap(err, "unmarshal setting")
}
cli, err := feishu.NewTenant(settings.AppId, settings.AppSecret)
if err != nil {
return nil, err
}
ret, err := cli.ChatList(0, "")
if err != nil {
return nil, err
}
chatIds := make([]string, 0)
for _, obj := range ret.Data.Groups {
chatIds = append(chatIds, obj.ChatId)
}
return &FeishuNotifier{
NotifierBase: NewNotifierBase(config),
Client: cli,
ChatIds: chatIds,
}, nil
}
func (fs *FeishuNotifier) Notify(ctx *alerting.EvalContext) error {
log.Infof("Sending alert notification to feishu")
errGrp := errgroup.Group{}
for _, cId := range fs.ChatIds {
errGrp.Go(func() error {
msg, err := fs.genCard(ctx, cId)
if err != nil {
return err
}
if _, err := fs.Client.SendMessage(*msg); err != nil {
log.Errorf("--feishu send msg error: %s, error: %v", jsonutils.Marshal(msg), err)
return err
}
log.Errorf("--feishu send msg: %s", jsonutils.Marshal(msg))
return nil
})
}
return errGrp.Wait()
}
func (fs *FeishuNotifier) getCommonInfoMod(config monitor.NotificationTemplateConfig) feishu.CardElement {
elem := feishu.CardElement{
Tag: feishu.TagDiv,
// Text: feishu.NewCardElementText(config.Title),
Fields: []*feishu.CardElementField{
feishu.NewCardElementTextField(false, fmt.Sprintf("**时间:** %s", config.StartTime)),
feishu.NewCardElementTextField(false, fmt.Sprintf("**级别:** %s", config.Level)),
},
}
return elem
}
func (fs *FeishuNotifier) getMetricElem(idx int, m monitor.EvalMatch) *feishu.CardElement {
var val string
if m.Value == nil {
val = "NaN"
} else {
val = fmt.Sprintf("%.2f", *m.Value)
}
elem := feishu.CardElement{
Tag: feishu.TagDiv,
Fields: []*feishu.CardElementField{
feishu.NewCardElementTextField(false,
fmt.Sprintf("**指标 %d:** %s", idx, m.Metric)),
feishu.NewCardElementTextField(false,
fmt.Sprintf("**当前值:** %s", val)),
feishu.NewCardElementTextField(true,
fmt.Sprintf("**触发条件:**\n%s", m.Condition)),
},
}
return &elem
}
func (fs *FeishuNotifier) getMetricTagElem(m monitor.EvalMatch) *feishu.CardElement {
inElems := make([]*feishu.CardElement, 0)
for val, key := range m.Tags {
inElems = append(inElems, feishu.NewCardElementText(fmt.Sprintf("%s: %s", val, key)))
}
elem := feishu.CardElement{
Tag: feishu.TagNote,
Elements: inElems,
}
return &elem
}
func (fs *FeishuNotifier) getMetricsMod(config monitor.NotificationTemplateConfig) []*feishu.CardElement {
inElems := make([]*feishu.CardElement, 0)
for idx, m := range config.Matches {
hrE := feishu.NewCardElementHR()
mE := fs.getMetricElem(idx+1, m)
mTE := fs.getMetricTagElem(m)
inElems = append(inElems, hrE, mE, mTE)
}
return inElems
}
func (fs *FeishuNotifier) genCard(ctx *alerting.EvalContext, chatId string) (*feishu.MsgReq, error) {
config := GetNotifyTemplateConfig(ctx)
commonElem := fs.getCommonInfoMod(config)
msElems := fs.getMetricsMod(config)
// 消息卡片: https://open.feishu.cn/document/ukTMukTMukTM/uYTNwUjL2UDM14iN1ATN
msg := &feishu.MsgReq{
ChatId: chatId,
MsgType: feishu.MsgTypeInteractive,
Card: &feishu.Card{
Config: &feishu.CardConfig{WideScreenMode: false},
CardLink: nil,
Header: &feishu.CardHeader{
Title: &feishu.CardHeaderTitle{
Tag: feishu.TagPlainText,
Content: config.Title,
},
},
Elements: []interface{}{
commonElem,
},
},
}
for _, elem := range msElems {
msg.Card.Elements = append(msg.Card.Elements, elem)
}
return msg, nil
}
func (fs *FeishuNotifier) genMsg(ctx *alerting.EvalContext, chatId string) (*feishu.MsgReq, error) {
config := GetNotifyTemplateConfig(ctx)
// 富文本: https://open.feishu.cn/document/ukTMukTMukTM/uMDMxEjLzATMx4yMwETM
return &feishu.MsgReq{
ChatId: chatId,
MsgType: feishu.MsgTypePost,
Content: &feishu.MsgContent{
Post: &feishu.MsgPost{
ZhCn: &feishu.MsgPostValue{
Title: config.Title,
Content: []interface{}{
[]interface{}{
feishu.MsgPostContentText{
Tag: "text",
UnEscape: true,
Text: "first line",
},
},
},
},
},
},
}, nil
}
+137
View File
@@ -0,0 +1,137 @@
// Copyright 2019 Yunion
//
// 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.
package notifiers
import (
"context"
"fmt"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"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"
"yunion.io/x/onecloud/pkg/mcclient/auth"
"yunion.io/x/onecloud/pkg/mcclient/modules/notify"
"yunion.io/x/onecloud/pkg/monitor/alerting"
"yunion.io/x/onecloud/pkg/monitor/alerting/notifiers/templates"
"yunion.io/x/onecloud/pkg/monitor/options"
)
func init() {
alerting.RegisterNotifier(&alerting.NotifierPlugin{
Type: monitor.AlertNotificationTypeOneCloud,
Factory: newOneCloudNotifier,
ValidateCreateData: func(cred mcclient.IIdentityProvider, input monitor.AlertNotificationCreateInput) (monitor.AlertNotificationCreateInput, error) {
settings := new(monitor.NotificationSettingOneCloud)
if err := input.Settings.Unmarshal(settings); err != nil {
return input, errors.Wrap(err, "unmarshal setting")
}
if settings.Channel == "" {
return input, httperrors.NewInputParameterError("channel is empty")
}
ids := make([]string, 0)
for _, uid := range settings.UserIds {
obj, err := db.UserCacheManager.FetchUserByIdOrName(context.TODO(), uid)
if err != nil {
return input, errors.Wrapf(err, "fetch setting uid %s", uid)
}
ids = append(ids, obj.GetId())
}
settings.UserIds = ids
input.Settings = jsonutils.Marshal(settings)
return input, nil
},
})
}
// OneCloudNotifier is responsible for sending
// alert notifications over onecloud notify service.
type OneCloudNotifier struct {
NotifierBase
Setting *monitor.NotificationSettingOneCloud
session *mcclient.ClientSession
}
func newOneCloudNotifier(config alerting.NotificationConfig) (alerting.Notifier, error) {
setting := new(monitor.NotificationSettingOneCloud)
if err := config.Settings.Unmarshal(setting); err != nil {
return nil, errors.Wrapf(err, "unmarshal onecloud setting %s", config.Settings)
}
return &OneCloudNotifier{
NotifierBase: NewNotifierBase(config),
Setting: setting,
session: auth.GetAdminSession(context.Background(), options.Options.Region, ""),
}, nil
}
func GetNotifyTemplateConfig(ctx *alerting.EvalContext) monitor.NotificationTemplateConfig {
priority := notify.NotifyPriorityNormal
level := "普通"
switch ctx.Rule.Level {
case "", "normal":
priority = notify.NotifyPriorityNormal
case "important":
priority = notify.NotifyPriorityImportant
level = "重要"
case "fatal", "critical":
priority = notify.NotifyPriorityCritical
level = "严重"
}
topic := fmt.Sprintf("[%s]", level)
isRecovery := false
if ctx.Rule.State == monitor.AlertStateOK {
isRecovery = true
topic = fmt.Sprintf("%s %s 告警已恢复", topic, ctx.GetRuleTitle())
} else {
topic = fmt.Sprintf("%s %s 发生告警", topic, ctx.GetRuleTitle())
}
config := ctx.GetNotificationTemplateConfig()
config.Title = topic
config.Level = level
config.Priority = string(priority)
config.IsRecovery = isRecovery
return config
}
// Notify sends the alert notification.
func (oc *OneCloudNotifier) Notify(ctx *alerting.EvalContext) error {
log.Infof("Sending alert notification %s to onecloud", ctx.GetRuleTitle())
config := GetNotifyTemplateConfig(ctx)
contentConfig := oc.buildContent(config)
content, err := contentConfig.GenerateMarkdown()
if err != nil {
return errors.Wrap(err, "build content")
}
msg := notify.SNotifyMessage{
Uid: oc.Setting.UserIds,
ContactType: notify.TNotifyChannel(oc.Setting.Channel),
Topic: config.Title,
Priority: notify.TNotifyPriority(config.Priority),
Msg: content,
}
log.Errorf("---send msg: %s", jsonutils.Marshal(msg))
return notify.Notifications.Send(oc.session, msg)
}
func (oc *OneCloudNotifier) buildContent(config monitor.NotificationTemplateConfig) *templates.TemplateConfig {
return templates.NewTemplateConfig(config)
}
@@ -0,0 +1 @@
package templates // import "yunion.io/x/onecloud/pkg/monitor/alerting/notifiers/templates"
@@ -0,0 +1,54 @@
// Copyright 2019 Yunion
//
// 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.
package templates
import "yunion.io/x/onecloud/pkg/apis/monitor"
type TemplateConfig struct {
monitor.NotificationTemplateConfig
}
func NewTemplateConfig(c monitor.NotificationTemplateConfig) *TemplateConfig {
return &TemplateConfig{
NotificationTemplateConfig: c,
}
}
const MarkdownTemplate = `
## {{.Title}}
- 时间: {{.StartTime}}
- 级别: {{.Level}}
{{range .Matches}}
- 指标: {{.Metric}}
- 当前值: {{.Value}}
### 触发条件:
> {{.Condition}}
### 标签
{{range $key, $value := .Tags}}
> {{ $key }}: {{ $value}}
{{end}}
{{end}}
`
func (c TemplateConfig) GenerateMarkdown() (string, error) {
return CompileTEmplateFromMap(MarkdownTemplate, c)
}
@@ -0,0 +1,29 @@
// Copyright 2019 Yunion
//
// 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.
package templates
import (
"bytes"
"text/template"
)
func CompileTEmplateFromMap(tmplt string, configMap interface{}) (string, error) {
out := new(bytes.Buffer)
t := template.Must(template.New("commpiled_template").Parse(tmplt))
if err := t.Execute(out, configMap); err != nil {
return "", err
}
return out.String(), nil
}
+130
View File
@@ -0,0 +1,130 @@
// Copyright 2019 Yunion
//
// 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.
package notifiers
import (
"bytes"
"context"
"crypto/tls"
"encoding/base64"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"strings"
"time"
"github.com/moul/http2curl"
"golang.org/x/net/context/ctxhttp"
"yunion.io/x/log"
"yunion.io/x/onecloud/pkg/apis/monitor"
)
// GetBasicAuthHeader returns a base64 encoded string from user and password.
func GetBasicAuthHeader(user string, password string) string {
var userAndPass = user + ":" + password
return "Basic " + base64.StdEncoding.EncodeToString([]byte(userAndPass))
}
// DecodeBasicAuthHeader decodes user and password from a basic auth header.
func DecodeBasicAuthHeader(header string) (string, string, error) {
var code string
parts := strings.SplitN(header, " ", 2)
if len(parts) == 2 && parts[0] == "Basic" {
code = parts[1]
}
decoded, err := base64.StdEncoding.DecodeString(code)
if err != nil {
return "", "", err
}
userAndPass := strings.SplitN(string(decoded), ":", 2)
if len(userAndPass) != 2 {
return "", "", fmt.Errorf("Invalid basic auth header")
}
return userAndPass[0], userAndPass[1], nil
}
var netTransport = &http.Transport{
TLSClientConfig: &tls.Config{
Renegotiation: tls.RenegotiateFreelyAsClient,
},
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 5 * time.Second,
}
var netClient = &http.Client{
Timeout: time.Second * 30,
Transport: netTransport,
}
func SendWebRequestSync(ctx context.Context, webhook *monitor.SendWebhookSync) error {
if webhook.HttpMethod == "" {
webhook.HttpMethod = http.MethodPost
}
request, err := http.NewRequest(webhook.HttpMethod, webhook.Url, bytes.NewReader([]byte(webhook.Body)))
if err != nil {
return err
}
if webhook.ContentType == "" {
webhook.ContentType = "application/json"
}
request.Header.Add("Content-Type", webhook.ContentType)
request.Header.Add("User-Agent", "OneCloud Monitor")
if webhook.User != "" && webhook.Password != "" {
request.Header.Add("Authorization", GetBasicAuthHeader(webhook.User, webhook.Password))
}
for k, v := range webhook.HttpHeader {
request.Header.Set(k, v)
}
curlCmd, _ := http2curl.GetCurlCommand(request)
log.Debugf("webhook curl: %s", curlCmd)
resp, err := ctxhttp.Do(ctx, netClient, request)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode/100 == 2 {
// flushing the body enables the transport to reuse the same connection
if _, err := io.Copy(ioutil.Discard, resp.Body); err != nil {
log.Errorf("Failed to copy resp.Body to ioutil.Discard: %v", err)
}
return nil
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
log.Errorf("Webhook failed statuscode: %s, body: %s", resp.Status, string(body))
return fmt.Errorf("Webhook response status %v", resp.Status)
}
+54
View File
@@ -0,0 +1,54 @@
// Copyright 2019 Yunion
//
// 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.
package alerting
import (
"sync"
"yunion.io/x/log"
"yunion.io/x/onecloud/pkg/monitor/models"
)
type ruleReader interface {
fetch() []*Rule
}
type defaultRuleReader struct {
sync.RWMutex
}
func newRuleReader() *defaultRuleReader {
ruleReader := &defaultRuleReader{}
return ruleReader
}
func (arr *defaultRuleReader) fetch() []*Rule {
alerts, err := models.AlertManager.FetchAllAlerts()
if err != nil {
log.Errorf("fetch alerts from db: %v", err)
return nil
}
res := make([]*Rule, 0)
for _, alert := range alerts {
obj, err := NewRuleFromDBAlert(&alert)
if err != nil {
log.Errorf("Build alert rule %s from db error: %v", alert.GetId(), err)
continue
}
res = append(res, obj)
}
return res
}
+84
View File
@@ -0,0 +1,84 @@
// Copyright 2019 Yunion
//
// 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.
package alerting
import (
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/monitor/models"
)
type resultHandler interface {
handle(ctx *EvalContext) error
}
type defaultResultHandler struct {
notifier *notificationService
}
func newResultHandler() *defaultResultHandler {
return &defaultResultHandler{
notifier: newNotificationService(),
}
}
func (handler *defaultResultHandler) handle(evalCtx *EvalContext) error {
execErr := ""
annotationData := jsonutils.NewDict()
if len(evalCtx.EvalMatches) > 0 {
annotationData.Add(jsonutils.Marshal(evalCtx.EvalMatches), "evalMatches")
}
if evalCtx.Error != nil {
execErr = evalCtx.Error.Error()
annotationData.Add(jsonutils.NewString(evalCtx.Error.Error()), "error")
} else if evalCtx.NoDataFound {
annotationData.Add(jsonutils.JSONTrue, "noData")
}
if evalCtx.shouldUpdateAlertState() {
log.Infof("New state change, alertId %s, prevState %s, newState %s", evalCtx.Rule.Id, evalCtx.PrevAlertState, evalCtx.Rule.State)
alert, err := models.AlertManager.GetAlert(evalCtx.Rule.Id)
if err != nil {
log.Errorf("get alert %s error: %v", evalCtx.Rule.Id, err)
return errors.Wrapf(err, "result get alert %s", evalCtx.Rule.Id)
}
input := models.AlertSetStateInput{
State: evalCtx.Rule.State,
ExecutionError: execErr,
EvalData: annotationData,
}
if err := alert.SetState(input); err != nil {
log.Errorf("Failed to set alert %s state: %v", evalCtx.Rule.Name, err)
} else {
// StateChanges is used for de duping alert notifications
// when two servers are raising. This makes sure that the server
// with the last state change always sends a notification
evalCtx.Rule.StateChanges = alert.StateChanges
// Update the last state change of the alert rule in memory
evalCtx.Rule.LastStateChange = time.Now()
}
// TODO: save opslog
}
if err := handler.notifier.SendIfNeeded(evalCtx); err != nil {
return err
}
return nil
}
+155
View File
@@ -0,0 +1,155 @@
// Copyright 2019 Yunion
//
// 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.
package alerting
import (
"regexp"
"strconv"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/apis/monitor"
"yunion.io/x/onecloud/pkg/monitor/models"
"yunion.io/x/onecloud/pkg/monitor/validators"
)
var (
// ErrFrequencyCannotBeZeroOrLess frequency cannot be below zero
ErrFrequencyCannotBeZeroOrLess = errors.Error(`"evaluate every" cannot be zero or below`)
// ErrFrequencyCouldNotBeParsed frequency cannot be parsed
ErrFrequencyCouldNotBeParsed = errors.Error(`"evaluate every" field could not be parsed`)
)
// Rule is the in-memory version of an alert rule.
type Rule struct {
Id string
Frequency int64
Title string
Name string
Message string
LastStateChange time.Time
For time.Duration
NoDataState monitor.NoDataOption
ExecutionErrorState monitor.ExecutionErrorOption
State monitor.AlertStateType
Conditions []Condition
Notifications []string
// AlertRuleTags []*models.AlertRuleTag
Level string
StateChanges int
}
var (
valueFormatRegex = regexp.MustCompile(`^\d+`)
unitFormatRegex = regexp.MustCompile(`\w{1}$`)
)
var unitMultiplier = map[string]int{
"s": 1,
"m": 60,
"h": 3600,
"d": 86400,
}
func getTimeDurationStringToSeconds(str string) (int64, error) {
multiplier := 1
matches := valueFormatRegex.FindAllString(str, 1)
if len(matches) <= 0 {
return 0, ErrFrequencyCouldNotBeParsed
}
value, err := strconv.Atoi(matches[0])
if err != nil {
return 0, err
}
if value == 0 {
return 0, ErrFrequencyCannotBeZeroOrLess
}
unit := unitFormatRegex.FindAllString(str, 1)[0]
if val, ok := unitMultiplier[unit]; ok {
multiplier = val
}
return int64(value * multiplier), nil
}
// NewRuleFromDBAlert maps an db version of
// alert to an in-memory version
func NewRuleFromDBAlert(ruleDef *models.SAlert) (*Rule, error) {
model := &Rule{}
model.Id = ruleDef.Id
model.Title = ruleDef.GetTitle()
model.Name = ruleDef.Name
model.Message = ruleDef.Message
model.State = monitor.AlertStateType(ruleDef.State)
model.LastStateChange = ruleDef.LastStateChange
model.For = time.Duration(ruleDef.For)
model.NoDataState = monitor.NoDataOption(ruleDef.NoDataState)
model.ExecutionErrorState = monitor.ExecutionErrorOption(ruleDef.ExecutionErrorState)
model.StateChanges = ruleDef.StateChanges
model.Frequency = ruleDef.Frequency
// frequency cannot be zero since that would not execute the alert rule.
// so we fallback to 60 seconds if `Frequency` is missing
if model.Frequency == 0 {
model.Frequency = 60
}
settings, err := ruleDef.GetSettings()
if err != nil {
return nil, err
}
model.Level = settings.Level
model.Notifications = settings.Notifications
// model.AlertRuleTags = ruleDef.GetTagsFromSettings()
for index, condition := range settings.Conditions {
condType := condition.Type
factory, exist := conditionFactories[condType]
if !exist {
return nil, errors.Wrapf(validators.ErrAlertConditionUnknown, "condition type %s", condType)
}
queryCond, err := factory(&condition, index)
if err != nil {
return nil, errors.Wrapf(err, "construct query condition %s", jsonutils.Marshal(condition))
}
model.Conditions = append(model.Conditions, queryCond)
}
if len(model.Conditions) == 0 {
return nil, validators.ErrAlertConditionEmpty
}
return model, nil
}
// ConditionFactory is the function signature for creating `Conditions`
type ConditionFactory func(model *monitor.AlertCondition, index int) (Condition, error)
var conditionFactories = make(map[string]ConditionFactory)
// RegisterCondition adds support for alerting conditions.
func RegisterCondition(typeName string, factory ConditionFactory) {
conditionFactories[typeName] = factory
}
+98
View File
@@ -0,0 +1,98 @@
// Copyright 2019 Yunion
//
// 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.
package alerting
import (
"math"
"time"
"yunion.io/x/log"
"yunion.io/x/onecloud/pkg/apis/monitor"
"yunion.io/x/onecloud/pkg/monitor/options"
)
type schedulerImpl struct {
jobs map[string]*Job
}
func newScheduler() scheduler {
return &schedulerImpl{
jobs: make(map[string]*Job),
}
}
func (s *schedulerImpl) Update(rules []*Rule) {
log.Debugf("Scheduling update, rule count %d", len(rules))
jobs := make(map[string]*Job)
for i, rule := range rules {
var job *Job
if s.jobs[rule.Id] != nil {
job = s.jobs[rule.Id]
} else {
job = &Job{}
job.SetRunning(false)
}
job.Rule = rule
offset := ((rule.Frequency * 1000) / int64(len(rules))) * int64(i)
job.Offset = int64(math.Floor(float64(offset) / 1000))
if job.Offset == 0 {
// zero offset causes division with 0 panics
job.Offset = 1
}
jobs[rule.Id] = job
}
s.jobs = jobs
}
func (s *schedulerImpl) Tick(tickTime time.Time, execQueue chan *Job) {
now := tickTime.Unix()
for _, job := range s.jobs {
if job.GetRunning() || job.Rule.State == monitor.AlertStatePaused {
continue
}
if job.OffsetWait && now%job.Offset == 0 {
job.OffsetWait = false
s.enqueue(job, execQueue)
continue
}
// Check the job frequency against the minium interval required
interval := job.Rule.Frequency
if interval < options.Options.AlertingMinIntervalSeconds {
interval = options.Options.AlertingMinIntervalSeconds
}
if now%interval == 0 {
if job.Offset > 0 {
job.OffsetWait = true
} else {
s.enqueue(job, execQueue)
}
}
}
}
func (s *schedulerImpl) enqueue(job *Job, execQueue chan *Job) {
log.Debugf("Scheduler: putting job into exec queue, name %s:%s", job.Rule.Name, job.Rule.Id)
execQueue <- job
}
+70
View File
@@ -0,0 +1,70 @@
// Copyright 2019 Yunion
//
// 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.
package alerting
import (
"time"
"github.com/benbjohnson/clock"
)
// Ticker is a ticker to power the alerting scheduler. it's like a time.Ticker, except:
// * it doesn't drop ticks for slow receivers, rather, it queues up. so that callers are in control to instrument what's going on.
// * it automatically ticks every second, which is the right thing in our current design
// * it ticks on second marks or very shortly after. this provides a predictable load pattern
// (this shouldn't cause too much load contention issues because the next steps in the pipeline just process at their own pace)
// * the timestamps are used to mark "last datapoint to query for" and as such, are a configurable amount of seconds in the past
// * because we want to allow:
// - a clean "resume where we left off" and "don't yield ticks we already did"
// - adjusting offset over time to compensate for storage backing up or getting fast and providing lower latency
// you specify a lastProcessed timestamp as well as an offset at creation, or runtime
type Ticker struct {
C chan time.Time
clock clock.Clock
last time.Time
offset time.Duration
newOffset chan time.Duration
}
// NewTicker returns a ticker that ticks on second marks or very shortly after, and never drops ticks
func NewTicker(last time.Time, initialOffset time.Duration, c clock.Clock) *Ticker {
t := &Ticker{
C: make(chan time.Time),
clock: c,
last: last,
offset: initialOffset,
newOffset: make(chan time.Duration),
}
go t.run()
return t
}
func (t *Ticker) run() {
for {
next := t.last.Add(time.Duration(1) * time.Second)
diff := t.clock.Now().Add(-t.offset).Sub(next)
if diff >= 0 {
t.C <- next
t.last = next
continue
}
// tick is too young. try again when ...
select {
case <-t.clock.After(-diff): // ...it'll definitely be old enough
case offset := <-t.newOffset: // ...it might be old enough
t.offset = offset
}
}
}
+238
View File
@@ -0,0 +1,238 @@
// Copyright 2019 Yunion
//
// 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.
package bus
import (
"context"
"reflect"
"yunion.io/x/pkg/errors"
)
// HandlerFunc defines a handler function interface.
type HandlerFunc interface{}
// CtxHandlerFunc defines a context handler function.
type CtxHandlerFunc func()
// Msg defines a message interface.
type Msg interface{}
// ErrHandlerNotFound defines an error if a handler is not found
var ErrHandlerNotFound = errors.Error("handler not found")
// TransactionManager defines a transaction interface
type TransactionManager interface {
InTransaction(ctx context.Context, fn func(ctx context.Context) error) error
}
// Bus type defines the bus interface structure
type Bus interface {
Dispatch(msg Msg) error
DispatchCtx(ctx context.Context, msg Msg) error
Publish(msg Msg) error
// InTransaction starts a transaction and store it in the context.
// The caller can then pass a function with multiple DispatchCtx calls that
// all will be executed in the same transaction. InTransaction will rollback if the
// callback returns an error.
InTransaction(ctx context.Context, fn func(ctx context.Context) error) error
AddHandler(handler HandlerFunc)
AddHandlerCtx(handler HandlerFunc)
AddEventListener(handler HandlerFunc)
// SetTransactionManager allows the user to replace the internal
// noop TransactionManager that is responsible for managing
// transactions in `InTransaction`
SetTransactionManager(tm TransactionManager)
}
type noopTransactionManager struct{}
func (*noopTransactionManager) InTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
return fn(ctx)
}
// InProcBus defines the bus structure
type InProcBus struct {
handlers map[string]HandlerFunc
handlersWithCtx map[string]HandlerFunc
listeners map[string][]HandlerFunc
txMng TransactionManager
}
// InTransaction defines an in transaction function
func (b *InProcBus) InTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
return b.txMng.InTransaction(ctx, fn)
}
// temp stuff, not sure how to handle bus instance, and init yet
var globalBus = New()
// New initialize the bus
func New() Bus {
bus := &InProcBus{}
bus.handlers = make(map[string]HandlerFunc)
bus.handlersWithCtx = make(map[string]HandlerFunc)
bus.listeners = make(map[string][]HandlerFunc)
bus.txMng = &noopTransactionManager{}
return bus
}
// Want to get rid of global bus
func GetBus() Bus {
return globalBus
}
// SetTransactionManager function assign a transaction manager to the bus.
func (b *InProcBus) SetTransactionManager(tm TransactionManager) {
b.txMng = tm
}
// DispatchCtx function dispatch a message to the bus context.
func (b *InProcBus) DispatchCtx(ctx context.Context, msg Msg) error {
var msgName = reflect.TypeOf(msg).Elem().Name()
var handler = b.handlersWithCtx[msgName]
if handler == nil {
return ErrHandlerNotFound
}
var params = []reflect.Value{}
params = append(params, reflect.ValueOf(ctx))
params = append(params, reflect.ValueOf(msg))
ret := reflect.ValueOf(handler).Call(params)
err := ret[0].Interface()
if err == nil {
return nil
}
return err.(error)
}
// Dispatch function dispatch a message to the bus.
func (b *InProcBus) Dispatch(msg Msg) error {
var msgName = reflect.TypeOf(msg).Elem().Name()
var handler = b.handlersWithCtx[msgName]
withCtx := true
if handler == nil {
withCtx = false
handler = b.handlers[msgName]
}
if handler == nil {
return ErrHandlerNotFound
}
var params = []reflect.Value{}
if withCtx {
params = append(params, reflect.ValueOf(context.Background()))
}
params = append(params, reflect.ValueOf(msg))
ret := reflect.ValueOf(handler).Call(params)
err := ret[0].Interface()
if err == nil {
return nil
}
return err.(error)
}
// Publish function publish a message to the bus listener.
func (b *InProcBus) Publish(msg Msg) error {
var msgName = reflect.TypeOf(msg).Elem().Name()
var listeners = b.listeners[msgName]
var params = make([]reflect.Value, 1)
params[0] = reflect.ValueOf(msg)
for _, listenerHandler := range listeners {
ret := reflect.ValueOf(listenerHandler).Call(params)
err := ret[0].Interface()
if err != nil {
return err.(error)
}
}
return nil
}
func (b *InProcBus) AddHandler(handler HandlerFunc) {
handlerType := reflect.TypeOf(handler)
queryTypeName := handlerType.In(0).Elem().Name()
b.handlers[queryTypeName] = handler
}
func (b *InProcBus) AddHandlerCtx(handler HandlerFunc) {
handlerType := reflect.TypeOf(handler)
queryTypeName := handlerType.In(1).Elem().Name()
b.handlersWithCtx[queryTypeName] = handler
}
func (b *InProcBus) AddEventListener(handler HandlerFunc) {
handlerType := reflect.TypeOf(handler)
eventName := handlerType.In(0).Elem().Name()
_, exists := b.listeners[eventName]
if !exists {
b.listeners[eventName] = make([]HandlerFunc, 0)
}
b.listeners[eventName] = append(b.listeners[eventName], handler)
}
// AddHandler attach a handler function to the global bus
// Package level function
func AddHandler(implName string, handler HandlerFunc) {
globalBus.AddHandler(handler)
}
// AddHandlerCtx attach a handler function to the global bus context
// Package level functions
func AddHandlerCtx(implName string, handler HandlerFunc) {
globalBus.AddHandlerCtx(handler)
}
// AddEventListener attach a handler function to the event listener
// Package level functions
func AddEventListener(handler HandlerFunc) {
globalBus.AddEventListener(handler)
}
func Dispatch(msg Msg) error {
return globalBus.Dispatch(msg)
}
func DispatchCtx(ctx context.Context, msg Msg) error {
return globalBus.DispatchCtx(ctx, msg)
}
func Publish(msg Msg) error {
return globalBus.Publish(msg)
}
// InTransaction starts a transaction and store it in the context.
// The caller can then pass a function with multiple DispatchCtx calls that
// all will be executed in the same transaction. InTransaction will rollback if the
// callback returns an error.
func InTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
return globalBus.InTransaction(ctx, fn)
}
func ClearBusHandlers() {
globalBus = New()
}
+144
View File
@@ -0,0 +1,144 @@
// Copyright 2019 Yunion
//
// 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.
package bus
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/require"
)
type testQuery struct {
ID int64
Resp string
}
func TestDispatch(t *testing.T) {
bus := New()
var invoked bool
bus.AddHandler(func(query *testQuery) error {
invoked = true
return nil
})
err := bus.Dispatch(&testQuery{})
require.NoError(t, err)
require.True(t, invoked, "expected handler to be called")
}
func TestDispatch_NoRegisteredHandler(t *testing.T) {
bus := New()
err := bus.Dispatch(&testQuery{})
require.Equal(t, err, ErrHandlerNotFound,
"expected bus to return HandlerNotFound since no handler is registered")
}
func TestDispatch_ContextHandler(t *testing.T) {
bus := New()
var invoked bool
bus.AddHandlerCtx(func(ctx context.Context, query *testQuery) error {
invoked = true
return nil
})
err := bus.Dispatch(&testQuery{})
require.NoError(t, err)
require.True(t, invoked, "expected handler to be called")
}
func TestDispatchCtx(t *testing.T) {
bus := New()
var invoked bool
bus.AddHandlerCtx(func(ctx context.Context, query *testQuery) error {
invoked = true
return nil
})
err := bus.DispatchCtx(context.Background(), &testQuery{})
require.NoError(t, err)
require.True(t, invoked, "expected handler to be called")
}
func TestDispatchCtx_NoRegisteredHandler(t *testing.T) {
bus := New()
err := bus.DispatchCtx(context.Background(), &testQuery{})
require.Equal(t, err, ErrHandlerNotFound,
"expected bus to return HandlerNotFound since no handler is registered")
}
func TestQuery(t *testing.T) {
bus := New()
want := "hello from handler"
bus.AddHandler(func(q *testQuery) error {
q.Resp = want
return nil
})
q := &testQuery{}
err := bus.Dispatch(q)
require.NoError(t, err, "unable to dispatch query")
require.Equal(t, want, q.Resp)
}
func TestQuery_HandlerReturnsError(t *testing.T) {
bus := New()
bus.AddHandler(func(query *testQuery) error {
return errors.New("handler error")
})
err := bus.Dispatch(&testQuery{})
require.Error(t, err, "expected error but got none")
}
func TestEvent(t *testing.T) {
bus := New()
var invoked bool
bus.AddEventListener(func(query *testQuery) error {
invoked = true
return nil
})
err := bus.Publish(&testQuery{})
require.NoError(t, err, "unable to publish event")
require.True(t, invoked)
}
func TestEvent_NoRegisteredListener(t *testing.T) {
bus := New()
err := bus.Publish(&testQuery{})
require.NoError(t, err, "unable to publish event")
}
+1
View File
@@ -0,0 +1 @@
package bus // import "yunion.io/x/onecloud/pkg/monitor/bus"
+1
View File
@@ -0,0 +1 @@
package expressions // import "yunion.io/x/onecloud/pkg/monitor/expressions"
+80
View File
@@ -0,0 +1,80 @@
// Copyright 2019 Yunion
//
// 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.
package expressions
type PrimitveType string
const (
Bool PrimitveType = "Bool"
DateTime PrimitveType = "DateTime"
Double PrimitveType = "Double"
String PrimitveType = "String"
Null PrimitveType = "NULL"
)
/*type ConstExp struct {
Bool bool
DateTime DateTime
Double Double
}*/
type ConstExp interface{}
type PropertyExp struct {
Property string `json:"property"`
Type PrimitveType `json:"type"`
}
type PrimitiveObject struct {
PropertyExp
ConstExp
}
type OperatorExp struct {
Left *PropertyExp `json:"left"`
Right *PrimitiveObject `json:"right"`
}
type LogicalExp struct {
EQ *OperatorExp `json:"eq"`
IN *OperatorExp `json:"in"`
LT *OperatorExp `json:"lt"`
GT *OperatorExp `json:"gt"`
AND []*LogicalExp `json:"and"`
OR []*LogicalExp `json:"or"`
NOT *LogicalExp `json:"not"`
}
type ArithmeticExp struct {
ADD *OperatorExp `json:"add"`
SUB *OperatorExp `json:"sub"`
}
type FilterExp struct {
LogicalExp
}
type AlignerExp struct {
Input *PropertyExp `json:"input"`
}
type MeasureExp struct {
Mean *AlignerExp `json:"mean"`
Min *AlignerExp `json:"min"`
}
type AggregateExp struct {
MeasureExps []MeasureExp
}
+308
View File
@@ -0,0 +1,308 @@
// Copyright 2019 Yunion
//
// 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.
package models
import (
"context"
"database/sql"
"strings"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/errors"
"yunion.io/x/sqlchemy"
"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"
"yunion.io/x/onecloud/pkg/monitor/validators"
"yunion.io/x/onecloud/pkg/util/rbacutils"
)
const (
AlertMetadataTitle = "alert_title"
)
var (
AlertManager *SAlertManager
)
func init() {
AlertManager = NewAlertManager(SAlert{}, "alert", "alerts")
}
type SAlertManager struct {
db.SVirtualResourceBaseManager
}
func NewAlertManager(dt interface{}, keyword, keywordPlural string) *SAlertManager {
man := &SAlertManager{
SVirtualResourceBaseManager: db.NewVirtualResourceBaseManager(
dt,
"alerts_tbl",
keyword,
keywordPlural),
}
man.SetVirtualObject(man)
return man
}
func (man *SAlertManager) FetchAllAlerts() ([]SAlert, error) {
objs := make([]SAlert, 0)
q := man.Query()
err := db.FetchModelObjects(man, q, &objs)
if err != nil && err != sql.ErrNoRows {
return nil, errors.Wrap(err, "db.FetchModelObjects")
}
return objs, nil
}
type SAlert struct {
db.SVirtualResourceBase
Frequency int64 `nullable:"false" list:"user" create:"required" update:"user"`
Settings jsonutils.JSONObject `nullable:"false" list:"user" create:"required" update:"user"`
Enabled bool `nullable:"false" default:"false" list:"user" create:"optional"`
Message string `charset:"utf8" list:"user" update:"user"`
State string `width:"36" charset:"ascii" list:"user"`
// Silenced bool
ExecutionError string `charset:"utf8" list:"user"`
For int64 `nullable:"false" list:"user"`
EvalData jsonutils.JSONObject `list:"user"`
LastStateChange time.Time `json:"last_state_change" list:"user"`
StateChanges int `default:"0" nullable:"false" list:"user" json:"state_changes"`
NoDataState string `charset:"utf8" list:"user"`
ExecutionErrorState string `charset:"utf8" list:"user"`
}
func (alert *SAlert) IsEnable() bool {
return alert.Enabled
}
func (alert *SAlert) SetEnable() error {
alert.Enabled = true
return nil
}
func (alert *SAlert) SetDisable() error {
alert.Enabled = false
return nil
}
func (alert *SAlert) SetTitle(ctx context.Context, t string) error {
return alert.SetMetadata(ctx, AlertMetadataTitle, t, nil)
}
func (alert *SAlert) GetTitle() string {
return alert.GetMetadata(AlertMetadataTitle, nil)
}
func (alert *SAlert) ShouldUpdateState(newState monitor.AlertStateType) bool {
return monitor.AlertStateType(alert.State) != newState
}
func (alert *SAlert) GetSettings() (*monitor.AlertSetting, error) {
setting := new(monitor.AlertSetting)
if alert.Settings == nil {
return setting, nil
}
if err := alert.Settings.Unmarshal(setting); err != nil {
return nil, errors.Wrapf(err, "alert %s unmarshal", alert.GetId())
}
return setting, nil
}
type AlertRuleTags map[string]AlertRuleTag
type AlertRuleTag struct {
Key string
Value string
}
func setAlertDefaultSetting(setting *monitor.AlertSetting, dsId string) *monitor.AlertSetting {
for idx, cond := range setting.Conditions {
cond = setAlertDefaultCondition(cond, dsId)
setting.Conditions[idx] = cond
}
return setting
}
func setAlertDefaultCreateData(data monitor.AlertCreateInput, dsId string) monitor.AlertCreateInput {
setting := setAlertDefaultSetting(&data.Settings, dsId)
data.Settings = *setting
enable := true
if data.Enabled == nil {
data.Enabled = &enable
}
return data
}
func setAlertDefaultCondition(cond monitor.AlertCondition, dsId string) monitor.AlertCondition {
if cond.Type == "" {
cond.Type = "query"
}
if cond.Query.To == "" {
cond.Query.To = "now"
}
if cond.Operator == "" {
cond.Operator = "and"
}
if cond.Query.DataSourceId == "" {
cond.Query.DataSourceId = dsId
}
return cond
}
func (man *SAlertManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, _ jsonutils.JSONObject, data monitor.AlertCreateInput) (monitor.AlertCreateInput, error) {
ds, err := DataSourceManager.GetDefaultSource()
if err != nil {
return data, errors.Wrap(err, "get default data source")
}
data = setAlertDefaultCreateData(data, ds.GetId())
if err := validators.ValidateAlertCreateInput(data); err != nil {
return data, err
}
return data, nil
}
func (man *SAlertManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, input monitor.AlertListInput) (*sqlchemy.SQuery, error) {
q, err := man.SVirtualResourceBaseManager.ListItemFilter(ctx, q, userCred, input.VirtualResourceListInput)
if err != nil {
return nil, err
}
q, err = db.ListEnableItemFilter(q, input.Enabled)
if err != nil {
return nil, err
}
return q, nil
}
func (man *SAlertManager) GetAlert(id string) (*SAlert, error) {
obj, err := man.FetchById(id)
if err != nil {
if errors.Cause(err) == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return obj.(*SAlert), nil
}
func GetMeasurementField(metric string) (string, string, error) {
parts := strings.Split(metric, ".")
if len(parts) != 2 {
return "", "", httperrors.NewInputParameterError("metric %s is invalid format, usage <measurement>.<field>", metric)
}
measurement, field := parts[0], parts[1]
return measurement, field, nil
}
func IsQuerySelectHasField(selects monitor.MetricQuerySelect, field string) bool {
for _, s := range selects {
if s.Type == "field" && len(s.Params) == 1 {
if s.Params[0] == field {
return true
}
}
}
return false
}
func (man *SAlertManager) CustomizeFilterList(
ctx context.Context, q *sqlchemy.SQuery,
userCred mcclient.TokenCredential, query jsonutils.JSONObject) (
*db.CustomizeListFilters, error) {
filters := db.NewCustomizeListFilters()
return filters, nil
}
func (alert *SAlert) CustomizeCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) error {
alert.LastStateChange = time.Now()
alert.State = string(monitor.AlertStateUnknown)
return alert.SVirtualResourceBase.CustomizeCreate(ctx, userCred, ownerId, query, data)
}
func (alert *SAlert) AllowPerformEnable(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool {
return db.AllowPerformEnable(alert, rbacutils.ScopeProject, userCred)
}
func (alert *SAlert) PerformEnable(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) {
return db.PerformEnable(alert, userCred)
}
func (alert *SAlert) AllowPerformDisable(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool {
return db.AllowPerformDisable(alert, rbacutils.ScopeProject, userCred)
}
func (alert *SAlert) PerformDisable(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) {
return db.PerformDisable(alert, userCred)
}
func (alert *SAlert) GetNotifications() ([]SAlertNotification, error) {
settings, err := alert.GetSettings()
if err != nil {
return nil, errors.Wrap(err, "get settings")
}
nIds := settings.Notifications
notis, err := AlertNotificationManager.GetNotifications(nIds)
if err != nil {
if errors.Cause(err) == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return notis, nil
}
const (
ErrAlertChannotChangeStateOnPaused = errors.Error("Cannot change state on pause alert")
)
type AlertSetStateInput struct {
State monitor.AlertStateType
EvalData jsonutils.JSONObject
ExecutionError string
}
func (alert *SAlert) SetState(input AlertSetStateInput) error {
if alert.State == string(monitor.AlertStatePaused) {
return ErrAlertChannotChangeStateOnPaused
}
if alert.State == string(input.State) {
return nil
}
_, err := db.Update(alert, func() error {
alert.State = string(input.State)
alert.LastStateChange = time.Now()
alert.EvalData = input.EvalData
alert.ExecutionError = input.ExecutionError
alert.StateChanges = alert.StateChanges + 1
return nil
})
return err
}
func (alert *SAlert) ValidateUpdateData(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input monitor.AlertUpdateInput) (*jsonutils.JSONDict, error) {
if input.Enabled == nil {
enable := true
input.Enabled = &enable
}
input.Settings = setAlertDefaultSetting(input.Settings, "")
return alert.SVirtualResourceBase.ValidateUpdateData(ctx, userCred, query, input.JSON(input))
}
+169
View File
@@ -0,0 +1,169 @@
// Copyright 2019 Yunion
//
// 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.
package models
import (
"context"
"database/sql"
"time"
"golang.org/x/sync/errgroup"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/tristate"
"yunion.io/x/pkg/util/wait"
"yunion.io/x/onecloud/pkg/apis/monitor"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/mcclient/auth"
"yunion.io/x/onecloud/pkg/monitor/options"
"yunion.io/x/onecloud/pkg/monitor/registry"
"yunion.io/x/onecloud/pkg/monitor/tsdb"
)
var (
DataSourceManager *SDataSourceManager
)
const (
DefaultDataSource = "default"
)
const (
ErrDataSourceDefaultNotFound = errors.Error("Default data source not found")
)
func init() {
DataSourceManager = &SDataSourceManager{
SStandaloneResourceBaseManager: db.NewStandaloneResourceBaseManager(
SDataSource{},
"datasources_tbl",
"datasource",
"datasources",
),
}
DataSourceManager.SetVirtualObject(DataSourceManager)
registry.RegisterService(DataSourceManager)
}
type SDataSourceManager struct {
db.SStandaloneResourceBaseManager
}
func (_ *SDataSourceManager) IsDisabled() bool {
return false
}
func (_ *SDataSourceManager) Init() error {
return nil
}
func (man *SDataSourceManager) Run(ctx context.Context) error {
errgrp, ctx := errgroup.WithContext(ctx)
errgrp.Go(func() error { return man.initDefaultDataSource(ctx) })
return errgrp.Wait()
}
func (man *SDataSourceManager) initDefaultDataSource(ctx context.Context) error {
region := options.Options.Region
initF := func() {
ds, err := man.GetDefaultSource()
if err != nil && err != ErrDataSourceDefaultNotFound {
log.Errorf("Get default datasource: %v", err)
return
}
if ds != nil {
return
}
s := auth.GetAdminSessionWithPublic(ctx, region, "")
if s == nil {
log.Errorf("get empty public session for region %s", region)
return
}
url, err := s.GetServiceURL("influxdb", auth.PublicEndpointType)
if err != nil {
log.Errorf("get influxdb public url: %v", err)
return
}
ds = &SDataSource{
Type: monitor.DataSourceTypeInfluxdb,
Url: url,
}
ds.Name = DefaultDataSource
if err := man.TableSpec().Insert(ds); err != nil {
log.Errorf("insert default influxdb: %v", err)
}
}
wait.Forever(initF, 30*time.Second)
return nil
}
func (man *SDataSourceManager) GetDefaultSource() (*SDataSource, error) {
obj, err := man.FetchByName(nil, DefaultDataSource)
if err != nil {
if err == sql.ErrNoRows {
return nil, ErrDataSourceDefaultNotFound
} else {
return nil, err
}
}
return obj.(*SDataSource), nil
}
type SDataSource struct {
db.SStandaloneResourceBase
Type string `nullable:"false" list:"user"`
Url string `nullable:"false" list:"user"`
User string `width:"64" charset:"utf8" nullable:"true"`
Password string `width:"64" charset:"utf8" nullable:"true"`
Database string `width:"64" charset:"utf8" nullable:"true"`
IsDefault tristate.TriState `nullable:"false" default:"false" create:"optional"`
/*
TimeInterval string
BasicAuth bool
BasicAuthUser string
BasicAuthPassword string
*/
}
func (m *SDataSourceManager) GetSource(id string) (*SDataSource, error) {
ret, err := m.FetchById(id)
if err != nil {
return nil, err
}
return ret.(*SDataSource), nil
}
func (ds *SDataSource) ToTSDBDataSource(db string) *tsdb.DataSource {
if db == "" {
db = ds.Database
}
return &tsdb.DataSource{
Id: ds.GetId(),
Name: ds.GetName(),
Type: ds.Type,
Url: ds.Url,
User: ds.User,
Password: ds.Password,
Database: db,
Updated: ds.UpdatedAt,
/*BasicAuth: ds.BasicAuth,
BasicAuthUser: ds.BasicAuthUser,
BasicAuthPassword: ds.BasicAuthPassword,
TimeInterval: ds.TimeInterval,*/
}
}
+1
View File
@@ -0,0 +1 @@
package models // import "yunion.io/x/onecloud/pkg/monitor/models"
+39
View File
@@ -0,0 +1,39 @@
// Copyright 2019 Yunion
//
// 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.
package models
import (
"yunion.io/x/log"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
)
func InitDB() error {
for _, manager := range []db.IModelManager{
/*
* Important!!!
* initialization order matters, do not change the order
*/
DataSourceManager,
AlertManager,
} {
err := manager.InitializeData()
if err != nil {
log.Errorf("Manager %s initializeData fail %s", manager.Keyword(), err)
// return err skip error table
}
}
return nil
}
+478
View File
@@ -0,0 +1,478 @@
// Copyright 2019 Yunion
//
// 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.
package models
import (
"context"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/sqlchemy"
"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"
"yunion.io/x/onecloud/pkg/mcclient/auth"
"yunion.io/x/onecloud/pkg/mcclient/modules"
"yunion.io/x/onecloud/pkg/monitor/options"
)
const (
MeterAlertMetadataType = "type"
MeterAlertMetadataProjectId = "project_id"
MeterAlertMetadataAccountId = "account_id"
MeterAlertMetadataProvider = "provider"
)
var MeterAlertManager *SMeterAlertManager
func init() {
MeterAlertManager = NewMeterAlertManager()
}
type IMeterAlertDriver interface {
GetType() string
GetName() string
ToAlertCreateInput(input monitor.MeterAlertCreateInput, notificatoins []string, allAccountIds []string) monitor.AlertCreateInput
}
type SMeterAlertManager struct {
SV1AlertManager
drivers map[string]IMeterAlertDriver
}
func NewMeterAlertManager() *SMeterAlertManager {
man := &SMeterAlertManager{
SV1AlertManager: SV1AlertManager{
*NewAlertManager(SMeterAlert{}, "meteralert", "meteralerts"),
},
}
man.SetVirtualObject(man)
man.registerDriver(man.newDailyFeeDriver())
man.registerDriver(man.newMonthFeeDriver())
return man
}
type SMeterAlert struct {
SV1Alert
}
func (man *SMeterAlertManager) newDailyFeeDriver() IMeterAlertDriver {
return new(sMeterDailyFee)
}
func (man *SMeterAlertManager) newMonthFeeDriver() IMeterAlertDriver {
return new(sMeterMonthFee)
}
func (man *SMeterAlertManager) registerDriver(drv IMeterAlertDriver) {
if man.drivers == nil {
man.drivers = make(map[string]IMeterAlertDriver, 0)
}
man.drivers[drv.GetType()] = drv
}
func (man *SMeterAlertManager) GetDriver(typ string) IMeterAlertDriver {
return man.drivers[typ]
}
func (man *SMeterAlertManager) genName(ownerId mcclient.IIdentityProvider, hint string) (string, error) {
return db.GenerateName(man, ownerId, hint)
}
func (man *SMeterAlertManager) getAllBillAccounts(ctx context.Context) ([]jsonutils.JSONObject, error) {
s := auth.GetAdminSession(ctx, options.Options.Region, "")
q := jsonutils.NewDict()
q.Add(jsonutils.NewString("accountList"), "account_id")
q.Add(jsonutils.NewInt(-1), "limit")
ret, err := modules.BillBalances.List(s, q)
if err != nil {
return nil, err
}
return ret.Data, nil
}
func (man *SMeterAlertManager) getAllBillAccountIds(ctx context.Context) ([]string, error) {
objs, err := man.getAllBillAccounts(ctx)
if err != nil {
return nil, err
}
ids := make([]string, len(objs))
for idx, obj := range objs {
id, err := obj.GetString("id")
if err != nil {
return nil, err
}
ids[idx] = id
}
return ids, nil
}
func (man *SMeterAlertManager) ValidateCreateData(
ctx context.Context, userCred mcclient.TokenCredential,
ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject,
data monitor.MeterAlertCreateInput) (*monitor.MeterAlertCreateInput, error) {
if data.Period == "" {
// default 30 minutes
data.Period = "30m"
}
if data.Window == "" {
// default 5 minutes
data.Window = "5m"
}
if _, err := time.ParseDuration(data.Period); err != nil {
return nil, httperrors.NewInputParameterError("Invalid period format: %s", data.Period)
}
if data.Recipients == "" {
return nil, httperrors.NewInputParameterError("recipients is empty")
}
notification, err := man.CreateNotification(ctx, userCred, data.Type, data.Channel, data.Recipients)
if err != nil {
return nil, errors.Wrap(err, "create notification")
}
if data.ProjectId == "" {
return nil, httperrors.NewInputParameterError("project_id is empty")
}
drv := man.GetDriver(data.Type)
if drv == nil {
return nil, httperrors.NewInputParameterError("not support type %q", data.Type)
}
name, err := man.genName(ownerId, drv.GetName())
if err != nil {
return nil, err
}
allAccountIds := []string{}
if data.AccountId == "" {
allAccountIds, err = man.getAllBillAccountIds(ctx)
if err != nil {
return nil, err
}
}
alertInput := drv.ToAlertCreateInput(
data, []string{notification.GetId()},
allAccountIds)
alertInput, err = AlertManager.ValidateCreateData(ctx, userCred, ownerId, query, alertInput)
if err != nil {
return nil, err
}
data.Name = name
data.AlertCreateInput = &alertInput
return &data, nil
}
type sMeterDailyFee struct{}
func (_ *sMeterDailyFee) GetType() string {
return monitor.MeterAlertTypeDailyResFee
}
func (_ *sMeterDailyFee) GetName() string {
return "日消费"
}
func (f *sMeterDailyFee) ToAlertCreateInput(
input monitor.MeterAlertCreateInput,
notifications []string,
allAccountIds []string,
) monitor.AlertCreateInput {
freq, _ := time.ParseDuration(input.Window)
ret := monitor.AlertCreateInput{
Name: f.GetName(),
Frequency: int64(freq / time.Second),
Settings: GetMeterAlertSetting(input, notifications,
"account_daily_resfee",
"meter_db", allAccountIds, "sumDate"),
}
return ret
}
type sMeterMonthFee struct{}
func (_ *sMeterMonthFee) GetType() string {
return monitor.MeterAlertTypeMonthResFee
}
func (_ *sMeterMonthFee) GetName() string {
return "月消费"
}
func (f *sMeterMonthFee) ToAlertCreateInput(
input monitor.MeterAlertCreateInput,
notifications []string,
allAccountIds []string,
) monitor.AlertCreateInput {
freq, _ := time.ParseDuration(input.Window)
ret := monitor.AlertCreateInput{
Name: f.GetName(),
Frequency: int64(freq / time.Second),
Settings: GetMeterAlertSetting(input, notifications,
"account_month_resfee",
"meter_db", allAccountIds, "sumMonth"),
}
return ret
}
func GetMeterAlertSetting(
input monitor.MeterAlertCreateInput,
ns []string,
measurement string,
db string,
accountIds []string,
groupByStr string,
) monitor.AlertSetting {
q, reducer, eval := GetMeterAlertQuery(input, measurement, db, accountIds, groupByStr)
return monitor.AlertSetting{
Level: input.Level,
Notifications: ns,
Conditions: []monitor.AlertCondition{
{
Type: "query",
Operator: "and",
Query: monitor.AlertQuery{
Model: q,
From: input.Period,
To: "now",
},
Reducer: reducer,
Evaluator: eval,
},
},
}
}
func GetMeterAlertQuery(
input monitor.MeterAlertCreateInput,
measurement string,
db string,
allAccountIds []string,
groupByStr string,
) (
monitor.MetricQuery,
monitor.Condition,
monitor.Condition) {
var (
evaluator, reducer monitor.Condition
alertType, field string
filters []monitor.MetricQueryTag
)
groupBy := []monitor.MetricQueryPart{}
evaluator = monitor.GetNodeAlertEvaluator(input.Comparator, input.Threshold)
if input.AccountId == "" {
reducer = monitor.Condition{Type: "sum"}
alertType = "overview"
field = "sum"
for _, aId := range allAccountIds {
filters = append(filters, monitor.MetricQueryTag{
Key: "accountId",
Value: aId,
Condition: "or",
})
}
} else {
reducer = monitor.Condition{Type: "avg"}
alertType = "account"
field = input.Type
groupBy = append(groupBy, monitor.MetricQueryPart{
Type: "field",
Params: []string{field},
})
filters = append(filters, monitor.MetricQueryTag{
Key: "accountId",
Value: input.AccountId,
Condition: "and",
})
filters = append(filters, monitor.MetricQueryTag{
Key: "provider",
Value: input.Provider,
})
}
log.Debugf("==alertType: %s", alertType)
if input.ProjectId != "" {
filters = append(filters, monitor.MetricQueryTag{
Key: "projectId",
Value: input.ProjectId,
})
}
groupBy = append(groupBy, monitor.MetricQueryPart{
Type: "field",
Params: []string{groupByStr},
})
sels := make([]monitor.MetricQuerySelect, 0)
sels = append(sels, monitor.NewMetricQuerySelect(
monitor.MetricQueryPart{
Type: "field",
Params: []string{input.Type},
}))
q := monitor.MetricQuery{
Selects: sels,
Tags: filters,
GroupBy: groupBy,
Measurement: measurement,
Database: db,
}
return q, reducer, evaluator
}
func (man *SMeterAlertManager) GetAlert(id string) (*SMeterAlert, error) {
obj, err := man.FetchById(id)
if err != nil {
return nil, err
}
return obj.(*SMeterAlert), nil
}
func (man *SMeterAlertManager) CustomizeFilterList(
ctx context.Context, q *sqlchemy.SQuery,
userCred mcclient.TokenCredential, query jsonutils.JSONObject) (
*db.CustomizeListFilters, error) {
filters, err := man.SV1AlertManager.CustomizeFilterList(ctx, q, userCred, query)
if err != nil {
return nil, err
}
input := new(monitor.MeterAlertListInput)
if err := query.Unmarshal(input); err != nil {
return nil, err
}
wrapF := func(f func(obj *SMeterAlert) (bool, error)) func(object jsonutils.JSONObject) (bool, error) {
return func(data jsonutils.JSONObject) (bool, error) {
id, err := data.GetString("id")
if err != nil {
return false, err
}
obj, err := man.GetAlert(id)
if err != nil {
return false, err
}
return f(obj)
}
}
if input.Type != "" {
filters.Append(wrapF(func(obj *SMeterAlert) (bool, error) {
return obj.getType() == input.Type, nil
}))
}
if input.AccountId != "" {
filters.Append(wrapF(func(obj *SMeterAlert) (bool, error) {
return obj.getAccountId() == input.AccountId, nil
}))
}
if input.Provider != "" {
filters.Append(wrapF(func(obj *SMeterAlert) (bool, error) {
return obj.getProvider() == input.Provider, nil
}))
}
if input.ProjectId != "" {
filters.Append(wrapF(func(obj *SMeterAlert) (bool, error) {
return obj.getProjectId() == input.ProjectId, nil
}))
}
return filters, nil
}
func (alert *SMeterAlert) setType(ctx context.Context, userCred mcclient.TokenCredential, t string) error {
return alert.SetMetadata(ctx, MeterAlertMetadataType, t, userCred)
}
func (alert *SMeterAlert) getType() string {
return alert.GetMetadata(MeterAlertMetadataType, nil)
}
func (alert *SMeterAlert) setProjectId(ctx context.Context, userCred mcclient.TokenCredential, id string) error {
return alert.SetMetadata(ctx, MeterAlertMetadataProjectId, id, userCred)
}
func (alert *SMeterAlert) getProjectId() string {
return alert.GetMetadata(MeterAlertMetadataProjectId, nil)
}
func (alert *SMeterAlert) setAccountId(ctx context.Context, userCred mcclient.TokenCredential, id string) error {
return alert.SetMetadata(ctx, MeterAlertMetadataAccountId, id, userCred)
}
func (alert *SMeterAlert) getAccountId() string {
return alert.GetMetadata(MeterAlertMetadataAccountId, nil)
}
func (alert *SMeterAlert) setProvider(ctx context.Context, userCred mcclient.TokenCredential, p string) error {
return alert.SetMetadata(ctx, MeterAlertMetadataProvider, p, userCred)
}
func (alert *SMeterAlert) getProvider() string {
return alert.GetMetadata(MeterAlertMetadataProvider, nil)
}
func (alert *SMeterAlert) PostCreate(ctx context.Context,
userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider,
query jsonutils.JSONObject, data jsonutils.JSONObject) {
alert.SVirtualResourceBase.PostCreate(ctx, userCred, ownerId, query, data)
input := new(monitor.MeterAlertCreateInput)
if err := data.Unmarshal(input); err != nil {
log.Errorf("post create unmarshal input: %v", err)
return
}
if input.Type != "" {
if err := alert.setType(ctx, userCred, input.Type); err != nil {
log.Errorf("set type: %v", err)
}
}
if input.Provider != "" {
if err := alert.setProvider(ctx, userCred, input.Provider); err != nil {
log.Errorf("set proider: %v", err)
}
}
if input.AccountId != "" {
if err := alert.setAccountId(ctx, userCred, input.AccountId); err != nil {
log.Errorf("set account_id: %v", err)
}
}
if input.ProjectId != "" {
if err := alert.setProjectId(ctx, userCred, input.ProjectId); err != nil {
log.Errorf("set project_id: %v", err)
}
}
}
func (alert *SMeterAlert) GetExtraDetails(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, isList bool) (monitor.MeterAlertDetails, error) {
var err error
out := monitor.MeterAlertDetails{}
commonDetails, err := alert.SV1Alert.GetExtraDetails(ctx, userCred, query, isList)
if err != nil {
return out, err
}
out.AlertV1Details = commonDetails
out.Type = alert.getType()
out.ProjectId = alert.getProjectId()
out.Provider = alert.getProvider()
out.AccountId = alert.getAccountId()
return out, nil
}
+611
View File
@@ -0,0 +1,611 @@
// Copyright 2019 Yunion
//
// 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.
package models
import (
"context"
"fmt"
"strings"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/sqlchemy"
"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"
"yunion.io/x/onecloud/pkg/mcclient/auth"
"yunion.io/x/onecloud/pkg/mcclient/modulebase"
"yunion.io/x/onecloud/pkg/mcclient/modules"
"yunion.io/x/onecloud/pkg/monitor/options"
)
const (
NodeAlertMetadataType = "type"
NodeAlertMetadataNodeId = "node_id"
NodeAlertMetadataNodeName = "node_name"
)
var NodeAlertManager *SNodeAlertManager
func init() {
NodeAlertManager = NewNodeAlertManager()
}
type SV1AlertManager struct {
SAlertManager
}
type SNodeAlertManager struct {
SV1AlertManager
}
func NewNodeAlertManager() *SNodeAlertManager {
man := &SNodeAlertManager{
SV1AlertManager: SV1AlertManager{
*NewAlertManager(SNodeAlert{}, "nodealert", "nodealerts"),
},
}
man.SetVirtualObject(man)
return man
}
type SV1Alert struct {
SAlert
}
type SNodeAlert struct {
SV1Alert
}
func (v1man *SV1AlertManager) CreateNotification(
ctx context.Context,
userCred mcclient.TokenCredential,
alertName string,
channel string,
recipients string) (*SAlertNotification, error) {
userIds := strings.Split(recipients, ",")
return AlertNotificationManager.CreateOneCloudNotification(ctx, userCred, alertName, channel, userIds)
}
func (man *SNodeAlertManager) ValidateCreateData(
ctx context.Context, userCred mcclient.TokenCredential,
ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject,
data monitor.NodeAlertCreateInput) (*monitor.NodeAlertCreateInput, error) {
if data.Period == "" {
data.Period = "5m"
}
if _, err := time.ParseDuration(data.Period); err != nil {
return nil, httperrors.NewInputParameterError("Invalid period format: %s", data.Period)
}
if data.Metric == "" {
return nil, httperrors.NewInputParameterError("metric is missing")
}
parts := strings.Split(data.Metric, ".")
if len(parts) != 2 {
return nil, httperrors.NewInputParameterError("metric %s is invalid format, usage <measurement>.<field>", data.Metric)
}
measurement, field, err := GetMeasurementField(data.Metric)
if err != nil {
return nil, err
}
if data.Recipients == "" {
return nil, httperrors.NewInputParameterError("recipients is empty")
}
notification, err := man.CreateNotification(ctx, userCred, data.Metric, data.Channel, data.Recipients)
if err != nil {
return nil, errors.Wrap(err, "create notification")
}
if data.NodeId == "" {
return nil, httperrors.NewInputParameterError("node_id is empty")
}
nodeName, resType, err := man.validateResourceId(ctx, data.Type, data.NodeId)
if err != nil {
return nil, err
}
data.NodeName = nodeName
name, err := man.genName(ownerId, resType, nodeName, data.Metric)
if err != nil {
return nil, err
}
alertInput := data.ToAlertCreateInput(name, field, measurement, "telegraf", []string{notification.GetId()})
alertInput, err = AlertManager.ValidateCreateData(ctx, userCred, ownerId, query, alertInput)
if err != nil {
return nil, err
}
data.AlertCreateInput = &alertInput
return &data, nil
}
func (man *SNodeAlertManager) genName(ownerId mcclient.IIdentityProvider, resType string, nodeName string, metric string) (string, error) {
nameHint := fmt.Sprintf("%s %s %s", resType, nodeName, metric)
name, err := db.GenerateName(man, ownerId, nameHint)
if err != nil {
return "", err
}
return name, nil
}
func (man *SNodeAlertManager) validateResourceId(ctx context.Context, nodeType, nodeId string) (string, string, error) {
var (
retType string
nodeName string
err error
)
switch nodeType {
case monitor.NodeAlertTypeHost:
retType = "宿主机"
nodeName, err = man.validateHostResource(ctx, nodeId)
case monitor.NodeAlertTypeGuest:
retType = "虚拟机"
nodeName, err = man.validateGuestResource(ctx, nodeId)
default:
return "", "", httperrors.NewInputParameterError("unsupported resource type %s", nodeType)
}
return nodeName, retType, err
}
func (man *SNodeAlertManager) validateGuestResource(ctx context.Context, id string) (string, error) {
return man.validateResourceByMod(ctx, &modules.Servers, id)
}
func (man *SNodeAlertManager) validateHostResource(ctx context.Context, id string) (string, error) {
return man.validateResourceByMod(ctx, &modules.Hosts, id)
}
func (man *SNodeAlertManager) validateResourceByMod(ctx context.Context, mod modulebase.Manager, id string) (string, error) {
s := auth.GetAdminSession(ctx, options.Options.Region, "")
ret, err := mod.Get(s, id, nil)
if err != nil {
return "", err
}
name, err := ret.GetString("name")
if err != nil {
return "", err
}
return name, nil
}
func (man *SNodeAlertManager) ValidateListConditions(ctx context.Context, userCred mcclient.TokenCredential, query *jsonutils.JSONDict) (*jsonutils.JSONDict, error) {
// hack: always use details in query to get more details
query.Set("details", jsonutils.JSONTrue)
return query, nil
}
func (man *SV1AlertManager) ListItemFilter(
ctx context.Context, q *sqlchemy.SQuery,
userCred mcclient.TokenCredential,
query monitor.NodeAlertListInput) (*sqlchemy.SQuery, error) {
return AlertManager.ListItemFilter(ctx, q, userCred, query.ToAlertListInput())
}
func (man *SNodeAlertManager) GetAlert(id string) (*SNodeAlert, error) {
obj, err := man.FetchById(id)
if err != nil {
return nil, err
}
return obj.(*SNodeAlert), nil
}
func (man *SNodeAlertManager) CustomizeFilterList(
ctx context.Context, q *sqlchemy.SQuery,
userCred mcclient.TokenCredential, query jsonutils.JSONObject) (
*db.CustomizeListFilters, error) {
filters, err := man.SV1AlertManager.CustomizeFilterList(ctx, q, userCred, query)
if err != nil {
return nil, err
}
input := new(monitor.NodeAlertListInput)
if err := query.Unmarshal(input); err != nil {
return nil, err
}
wrapF := func(f func(obj *SNodeAlert) (bool, error)) func(object jsonutils.JSONObject) (bool, error) {
return func(data jsonutils.JSONObject) (bool, error) {
id, err := data.GetString("id")
if err != nil {
return false, err
}
obj, err := man.GetAlert(id)
if err != nil {
return false, err
}
return f(obj)
}
}
if input.Metric != "" {
metric := input.Metric
meaurement, field, err := GetMeasurementField(metric)
if err != nil {
return nil, err
}
mF := func(obj *SNodeAlert) (bool, error) {
settings := new(monitor.AlertSetting)
if err := obj.Settings.Unmarshal(settings, "settings"); err != nil {
return false, errors.Wrapf(err, "alert %s unmarshal", obj.GetId())
}
for _, s := range settings.Conditions {
if s.Query.Model.Measurement == meaurement && len(s.Query.Model.Selects) == 1 {
if IsQuerySelectHasField(s.Query.Model.Selects[0], field) {
return true, nil
}
}
}
return false, nil
}
filters.Append(wrapF(mF))
}
if input.NodeName != "" {
nf := func(obj *SNodeAlert) (bool, error) {
return obj.getNodeName() == input.NodeName, nil
}
filters.Append(wrapF(nf))
}
if input.NodeId != "" {
filters.Append(wrapF(func(obj *SNodeAlert) (bool, error) {
return obj.getNodeId() == input.NodeId, nil
}))
}
if input.Type != "" {
filters.Append(wrapF(func(obj *SNodeAlert) (bool, error) {
return obj.getType() == input.Type, nil
}))
}
return filters, nil
}
func (alert *SNodeAlert) getNodeId() string {
return alert.GetMetadata(NodeAlertMetadataNodeId, nil)
}
func (alert *SNodeAlert) setNodeId(ctx context.Context, userCred mcclient.TokenCredential, id string) error {
return alert.SetMetadata(ctx, NodeAlertMetadataNodeId, id, userCred)
}
func (alert *SNodeAlert) getNodeName() string {
return alert.GetMetadata(NodeAlertMetadataNodeName, nil)
}
func (alert *SNodeAlert) setNodeName(ctx context.Context, userCred mcclient.TokenCredential, name string) error {
return alert.SetMetadata(ctx, NodeAlertMetadataNodeName, name, userCred)
}
func (alert *SNodeAlert) getType() string {
return alert.GetMetadata(NodeAlertMetadataType, nil)
}
func (alert *SNodeAlert) setType(ctx context.Context, userCred mcclient.TokenCredential, typ string) error {
return alert.SetMetadata(ctx, NodeAlertMetadataType, typ, userCred)
}
func (alert *SNodeAlert) PostCreate(ctx context.Context,
userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider,
query jsonutils.JSONObject, data jsonutils.JSONObject) {
alert.SVirtualResourceBase.PostCreate(ctx, userCred, ownerId, query, data)
input := new(monitor.NodeAlertCreateInput)
if err := data.Unmarshal(input); err != nil {
log.Errorf("post create unmarshal input: %v", err)
return
}
if err := alert.setNodeId(ctx, userCred, input.NodeId); err != nil {
log.Errorf("set node id: %v", err)
return
}
if err := alert.setNodeName(ctx, userCred, input.NodeName); err != nil {
log.Errorf("set node name: %v", err)
return
}
if err := alert.setType(ctx, userCred, input.Type); err != nil {
log.Errorf("set type: %v", err)
return
}
}
func (alert *SV1Alert) GetExtraDetails(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, isList bool) (monitor.AlertV1Details, error) {
var err error
out := monitor.AlertV1Details{}
out.VirtualResourceDetails, err = alert.SVirtualResourceBase.GetExtraDetails(ctx, userCred, query, isList)
if err != nil {
return out, err
}
out.Name = alert.GetName()
if alert.Frequency < 60 {
out.Window = fmt.Sprintf("%ds", alert.Frequency)
} else {
out.Window = fmt.Sprintf("%dm", alert.Frequency/60)
}
setting, err := alert.GetSettings()
if err != nil {
return out, err
}
if len(setting.Conditions) == 0 {
return out, nil
}
cond := setting.Conditions[0]
cmp := ""
switch cond.Evaluator.Type {
case "gt":
cmp = ">="
case "lt":
cmp = "<="
}
out.Level = setting.Level
out.Comparator = cmp
out.Threshold = cond.Evaluator.Params[0]
out.Period = cond.Query.From
notification := alert.GetNotificationBySetting(setting)
if notification != nil {
out.Recipients = strings.Join(notification.UserIds, ",")
out.Channel = notification.Channel
}
q := cond.Query
measurement := q.Model.Measurement
field := q.Model.Selects[0][0].Params[0]
db := q.Model.Database
out.Measurement = measurement
out.Field = field
out.DB = db
noti, err := alert.GetNotification()
if err != nil {
return out, err
}
if noti != nil {
out.NotifierId = noti.GetId()
}
return out, nil
}
func (alert *SNodeAlert) GetExtraDetails(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, isList bool) (monitor.NodeAlertDetails, error) {
var err error
out := monitor.NodeAlertDetails{}
commonDetails, err := alert.SV1Alert.GetExtraDetails(ctx, userCred, query, isList)
if err != nil {
return out, err
}
out.AlertV1Details = commonDetails
out.Type = alert.getType()
out.NodeId = alert.getNodeId()
out.NodeName = alert.getNodeName()
setting, err := alert.GetSettings()
if err != nil {
return out, err
}
if len(setting.Conditions) == 0 {
return out, nil
}
out.Metric = fmt.Sprintf("%s.%s", out.Measurement, out.Field)
return out, nil
}
func (alert *SV1Alert) GetNotification() (*SAlertNotification, error) {
setting, err := alert.GetSettings()
if err != nil {
return nil, err
}
nIds := setting.Notifications
if len(nIds) == 0 {
return nil, nil
}
// only get first notification setting
nId := nIds[0]
obj, err := AlertNotificationManager.GetNotification(nId)
if err != nil {
return nil, errors.Wrapf(err, "Get notificatoin %s", nId)
}
return obj, nil
}
func (alert *SV1Alert) UpdateNotification(channel, recipients *string) error {
obj, err := alert.GetNotification()
if err != nil {
return errors.Wrap(err, "Get notification when update")
}
if obj == nil {
return nil
}
setting := new(monitor.NotificationSettingOneCloud)
if err := obj.Settings.Unmarshal(setting); err != nil {
return errors.Wrap(err, "unmarshal onecloud notification setting")
}
if channel != nil {
setting.Channel = *channel
}
if recipients != nil {
setting.UserIds = strings.Split(*recipients, ",")
}
_, err = db.Update(obj, func() error {
obj.Settings = jsonutils.Marshal(setting)
return nil
})
return err
}
func (alert *SV1Alert) GetNotificationBySetting(setting *monitor.AlertSetting) *monitor.NotificationSettingOneCloud {
nIds := setting.Notifications
if len(nIds) == 0 {
return nil
}
// only get first notification setting
nId := nIds[0]
obj, err := AlertNotificationManager.GetNotification(nId)
if err != nil {
log.Errorf("Get notification by %s: %v", nId, err)
return nil
}
if obj == nil {
return nil
}
ocSetting := new(monitor.NotificationSettingOneCloud)
if err := obj.Settings.Unmarshal(ocSetting); err != nil {
log.Errorf("Unmarshal notification %s setting: %v", nId, err)
return nil
}
return ocSetting
}
func (alert *SNodeAlert) 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.CustomizeDelete(ctx, userCred, query, data); err != nil {
return err
}
if err := noti.Delete(ctx, userCred); err != nil {
return err
}
}
return nil
}
func (alert *SNodeAlert) ValidateUpdateData(
ctx context.Context, userCred mcclient.TokenCredential,
query jsonutils.JSONObject, input monitor.NodeAlertUpdateInput) (*jsonutils.JSONDict, error) {
ret := monitor.AlertUpdateInput{}
details, err := alert.GetExtraDetails(context.TODO(), nil, nil, false)
if err != nil {
return nil, err
}
nameChange := false
if input.NodeId != nil && *input.NodeId != details.NodeId {
nameChange = true
ret.ResourceId = input.NodeId
details.NodeId = *input.NodeId
if err := alert.setNodeId(ctx, userCred, details.NodeId); err != nil {
return nil, err
}
}
if input.Type != nil && *input.Type != details.Type {
nameChange = true
ret.ResourceType = input.Type
details.Type = *input.Type
if err := alert.setType(ctx, userCred, details.Type); err != nil {
return nil, err
}
}
nodeName, resType, err := NodeAlertManager.validateResourceId(ctx, details.Type, details.NodeId)
if err != nil {
return nil, err
}
if details.NodeName != nodeName {
nameChange = true
if err := alert.setNodeName(ctx, userCred, nodeName); err != nil {
return nil, err
}
details.NodeName = nodeName
}
if input.Level != nil && *input.Level != details.Level {
details.Level = *input.Level
}
if input.Window != nil && *input.Window != details.Window {
details.Window = *input.Window
freq, err := time.ParseDuration(details.Window)
if err != nil {
return nil, err
}
freqSec := int64(freq / time.Second)
ret.Frequency = &freqSec
}
if input.Threshold != nil && *input.Threshold != details.Threshold {
details.Threshold = *input.Threshold
}
if input.Comparator != nil && *input.Comparator != details.Comparator {
details.Comparator = *input.Comparator
}
if input.Period != nil && *input.Period != details.Period {
details.Period = *input.Period
}
if input.Metric != nil && *input.Metric != details.Metric {
details.Metric = *input.Metric
measurement, field, err := GetMeasurementField(*input.Metric)
if err != nil {
return nil, err
}
details.Measurement = measurement
details.Field = field
}
name := alert.Name
if nameChange {
name, err = NodeAlertManager.genName(userCred, resType, details.NodeName, details.Metric)
if err != nil {
return nil, err
}
ret.Name = &name
}
ds, err := DataSourceManager.GetDefaultSource()
if err != nil {
return nil, errors.Wrap(err, "get default data source")
}
// hack: update notification here
if err := alert.UpdateNotification(input.Channel, input.Recipients); err != nil {
return nil, errors.Wrap(err, "update notification")
}
tmpS := alert.getUpdateSetting(name, details, ds.GetId())
os, err := alert.GetSettings()
if err != nil {
return nil, errors.Wrap(err, "get origin setting")
}
tmpS.Notifications = os.Notifications
ret.Settings = &tmpS
return alert.SAlert.ValidateUpdateData(ctx, userCred, query, ret)
}
func (alert *SNodeAlert) getUpdateSetting(
name string,
details monitor.NodeAlertDetails,
dsId string,
) monitor.AlertSetting {
data := monitor.NodeAlertCreateInput{
ResourceAlertV1CreateInput: monitor.ResourceAlertV1CreateInput{
Period: details.Period,
Window: details.Window,
Comparator: details.Comparator,
Threshold: details.Threshold,
Level: details.Level,
Channel: details.Channel,
Recipients: details.Recipients,
},
Metric: details.Metric,
Type: details.Type,
NodeId: details.NodeId,
}
out := data.ToAlertCreateInput(name, details.Field, details.Measurement, details.DB, []string{details.NotifierId})
out.Settings = *setAlertDefaultSetting(&out.Settings, dsId)
return out.Settings
}
+313
View File
@@ -0,0 +1,313 @@
// Copyright 2019 Yunion
//
// 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.
package models
import (
"context"
"database/sql"
"fmt"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/errors"
"yunion.io/x/sqlchemy"
"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"
"yunion.io/x/onecloud/pkg/monitor/notifydrivers"
)
var (
AlertNotificationManager *SAlertNotificationManager
AlertNotificationStateManager *SAlertNotificationStateManager
)
func init() {
AlertNotificationManager = NewAlertNotificationManager()
AlertNotificationStateManager = NewAlertNotificationStateManager()
}
type SAlertNotificationManager struct {
db.SVirtualResourceBaseManager
}
type SAlertNotificationStateManager struct {
db.SStandaloneResourceBaseManager
}
func NewAlertNotificationManager() *SAlertNotificationManager {
man := &SAlertNotificationManager{
SVirtualResourceBaseManager: db.NewVirtualResourceBaseManager(
SAlertNotification{},
"alert_notifications_tbl",
"alert_notification",
"alert_notifications",
),
}
man.SetVirtualObject(man)
return man
}
func NewAlertNotificationStateManager() *SAlertNotificationStateManager {
man := &SAlertNotificationStateManager{
SStandaloneResourceBaseManager: db.NewStandaloneResourceBaseManager(
SAlertNotificationState{},
"alert_notification_states_tbl",
"alert_notification_state",
"alert_notification_states",
),
}
man.SetVirtualObject(man)
return man
}
type SAlertNotification struct {
db.SVirtualResourceBase
Type string `nullable:"false" list:"user" create:"required"`
IsDefault bool `nullable:"false" default:"false" list:"user" create:"optional" update:"user"`
SendReminder bool `nullable:"false" default:"false" list:"user" create:"optional" update:"user"`
DisableResolveMessage bool `nullable:"false" default:"false" list:"user" create:"optional" update:"user"`
Frequency int64 `nullable:"false" default:"0" list:"user" create:"optional" update:"user"`
Settings jsonutils.JSONObject `nullable:"false" list:"user" create:"required" update:"user"`
}
type SAlertNotificationState struct {
db.SStandaloneResourceBase
AlertId string `nullable:"false" list:"user" create:"required"`
NotifierId string `nullable:"false" list:"user" create:"required"`
State string `nullable:"false" list:"user" create:"required"`
}
func (man *SAlertNotificationManager) GetPlugin(typ string) (*notifydrivers.NotifierPlugin, error) {
drv, err := notifydrivers.GetPlugin(typ)
if err != nil {
if errors.Cause(err) == notifydrivers.ErrUnsupportedNotificationType {
return nil, httperrors.NewInputParameterError("unsupported notification type %s", typ)
} else {
return nil, err
}
}
return drv, nil
}
func (man *SAlertNotificationManager) GetNotification(id string) (*SAlertNotification, error) {
obj, err := man.FetchById(id)
if err != nil {
if errors.Cause(err) == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return obj.(*SAlertNotification), nil
}
func (man *SAlertNotificationManager) GetNotifications(ids []string) ([]SAlertNotification, error) {
objs := make([]SAlertNotification, 0)
notis := man.Query().SubQuery()
q := notis.Query().Filter(sqlchemy.In(notis.Field("id"), ids))
if err := db.FetchModelObjects(man, q, &objs); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return objs, nil
}
func (man *SAlertNotificationManager) GetNotificationsWithDefault(ids []string) ([]SAlertNotification, error) {
objs := make([]SAlertNotification, 0)
notis := man.Query().SubQuery()
q := notis.Query().Filter(
sqlchemy.OR(
sqlchemy.IsTrue(notis.Field("is_default")),
sqlchemy.In(notis.Field("id"), ids)))
if err := db.FetchModelObjects(man, q, &objs); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return objs, nil
}
func (man *SAlertNotificationManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, _ jsonutils.JSONObject, input monitor.AlertNotificationCreateInput) (monitor.AlertNotificationCreateInput, error) {
if input.Type == "" {
return input, httperrors.NewInputParameterError("notification type is empty")
}
if input.SendReminder == nil {
sendReminder := true
input.SendReminder = &sendReminder
}
if input.DisableResolveMessage == nil {
dr := false
input.DisableResolveMessage = &dr
}
plug, err := man.GetPlugin(input.Type)
if err != nil {
return input, err
}
return plug.ValidateCreateData(userCred, input)
}
func (man *SAlertNotificationManager) CreateOneCloudNotification(
ctx context.Context,
userCred mcclient.TokenCredential,
alertName string,
channel string,
userIds []string) (*SAlertNotification, error) {
settings := &monitor.NotificationSettingOneCloud{
Channel: channel,
UserIds: userIds,
}
newName, err := db.GenerateName(man, userCred, alertName)
if err != nil {
return nil, errors.Wrapf(err, "generate name: %s", alertName)
}
input := &monitor.AlertNotificationCreateInput{
Name: newName,
Type: monitor.AlertNotificationTypeOneCloud,
Settings: jsonutils.Marshal(settings),
}
obj, err := db.DoCreate(man, ctx, userCred, nil, input.JSON(input), userCred)
if err != nil {
return nil, errors.Wrapf(err, "create notification input: %s", input.JSON(input))
}
return obj.(*SAlertNotification), nil
}
func (n *SAlertNotification) GetStates() ([]SAlertNotificationState, error) {
states := AlertNotificationStateManager.Query().SubQuery()
q := states.Query().Filter(sqlchemy.Equals(states.Field("notifier_id"), n.GetId()))
objs := make([]SAlertNotificationState, 0)
if err := db.FetchModelObjects(AlertNotificationStateManager, q, &objs); err != nil {
return nil, err
}
return objs, nil
}
func (n *SAlertNotification) CustomizeDelete(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) error {
stats, err := n.GetStates()
if err != nil {
return err
}
for _, stat := range stats {
if err := stat.Delete(ctx, userCred); err != nil {
return err
}
}
return nil
}
func (man *SAlertNotificationStateManager) ValidateCreateData(
ctx context.Context,
userCred mcclient.TokenCredential,
ownerId mcclient.IIdentityProvider,
_ jsonutils.JSONObject,
input monitor.AlertNotificationStateCreateInput) (monitor.AlertNotificationStateCreateInput, error) {
if input.AlertId == "" {
return input, httperrors.NewNotEmptyError("alert_id is empty")
}
if input.NotifierId == "" {
return input, httperrors.NewNotEmptyError("notifier_id is empty")
}
var name string
if obj, err := AlertManager.FetchById(input.AlertId); err != nil {
return input, err
} else {
name = obj.GetName()
}
if obj, err := AlertNotificationManager.FetchById(input.NotifierId); err != nil {
return input, err
} else {
name = fmt.Sprintf("%s_%s", name, obj.GetName())
}
name, err := db.GenerateName(man, ownerId, name)
if err != nil {
return input, err
}
input.Name = name
return input, nil
}
func (man *SAlertNotificationStateManager) CreateState(
ctx context.Context,
userCred mcclient.TokenCredential,
input monitor.AlertNotificationStateCreateInput) (*SAlertNotificationState, error) {
obj, err := db.DoCreate(man, ctx, userCred, nil, input.JSON(input), userCred)
if err != nil {
return nil, errors.Wrapf(err, "create notification state: %s", input.JSON(input))
}
return obj.(*SAlertNotificationState), nil
}
func (man *SAlertNotificationStateManager) GetState(alertId, notifierId string) (*SAlertNotificationState, error) {
state := man.Query().SubQuery()
q := state.Query().Filter(sqlchemy.AND(
sqlchemy.Equals(state.Field("alert_id"), alertId),
sqlchemy.Equals(state.Field("notifier_id"), notifierId)))
obj := new(SAlertNotificationState)
err := q.First(obj)
if err != nil {
if errors.Cause(err) == sql.ErrNoRows {
return nil, nil
} else {
return nil, err
}
}
return obj, nil
}
func (man *SAlertNotificationStateManager) GetOrCreateState(
ctx context.Context,
userCred mcclient.TokenCredential,
alertId string,
notifierId string) (*SAlertNotificationState, error) {
state, err := man.GetState(alertId, notifierId)
if err != nil {
return nil, err
}
if state == nil {
return man.CreateState(ctx, userCred, monitor.AlertNotificationStateCreateInput{
AlertId: alertId,
NotifierId: notifierId,
State: monitor.AlertNotificationStateUnknown,
})
}
state.SetModelManager(man, state)
return state, nil
}
func (state *SAlertNotificationState) SetToPending() error {
return state.setState(monitor.AlertNotificationStatePending)
}
func (state *SAlertNotificationState) SetToCompleted() error {
return state.setState(monitor.AlertNotificationStateCompleted)
}
func (state *SAlertNotificationState) setState(changeState monitor.AlertNotificationStateType) error {
_, err := db.Update(state, func() error {
state.State = string(changeState)
return nil
})
return err
}
func (state *SAlertNotificationState) GetState() monitor.AlertNotificationStateType {
return monitor.AlertNotificationStateType(state.State)
}
+1
View File
@@ -0,0 +1 @@
package notifydrivers // import "yunion.io/x/onecloud/pkg/monitor/notifydrivers"
+91
View File
@@ -0,0 +1,91 @@
// Copyright 2019 Yunion
//
// 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.
package notifydrivers
import (
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/apis/monitor"
"yunion.io/x/onecloud/pkg/mcclient"
)
const (
ErrUnsupportedNotificationType = errors.Error("Unsupported notification type")
)
// Notifier is responsible for sending alert notifications.
type Notifier interface {
GetType() string
GetNotifierId() string
// GetIsDefault() bool
GetSendReminder() bool
GetDisableResolveMessage() bool
GetFrequency() time.Duration
}
type NotificationConfig struct {
Id string
Name string
Type string
SendReminder bool
DisableResolveMessage bool
Frequency time.Duration
Settings jsonutils.JSONObject
}
type NotifierFactory func(notification NotificationConfig) (Notifier, error)
var notifierFactories = make(map[string]*NotifierPlugin)
type NotifierPlugin struct {
Type string
Factory NotifierFactory
ValidateCreateData func(cred mcclient.IIdentityProvider, input monitor.AlertNotificationCreateInput) (monitor.AlertNotificationCreateInput, error)
}
func RegisterNotifier(plugin *NotifierPlugin) {
notifierFactories[plugin.Type] = plugin
}
func GetNotifiers() []*NotifierPlugin {
list := make([]*NotifierPlugin, 0)
for _, value := range notifierFactories {
list = append(list, value)
}
return list
}
func GetPlugin(typ string) (*NotifierPlugin, error) {
plugin, found := notifierFactories[typ]
if !found {
return nil, errors.Wrapf(ErrUnsupportedNotificationType, "type %s", typ)
}
return plugin, nil
}
// InitNotifier instantiate a new notifier based on the model
func InitNotifier(config NotificationConfig) (Notifier, error) {
plugin, err := GetPlugin(config.Type)
if err != nil {
return nil, err
}
return plugin.Factory(config)
}
+132
View File
@@ -0,0 +1,132 @@
// Copyright 2019 Yunion
//
// 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.
package feishu
import (
"context"
"fmt"
"net/http"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/util/httputils"
)
const (
// 获取 tenant_access_token(企业自建应用)
ApiTenantAccessTokenInternal = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/"
// 获取群列表
ApiChatList = "https://open.feishu.cn/open-apis/chat/v4/list"
// 机器人发送消息
ApiRobotSendMessage = "https://open.feishu.cn/open-apis/message/v4/send/"
)
var (
cli = &http.Client{
Transport: httputils.GetTransport(true),
}
ctx = context.Background()
)
func Request(method httputils.THttpMethod, url string, header http.Header, body jsonutils.JSONObject) (jsonutils.JSONObject, error) {
_, resp, err := httputils.JSONRequest(cli, ctx, method, url, header, body, false)
return resp, err
}
func checkErr(resp CommonResponser) error {
if resp.GetCode() != 0 {
return errors.Error(fmt.Sprintf("response error, code: %d, msg: %s", resp.GetCode(), resp.GetMsg()))
}
return nil
}
func unmarshal(resp jsonutils.JSONObject, obj CommonResponser) error {
if err := resp.Unmarshal(obj); err != nil {
return errors.Wrap(err, "unmarshal json")
}
return checkErr(obj)
}
// 获取 tenant_access_token(企业自建应用)https://open.feishu.cn/document/ukTMukTMukTM/uIjNz4iM2MjLyYzM
func GetTenantAccessTokenInternal(appId string, appSecret string) (*TenantAccesstokenResp, error) {
body := jsonutils.NewDict()
body.Add(jsonutils.NewString(appId), "app_id")
body.Add(jsonutils.NewString(appSecret), "app_secret")
ret, err := Request(httputils.POST, ApiTenantAccessTokenInternal, http.Header{}, body)
if err != nil {
return nil, err
}
obj := new(TenantAccesstokenResp)
err = unmarshal(ret, obj)
return obj, err
}
type Tenant struct {
AccessToken string
}
func BuildTokenHeader(token string) http.Header {
h := http.Header{}
h.Add("Authorization", fmt.Sprintf("Bearer "+token))
return h
}
func NewTenant(appId, appSecret string) (*Tenant, error) {
resp, err := GetTenantAccessTokenInternal(appId, appSecret)
if err != nil {
return nil, err
}
return &Tenant{
AccessToken: resp.TenantAccessToken,
}, nil
}
func (t *Tenant) request(method httputils.THttpMethod, url string, data jsonutils.JSONObject, out CommonResponser) error {
obj, err := Request(method, url, BuildTokenHeader(t.AccessToken), data)
if err != nil {
return err
}
err = unmarshal(obj, out)
return err
}
func (t *Tenant) get(url string, query jsonutils.JSONObject, out CommonResponser) error {
return t.request(httputils.GET, url, query, out)
}
func (t *Tenant) post(url string, body jsonutils.JSONObject, out CommonResponser) error {
return t.request(httputils.POST, url, body, out)
}
func (t *Tenant) ChatList(pageSize int, pageToken string) (*GroupListResp, error) {
query := jsonutils.NewDict()
if pageSize > 0 {
query.Add(jsonutils.NewInt(int64(pageSize)), "page_size")
}
if pageToken != "" {
query.Add(jsonutils.NewString(pageToken), "page_token")
}
resp := new(GroupListResp)
err := t.get(ApiChatList, query, resp)
return resp, err
}
func (t *Tenant) SendMessage(msg MsgReq) (*MsgResp, error) {
body := jsonutils.Marshal(msg)
resp := new(MsgResp)
err := t.post(ApiRobotSendMessage, body, resp)
return resp, err
}
+1
View File
@@ -0,0 +1 @@
package feishu // import "yunion.io/x/onecloud/pkg/monitor/notifydrivers/feishu"
+226
View File
@@ -0,0 +1,226 @@
// Copyright 2019 Yunion
//
// 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.
package feishu
type CommonResp struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
func (r CommonResp) GetCode() int {
return r.Code
}
func (r CommonResp) GetMsg() string {
return r.Msg
}
type CommonResponser interface {
GetCode() int
GetMsg() string
}
type TenantAccesstokenResp struct {
CommonResp
TenantAccessToken string `json:"tenant_access_token"`
Expire int64 `json:"expire"`
}
type GroupListResp struct {
CommonResp
Data *UserGroupListData `json:"data"`
}
type UserGroupListData struct {
HasMore bool `json:"has_more"`
PageToken string `json:"page_token"`
Groups []GroupData `json:"groups"`
}
type GroupData struct {
Avatar string `json:"avatar"`
ChatId string `json:"chat_id"`
Description string `json:"description"`
Name string `json:"name"`
OwnerOpenId string `json:"owner_open_id"`
OwnerUserId string `json:"owner_user_id"`
}
type ChatMembersResp struct {
CommonResp
Data *ChatGroupData `json:"data"`
}
type ChatGroupData struct {
ChatId string `json:"chat_id"`
HasMore bool `json:"has_more"`
Members []MemberData `json:"members"`
}
type MemberData struct {
OpenId string `json:"open_id"`
UserId string `json:"user_id"`
Name string `json:"name"`
}
const (
MsgTypePost = "post"
MsgTypeInteractive = "interactive"
)
//定义参照: https://open.feishu.cn/open-apis/message/v4/send/
type MsgReq struct {
OpenId string `json:"open_id,omitempty"`
UserId string `json:"user_id,omitempty"`
Email string `json:"email,omitempty"`
ChatId string `json:"chat_id,omitempty"`
MsgType string `json:"msg_type"`
RootId string `json:"root_id,omitempty"`
UpdateMulti bool `json:"update_multi"`
Card *Card `json:"card,omitempty"`
Content *MsgContent `json:"content,omitempty"`
}
type MsgContent struct {
Text string `json:"text"`
ImageKey string `json:"image_key"`
Post *MsgPost `json:"post,omitempty"`
}
type MsgPost struct {
ZhCn *MsgPostValue `json:"zh_cn,omitempty"`
EnUs *MsgPostValue `json:"en_us,omitempty"`
JaJp *MsgPostValue `json:"ja_jp,omitempty"`
}
type MsgPostValue struct {
Title string `json:"title"`
Content interface{} `json:"content"`
}
type MsgPostContentText struct {
Tag string `json:"tag"`
UnEscape bool `json:"un_escape"`
Text string `json:"text"`
}
type MsgPostContentA struct {
Tag string `json:"tag"`
Text string `json:"text"`
Href string `json:"href"`
}
type MsgPostContentAt struct {
Tag string `json:"tag"`
UserId string `json:"user_id"`
}
type MsgPostContentImage struct {
Tag string `json:"tag"`
ImageKey string `json:"image_key"`
Width float64 `json:"width"`
Height float64 `json:"height"`
}
//机器人消息Card字段数据格式定义
type Card struct {
Config *CardConfig `json:"config,omitempty"`
CardLink *CardElementUrl `json:"card_link,omitempty"`
Header *CardHeader `json:"header,omitempty"`
I18nElements *I18nElement `json:"i18n_elements"`
Elements []interface{} `json:"elements"`
}
type CardConfig struct {
WideScreenMode bool `json:"wide_screen_mode"`
}
type CardHeader struct {
Title *CardHeaderTitle `json:"title,omitempty"`
}
type CardHeaderTitle struct {
Tag string `json:"tag"`
Content string `json:"content"`
Lines int `json:"lines,omitempty"`
I18n *CardI18n `json:"i18n,omitempty"`
}
type CardI18n struct {
ZhCn string `json:"zh_cn"`
EnUs string `json:"en_us"`
JaJp string `json:"ja_jp"`
}
type CardElementUrl struct {
Url string `json:"url"`
AndroidUrl string `json:"android_url"`
IosUrl string `json:"ios_url"`
PcUrl string `json:"pc_url"`
}
const (
TagDiv = "div"
TagPlainText = "plain_text"
TagImg = "img"
TagNote = "note"
TagLarkMd = "lark_md"
TagHR = "hr"
)
type CardElement struct {
Tag string `json:"tag"`
Content string `json:"content"`
Text *CardElement `json:"text"`
Fields []*CardElementField `json:"fields"`
Elements []*CardElement `json:"elements"`
}
type CardElementField struct {
IsShort bool `json:"is_short"`
Text *CardElement `json:"text"`
}
func NewCardElementTextField(isShort bool, content string) *CardElementField {
return &CardElementField{
IsShort: isShort,
Text: &CardElement{Tag: TagLarkMd, Content: content},
}
}
func NewCardElementHR() *CardElement {
return &CardElement{Tag: TagHR}
}
func NewCardElementText(content string) *CardElement {
return &CardElement{Tag: TagPlainText, Content: content}
}
type I18nElement struct {
ZhCn []interface{} `json:"zh_cn"`
EnUs []interface{} `json:"en_us"`
JaJp []interface{} `json:"ja_jp"`
}
type MsgResp struct {
CommonResp
Data MsgRespData `json:"data"`
}
type MsgRespData struct {
MessageId string `json:"message_id"`
}
+1
View File
@@ -0,0 +1 @@
package options // import "yunion.io/x/onecloud/pkg/monitor/options"
+34
View File
@@ -0,0 +1,34 @@
// Copyright 2019 Yunion
//
// 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.
package options
import (
common_options "yunion.io/x/onecloud/pkg/cloudcommon/options"
)
type AlerterOptions struct {
common_options.CommonOptions
common_options.DBOptions
DataProxyTimeout int `help:"query data source proxy timeout" default:"30"`
AlertingMinIntervalSeconds int64 `help:"alerting min schedule frequency" default:"10"`
AlertingMaxAttempts int `help:"alerting engine max attempt" default:"3"`
AlertingEvaluationTimeoutSeconds int64 `help:"alerting evaluation timeout" default:"5"`
AlertingNotificationTimeoutSeconds int64 `help:"alerting notification timeout" default:"30"`
}
var (
Options AlerterOptions
)
+1
View File
@@ -0,0 +1 @@
package registry // import "yunion.io/x/onecloud/pkg/monitor/registry"
+118
View File
@@ -0,0 +1,118 @@
// Copyright 2019 Yunion
//
// 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.
package registry
import (
"context"
"reflect"
"sort"
)
type Descriptor struct {
Name string
Instance Service
InitPriority Priority
}
var services []*Descriptor
func RegisterService(instance Service) {
services = append(services, &Descriptor{
Name: reflect.TypeOf(instance).Elem().Name(),
Instance: instance,
InitPriority: Low,
})
}
func Register(descriptor *Descriptor) {
services = append(services, descriptor)
}
func GetServices() []*Descriptor {
slice := getServicesWithOverrides()
sort.Slice(slice, func(i, j int) bool {
return slice[i].InitPriority > slice[j].InitPriority
})
return slice
}
type OverrideServiceFunc func(descriptor Descriptor) (*Descriptor, bool)
var overrides []OverrideServiceFunc
func getServicesWithOverrides() []*Descriptor {
slice := []*Descriptor{}
for _, s := range services {
var descriptor *Descriptor
for _, fn := range overrides {
if newDescriptor, override := fn(*s); override {
descriptor = newDescriptor
break
}
}
if descriptor != nil {
slice = append(slice, descriptor)
} else {
slice = append(slice, s)
}
}
return slice
}
// Service interface is the lowest common shape that services
// are expected to forfill to be started within monitor.
type Service interface {
// Init is called by monitor main process which gives the service
// the possibility do some initial work before its started. Things
// like adding routes, bus handlers should be done in the Init function
Init() error
}
// CanBeDisabled allows the services to decide if it should
// be started or not by itself. This is useful for services
// that might not always be started, ex alerting.
// This will be called after `Init()`.
type CanBeDisabled interface {
// IsDisabled should return a bool saying if it can be started or not.
IsDisabled() bool
}
// BackgroundService should be implemented for services that have
// long running tasks in the background.
type BackgroundService interface {
// Run starts the background process of the service after `Init` have been called
// on all services. The `context.Context` passed into the function should be used
// to subscribe to ctx.Done() so the service can be notified when monitor shuts down.
Run(ctx context.Context) error
}
// IsDisabled takes an service and return true if its disabled
func IsDisabled(srv Service) bool {
canBeDisabled, ok := srv.(CanBeDisabled)
return ok && canBeDisabled.IsDisabled()
}
type Priority int
const (
High Priority = 100
Low Priority = 0
)
+1
View File
@@ -0,0 +1 @@
package service // import "yunion.io/x/onecloud/pkg/monitor/service"
+43
View File
@@ -0,0 +1,43 @@
// Copyright 2019 Yunion
//
// 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.
package service
import (
"yunion.io/x/onecloud/pkg/appsrv"
"yunion.io/x/onecloud/pkg/appsrv/dispatcher"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/monitor/models"
)
func InitHandlers(app *appsrv.Application) {
db.InitAllManagers()
db.RegisterModelManager(db.UserCacheManager)
db.RegisterModelManager(db.TenantCacheManager)
for _, manager := range []db.IModelManager{
db.OpsLog,
db.Metadata,
models.DataSourceManager,
models.AlertManager,
models.NodeAlertManager,
models.MeterAlertManager,
models.AlertNotificationManager,
models.AlertNotificationStateManager,
} {
db.RegisterModelManager(manager)
handler := db.NewModelHandler(manager)
dispatcher.AddModelDispatcher("", app, handler)
}
}
+106
View File
@@ -0,0 +1,106 @@
// Copyright 2019 Yunion
//
// 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.
package service
import (
"context"
"os"
_ "github.com/go-sql-driver/mysql"
"golang.org/x/sync/errgroup"
"yunion.io/x/log"
"yunion.io/x/onecloud/pkg/cloudcommon"
common_app "yunion.io/x/onecloud/pkg/cloudcommon/app"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
common_options "yunion.io/x/onecloud/pkg/cloudcommon/options"
_ "yunion.io/x/onecloud/pkg/monitor/alerting"
_ "yunion.io/x/onecloud/pkg/monitor/alerting/conditions"
_ "yunion.io/x/onecloud/pkg/monitor/alerting/notifiers"
"yunion.io/x/onecloud/pkg/monitor/models"
_ "yunion.io/x/onecloud/pkg/monitor/notifydrivers"
"yunion.io/x/onecloud/pkg/monitor/options"
"yunion.io/x/onecloud/pkg/monitor/registry"
_ "yunion.io/x/onecloud/pkg/monitor/tsdb/driver/influxdb"
)
func StartService() {
opts := &options.Options
common_options.ParseOptions(opts, os.Args, "alerter.conf", "alerter")
commonOpts := &opts.CommonOptions
common_app.InitAuth(commonOpts, func() {
log.Infof("Auth complete")
})
dbOpts := &opts.DBOptions
baseOpts := &opts.BaseOptions
app := common_app.InitApp(baseOpts, false)
InitHandlers(app)
db.EnsureAppInitSyncDB(app, dbOpts, models.InitDB)
defer cloudcommon.CloseDB()
go startServices()
common_app.ServeForever(app, baseOpts)
}
func startServices() {
services := registry.GetServices()
// Initialize services
for _, svc := range services {
if registry.IsDisabled(svc.Instance) {
continue
}
log.Infof("Initializing " + svc.Name)
if err := svc.Instance.Init(); err != nil {
log.Fatalf("Service %s init failed", svc.Name)
}
}
childRoutines, ctx := errgroup.WithContext(context.Background())
// Start background services
for _, svc := range services {
service, ok := svc.Instance.(registry.BackgroundService)
if !ok {
continue
}
if registry.IsDisabled(svc.Instance) {
continue
}
// Variable is needed for accessing loop variable in callback
descriptor := svc
childRoutines.Go(func() error {
if err := service.Run(ctx); err != nil {
log.Errorf("Stopped %s: %v", descriptor.Name, err)
return err
}
return nil
})
}
defer func() {
log.Debugf("Waiting on services...")
if waitErr := childRoutines.Wait(); waitErr != nil {
log.Errorf("A service failed: %v", waitErr)
}
}()
}
+170
View File
@@ -0,0 +1,170 @@
// Copyright 2019 Yunion
//
// 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.
package tsdb
import (
"crypto/tls"
"net"
"net/http"
"sync"
"time"
"yunion.io/x/onecloud/pkg/monitor/options"
)
type DataSource struct {
Id string
Name string
Type string
Url string
User string
Password string
Database string
BasicAuth bool
BasicAuthUser string
BasicAuthPassword string
TimeInterval string
Updated time.Time
}
type proxyTransportCache struct {
cache map[string]cachedTransport
sync.Mutex
}
// dataSourceTransport implements http.RoundTripper (https://golang.org/pkg/net/http/#RoundTripper)
type dataSourceTransport struct {
headers map[string]string
transport *http.Transport
}
// RoundTrip executes a single HTTP transaction, returning a Response for the provided Request.
func (d *dataSourceTransport) RoundTrip(req *http.Request) (*http.Response, error) {
for key, value := range d.headers {
req.Header.Set(key, value)
}
return d.transport.RoundTrip(req)
}
type cachedTransport struct {
updated time.Time
*dataSourceTransport
}
var ptc = proxyTransportCache{
cache: make(map[string]cachedTransport),
}
func (ds *DataSource) GetHttpClient() (*http.Client, error) {
transport, err := ds.GetHttpTransport()
if err != nil {
return nil, err
}
return &http.Client{
Timeout: 30 * time.Second,
Transport: transport,
}, nil
}
// getCustomHeaders returns a map with all the to be set headers
// The map key represents the HeaderName and the value represetns this header's value
func (ds *DataSource) getCustomHeaders() map[string]string {
headers := make(map[string]string)
// TODO: datasource support config customize headers
return headers
}
func (ds *DataSource) GetHttpTransport() (*dataSourceTransport, error) {
ptc.Lock()
defer ptc.Unlock()
if t, present := ptc.cache[ds.Id]; present && ds.Updated.Equal(t.updated) {
return t.dataSourceTransport, nil
}
tlsConfig, err := ds.GetTLSConfig()
if err != nil {
return nil, err
}
tlsConfig.Renegotiation = tls.RenegotiateFreelyAsClient
// Create transport which adds all
customHeaders := ds.getCustomHeaders()
transport := &http.Transport{
TLSClientConfig: tlsConfig,
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: time.Duration(options.Options.DataProxyTimeout) * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
}
dsTransport := &dataSourceTransport{
headers: customHeaders,
transport: transport,
}
ptc.cache[ds.Id] = cachedTransport{
dataSourceTransport: dsTransport,
updated: ds.Updated,
}
return dsTransport, nil
}
func (ds *DataSource) GetTLSConfig() (*tls.Config, error) {
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
}
return tlsConfig, nil
}
/*
func (ds *DataSource) DecryptedBasicAuthPassword() string {
return ds.decryptedValue("basicAuthPassword", ds.BasicAuthPassword)
}
func (ds *DataSource) DecryptedPassword() string {
return ds.decryptedValue("password", ds.Password)
}
func (ds *DataSource) decryptedValue(field string, fallback string) string {
if value, ok := ds.DecryptedValue(field); ok {
return value
}
return fallback
}
// DecryptedValue returns cached decrypted value from cached data
func (ds *DataSource) DecryptedValue(key string) (string, bool) {
value, exists := ds.DecryptedValues()[key]
return value, exists
}
var dsDescryptionCache =
func (ds *DataSource) DecryptedValues() map[string]string {
}*/
+1
View File
@@ -0,0 +1 @@
package tsdb // import "yunion.io/x/onecloud/pkg/monitor/tsdb"
+1
View File
@@ -0,0 +1 @@
package influxdb // import "yunion.io/x/onecloud/pkg/monitor/tsdb/driver/influxdb"
@@ -0,0 +1,165 @@
// Copyright 2019 Yunion
//
// 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.
package influxdb
import (
"context"
"encoding/json"
"net/http"
"net/url"
"path"
"strings"
"github.com/moul/http2curl"
"golang.org/x/net/context/ctxhttp"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/monitor/tsdb"
)
const (
ErrInfluxdbInvalidResponse = errors.Error("Influxdb invalid status")
)
func init() {
tsdb.RegisterTsdbQueryEndpoint("influxdb", NewInfluxdbExecutor)
}
type InfluxdbExecutor struct {
QueryParser *InfluxdbQueryParser
ResponseParser *ResponseParser
}
func NewInfluxdbExecutor(datasource *tsdb.DataSource) (tsdb.TsdbQueryEndpoint, error) {
return &InfluxdbExecutor{
QueryParser: &InfluxdbQueryParser{},
ResponseParser: &ResponseParser{},
}, nil
}
func (e *InfluxdbExecutor) Query(ctx context.Context, dsInfo *tsdb.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) {
result := &tsdb.Response{}
query, err := e.getQuery(dsInfo, tsdbQuery.Queries, tsdbQuery)
if err != nil {
return nil, err
}
rawQuery, err := query.Build(tsdbQuery)
if err != nil {
return nil, err
}
db := dsInfo.Database
if db == "" {
db = tsdbQuery.Queries[0].Database
}
dsInfo.Database = db
req, err := e.createRequest(dsInfo, rawQuery)
if err != nil {
return nil, err
}
httpClient, err := dsInfo.GetHttpClient()
if err != nil {
return nil, err
}
resp, err := ctxhttp.Do(ctx, httpClient, req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode/100 != 2 {
// TODO: convert status code err
return nil, errors.Wrapf(ErrInfluxdbInvalidResponse, "status code: %v", resp.Status)
}
var response Response
dec := json.NewDecoder(resp.Body)
dec.UseNumber()
if err := dec.Decode(&response); err != nil {
return nil, err
}
if response.Err != nil {
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{
RawQuery: rawQuery,
}
result.Results["A"] = ret
return result, nil
}
func (e *InfluxdbExecutor) getQuery(dsInfo *tsdb.DataSource, queries []*tsdb.Query, context *tsdb.TsdbQuery) (*Query, error) {
// The model supports multiple queries, but right now this is only used from
// alerting so we only need to support batch executing 1 query at a time.
if len(queries) > 0 {
query, err := e.QueryParser.Parse(queries[0], dsInfo)
if err != nil {
return nil, err
}
return query, nil
}
return nil, errors.Error("query request contains no queries")
}
func (e *InfluxdbExecutor) createRequest(dsInfo *tsdb.DataSource, query string) (*http.Request, error) {
u, _ := url.Parse(dsInfo.Url)
u.Path = path.Join(u.Path, "query")
req, err := func() (*http.Request, error) {
// use POST mode
bodyValues := url.Values{}
bodyValues.Add("q", query)
body := bodyValues.Encode()
return http.NewRequest(http.MethodPost, u.String(), strings.NewReader(body))
}()
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "OneCloud Monitor")
params := req.URL.Query()
params.Set("db", dsInfo.Database)
params.Set("epoch", "s")
req.Header.Set("Content-type", "application/x-www-form-urlencoded")
req.URL.RawQuery = params.Encode()
/*if dsInfo.BasicAuth {
req.SetBasicAuth(dsinfo.BasicAuthUser, dsInfo.DecryptedBasicAuthPassword())
}
if !dsInfo.BasicAuth && dsInfo.User != "" {
req.SetBasicAuth(dsInfo.User, dsInfo.DecryptedPassword())
}*/
curlCmd, _ := http2curl.GetCurlCommand(req)
log.Debugf("Influxdb raw query: %q from db %s, curl: %s", query, dsInfo.Database, curlCmd)
return req, nil
}
@@ -0,0 +1,58 @@
// Copyright 2019 Yunion
//
// 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.
package influxdb
import (
"time"
api "yunion.io/x/onecloud/pkg/apis/monitor"
)
type Query struct {
Measurement string
Policy string
ResultFormat string
Tags []api.MetricQueryTag
GroupBy []*QueryPart
Selects []*Select
Alias string
Tz string
Interval time.Duration
}
type Select []QueryPart
type Response struct {
Results []Result
Err error
}
type Result struct {
Series []Row
Message []*Message
Err error
}
type Message struct {
Level string `json:"level,omitempty"`
Text string `json:"text,omitempty"`
}
type Row struct {
Name string `json:"name,omitempty"`
Tags map[string]string `json:"tags,omitempty"`
Columns []string `json:"columns,omitempty"`
Values [][]interface{} `json:"values,omitempty"`
}
+172
View File
@@ -0,0 +1,172 @@
// Copyright 2019 Yunion
//
// 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.
package influxdb
import (
"fmt"
"regexp"
"strconv"
"strings"
"yunion.io/x/onecloud/pkg/monitor/tsdb"
)
var (
regexpOperatorPattern = regexp.MustCompile(`^\/.*\/$`)
regexpMeasurementPattern = regexp.MustCompile(`^\/.*\/$`)
)
func (query *Query) Build(queryCtx *tsdb.TsdbQuery) (string, error) {
var res string
res = query.renderSelectors(queryCtx)
res += query.renderMeasurement()
res += query.renderWhereClause()
res += query.renderTimeFilter(queryCtx)
res += query.renderGroupBy(queryCtx)
res += query.renderTz()
calculator := tsdb.NewIntervalCalculator(&tsdb.IntervalOptions{})
interval := calculator.Calculate(queryCtx.TimeRange, query.Interval)
res = strings.Replace(res, "$timeFilter", query.renderTimeFilter(queryCtx), -1)
res = strings.Replace(res, "$interval", interval.Text, -1)
res = strings.Replace(res, "$__interval_ms", strconv.FormatInt(interval.Milliseconds(), 10), -1)
res = strings.Replace(res, "$__interval", interval.Text, -1)
return res, nil
}
func (query *Query) renderTags() []string {
var res []string
for i, tag := range query.Tags {
str := ""
if i > 0 {
if tag.Condition == "" {
str += "AND"
} else {
str += tag.Condition
}
str += " "
}
// If the operator is missing we fall back to sensible defaults
if tag.Operator == "" {
if regexpOperatorPattern.Match([]byte(tag.Value)) {
tag.Operator = "=~"
} else {
tag.Operator = "="
}
}
// quote value unless regex or number
var textValue string
if tag.Operator == "=~" || tag.Operator == "!~" {
textValue = tag.Value
} else if tag.Operator == "<" || tag.Operator == ">" {
textValue = tag.Value
} else {
textValue = fmt.Sprintf("'%s'", strings.Replace(tag.Value, `\`, `\\`, -1))
}
res = append(res, fmt.Sprintf(`%s"%s" %s %s`, str, tag.Key, tag.Operator, textValue))
}
return res
}
func (query *Query) renderTimeFilter(queryCtx *tsdb.TsdbQuery) string {
from := "now() - " + queryCtx.TimeRange.From
to := ""
if queryCtx.TimeRange.To != "now" && queryCtx.TimeRange.To != "" {
to = " and time < now() - " + strings.Replace(queryCtx.TimeRange.To, "now-", "", 1)
}
return fmt.Sprintf("time > %s%s", from, to)
}
func (query *Query) renderSelectors(queryCtx *tsdb.TsdbQuery) string {
res := "SELECT "
var selectors []string
for _, sel := range query.Selects {
stk := ""
for _, s := range *sel {
stk = s.Render(query, queryCtx, stk)
}
selectors = append(selectors, stk)
}
return res + strings.Join(selectors, ", ")
}
func (query *Query) renderMeasurement() string {
var policy string
if query.Policy == "" || query.Policy == "default" {
policy = ""
} else {
policy = `"` + query.Policy + `".`
}
measurement := query.Measurement
if !regexpMeasurementPattern.Match([]byte(measurement)) {
measurement = fmt.Sprintf(`"%s"`, measurement)
}
return fmt.Sprintf(` FROM %s%s`, policy, measurement)
}
func (query *Query) renderWhereClause() string {
res := " WHERE "
conditions := query.renderTags()
if len(conditions) > 0 {
if len(conditions) > 1 {
res += "(" + strings.Join(conditions, " ") + ")"
} else {
res += conditions[0]
}
res += " AND "
}
return res
}
func (query *Query) renderGroupBy(queryContext *tsdb.TsdbQuery) string {
groupBy := ""
for i, group := range query.GroupBy {
if i == 0 {
groupBy += " GROUP BY"
}
if i > 0 && group.Type != "fill" {
groupBy += ", " //fill is so very special. fill is a creep, fill is a weirdo
} else {
groupBy += " "
}
groupBy += group.Render(query, queryContext, "")
}
return groupBy
}
func (query *Query) renderTz() string {
tz := query.Tz
if tz == "" {
return ""
}
return fmt.Sprintf(" tz('%s')", tz)
}
@@ -0,0 +1,97 @@
// Copyright 2019 Yunion
//
// 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.
package influxdb
import (
"time"
api "yunion.io/x/onecloud/pkg/apis/monitor"
"yunion.io/x/onecloud/pkg/monitor/tsdb"
)
type InfluxdbQueryParser struct{}
func (qp *InfluxdbQueryParser) Parse(model *tsdb.Query, dsInfo *tsdb.DataSource) (*Query, error) {
policy := "default"
if model.Policy != "" {
policy = model.Policy
}
alias := model.Alias
tz := model.Tz
measurement := model.Measurement
resultFormat := model.ResultFormat
tags := model.Tags
groupBys, err := qp.parseGroupBy(model.GroupBy)
if err != nil {
return nil, err
}
selects, err := qp.parseSelects(model.Selects)
if err != nil {
return nil, err
}
parsedInterval, err := tsdb.GetIntervalFrom(dsInfo, model, time.Millisecond*1)
if err != nil {
return nil, err
}
return &Query{
Measurement: measurement,
Policy: policy,
ResultFormat: resultFormat,
GroupBy: groupBys,
Tags: tags,
Selects: selects,
Interval: parsedInterval,
Alias: alias,
Tz: tz,
}, nil
}
func (qp *InfluxdbQueryParser) parseSelects(selects []api.MetricQuerySelect) ([]*Select, error) {
var result []*Select
for _, selectObj := range selects {
var parts Select
for _, part := range selectObj {
queryPart, err := qp.parseQueryPart(part)
if err != nil {
return nil, err
}
parts = append(parts, *queryPart)
}
result = append(result, &parts)
}
return result, nil
}
func (qp *InfluxdbQueryParser) parseGroupBy(groupBy []api.MetricQueryPart) ([]*QueryPart, error) {
var result []*QueryPart
for _, gb := range groupBy {
queryPart, err := qp.parseQueryPart(gb)
if err != nil {
return nil, err
}
result = append(result, queryPart)
}
return result, nil
}
func (qp *InfluxdbQueryParser) parseQueryPart(part api.MetricQueryPart) (*QueryPart, error) {
return NewQueryPart(part.Type, part.Params)
}
@@ -0,0 +1,111 @@
// Copyright 2019 Yunion
//
// 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.
package influxdb
import (
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
"yunion.io/x/jsonutils"
"yunion.io/x/onecloud/pkg/monitor/tsdb"
)
func TestInfluxdbQueryParser(t *testing.T) {
Convey("Influxdb query parser", t, func() {
parser := &InfluxdbQueryParser{}
Convey("can parse influxdb json model", func() {
json := `
{
"group_by": [
{
"params": ["$interval"],
"type": "time"
},
{
"params": ["datacenter"],
"type": "tag"
},
{
"params": ["none"],
"type": "fill"
}
],
"measurement": "logins.count",
"tz": "Asia/Shanghai",
"policy": "default",
"refId": "B",
"result_format": "time_series",
"select": [
[
{
"type": "field",
"params": ["value"]
},
{
"type": "count",
"params": []
}
],
[
{
"type": "field",
"params": ["value"]
},
{
"type": "bottom",
"params": ["3"]
}
],
[
{
"type": "field",
"params": ["value"]
},
{
"type": "mean",
"params": []
},
{
"type": "math",
"params": [" / 100"]
}
]
],
"alias": "serie alias",
"tags": [
{"key": "datacenter", "operator": "=", "value": "America"},
{"condition": "OR", "key": "hostname", "operator": "=", "value": "server1"}
]
}
`
obj, err := jsonutils.Parse([]byte(json))
So(err, ShouldBeNil)
apiQuery := new(tsdb.Query)
So(obj.Unmarshal(apiQuery), ShouldBeNil)
dsInfo := &tsdb.DataSource{TimeInterval: ">20s"}
res, err := parser.Parse(apiQuery, dsInfo)
So(err, ShouldBeNil)
So(len(res.GroupBy), ShouldEqual, 3)
So(len(res.Selects), ShouldEqual, 3)
So(len(res.Tags), ShouldEqual, 2)
So(res.Tz, ShouldEqual, "Asia/Shanghai")
So(res.Interval, ShouldEqual, time.Second*20)
So(res.Alias, ShouldEqual, "serie alias")
})
})
}
@@ -0,0 +1,182 @@
// Copyright 2019 Yunion
//
// 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.
package influxdb
import (
"fmt"
"strings"
"yunion.io/x/onecloud/pkg/monitor/tsdb"
)
var renders map[string]QueryDefinition
type DefinitionParameters struct {
Name string
Type string
}
type QueryDefinition struct {
Renderer func(query *Query, queryCtx *tsdb.TsdbQuery, part *QueryPart, innerExpr string) string
Params []DefinitionParameters
}
func init() {
renders = make(map[string]QueryDefinition)
renders["field"] = QueryDefinition{Renderer: fieldRenderer}
renders["spread"] = QueryDefinition{Renderer: functionRenderer}
renders["count"] = QueryDefinition{Renderer: functionRenderer}
renders["distinct"] = QueryDefinition{Renderer: functionRenderer}
renders["integral"] = QueryDefinition{Renderer: functionRenderer}
renders["mean"] = QueryDefinition{Renderer: functionRenderer}
renders["median"] = QueryDefinition{Renderer: functionRenderer}
renders["sum"] = QueryDefinition{Renderer: functionRenderer}
renders["mode"] = QueryDefinition{Renderer: functionRenderer}
renders["cumulative_sum"] = QueryDefinition{Renderer: functionRenderer}
renders["non_negative_difference"] = QueryDefinition{Renderer: functionRenderer}
renders["holt_winters"] = QueryDefinition{
Renderer: functionRenderer,
Params: []DefinitionParameters{{Name: "number", Type: "number"}, {Name: "season", Type: "number"}},
}
renders["holt_winters_with_fit"] = QueryDefinition{
Renderer: functionRenderer,
Params: []DefinitionParameters{{Name: "number", Type: "number"}, {Name: "season", Type: "number"}},
}
renders["derivative"] = QueryDefinition{
Renderer: functionRenderer,
Params: []DefinitionParameters{{Name: "duration", Type: "interval"}},
}
renders["non_negative_derivative"] = QueryDefinition{
Renderer: functionRenderer,
Params: []DefinitionParameters{{Name: "duration", Type: "interval"}},
}
renders["difference"] = QueryDefinition{Renderer: functionRenderer}
renders["moving_average"] = QueryDefinition{
Renderer: functionRenderer,
Params: []DefinitionParameters{{Name: "window", Type: "number"}},
}
renders["stddev"] = QueryDefinition{Renderer: functionRenderer}
renders["time"] = QueryDefinition{
Renderer: functionRenderer,
Params: []DefinitionParameters{{Name: "interval", Type: "time"}, {Name: "offset", Type: "time"}},
}
renders["fill"] = QueryDefinition{
Renderer: functionRenderer,
Params: []DefinitionParameters{{Name: "fill", Type: "string"}},
}
renders["elapsed"] = QueryDefinition{
Renderer: functionRenderer,
Params: []DefinitionParameters{{Name: "duration", Type: "interval"}},
}
renders["bottom"] = QueryDefinition{
Renderer: functionRenderer,
Params: []DefinitionParameters{{Name: "count", Type: "int"}},
}
renders["first"] = QueryDefinition{Renderer: functionRenderer}
renders["last"] = QueryDefinition{Renderer: functionRenderer}
renders["max"] = QueryDefinition{Renderer: functionRenderer}
renders["min"] = QueryDefinition{Renderer: functionRenderer}
renders["percentile"] = QueryDefinition{
Renderer: functionRenderer,
Params: []DefinitionParameters{{Name: "nth", Type: "int"}},
}
renders["top"] = QueryDefinition{
Renderer: functionRenderer,
Params: []DefinitionParameters{{Name: "count", Type: "int"}},
}
renders["tag"] = QueryDefinition{
Renderer: tagRenderer,
Params: []DefinitionParameters{{Name: "tag", Type: "string"}},
}
renders["math"] = QueryDefinition{Renderer: suffixRenderer}
renders["alias"] = QueryDefinition{Renderer: aliasRenderer}
}
func fieldRenderer(query *Query, queryCtx *tsdb.TsdbQuery, part *QueryPart, innerExpr string) string {
if part.Params[0] == "*" {
// return "*::field"
return "*"
}
// return fmt.Sprintf(`"%s"::field`, part.Params[0])
return fmt.Sprintf(`"%s"`, part.Params[0])
}
func tagRenderer(query *Query, queryCtx *tsdb.TsdbQuery, part *QueryPart, innerExpr string) string {
if part.Params[0] == "*" {
// return "*::tag"
return "*"
}
// return fmt.Sprintf(`"%s"::tag`, part.Params[0])
return fmt.Sprintf(`"%s"`, part.Params[0])
}
func functionRenderer(query *Query, queryCtx *tsdb.TsdbQuery, part *QueryPart, innerExpr string) string {
for i, param := range part.Params {
if part.Type == "time" && param == "auto" {
part.Params[i] = "$__interval"
}
}
if innerExpr != "" {
part.Params = append([]string{innerExpr}, part.Params...)
}
params := strings.Join(part.Params, ", ")
return fmt.Sprintf("%s(%s)", part.Type, params)
}
func suffixRenderer(query *Query, queryCtx *tsdb.TsdbQuery, part *QueryPart, innerExpr string) string {
return fmt.Sprintf("%s %s", innerExpr, part.Params[0])
}
func aliasRenderer(query *Query, queryCtx *tsdb.TsdbQuery, part *QueryPart, innerExpr string) string {
return fmt.Sprintf(`%s AS "%s"`, innerExpr, part.Params[0])
}
func (r QueryDefinition) Render(query *Query, queryCtx *tsdb.TsdbQuery, part *QueryPart, innerExpr string) string {
return r.Renderer(query, queryCtx, part, innerExpr)
}
func NewQueryPart(typ string, params []string) (*QueryPart, error) {
def, exist := renders[typ]
if !exist {
return nil, fmt.Errorf("Missing query definition for %s", typ)
}
return &QueryPart{
Def: def,
Type: typ,
Params: params,
}, nil
}
type QueryPart struct {
Def QueryDefinition
Type string
Params []string
}
func (qp *QueryPart) Render(query *Query, queryCtx *tsdb.TsdbQuery, expr string) string {
return qp.Def.Renderer(query, queryCtx, qp, expr)
}
@@ -0,0 +1,58 @@
// Copyright 2019 Yunion
//
// 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.
package influxdb
import (
"testing"
"yunion.io/x/onecloud/pkg/monitor/tsdb"
)
func TestInfluxdbQueryPart(t *testing.T) {
tcs := []struct {
mode string
input string
params []string
expected string
}{
{mode: "field", params: []string{"value"}, input: "value", expected: `"value"`},
{mode: "derivative", params: []string{"10s"}, input: "mean(value)", expected: `derivative(mean(value), 10s)`},
{mode: "bottom", params: []string{"3"}, input: "value", expected: `bottom(value, 3)`},
{mode: "time", params: []string{"$interval"}, input: "", expected: `time($interval)`},
{mode: "time", params: []string{"auto"}, input: "", expected: `time($__interval)`},
{mode: "spread", params: []string{}, input: "value", expected: `spread(value)`},
{mode: "math", params: []string{"/ 100"}, input: "mean(value)", expected: `mean(value) / 100`},
{mode: "alias", params: []string{"test"}, input: "mean(value)", expected: `mean(value) AS "test"`},
{mode: "count", params: []string{}, input: "distinct(value)", expected: `count(distinct(value))`},
{mode: "mode", params: []string{}, input: "value", expected: `mode(value)`},
{mode: "cumulative_sum", params: []string{}, input: "mean(value)", expected: `cumulative_sum(mean(value))`},
{mode: "non_negative_difference", params: []string{}, input: "max(value)", expected: `non_negative_difference(max(value))`},
}
queryCtx := &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("5m", "now")}
query := &Query{}
for _, tc := range tcs {
part, err := NewQueryPart(tc.mode, tc.params)
if err != nil {
t.Errorf("Expected NewQueryPart to not return an error. error: %v", err)
}
res := part.Render(query, queryCtx, tc.input)
if res != tc.expected {
t.Errorf("expected %v to render into %s", tc, tc.expected)
}
}
}
@@ -0,0 +1,205 @@
// Copyright 2019 Yunion
//
// 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.
package influxdb
import (
"strings"
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
api "yunion.io/x/onecloud/pkg/apis/monitor"
"yunion.io/x/onecloud/pkg/monitor/tsdb"
)
func TestInfluxdbQueryBuilder(t *testing.T) {
Convey("Influxdb query builder", t, func() {
qp1, _ := NewQueryPart("field", []string{"value"})
qp2, _ := NewQueryPart("mean", []string{})
mathPartDivideBy100, _ := NewQueryPart("math", []string{"/ 100"})
mathPartDivideByIntervalMs, _ := NewQueryPart("math", []string{"/ $__interval_ms"})
groupBy1, _ := NewQueryPart("time", []string{"$__interval"})
groupBy2, _ := NewQueryPart("tag", []string{"datacenter"})
groupBy3, _ := NewQueryPart("fill", []string{"null"})
groupByOldInterval, _ := NewQueryPart("time", []string{"$interval"})
tag1 := api.MetricQueryTag{Key: "hostname", Value: "server1", Operator: "="}
tag2 := api.MetricQueryTag{Key: "hostname", Value: "server2", Operator: "=", Condition: "OR"}
queryContext := &tsdb.TsdbQuery{
TimeRange: tsdb.NewTimeRange("5m", "now"),
}
Convey("can build simple query", func() {
query := &Query{
Selects: []*Select{{*qp1, *qp2}},
Measurement: "cpu",
Policy: "policy",
GroupBy: []*QueryPart{groupBy1, groupBy3},
Interval: time.Second * 10,
}
rawQuery, err := query.Build(queryContext)
So(err, ShouldBeNil)
So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "policy"."cpu" WHERE time > now() - 5m GROUP BY time(10s) fill(null)`)
})
Convey("can build query with tz", func() {
query := &Query{
Selects: []*Select{{*qp1, *qp2}},
Measurement: "cpu",
GroupBy: []*QueryPart{groupBy1},
Tz: "Europe/Paris",
Interval: time.Second * 5,
}
rawQuery, err := query.Build(queryContext)
So(err, ShouldBeNil)
So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "cpu" WHERE time > now() - 5m GROUP BY time(5s) tz('Europe/Paris')`)
})
Convey("can build query with group bys", func() {
query := &Query{
Selects: []*Select{{*qp1, *qp2}},
Measurement: "cpu",
GroupBy: []*QueryPart{groupBy1, groupBy2, groupBy3},
Tags: []api.MetricQueryTag{tag1, tag2},
Interval: time.Second * 5,
}
rawQuery, err := query.Build(queryContext)
So(err, ShouldBeNil)
So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "cpu" WHERE ("hostname" = 'server1' OR "hostname" = 'server2') AND time > now() - 5m GROUP BY time(5s), "datacenter" fill(null)`)
})
Convey("can build query with math part", func() {
query := &Query{
Selects: []*Select{{*qp1, *qp2, *mathPartDivideBy100}},
Measurement: "cpu",
Interval: time.Second * 5,
}
rawQuery, err := query.Build(queryContext)
So(err, ShouldBeNil)
So(rawQuery, ShouldEqual, `SELECT mean("value") / 100 FROM "cpu" WHERE time > now() - 5m`)
})
Convey("can build query with math part using $__interval_ms variable", func() {
query := &Query{
Selects: []*Select{{*qp1, *qp2, *mathPartDivideByIntervalMs}},
Measurement: "cpu",
Interval: time.Second * 5,
}
rawQuery, err := query.Build(queryContext)
So(err, ShouldBeNil)
So(rawQuery, ShouldEqual, `SELECT mean("value") / 5000 FROM "cpu" WHERE time > now() - 5m`)
})
Convey("can build query with old $interval variable", func() {
query := &Query{
Selects: []*Select{{*qp1, *qp2}},
Measurement: "cpu",
Policy: "",
GroupBy: []*QueryPart{groupByOldInterval},
}
rawQuery, err := query.Build(queryContext)
So(err, ShouldBeNil)
So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "cpu" WHERE time > now() - 5m GROUP BY time(200ms)`)
})
Convey("can render time range", func() {
query := Query{}
Convey("render from: 2h to now-1h", func() {
query := Query{}
queryContext := &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("2h", "now-1h")}
So(query.renderTimeFilter(queryContext), ShouldEqual, "time > now() - 2h and time < now() - 1h")
})
Convey("render from: 10m", func() {
queryContext := &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("10m", "now")}
So(query.renderTimeFilter(queryContext), ShouldEqual, "time > now() - 10m")
})
})
Convey("can render normal tags without operator", func() {
query := &Query{Tags: []api.MetricQueryTag{{Operator: "", Value: `value`, Key: "key"}}}
So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" = 'value'`)
})
Convey("can render regex tags without operator", func() {
query := &Query{Tags: []api.MetricQueryTag{{Operator: "", Value: `/value/`, Key: "key"}}}
So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" =~ /value/`)
})
Convey("can render regex tags", func() {
query := &Query{Tags: []api.MetricQueryTag{{Operator: "=~", Value: `/value/`, Key: "key"}}}
So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" =~ /value/`)
})
Convey("can render number tags", func() {
query := &Query{Tags: []api.MetricQueryTag{{Operator: "=", Value: "10001", Key: "key"}}}
So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" = '10001'`)
})
Convey("can render numbers less then condition tags", func() {
query := &Query{Tags: []api.MetricQueryTag{{Operator: "<", Value: "10001", Key: "key"}}}
So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" < 10001`)
})
Convey("can render number greater then condition tags", func() {
query := &Query{Tags: []api.MetricQueryTag{{Operator: ">", Value: "10001", Key: "key"}}}
So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" > 10001`)
})
Convey("can render string tags", func() {
query := &Query{Tags: []api.MetricQueryTag{{Operator: "=", Value: "value", Key: "key"}}}
So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" = 'value'`)
})
Convey("can escape backslashes when rendering string tags", func() {
query := &Query{Tags: []api.MetricQueryTag{{Operator: "=", Value: `C:\test\`, Key: "key"}}}
So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" = 'C:\\test\\'`)
})
Convey("can render regular measurement", func() {
query := &Query{Measurement: `apa`, Policy: "policy"}
So(query.renderMeasurement(), ShouldEqual, ` FROM "policy"."apa"`)
})
Convey("can render regexp measurement", func() {
query := &Query{Measurement: `/apa/`, Policy: "policy"}
So(query.renderMeasurement(), ShouldEqual, ` FROM "policy"./apa/`)
})
})
}
@@ -0,0 +1,160 @@
// Copyright 2019 Yunion
//
// 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.
package influxdb
import (
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
"yunion.io/x/onecloud/pkg/monitor/tsdb"
)
type ResponseParser struct{}
var (
legendFormat *regexp.Regexp
)
func init() {
legendFormat = regexp.MustCompile(`\[\[(\w+)(\.\w+)*\]\]*|\$\s*(\w+?)*`)
}
func (rp *ResponseParser) Parse(response *Response, query *Query) *tsdb.QueryResult {
queryRes := tsdb.NewQueryResult()
for _, result := range response.Results {
queryRes.Series = append(queryRes.Series, rp.transformRows(result.Series, queryRes, query)...)
}
return queryRes
}
func (rp *ResponseParser) transformRows(rows []Row, queryResult *tsdb.QueryResult, query *Query) tsdb.TimeSeriesSlice {
var result tsdb.TimeSeriesSlice
for _, row := range rows {
for columnIndex, column := range row.Columns {
if column == "time" {
continue
}
var points tsdb.TimeSeriesPoints
for _, valuePair := range row.Values {
point, err := rp.parseTimepoint(valuePair, columnIndex)
if err == nil {
points = append(points, point)
}
}
result = append(result, &tsdb.TimeSeries{
Name: rp.formatSerieName(row, column, query),
Points: points,
Tags: row.Tags,
})
}
}
return result
}
func (rp *ResponseParser) formatSerieName(row Row, column string, query *Query) string {
if query.Alias == "" {
return rp.buildSerieNameFromQuery(row, column)
}
nameSegment := strings.Split(row.Name, ".")
result := legendFormat.ReplaceAllFunc([]byte(query.Alias), func(in []byte) []byte {
aliasFormat := string(in)
aliasFormat = strings.Replace(aliasFormat, "[[", "", 1)
aliasFormat = strings.Replace(aliasFormat, "]]", "", 1)
aliasFormat = strings.Replace(aliasFormat, "$", "", 1)
if aliasFormat == "m" || aliasFormat == "measurement" {
return []byte(query.Measurement)
}
if aliasFormat == "col" {
return []byte(column)
}
pos, err := strconv.Atoi(aliasFormat)
if err == nil && len(nameSegment) >= pos {
return []byte(nameSegment[pos])
}
if !strings.HasPrefix(aliasFormat, "tag_") {
return in
}
tagKey := strings.Replace(aliasFormat, "tag_", "", 1)
tagValue, exist := row.Tags[tagKey]
if exist {
return []byte(tagValue)
}
return in
})
return string(result)
}
func (rp *ResponseParser) buildSerieNameFromQuery(row Row, column string) string {
/*var tags []string
for k, v := range row.Tags {
tags = append(tags, fmt.Sprintf("%s: %s", k, v))
}
tagText := ""
if len(tags) > 0 {
tagText = fmt.Sprintf(" { %s }", strings.Join(tags, " "))
}
return fmt.Sprintf("%s.%s%s", row.Name, column, tagText)*/
return fmt.Sprintf("%s.%s", row.Name, column)
}
func (rp *ResponseParser) parseTimepoint(valuePair []interface{}, valuePosition int) (tsdb.TimePoint, error) {
var value *float64 = rp.parseValue(valuePair[valuePosition])
timestampNumber, _ := valuePair[0].(json.Number)
timestamp, err := timestampNumber.Float64()
if err != nil {
return tsdb.TimePoint{}, err
}
return tsdb.NewTimePoint(value, timestamp), nil
}
func (rp *ResponseParser) parseValue(value interface{}) *float64 {
number, ok := value.(json.Number)
if !ok {
return nil
}
fvalue, err := number.Float64()
if err == nil {
return &fvalue
}
ivalue, err := number.Int64()
if err == nil {
ret := float64(ivalue)
return &ret
}
return nil
}
@@ -0,0 +1,185 @@
// Copyright 2019 Yunion
//
// 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.
package influxdb
import (
"encoding/json"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestInfluxdbResponseParser(t *testing.T) {
Convey("Influxdb response parser", t, func() {
Convey("Response parser", func() {
parser := &ResponseParser{}
response := &Response{
Results: []Result{
{
Series: []Row{
{
Name: "cpu",
Columns: []string{"time", "mean", "sum"},
Tags: map[string]string{"datacenter": "America"},
Values: [][]interface{}{
{json.Number("111"), json.Number("222"), json.Number("333")},
{json.Number("111"), json.Number("222"), json.Number("333")},
{json.Number("111"), json.Number("null"), json.Number("333")},
},
},
},
},
},
}
query := &Query{}
result := parser.Parse(response, query)
Convey("can parse all series", func() {
So(len(result.Series), ShouldEqual, 2)
})
Convey("can parse all points", func() {
So(len(result.Series[0].Points), ShouldEqual, 3)
So(len(result.Series[1].Points), ShouldEqual, 3)
})
Convey("can parse multi row result", func() {
So(result.Series[0].Points[1].Value(), ShouldEqual, float64(222))
So(result.Series[1].Points[1].Value(), ShouldEqual, float64(333))
})
Convey("can parse null points", func() {
So(result.Series[0].Points[2].IsValid(), ShouldBeFalse)
})
Convey("can format serie names", func() {
So(result.Series[0].Name, ShouldEqual, "cpu.mean")
So(result.Series[0].Tags, ShouldResemble, map[string]string{"datacenter": "America"})
So(result.Series[1].Name, ShouldEqual, "cpu.sum")
So(result.Series[1].Tags, ShouldResemble, map[string]string{"datacenter": "America"})
})
})
Convey("Response parser with alias", func() {
parser := &ResponseParser{}
response := &Response{
Results: []Result{
{
Series: []Row{
{
Name: "cpu.upc",
Columns: []string{"time", "mean", "sum"},
Tags: map[string]string{
"datacenter": "America",
"dc.region.name": "Northeast",
},
Values: [][]interface{}{
{json.Number("111"), json.Number("222"), json.Number("333")},
},
},
},
},
},
}
Convey("$ alias", func() {
Convey("simple alias", func() {
query := &Query{Alias: "serie alias"}
result := parser.Parse(response, query)
So(result.Series[0].Name, ShouldEqual, "serie alias")
})
Convey("measurement alias", func() {
query := &Query{Alias: "alias $m $measurement", Measurement: "10m"}
result := parser.Parse(response, query)
So(result.Series[0].Name, ShouldEqual, "alias 10m 10m")
})
Convey("column alias", func() {
query := &Query{Alias: "alias $col", Measurement: "10m"}
result := parser.Parse(response, query)
So(result.Series[0].Name, ShouldEqual, "alias mean")
So(result.Series[1].Name, ShouldEqual, "alias sum")
})
Convey("tag alias", func() {
query := &Query{Alias: "alias $tag_datacenter"}
result := parser.Parse(response, query)
So(result.Series[0].Name, ShouldEqual, "alias America")
})
Convey("segment alias", func() {
query := &Query{Alias: "alias $1"}
result := parser.Parse(response, query)
So(result.Series[0].Name, ShouldEqual, "alias upc")
})
Convey("segment position out of bound", func() {
query := &Query{Alias: "alias $5"}
result := parser.Parse(response, query)
So(result.Series[0].Name, ShouldEqual, "alias $5")
})
})
Convey("[[]] alias", func() {
Convey("simple alias", func() {
query := &Query{Alias: "serie alias"}
result := parser.Parse(response, query)
So(result.Series[0].Name, ShouldEqual, "serie alias")
})
Convey("measurement alias", func() {
query := &Query{Alias: "alias [[m]] [[measurement]]", Measurement: "10m"}
result := parser.Parse(response, query)
So(result.Series[0].Name, ShouldEqual, "alias 10m 10m")
})
Convey("column alias", func() {
query := &Query{Alias: "alias [[col]]", Measurement: "10m"}
result := parser.Parse(response, query)
So(result.Series[0].Name, ShouldEqual, "alias mean")
So(result.Series[1].Name, ShouldEqual, "alias sum")
})
Convey("tag alias", func() {
query := &Query{Alias: "alias [[tag_datacenter]]"}
result := parser.Parse(response, query)
So(result.Series[0].Name, ShouldEqual, "alias America")
})
Convey("tag alias with periods", func() {
query := &Query{Alias: "alias [[tag_dc.region.name]]"}
result := parser.Parse(response, query)
So(result.Series[0].Name, ShouldEqual, "alias Northeast")
})
})
})
})
}
+219
View File
@@ -0,0 +1,219 @@
// Copyright 2019 Yunion
//
// 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.
package tsdb
import (
"fmt"
"strings"
"time"
)
var (
defaultRes int64 = 1500
defaultMinInterval = time.Millisecond * 1
year = time.Hour * 24 * 365
day = time.Hour * 24
)
type Interval struct {
Text string
Value time.Duration
}
type intervalCalculator struct {
minInterval time.Duration
}
type IntervalCalculator interface {
Calculate(timeRange *TimeRange, minInterval time.Duration) Interval
}
type IntervalOptions struct {
MinInterval time.Duration
}
func NewIntervalCalculator(opt *IntervalOptions) *intervalCalculator {
if opt == nil {
opt = &IntervalOptions{}
}
calc := &intervalCalculator{}
if opt.MinInterval == 0 {
calc.minInterval = defaultMinInterval
} else {
calc.minInterval = opt.MinInterval
}
return calc
}
func (i *Interval) Milliseconds() int64 {
return i.Value.Nanoseconds() / int64(time.Millisecond)
}
func (ic *intervalCalculator) Calculate(timerange *TimeRange, minInterval time.Duration) Interval {
to := timerange.MustGetTo().UnixNano()
from := timerange.MustGetFrom().UnixNano()
interval := time.Duration((to - from) / defaultRes)
if interval < minInterval {
return Interval{Text: FormatDuration(minInterval), Value: minInterval}
}
rounded := roundInterval(interval)
return Interval{Text: FormatDuration(rounded), Value: rounded}
}
func GetIntervalFrom(dsInfo *DataSource, queryModel *Query, defaultInterval time.Duration) (time.Duration, error) {
interval := queryModel.Interval
if interval == "" && dsInfo.TimeInterval != "" {
interval = dsInfo.TimeInterval
}
if interval == "" {
return defaultInterval, nil
}
interval = strings.Replace(strings.Replace(interval, "<", "", 1), ">", "", 1)
parsedInterval, err := time.ParseDuration(interval)
if err != nil {
return time.Duration(0), err
}
return parsedInterval, nil
}
// FormatDuration converts a duration into the kbn format e.g. 1m 2h or 3d
func FormatDuration(inter time.Duration) string {
if inter >= year {
return fmt.Sprintf("%dy", inter/year)
}
if inter >= day {
return fmt.Sprintf("%dd", inter/day)
}
if inter >= time.Hour {
return fmt.Sprintf("%dh", inter/time.Hour)
}
if inter >= time.Minute {
return fmt.Sprintf("%dm", inter/time.Minute)
}
if inter >= time.Second {
return fmt.Sprintf("%ds", inter/time.Second)
}
if inter >= time.Millisecond {
return fmt.Sprintf("%dms", inter/time.Millisecond)
}
return "1ms"
}
func roundInterval(interval time.Duration) time.Duration {
switch true {
// 0.015s
case interval <= 15*time.Millisecond:
return time.Millisecond * 10 // 0.01s
// 0.035s
case interval <= 35*time.Millisecond:
return time.Millisecond * 20 // 0.02s
// 0.075s
case interval <= 75*time.Millisecond:
return time.Millisecond * 50 // 0.05s
// 0.15s
case interval <= 150*time.Millisecond:
return time.Millisecond * 100 // 0.1s
// 0.35s
case interval <= 350*time.Millisecond:
return time.Millisecond * 200 // 0.2s
// 0.75s
case interval <= 750*time.Millisecond:
return time.Millisecond * 500 // 0.5s
// 1.5s
case interval <= 1500*time.Millisecond:
return time.Millisecond * 1000 // 1s
// 3.5s
case interval <= 3500*time.Millisecond:
return time.Millisecond * 2000 // 2s
// 7.5s
case interval <= 7500*time.Millisecond:
return time.Millisecond * 5000 // 5s
// 12.5s
case interval <= 12500*time.Millisecond:
return time.Millisecond * 10000 // 10s
// 17.5s
case interval <= 17500*time.Millisecond:
return time.Millisecond * 15000 // 15s
// 25s
case interval <= 25000*time.Millisecond:
return time.Millisecond * 20000 // 20s
// 45s
case interval <= 45000*time.Millisecond:
return time.Millisecond * 30000 // 30s
// 1.5m
case interval <= 90000*time.Millisecond:
return time.Millisecond * 60000 // 1m
// 3.5m
case interval <= 210000*time.Millisecond:
return time.Millisecond * 120000 // 2m
// 7.5m
case interval <= 450000*time.Millisecond:
return time.Millisecond * 300000 // 5m
// 12.5m
case interval <= 750000*time.Millisecond:
return time.Millisecond * 600000 // 10m
// 12.5m
case interval <= 1050000*time.Millisecond:
return time.Millisecond * 900000 // 15m
// 25m
case interval <= 1500000*time.Millisecond:
return time.Millisecond * 1200000 // 20m
// 45m
case interval <= 2700000*time.Millisecond:
return time.Millisecond * 1800000 // 30m
// 1.5h
case interval <= 5400000*time.Millisecond:
return time.Millisecond * 3600000 // 1h
// 2.5h
case interval <= 9000000*time.Millisecond:
return time.Millisecond * 7200000 // 2h
// 4.5h
case interval <= 16200000*time.Millisecond:
return time.Millisecond * 10800000 // 3h
// 9h
case interval <= 32400000*time.Millisecond:
return time.Millisecond * 21600000 // 6h
// 24h
case interval <= 86400000*time.Millisecond:
return time.Millisecond * 43200000 // 12h
// 48h
case interval <= 172800000*time.Millisecond:
return time.Millisecond * 86400000 // 24h
// 1w
case interval <= 604800000*time.Millisecond:
return time.Millisecond * 86400000 // 24h
// 3w
case interval <= 1814400000*time.Millisecond:
return time.Millisecond * 604800000 // 1w
// 2y
case interval < 3628800000*time.Millisecond:
return time.Millisecond * 2592000000 // 30d
default:
return time.Millisecond * 31536000000 // 1y
}
}
+69
View File
@@ -0,0 +1,69 @@
// Copyright 2019 Yunion
//
// 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.
package tsdb
import (
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
)
func TestInterval(t *testing.T) {
Convey("Default interval", t, func() {
calculator := NewIntervalCalculator(&IntervalOptions{})
Convey("for 5min", func() {
tr := NewTimeRange("5m", "now")
interval := calculator.Calculate(tr, time.Millisecond*1)
So(interval.Text, ShouldEqual, "200ms")
})
Convey("for 15min", func() {
tr := NewTimeRange("15m", "now")
interval := calculator.Calculate(tr, time.Millisecond*1)
So(interval.Text, ShouldEqual, "500ms")
})
Convey("for 30min", func() {
tr := NewTimeRange("30m", "now")
interval := calculator.Calculate(tr, time.Millisecond*1)
So(interval.Text, ShouldEqual, "1s")
})
Convey("for 1h", func() {
tr := NewTimeRange("1h", "now")
interval := calculator.Calculate(tr, time.Millisecond*1)
So(interval.Text, ShouldEqual, "2s")
})
Convey("Round interval", func() {
So(roundInterval(time.Millisecond*30), ShouldEqual, time.Millisecond*20)
So(roundInterval(time.Millisecond*45), ShouldEqual, time.Millisecond*50)
})
Convey("Format value", func() {
So(FormatDuration(time.Second*61), ShouldEqual, "1m")
So(FormatDuration(time.Millisecond*30), ShouldEqual, "30ms")
So(FormatDuration(time.Hour*23), ShouldEqual, "23h")
So(FormatDuration(time.Hour*24), ShouldEqual, "1d")
So(FormatDuration(time.Hour*24*367), ShouldEqual, "1y")
})
})
}
+114
View File
@@ -0,0 +1,114 @@
// Copyright 2019 Yunion
//
// 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.
package tsdb
import api "yunion.io/x/onecloud/pkg/apis/monitor"
type TsdbQuery struct {
TimeRange *TimeRange
Queries []*Query
Debug bool
}
type Query struct {
RefId string
api.MetricQuery
DataSource DataSource
MaxDataPoints int64
IntervalMs int64
}
type Response struct {
Results map[string]*QueryResult `json:"results"`
Message string `json:"message,omitempty"`
}
type QueryResultMeta struct {
RawQuery string `json:"raw_query"`
}
type QueryResult struct {
Error error `json:"-"`
ErrorString string `json:"error,omitempty"`
RefId string `json:"ref_id"`
Meta QueryResultMeta `json:"meta"`
Series TimeSeriesSlice `json:"series"`
Tables []*Table `json:"tables"`
Dataframes [][]byte `json:"dataframes"`
}
type TimeSeries struct {
RawName string `json:"raw_name"`
Name string `json:"name"`
Points TimeSeriesPoints `json:"points"`
Tags map[string]string `json:"tags,omitempty"`
}
type Table struct {
Columns []TableColumn `json:"columns"`
Rows []RowValues `json:"rows"`
}
type TableColumn struct {
Text string `json:"text"`
}
type RowValues []interface{}
type TimePoint [2]interface{}
type TimeSeriesPoints []TimePoint
type TimeSeriesSlice []*TimeSeries
func NewQueryResult() *QueryResult {
return &QueryResult{
Series: make(TimeSeriesSlice, 0),
}
}
func NewTimePoint(value *float64, timestamp float64) TimePoint {
return TimePoint{value, timestamp}
}
func NewTimePointByVal(value float64, timestamp float64) TimePoint {
return NewTimePoint(&value, timestamp)
}
func (p TimePoint) IsValid() bool {
return p[0].(*float64) != nil
}
func (p TimePoint) Value() float64 {
return *(p[0].(*float64))
}
func (p TimePoint) Timestamp() float64 {
return p[1].(float64)
}
func NewTimeSeriesPointsFromArgs(values ...float64) TimeSeriesPoints {
points := make(TimeSeriesPoints, 0)
for i := 0; i < len(values); i += 2 {
points = append(points, NewTimePoint(&values[i], values[i+1]))
}
return points
}
func NewTimeSeries(name string, points TimeSeriesPoints) *TimeSeries {
return &TimeSeries{
Name: name,
Points: points,
}
}
+52
View File
@@ -0,0 +1,52 @@
// Copyright 2019 Yunion
//
// 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.
package tsdb
import (
"context"
"yunion.io/x/pkg/errors"
)
type TsdbQueryEndpoint interface {
Query(ctx context.Context, ds *DataSource, query *TsdbQuery) (*Response, error)
}
var registry map[string]GetTsdbQueryEndpointFn
type GetTsdbQueryEndpointFn func(dsInfo *DataSource) (TsdbQueryEndpoint, error)
func init() {
registry = make(map[string]GetTsdbQueryEndpointFn)
}
const (
ErrorNotFoundExecutorDataSource = errors.Error("Not find executor for data source")
)
func getTsdbQueryEndpointFor(dsInfo *DataSource) (TsdbQueryEndpoint, error) {
if fn, exists := registry[dsInfo.Type]; exists {
executor, err := fn(dsInfo)
if err != nil {
return nil, err
}
return executor, nil
}
return nil, errors.Wrapf(ErrorNotFoundExecutorDataSource, "type: %s", dsInfo.Type)
}
func RegisterTsdbQueryEndpoint(dataSourceType string, fn GetTsdbQueryEndpointFn) {
registry[dataSourceType] = fn
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright 2019 Yunion
//
// 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.
package tsdb
import (
"context"
)
type HandleRequestFunc func(ctx context.Context, dsInfo *DataSource, req *TsdbQuery) (*Response, error)
func HandleRequest(ctx context.Context, dsInfo *DataSource, req *TsdbQuery) (*Response, error) {
endpoint, err := getTsdbQueryEndpointFor(dsInfo)
if err != nil {
return nil, err
}
return endpoint.Query(ctx, dsInfo, req)
}
+134
View File
@@ -0,0 +1,134 @@
// Copyright 2019 Yunion
//
// 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.
package tsdb
import (
"fmt"
"strconv"
"strings"
"time"
)
type TimeRange struct {
From string
To string
now time.Time
}
func NewTimeRange(from, to string) *TimeRange {
return &TimeRange{
From: from,
To: to,
now: time.Now(),
}
}
func tryParseUnixMsEpoch(val string) (time.Time, bool) {
if val, err := strconv.ParseInt(val, 10, 64); err == nil {
seconds := val / 1000
nano := (val - seconds*1000) * 1000000
return time.Unix(seconds, nano), true
}
return time.Time{}, false
}
func (tr *TimeRange) ParseFrom() (time.Time, error) {
if res, ok := tryParseUnixMsEpoch(tr.From); ok {
return res, nil
}
fromRaw := strings.Replace(tr.From, "now-", "", 1)
diff, err := time.ParseDuration("-" + fromRaw)
if err != nil {
return time.Time{}, err
}
return tr.now.Add(diff), nil
}
func (tr *TimeRange) ParseTo() (time.Time, error) {
if tr.To == "now" {
return tr.now, nil
} else if strings.HasPrefix(tr.To, "now-") {
withoutNow := strings.Replace(tr.To, "now-", "", 1)
diff, err := time.ParseDuration("-" + withoutNow)
if err != nil {
return time.Time{}, nil
}
return tr.now.Add(diff), nil
}
if res, ok := tryParseUnixMsEpoch(tr.To); ok {
return res, nil
}
return time.Time{}, fmt.Errorf("cannot parse to value %s", tr.To)
}
func (tr *TimeRange) MustGetFrom() time.Time {
res, err := tr.ParseFrom()
if err != nil {
return time.Unix(0, 0)
}
return res
}
func (tr *TimeRange) MustGetTo() time.Time {
res, err := tr.ParseTo()
if err != nil {
return time.Unix(0, 0)
}
return res
}
func (tr *TimeRange) GetFromAsMsEpoch() int64 {
return tr.MustGetFrom().UnixNano() / int64(time.Millisecond)
}
func (tr *TimeRange) GetFromAsSecondsEpoch() int64 {
return tr.GetFromAsMsEpoch() / 1000
}
func (tr *TimeRange) GetFromAsTimeUTC() time.Time {
return tr.MustGetFrom().UTC()
}
func (tr *TimeRange) GetToAsMsEpoch() int64 {
return tr.MustGetTo().UnixNano() / int64(time.Millisecond)
}
func (tr *TimeRange) GetToAsSecondsEpoch() int64 {
return tr.GetToAsMsEpoch() / 1000
}
func (tr *TimeRange) GetToAsTimeUTC() time.Time {
return tr.MustGetTo().UTC()
}
// EpochPrecisionToMs converts epoch precision to millisecond, if needed.
// Only seconds to milliseconds supported right now
func EpochPrecisionToMs(value float64) float64 {
s := strconv.FormatFloat(value, 'e', -1, 64)
if strings.HasSuffix(s, "e+09") {
return value * float64(1e3)
}
if strings.HasSuffix(s, "e+18") {
return value / float64(time.Millisecond)
}
return value
}

Some files were not shown because too many files have changed in this diff Show More