mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-08-30 17:13:08 +08:00
add monitor service
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
FROM registry.cn-beijing.aliyuncs.com/yunionio/onecloud-base:latest
|
||||
|
||||
ADD ./_output/bin/monitor /opt/yunion/bin/monitor
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package monitor // import "yunion.io/x/onecloud/pkg/apis/monitor"
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
// 用户列表
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package monitor // import "yunion.io/x/onecloud/pkg/mcclient/modules/monitor"
|
||||
@@ -0,0 +1 @@
|
||||
package monitor // import "yunion.io/x/onecloud/pkg/mcclient/options/monitor"
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package alerting // import "yunion.io/x/onecloud/pkg/monitor/alerting"
|
||||
@@ -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)
|
||||
}()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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 ¬ificationService{}
|
||||
}
|
||||
|
||||
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, ¬ifierState{
|
||||
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(¬ifydrivers.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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package notifiers // import "yunion.io/x/onecloud/pkg/monitor/alerting/notifiers"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package bus // import "yunion.io/x/onecloud/pkg/monitor/bus"
|
||||
@@ -0,0 +1 @@
|
||||
package expressions // import "yunion.io/x/onecloud/pkg/monitor/expressions"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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,*/
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package models // import "yunion.io/x/onecloud/pkg/monitor/models"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package notifydrivers // import "yunion.io/x/onecloud/pkg/monitor/notifydrivers"
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package feishu // import "yunion.io/x/onecloud/pkg/monitor/notifydrivers/feishu"
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package options // import "yunion.io/x/onecloud/pkg/monitor/options"
|
||||
@@ -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
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
package registry // import "yunion.io/x/onecloud/pkg/monitor/registry"
|
||||
@@ -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
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
package service // import "yunion.io/x/onecloud/pkg/monitor/service"
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}*/
|
||||
@@ -0,0 +1 @@
|
||||
package tsdb // import "yunion.io/x/onecloud/pkg/monitor/tsdb"
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user