diff --git a/Makefile b/Makefile
index f726f6c9..22a6dca4 100644
--- a/Makefile
+++ b/Makefile
@@ -2,7 +2,7 @@
NOW = $(shell date -u '+%Y%m%d%I%M%S')
-RELEASE_VERSION = 5.7.1
+RELEASE_VERSION = 5.8.0
APP = n9e
SERVER_BIN = $(APP)
@@ -15,7 +15,7 @@ SERVER_BIN = $(APP)
all: build
build:
- go build -ldflags "-w -s -X main.VERSION=$(RELEASE_VERSION)" -o $(SERVER_BIN) ./src
+ go build -ldflags "-w -s -X github.com/didi/nightingale/v5/src/pkg/version.VERSION=$(RELEASE_VERSION)" -o $(SERVER_BIN) ./src
# start:
# @go run -ldflags "-X main.VERSION=$(RELEASE_TAG)" ./cmd/${APP}/main.go web -c ./configs/config.toml -m ./configs/model.conf --menu ./configs/menu.yaml
diff --git a/README.md b/README.md
index 606729e2..ee65177a 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,5 @@
+
+
# Nightingale
Nightingale is an enterprise-level cloud-native monitoring system, which can be used as drop-in replacement of Prometheus for alerting and management.
diff --git a/README_ZH.md b/README_ZH.md
index d7138e42..d0d0f9a4 100644
--- a/README_ZH.md
+++ b/README_ZH.md
@@ -1,3 +1,5 @@
+
+
# Nightingale
[English](./README.md) | [中文](./README_ZH.md)
diff --git a/doc/img/ccf-n9e.png b/doc/img/ccf-n9e.png
new file mode 100644
index 00000000..6903103d
Binary files /dev/null and b/doc/img/ccf-n9e.png differ
diff --git a/docker/initsql/a-n9e.sql b/docker/initsql/a-n9e.sql
index d544d09e..543af628 100644
--- a/docker/initsql/a-n9e.sql
+++ b/docker/initsql/a-n9e.sql
@@ -152,6 +152,28 @@ CREATE TABLE `busi_group_member` (
insert into busi_group_member(busi_group_id, user_group_id, perm_flag) values(1, 1, "rw");
+-- for dashboard new version
+CREATE TABLE `board` (
+ `id` bigint unsigned not null auto_increment,
+ `group_id` bigint not null default 0 comment 'busi group id',
+ `name` varchar(191) not null,
+ `tags` varchar(255) not null comment 'split by space',
+ `create_at` bigint not null default 0,
+ `create_by` varchar(64) not null default '',
+ `update_at` bigint not null default 0,
+ `update_by` varchar(64) not null default '',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY (`group_id`, `name`)
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
+
+-- for dashboard new version
+CREATE TABLE `board_payload` (
+ `id` bigint unsigned not null comment 'dashboard id',
+ `payload` mediumtext not null,
+ UNIQUE KEY (`id`)
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
+
+-- deprecated
CREATE TABLE `dashboard` (
`id` bigint unsigned not null auto_increment,
`group_id` bigint not null default 0 comment 'busi group id',
@@ -166,6 +188,7 @@ CREATE TABLE `dashboard` (
UNIQUE KEY (`group_id`, `name`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
+-- deprecated
-- auto create the first subclass 'Default chart group' of dashboard
CREATE TABLE `chart_group` (
`id` bigint unsigned not null auto_increment,
@@ -176,6 +199,7 @@ CREATE TABLE `chart_group` (
KEY (`dashboard_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
+-- deprecated
CREATE TABLE `chart` (
`id` bigint unsigned not null auto_increment,
`group_id` bigint unsigned not null comment 'chart group id',
@@ -200,7 +224,11 @@ CREATE TABLE `alert_rule` (
`group_id` bigint not null default 0 comment 'busi group id',
`cluster` varchar(128) not null,
`name` varchar(255) not null,
- `note` varchar(255) not null,
+ `note` varchar(1024) not null default '',
+ `prod` varchar(255) not null default '',
+ `algorithm` varchar(255) not null default '',
+ `algo_params` varchar(255),
+ `delay` int not null default 0,
`severity` tinyint(1) not null comment '0:Emergency 1:Warning 2:Notice',
`disabled` tinyint(1) not null comment '0:enabled 1:disabled',
`prom_for_duration` int not null comment 'prometheus for, unit:s',
@@ -333,7 +361,9 @@ CREATE TABLE `alert_cur_event` (
`hash` varchar(64) not null comment 'rule_id + vector_pk',
`rule_id` bigint unsigned not null,
`rule_name` varchar(255) not null,
- `rule_note` varchar(512) not null default 'alert rule note',
+ `rule_note` varchar(2048) not null default 'alert rule note',
+ `rule_prod` varchar(255) not null default '',
+ `rule_algo` varchar(255) not null default '',
`severity` tinyint(1) not null comment '0:Emergency 1:Warning 2:Notice',
`prom_for_duration` int not null comment 'prometheus for, unit:s',
`prom_ql` varchar(8192) not null comment 'promql',
@@ -365,7 +395,9 @@ CREATE TABLE `alert_his_event` (
`hash` varchar(64) not null comment 'rule_id + vector_pk',
`rule_id` bigint unsigned not null,
`rule_name` varchar(255) not null,
- `rule_note` varchar(512) not null default 'alert rule note',
+ `rule_note` varchar(2048) not null default 'alert rule note',
+ `rule_prod` varchar(255) not null default '',
+ `rule_algo` varchar(255) not null default '',
`severity` tinyint(1) not null comment '0:Emergency 1:Warning 2:Notice',
`prom_for_duration` int not null comment 'prometheus for, unit:s',
`prom_ql` varchar(8192) not null comment 'promql',
diff --git a/etc/server.conf b/etc/server.conf
index fdf8ff24..7a3bc5eb 100644
--- a/etc/server.conf
+++ b/etc/server.conf
@@ -123,7 +123,9 @@ Address = "127.0.0.1:6379"
# UseTLS = false
# TLSMinVersion = "1.2"
-[Gorm]
+[DB]
+# postgres: host=%s port=%s user=%s dbname=%s password=%s sslmode=%s
+DSN="root:1234@tcp(127.0.0.1:3306)/n9e_v5?charset=utf8mb4&parseTime=True&loc=Local&allowNativePasswords=true"
# enable debug mode or not
Debug = false
# mysql postgres
@@ -137,31 +139,7 @@ MaxIdleConns = 50
# table prefix
TablePrefix = ""
# enable auto migrate or not
-EnableAutoMigrate = false
-
-[MySQL]
-# mysql address host:port
-Address = "127.0.0.1:3306"
-# mysql username
-User = "root"
-# mysql password
-Password = "1234"
-# database name
-DBName = "n9e_v5"
-# connection params
-Parameters = "charset=utf8mb4&parseTime=True&loc=Local&allowNativePasswords=true"
-
-[Postgres]
-# pg address host:port
-Address = "127.0.0.1:5432"
-# pg user
-User = "root"
-# pg password
-Password = "1234"
-# database name
-DBName = "n9e_v5"
-# ssl mode
-SSLMode = "disable"
+# EnableAutoMigrate = false
[Reader]
# prometheus base url
diff --git a/etc/webapi.conf b/etc/webapi.conf
index ddcd15b7..7cbd2642 100644
--- a/etc/webapi.conf
+++ b/etc/webapi.conf
@@ -144,9 +144,10 @@ Address = "127.0.0.1:6379"
# UseTLS = false
# TLSMinVersion = "1.2"
-[Gorm]
+[DB]
+DSN="root:1234@tcp(127.0.0.1:3306)/n9e_v5?charset=utf8mb4&parseTime=True&loc=Local&allowNativePasswords=true"
# enable debug mode or not
-Debug = true
+Debug = false
# mysql postgres
DBType = "mysql"
# unit: s
@@ -158,31 +159,7 @@ MaxIdleConns = 50
# table prefix
TablePrefix = ""
# enable auto migrate or not
-EnableAutoMigrate = false
-
-[MySQL]
-# mysql address host:port
-Address = "127.0.0.1:3306"
-# mysql username
-User = "root"
-# mysql password
-Password = "1234"
-# database name
-DBName = "n9e_v5"
-# connection params
-Parameters = "charset=utf8mb4&parseTime=True&loc=Local&allowNativePasswords=true"
-
-[Postgres]
-# pg address host:port
-Address = "127.0.0.1:5432"
-# pg user
-User = "root"
-# pg password
-Password = "1234"
-# database name
-DBName = "n9e_v5"
-# ssl mode
-SSLMode = "disable"
+# EnableAutoMigrate = false
[[Clusters]]
# Prometheus cluster name
diff --git a/src/main.go b/src/main.go
index 88500c4d..bdeb986c 100644
--- a/src/main.go
+++ b/src/main.go
@@ -7,17 +7,15 @@ import (
"github.com/toolkits/pkg/runner"
"github.com/urfave/cli/v2"
+ "github.com/didi/nightingale/v5/src/pkg/version"
"github.com/didi/nightingale/v5/src/server"
"github.com/didi/nightingale/v5/src/webapi"
)
-// VERSION go build -ldflags "-X main.VERSION=x.x.x"
-var VERSION = "not specified"
-
func main() {
app := cli.NewApp()
app.Name = "n9e"
- app.Version = VERSION
+ app.Version = version.VERSION
app.Usage = "Nightingale, enterprise prometheus management"
app.Commands = []*cli.Command{
newWebapiCmd(),
@@ -44,7 +42,7 @@ func newWebapiCmd() *cli.Command {
if c.String("conf") != "" {
opts = append(opts, webapi.SetConfigFile(c.String("conf")))
}
- opts = append(opts, webapi.SetVersion(VERSION))
+ opts = append(opts, webapi.SetVersion(version.VERSION))
webapi.Run(opts...)
return nil
@@ -70,7 +68,7 @@ func newServerCmd() *cli.Command {
if c.String("conf") != "" {
opts = append(opts, server.SetConfigFile(c.String("conf")))
}
- opts = append(opts, server.SetVersion(VERSION))
+ opts = append(opts, server.SetVersion(version.VERSION))
server.Run(opts...)
return nil
diff --git a/src/models/alert_cur_event.go b/src/models/alert_cur_event.go
index 580c9d4a..7fa101ca 100644
--- a/src/models/alert_cur_event.go
+++ b/src/models/alert_cur_event.go
@@ -1,9 +1,13 @@
package models
import (
+ "bytes"
"fmt"
+ "html/template"
"strconv"
"strings"
+
+ "github.com/didi/nightingale/v5/src/pkg/tplx"
)
type AlertCurEvent struct {
@@ -15,6 +19,8 @@ type AlertCurEvent struct {
RuleId int64 `json:"rule_id"`
RuleName string `json:"rule_name"`
RuleNote string `json:"rule_note"`
+ RuleProd string `json:"rule_prod"`
+ RuleAlgo string `json:"rule_algo"`
Severity int `json:"severity"`
PromForDuration int `json:"prom_for_duration"`
PromQl string `json:"prom_ql"`
@@ -54,6 +60,34 @@ type AggrRule struct {
Value string
}
+func (e *AlertCurEvent) ParseRuleNote() error {
+ e.RuleNote = strings.TrimSpace(e.RuleNote)
+
+ if e.RuleNote == "" {
+ return nil
+ }
+
+ var defs = []string{
+ "{{$labels := .TagsMap}}",
+ "{{$value := .TriggerValue}}",
+ }
+
+ text := strings.Join(append(defs, e.RuleNote), "")
+ t, err := template.New(fmt.Sprint(e.RuleId)).Funcs(tplx.TemplateFuncMap).Parse(text)
+ if err != nil {
+ return err
+ }
+
+ var body bytes.Buffer
+ err = t.Execute(&body, e)
+ if err != nil {
+ return err
+ }
+
+ e.RuleNote = body.String()
+ return nil
+}
+
func (e *AlertCurEvent) GenCardTitle(rules []*AggrRule) string {
arr := make([]string, len(rules))
for i := 0; i < len(rules); i++ {
@@ -125,6 +159,8 @@ func (e *AlertCurEvent) ToHis() *AlertHisEvent {
Hash: e.Hash,
RuleId: e.RuleId,
RuleName: e.RuleName,
+ RuleProd: e.RuleProd,
+ RuleAlgo: e.RuleAlgo,
RuleNote: e.RuleNote,
Severity: e.Severity,
PromForDuration: e.PromForDuration,
diff --git a/src/models/alert_his_event.go b/src/models/alert_his_event.go
index d0222028..7b01404d 100644
--- a/src/models/alert_his_event.go
+++ b/src/models/alert_his_event.go
@@ -15,6 +15,8 @@ type AlertHisEvent struct {
RuleId int64 `json:"rule_id"`
RuleName string `json:"rule_name"`
RuleNote string `json:"rule_note"`
+ RuleProd string `json:"rule_prod"`
+ RuleAlgo string `json:"rule_algo"`
Severity int `json:"severity"`
PromForDuration int `json:"prom_for_duration"`
PromQl string `json:"prom_ql"`
diff --git a/src/models/alert_rule.go b/src/models/alert_rule.go
index c78e05ca..6a186a59 100644
--- a/src/models/alert_rule.go
+++ b/src/models/alert_rule.go
@@ -1,6 +1,7 @@
package models
import (
+ "encoding/json"
"fmt"
"strconv"
"strings"
@@ -18,6 +19,11 @@ type AlertRule struct {
Cluster string `json:"cluster"` // take effect by cluster
Name string `json:"name"` // rule name
Note string `json:"note"` // will sent in notify
+ Prod string `json:"prod"` // product empty means n9e
+ Algorithm string `json:"algorithm"` // algorithm (''|holtwinters), empty means threshold
+ AlgoParams string `json:"-" gorm:"algo_params"` // params algorithm need
+ AlgoParamsJson interface{} `json:"algo_params" gorm:"-"` //
+ Delay int `json:"delay"` // Time (in seconds) to delay evaluation
Severity int `json:"severity"` // 0: Emergency 1: Warning 2: Notice
Disabled int `json:"disabled"` // 0: enabled, 1: disabled
PromForDuration int `json:"prom_for_duration"` // prometheus for, unit:s
@@ -145,7 +151,11 @@ func (ar *AlertRule) Update(arf AlertRule) error {
}
}
- arf.FE2DB()
+ err := arf.FE2DB()
+ if err != nil {
+ return err
+ }
+
arf.Id = ar.Id
arf.GroupId = ar.GroupId
arf.CreateAt = ar.CreateAt
@@ -203,12 +213,19 @@ func (ar *AlertRule) FillNotifyGroups(cache map[int64]*UserGroup) error {
return nil
}
-func (ar *AlertRule) FE2DB() {
+func (ar *AlertRule) FE2DB() error {
ar.EnableDaysOfWeek = strings.Join(ar.EnableDaysOfWeekJSON, " ")
ar.NotifyChannels = strings.Join(ar.NotifyChannelsJSON, " ")
ar.NotifyGroups = strings.Join(ar.NotifyGroupsJSON, " ")
ar.Callbacks = strings.Join(ar.CallbacksJSON, " ")
ar.AppendTags = strings.Join(ar.AppendTagsJSON, " ")
+ algoParamsByte, err := json.Marshal(ar.AlgoParamsJson)
+ if err != nil {
+ return fmt.Errorf("marshal algo_params err:%v", err)
+ }
+
+ ar.AlgoParams = string(algoParamsByte)
+ return nil
}
func (ar *AlertRule) DB2FE() {
@@ -217,6 +234,7 @@ func (ar *AlertRule) DB2FE() {
ar.NotifyGroupsJSON = strings.Fields(ar.NotifyGroups)
ar.CallbacksJSON = strings.Fields(ar.Callbacks)
ar.AppendTagsJSON = strings.Fields(ar.AppendTags)
+ json.Unmarshal([]byte(ar.AlgoParams), &ar.AlgoParamsJson)
}
func AlertRuleDels(ids []int64, busiGroupId int64) error {
@@ -254,7 +272,7 @@ func AlertRuleGets(groupId int64) ([]AlertRule, error) {
}
func AlertRuleGetsByCluster(cluster string) ([]*AlertRule, error) {
- session := DB().Where("disabled = ?", 0)
+ session := DB().Where("disabled = ? and prod = ?", 0, "")
if cluster != "" {
session = session.Where("cluster = ?", cluster)
@@ -271,6 +289,20 @@ func AlertRuleGetsByCluster(cluster string) ([]*AlertRule, error) {
return lst, err
}
+func AlertRulesGetByProds(prods []string) ([]*AlertRule, error) {
+ session := DB().Where("disabled = ? and prod IN (?)", 0, prods)
+
+ var lst []*AlertRule
+ err := session.Find(&lst).Error
+ if err == nil {
+ for i := 0; i < len(lst); i++ {
+ lst[i].DB2FE()
+ }
+ }
+
+ return lst, err
+}
+
func AlertRuleGet(where string, args ...interface{}) (*AlertRule, error) {
var lst []*AlertRule
err := DB().Where(where, args...).Find(&lst).Error
@@ -306,7 +338,7 @@ func AlertRuleGetName(id int64) (string, error) {
}
func AlertRuleStatistics(cluster string) (*Statistics, error) {
- session := DB().Model(&AlertRule{}).Select("count(*) as total", "max(update_at) as last_updated").Where("disabled = ?", 0)
+ session := DB().Model(&AlertRule{}).Select("count(*) as total", "max(update_at) as last_updated").Where("disabled = ? and prod = ?", 0, "")
if cluster != "" {
session = session.Where("cluster = ?", cluster)
diff --git a/src/models/board.go b/src/models/board.go
new file mode 100644
index 00000000..397e844b
--- /dev/null
+++ b/src/models/board.go
@@ -0,0 +1,125 @@
+package models
+
+import (
+ "strings"
+ "time"
+
+ "github.com/pkg/errors"
+ "github.com/toolkits/pkg/str"
+ "gorm.io/gorm"
+)
+
+type Board struct {
+ Id int64 `json:"id" gorm:"primaryKey"`
+ GroupId int64 `json:"group_id"`
+ Name string `json:"name"`
+ Tags string `json:"tags"`
+ CreateAt int64 `json:"create_at"`
+ CreateBy string `json:"create_by"`
+ UpdateAt int64 `json:"update_at"`
+ UpdateBy string `json:"update_by"`
+ Configs string `json:"configs" gorm:"-"`
+}
+
+func (b *Board) TableName() string {
+ return "board"
+}
+
+func (b *Board) Verify() error {
+ if b.Name == "" {
+ return errors.New("Name is blank")
+ }
+
+ if str.Dangerous(b.Name) {
+ return errors.New("Name has invalid characters")
+ }
+
+ return nil
+}
+
+func (b *Board) Add() error {
+ if err := b.Verify(); err != nil {
+ return err
+ }
+
+ now := time.Now().Unix()
+ b.CreateAt = now
+ b.UpdateAt = now
+
+ return Insert(b)
+}
+
+func (b *Board) Update(selectField interface{}, selectFields ...interface{}) error {
+ if err := b.Verify(); err != nil {
+ return err
+ }
+
+ return DB().Model(b).Select(selectField, selectFields...).Updates(b).Error
+}
+
+func (b *Board) Del() error {
+ return DB().Transaction(func(tx *gorm.DB) error {
+ if err := tx.Where("id=?", b.Id).Delete(&BoardPayload{}).Error; err != nil {
+ return err
+ }
+
+ if err := tx.Where("id=?", b.Id).Delete(&Board{}).Error; err != nil {
+ return err
+ }
+
+ return nil
+ })
+}
+
+// BoardGet for detail page
+func BoardGet(where string, args ...interface{}) (*Board, error) {
+ var lst []*Board
+ err := DB().Where(where, args...).Find(&lst).Error
+ if err != nil {
+ return nil, err
+ }
+
+ if len(lst) == 0 {
+ return nil, nil
+ }
+
+ payload, err := BoardPayloadGet(lst[0].Id)
+ if err != nil {
+ return nil, err
+ }
+
+ lst[0].Configs = payload
+
+ return lst[0], nil
+}
+
+func BoardCount(where string, args ...interface{}) (num int64, err error) {
+ return Count(DB().Model(&Board{}).Where(where, args...))
+}
+
+func BoardExists(where string, args ...interface{}) (bool, error) {
+ num, err := BoardCount(where, args...)
+ return num > 0, err
+}
+
+// BoardGets for list page
+func BoardGets(groupId int64, query string) ([]Board, error) {
+ session := DB().Where("group_id=?", groupId).Order("name")
+
+ arr := strings.Fields(query)
+ if len(arr) > 0 {
+ for i := 0; i < len(arr); i++ {
+ if strings.HasPrefix(arr[i], "-") {
+ q := "%" + arr[i][1:] + "%"
+ session = session.Where("name not like ? and tags not like ?", q, q)
+ } else {
+ q := "%" + arr[i] + "%"
+ session = session.Where("(name like ? or tags like ?)", q, q)
+ }
+ }
+ }
+
+ var objs []Board
+ err := session.Find(&objs).Error
+ return objs, err
+}
diff --git a/src/models/board_payload.go b/src/models/board_payload.go
new file mode 100644
index 00000000..a988227c
--- /dev/null
+++ b/src/models/board_payload.go
@@ -0,0 +1,58 @@
+package models
+
+import "errors"
+
+type BoardPayload struct {
+ Id int64 `json:"id" gorm:"primaryKey"`
+ Payload string `json:"payload"`
+}
+
+func (p *BoardPayload) TableName() string {
+ return "board_payload"
+}
+
+func (p *BoardPayload) Update(selectField interface{}, selectFields ...interface{}) error {
+ return DB().Model(p).Select(selectField, selectFields...).Updates(p).Error
+}
+
+func BoardPayloadGets(ids []int64) ([]*BoardPayload, error) {
+ if len(ids) == 0 {
+ return nil, errors.New("empty ids")
+ }
+
+ var arr []*BoardPayload
+ err := DB().Where("id in ?", ids).Find(&arr).Error
+ return arr, err
+}
+
+func BoardPayloadGet(id int64) (string, error) {
+ payloads, err := BoardPayloadGets([]int64{id})
+ if err != nil {
+ return "", err
+ }
+
+ if len(payloads) == 0 {
+ return "", nil
+ }
+
+ return payloads[0].Payload, nil
+}
+
+func BoardPayloadSave(id int64, payload string) error {
+ var bp BoardPayload
+ err := DB().Where("id = ?", id).Find(&bp).Error
+ if err != nil {
+ return err
+ }
+
+ if bp.Id > 0 {
+ // already exists
+ bp.Payload = payload
+ return bp.Update("payload")
+ }
+
+ return Insert(&BoardPayload{
+ Id: id,
+ Payload: payload,
+ })
+}
diff --git a/src/models/dashboard.go b/src/models/dashboard.go
index 45f47874..6d083b9b 100644
--- a/src/models/dashboard.go
+++ b/src/models/dashboard.go
@@ -160,3 +160,9 @@ func DashboardGetsByIds(ids []int64) ([]Dashboard, error) {
err := DB().Where("id in ?", ids).Order("name").Find(&lst).Error
return lst, err
}
+
+func DashboardGetAll() ([]Dashboard, error) {
+ var lst []Dashboard
+ err := DB().Find(&lst).Error
+ return lst, err
+}
diff --git a/src/pkg/ormx/ormx.go b/src/pkg/ormx/ormx.go
index 6eb403a5..814c00e7 100644
--- a/src/pkg/ormx/ormx.go
+++ b/src/pkg/ormx/ormx.go
@@ -1,18 +1,17 @@
package ormx
import (
+ "time"
"fmt"
"strings"
- "time"
-
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
-// Config GORM Config
-type Config struct {
+// DBConfig GORM DBConfig
+type DBConfig struct {
Debug bool
DBType string
DSN string
@@ -23,7 +22,7 @@ type Config struct {
}
// New Create gorm.DB instance
-func New(c Config) (*gorm.DB, error) {
+func New(c DBConfig) (*gorm.DB, error) {
var dialector gorm.Dialector
switch strings.ToLower(c.DBType) {
diff --git a/src/server/common/poster/post.go b/src/pkg/poster/post.go
similarity index 100%
rename from src/server/common/poster/post.go
rename to src/pkg/poster/post.go
diff --git a/src/pkg/tplx/tplx.go b/src/pkg/tplx/tplx.go
new file mode 100644
index 00000000..5f6a225c
--- /dev/null
+++ b/src/pkg/tplx/tplx.go
@@ -0,0 +1,25 @@
+package tplx
+
+import (
+ "html/template"
+ "time"
+)
+
+var TemplateFuncMap = template.FuncMap{
+ "unescaped": func(str string) interface{} { return template.HTML(str) },
+ "urlconvert": func(str string) interface{} { return template.URL(str) },
+ "timeformat": func(ts int64, pattern ...string) string {
+ defp := "2006-01-02 15:04:05"
+ if len(pattern) > 0 {
+ defp = pattern[0]
+ }
+ return time.Unix(ts, 0).Format(defp)
+ },
+ "timestamp": func(pattern ...string) string {
+ defp := "2006-01-02 15:04:05"
+ if len(pattern) > 0 {
+ defp = pattern[0]
+ }
+ return time.Now().Format(defp)
+ },
+}
diff --git a/src/pkg/version/version.go b/src/pkg/version/version.go
new file mode 100644
index 00000000..d0dc2a1f
--- /dev/null
+++ b/src/pkg/version/version.go
@@ -0,0 +1,4 @@
+package version
+
+// VERSION go build -ldflags "-X pkg.version.VERSION=x.x.x"
+var VERSION = "not specified"
diff --git a/src/server/common/conv/conv.go b/src/server/common/conv/conv.go
index 561eb51f..717cbccc 100644
--- a/src/server/common/conv/conv.go
+++ b/src/server/common/conv/conv.go
@@ -14,6 +14,10 @@ type Vector struct {
}
func ConvertVectors(value model.Value) (lst []Vector) {
+ if value == nil {
+ return
+ }
+
switch value.Type() {
case model.ValVector:
items, ok := value.(model.Vector)
diff --git a/src/server/common/sender/dingtalk.go b/src/server/common/sender/dingtalk.go
index e845e2d5..178e62ce 100644
--- a/src/server/common/sender/dingtalk.go
+++ b/src/server/common/sender/dingtalk.go
@@ -5,7 +5,7 @@ import (
"strings"
"time"
- "github.com/didi/nightingale/v5/src/server/common/poster"
+ "github.com/didi/nightingale/v5/src/pkg/poster"
"github.com/toolkits/pkg/logger"
)
diff --git a/src/server/common/sender/feishu.go b/src/server/common/sender/feishu.go
index e78d9eaf..829a06b4 100644
--- a/src/server/common/sender/feishu.go
+++ b/src/server/common/sender/feishu.go
@@ -3,7 +3,7 @@ package sender
import (
"time"
- "github.com/didi/nightingale/v5/src/server/common/poster"
+ "github.com/didi/nightingale/v5/src/pkg/poster"
"github.com/toolkits/pkg/logger"
)
diff --git a/src/server/common/sender/wecom.go b/src/server/common/sender/wecom.go
index 5671fd9a..1634247e 100644
--- a/src/server/common/sender/wecom.go
+++ b/src/server/common/sender/wecom.go
@@ -3,7 +3,7 @@ package sender
import (
"time"
- "github.com/didi/nightingale/v5/src/server/common/poster"
+ "github.com/didi/nightingale/v5/src/pkg/poster"
"github.com/toolkits/pkg/logger"
)
diff --git a/src/server/config/config.go b/src/server/config/config.go
index ae6fe16c..19c84075 100644
--- a/src/server/config/config.go
+++ b/src/server/config/config.go
@@ -13,6 +13,7 @@ import (
"github.com/didi/nightingale/v5/src/pkg/httpx"
"github.com/didi/nightingale/v5/src/pkg/logx"
+ "github.com/didi/nightingale/v5/src/pkg/ormx"
"github.com/didi/nightingale/v5/src/server/reader"
"github.com/didi/nightingale/v5/src/server/writer"
"github.com/didi/nightingale/v5/src/storage"
@@ -122,6 +123,7 @@ type Config struct {
RunMode string
ClusterName string
BusiGroupLabelKey string
+ AnomalyDataApi []string
EngineDelay int64
DisableUsageReport bool
Log logx.Config
@@ -132,9 +134,7 @@ type Config struct {
Alerting Alerting
NoData NoData
Redis storage.RedisConfig
- Gorm storage.Gorm
- MySQL storage.MySQL
- Postgres storage.Postgres
+ DB ormx.DBConfig
WriterOpt writer.GlobalOpt
Writers []writer.Options
Reader reader.Options
diff --git a/src/server/engine/callback.go b/src/server/engine/callback.go
index 2c21a3d6..b5578beb 100644
--- a/src/server/engine/callback.go
+++ b/src/server/engine/callback.go
@@ -9,7 +9,7 @@ import (
"github.com/didi/nightingale/v5/src/models"
"github.com/didi/nightingale/v5/src/pkg/ibex"
- "github.com/didi/nightingale/v5/src/server/common/poster"
+ "github.com/didi/nightingale/v5/src/pkg/poster"
"github.com/didi/nightingale/v5/src/server/config"
"github.com/didi/nightingale/v5/src/server/memsto"
)
diff --git a/src/server/engine/consume.go b/src/server/engine/consume.go
index c34bb421..273c8d84 100644
--- a/src/server/engine/consume.go
+++ b/src/server/engine/consume.go
@@ -2,6 +2,7 @@ package engine
import (
"context"
+ "fmt"
"strconv"
"time"
@@ -42,7 +43,12 @@ func consume(events []interface{}, sema *semaphore.Semaphore) {
}
func consumeOne(event *models.AlertCurEvent) {
- logEvent(event, "consume")
+ LogEvent(event, "consume")
+
+ if err := event.ParseRuleNote(); err != nil {
+ event.RuleNote = fmt.Sprintf("failed to parse rule note: %v", err)
+ }
+
persist(event)
if event.IsRecovered && event.NotifyRecovered == 0 {
diff --git a/src/server/engine/logger.go b/src/server/engine/logger.go
index 9ee48936..f72d6eb6 100644
--- a/src/server/engine/logger.go
+++ b/src/server/engine/logger.go
@@ -5,7 +5,7 @@ import (
"github.com/toolkits/pkg/logger"
)
-func logEvent(event *models.AlertCurEvent, location string, err ...error) {
+func LogEvent(event *models.AlertCurEvent, location string, err ...error) {
status := "triggered"
if event.IsRecovered {
status = "recovered"
diff --git a/src/server/engine/notify.go b/src/server/engine/notify.go
index 8a4a2935..359504e3 100644
--- a/src/server/engine/notify.go
+++ b/src/server/engine/notify.go
@@ -23,6 +23,7 @@ import (
"github.com/didi/nightingale/v5/src/models"
"github.com/didi/nightingale/v5/src/pkg/sys"
+ "github.com/didi/nightingale/v5/src/pkg/tplx"
"github.com/didi/nightingale/v5/src/server/common/sender"
"github.com/didi/nightingale/v5/src/server/config"
"github.com/didi/nightingale/v5/src/server/memsto"
@@ -31,25 +32,6 @@ import (
var tpls = make(map[string]*template.Template)
-var fns = template.FuncMap{
- "unescaped": func(str string) interface{} { return template.HTML(str) },
- "urlconvert": func(str string) interface{} { return template.URL(str) },
- "timeformat": func(ts int64, pattern ...string) string {
- defp := "2006-01-02 15:04:05"
- if len(pattern) > 0 {
- defp = pattern[0]
- }
- return time.Unix(ts, 0).Format(defp)
- },
- "timestamp": func(pattern ...string) string {
- defp := "2006-01-02 15:04:05"
- if len(pattern) > 0 {
- defp = pattern[0]
- }
- return time.Now().Format(defp)
- },
-}
-
func initTpls() error {
if config.C.Alerting.TemplatesDir == "" {
config.C.Alerting.TemplatesDir = path.Join(runner.Cwd, "etc", "template")
@@ -78,7 +60,7 @@ func initTpls() error {
for i := 0; i < len(tplFiles); i++ {
tplpath := path.Join(config.C.Alerting.TemplatesDir, tplFiles[i])
- tpl, err := template.New(tplFiles[i]).Funcs(fns).ParseFiles(tplpath)
+ tpl, err := template.New(tplFiles[i]).Funcs(tplx.TemplateFuncMap).ParseFiles(tplpath)
if err != nil {
return errors.WithMessage(err, "failed to parse tpl: "+tplpath)
}
@@ -249,7 +231,7 @@ func handleNotice(notice Notice, bs []byte) {
}
func notify(event *models.AlertCurEvent) {
- logEvent(event, "notify")
+ LogEvent(event, "notify")
notice := genNotice(event)
stdinBytes, err := json.Marshal(notice)
@@ -355,7 +337,7 @@ func handleSubscribe(event models.AlertCurEvent, sub *models.AlertSubscribe) {
return
}
- logEvent(&event, "subscribe")
+ LogEvent(&event, "subscribe")
fillUsers(&event)
diff --git a/src/server/engine/worker.go b/src/server/engine/worker.go
index 66bfe2ef..84e2be2e 100644
--- a/src/server/engine/worker.go
+++ b/src/server/engine/worker.go
@@ -3,11 +3,14 @@ package engine
import (
"context"
"fmt"
+ "math/rand"
"sort"
"strings"
"time"
+ "github.com/prometheus/common/model"
"github.com/toolkits/pkg/logger"
+ "github.com/toolkits/pkg/net/httplib"
"github.com/toolkits/pkg/str"
"github.com/didi/nightingale/v5/src/models"
@@ -89,6 +92,11 @@ func (r RuleEval) Start() {
}
}
+type AnomalyPoint struct {
+ Data model.Matrix `json:"data"`
+ Err string `json:"error"`
+}
+
func (r RuleEval) Work() {
promql := strings.TrimSpace(r.rule.PromQl)
if promql == "" {
@@ -96,15 +104,37 @@ func (r RuleEval) Work() {
return
}
- value, warnings, err := reader.Reader.Client.Query(context.Background(), promql, time.Now())
- if err != nil {
- logger.Errorf("rule_eval:%d promql:%s, error:%v", r.RuleID(), promql, err)
- return
- }
+ var value model.Value
+ var err error
+ if r.rule.Algorithm == "" {
+ var warnings reader.Warnings
+ value, warnings, err = reader.Reader.Client.Query(context.Background(), promql, time.Now())
+ if err != nil {
+ logger.Errorf("rule_eval:%d promql:%s, error:%v", r.RuleID(), promql, err)
+ return
+ }
- if len(warnings) > 0 {
- logger.Errorf("rule_eval:%d promql:%s, warnings:%v", r.RuleID(), promql, warnings)
- return
+ if len(warnings) > 0 {
+ logger.Errorf("rule_eval:%d promql:%s, warnings:%v", r.RuleID(), promql, warnings)
+ return
+ }
+ } else {
+ var res AnomalyPoint
+ count := len(config.C.AnomalyDataApi)
+ for _, i := range rand.Perm(count) {
+ url := fmt.Sprintf("%s?rid=%d", config.C.AnomalyDataApi[i], r.rule.Id)
+ err = httplib.Get(url).SetTimeout(time.Duration(3000) * time.Millisecond).ToJSON(&res)
+ if err != nil {
+ logger.Errorf("curl %s fail: %v", url, err)
+ continue
+ }
+ if res.Err != "" {
+ logger.Errorf("curl %s fail: %s", url, res.Err)
+ continue
+ }
+ value = res.Data
+ logger.Debugf("curl %s get: %+v", url, res.Data)
+ }
}
r.judge(conv.ConvertVectors(value))
@@ -250,6 +280,8 @@ func (r RuleEval) judge(vectors []conv.Vector) {
event.RuleId = r.rule.Id
event.RuleName = r.rule.Name
event.RuleNote = r.rule.Note
+ event.RuleProd = r.rule.Prod
+ event.RuleAlgo = r.rule.Algorithm
event.Severity = r.rule.Severity
event.PromForDuration = r.rule.PromForDuration
event.PromQl = r.rule.PromQl
@@ -364,6 +396,8 @@ func (r RuleEval) recoverRule(alertingKeys map[string]struct{}, now int64) {
// 当然,其实rule的各个字段都可能发生变化了,都更新一下吧
event.RuleName = r.rule.Name
event.RuleNote = r.rule.Note
+ event.RuleProd = r.rule.Prod
+ event.RuleAlgo = r.rule.Algorithm
event.Severity = r.rule.Severity
event.PromForDuration = r.rule.PromForDuration
event.PromQl = r.rule.PromQl
@@ -387,7 +421,7 @@ func (r RuleEval) pushEventToQueue(event *models.AlertCurEvent) {
}
promstat.CounterAlertsTotal.WithLabelValues(config.C.ClusterName).Inc()
- logEvent(event, "push_queue")
+ LogEvent(event, "push_queue")
if !EventQueue.PushFront(event) {
logger.Warningf("event_push_queue: queue is full")
}
diff --git a/src/server/router/router.go b/src/server/router/router.go
index 41937579..8c7b40c3 100644
--- a/src/server/router/router.go
+++ b/src/server/router/router.go
@@ -91,4 +91,7 @@ func configRoute(r *gin.Engine, version string) {
r.GET("/memory/user-group", userGroupGet)
r.GET("/metrics", gin.WrapH(promhttp.Handler()))
+
+ service := r.Group("/v1/n9e")
+ service.POST("/event", pushEventToQueue)
}
diff --git a/src/server/router/router_event.go b/src/server/router/router_event.go
new file mode 100644
index 00000000..e7da20ce
--- /dev/null
+++ b/src/server/router/router_event.go
@@ -0,0 +1,31 @@
+package router
+
+import (
+ "fmt"
+
+ "github.com/didi/nightingale/v5/src/models"
+ "github.com/didi/nightingale/v5/src/server/config"
+ "github.com/didi/nightingale/v5/src/server/engine"
+ promstat "github.com/didi/nightingale/v5/src/server/stat"
+
+ "github.com/gin-gonic/gin"
+ "github.com/toolkits/pkg/ginx"
+ "github.com/toolkits/pkg/logger"
+)
+
+func pushEventToQueue(c *gin.Context) {
+ var event models.AlertCurEvent
+ ginx.BindJSON(c, &event)
+ if event.RuleId == 0 {
+ ginx.Bomb(200, "event is illegal")
+ }
+
+ promstat.CounterAlertsTotal.WithLabelValues(config.C.ClusterName).Inc()
+ engine.LogEvent(&event, "http_push_queue")
+ if !engine.EventQueue.PushFront(event) {
+ msg := fmt.Sprintf("event:%+v push_queue err: queue is full", event)
+ ginx.Bomb(200, msg)
+ logger.Warningf(msg)
+ }
+ ginx.NewRender(c).Message(nil)
+}
diff --git a/src/server/server.go b/src/server/server.go
index c5ca15ad..7c9ddf83 100644
--- a/src/server/server.go
+++ b/src/server/server.go
@@ -105,11 +105,7 @@ func (s Server) initialize() (func(), error) {
}
// init database
- if err = storage.InitDB(storage.DBConfig{
- Gorm: config.C.Gorm,
- MySQL: config.C.MySQL,
- Postgres: config.C.Postgres,
- }); err != nil {
+ if err = storage.InitDB(config.C.DB); err != nil {
return fns.Ret(), err
}
diff --git a/src/server/usage/usage.go b/src/server/usage/usage.go
index 2ce8ab75..f2b0055a 100644
--- a/src/server/usage/usage.go
+++ b/src/server/usage/usage.go
@@ -10,6 +10,8 @@ import (
"os"
"time"
+ "github.com/didi/nightingale/v5/src/models"
+ "github.com/didi/nightingale/v5/src/pkg/version"
"github.com/didi/nightingale/v5/src/server/common/conv"
"github.com/didi/nightingale/v5/src/server/reader"
)
@@ -21,8 +23,10 @@ const (
type Usage struct {
Samples float64 `json:"samples"` // per second
+ Users float64 `json:"users"` // user total
Maintainer string `json:"maintainer"`
Hostname string `json:"hostname"`
+ Version string `json:"version"`
}
func getSamples() (float64, error) {
@@ -61,12 +65,19 @@ func report() {
return
}
+ num, err := models.UserTotal("")
+ if err != nil {
+ return
+ }
+
maintainer := "blank"
u := Usage{
Samples: sps,
+ Users: float64(num),
Hostname: hostname,
Maintainer: maintainer,
+ Version: version.VERSION,
}
post(u)
diff --git a/src/storage/storage.go b/src/storage/storage.go
index c9f0c960..99179cac 100644
--- a/src/storage/storage.go
+++ b/src/storage/storage.go
@@ -2,14 +2,10 @@ package storage
import (
"context"
- "errors"
"fmt"
"os"
- "strings"
-
"github.com/go-redis/redis/v8"
"gorm.io/gorm"
-
"github.com/didi/nightingale/v5/src/pkg/ormx"
"github.com/didi/nightingale/v5/src/pkg/tls"
)
@@ -23,85 +19,16 @@ type RedisConfig struct {
tls.ClientConfig
}
-type DBConfig struct {
- Gorm Gorm
- MySQL MySQL
- Postgres Postgres
-}
-
-type Gorm struct {
- Debug bool
- DBType string
- MaxLifetime int
- MaxOpenConns int
- MaxIdleConns int
- TablePrefix string
- EnableAutoMigrate bool
-}
-
-type MySQL struct {
- Address string
- User string
- Password string
- DBName string
- Parameters string
-}
-
-func (a MySQL) DSN() string {
- return fmt.Sprintf("%s:%s@tcp(%s)/%s?%s",
- a.User, a.Password, a.Address, a.DBName, a.Parameters)
-}
-
-type Postgres struct {
- Address string
- User string
- Password string
- DBName string
- SSLMode string
-}
-
-func (a Postgres) DSN() string {
- arr := strings.Split(a.Address, ":")
- if len(arr) != 2 {
- panic("pg address(" + a.Address + ") invalid")
- }
-
- return fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s sslmode=%s",
- arr[0], arr[1], a.User, a.DBName, a.Password, a.SSLMode)
-}
-
var DB *gorm.DB
-func InitDB(cfg DBConfig) error {
- db, err := newGormDB(cfg)
+func InitDB(cfg ormx.DBConfig) error {
+ db, err := ormx.New(cfg)
if err == nil {
DB = db
}
return err
}
-func newGormDB(cfg DBConfig) (*gorm.DB, error) {
- var dsn string
- switch cfg.Gorm.DBType {
- case "mysql":
- dsn = cfg.MySQL.DSN()
- case "postgres":
- dsn = cfg.Postgres.DSN()
- default:
- return nil, errors.New("unknown DBType")
- }
-
- return ormx.New(ormx.Config{
- Debug: cfg.Gorm.Debug,
- DBType: cfg.Gorm.DBType,
- DSN: dsn,
- MaxIdleConns: cfg.Gorm.MaxIdleConns,
- MaxLifetime: cfg.Gorm.MaxLifetime,
- MaxOpenConns: cfg.Gorm.MaxOpenConns,
- TablePrefix: cfg.Gorm.TablePrefix,
- })
-}
-
var Redis *redis.Client
func InitRedis(cfg RedisConfig) (func(), error) {
diff --git a/src/webapi/config/config.go b/src/webapi/config/config.go
index 4696f91d..d158a154 100644
--- a/src/webapi/config/config.go
+++ b/src/webapi/config/config.go
@@ -13,6 +13,7 @@ import (
"github.com/didi/nightingale/v5/src/pkg/ldapx"
"github.com/didi/nightingale/v5/src/pkg/logx"
"github.com/didi/nightingale/v5/src/pkg/oidcc"
+ "github.com/didi/nightingale/v5/src/pkg/ormx"
"github.com/didi/nightingale/v5/src/storage"
"github.com/didi/nightingale/v5/src/webapi/prom"
)
@@ -90,9 +91,7 @@ type Config struct {
AnonymousAccess AnonymousAccess
LDAP ldapx.LdapSection
Redis storage.RedisConfig
- Gorm storage.Gorm
- MySQL storage.MySQL
- Postgres storage.Postgres
+ DB ormx.DBConfig
Clusters []prom.Options
Ibex Ibex
OIDC oidcc.Config
diff --git a/src/webapi/router/router.go b/src/webapi/router/router.go
index 81b0e0b4..29ff4057 100644
--- a/src/webapi/router/router.go
+++ b/src/webapi/router/router.go
@@ -161,12 +161,30 @@ func configRoute(r *gin.Engine, version string) {
pages.GET("/targets", jwtAuth(), user(), targetGets)
pages.DELETE("/targets", jwtAuth(), user(), perm("/targets/del"), targetDel)
pages.GET("/targets/tags", jwtAuth(), user(), targetGetTags)
- pages.POST("/targets/tags", jwtAuth(), user(), perm("/targets/put"), targetBindTags)
- pages.DELETE("/targets/tags", jwtAuth(), user(), perm("/targets/put"), targetUnbindTags)
+ pages.POST("/targets/tags", jwtAuth(), user(), perm("/targets/put"), targetBindTagsByFE)
+ pages.DELETE("/targets/tags", jwtAuth(), user(), perm("/targets/put"), targetUnbindTagsByFE)
pages.PUT("/targets/note", jwtAuth(), user(), perm("/targets/put"), targetUpdateNote)
pages.PUT("/targets/bgid", jwtAuth(), user(), perm("/targets/put"), targetUpdateBgid)
- pages.GET("/dashboards/builtin/list", dashboardBuiltinList)
+ pages.GET("/builtin-boards", builtinBoardGets)
+ pages.GET("/builtin-board/:name", builtinBoardGet)
+
+ pages.GET("/busi-group/:id/boards", jwtAuth(), user(), perm("/dashboards"), bgro(), boardGets)
+ pages.POST("/busi-group/:id/boards", jwtAuth(), user(), perm("/dashboards/add"), bgrw(), boardAdd)
+ pages.POST("/busi-group/:id/board/:bid/clone", jwtAuth(), user(), perm("/dashboards/add"), bgrw(), boardClone)
+
+ pages.GET("/board/:bid", jwtAuth(), user(), boardGet)
+ pages.PUT("/board/:bid", jwtAuth(), user(), perm("/dashboards/put"), boardPut)
+ pages.PUT("/board/:bid/configs", jwtAuth(), user(), perm("/dashboards/put"), boardPutConfigs)
+ pages.DELETE("/boards", jwtAuth(), user(), perm("/dashboards/del"), boardDel)
+
+ // migrate v5.8.0
+ pages.GET("/dashboards", jwtAuth(), admin(), migrateDashboards)
+ pages.GET("/dashboard/:id", jwtAuth(), admin(), migrateDashboardGet)
+ pages.PUT("/dashboard/:id/migrate", jwtAuth(), admin(), migrateDashboard)
+
+ // deprecated ↓
+ pages.GET("/dashboards/builtin/list", builtinBoardGets)
pages.POST("/busi-group/:id/dashboards/builtin", jwtAuth(), user(), perm("/dashboards/add"), bgrw(), dashboardBuiltinImport)
pages.GET("/busi-group/:id/dashboards", jwtAuth(), user(), perm("/dashboards"), bgro(), dashboardGets)
pages.POST("/busi-group/:id/dashboards", jwtAuth(), user(), perm("/dashboards/add"), bgrw(), dashboardAdd)
@@ -186,6 +204,7 @@ func configRoute(r *gin.Engine, version string) {
pages.POST("/busi-group/:id/charts", jwtAuth(), user(), bgrw(), chartAdd)
pages.PUT("/busi-group/:id/charts", jwtAuth(), user(), bgrw(), chartPut)
pages.DELETE("/busi-group/:id/charts", jwtAuth(), user(), bgrw(), chartDel)
+ // deprecated ↑
pages.GET("/share-charts", chartShareGets)
pages.POST("/share-charts", jwtAuth(), chartShareAdd)
@@ -193,10 +212,10 @@ func configRoute(r *gin.Engine, version string) {
pages.GET("/alert-rules/builtin/list", alertRuleBuiltinList)
pages.POST("/busi-group/:id/alert-rules/builtin", jwtAuth(), user(), perm("/alert-rules/add"), bgrw(), alertRuleBuiltinImport)
pages.GET("/busi-group/:id/alert-rules", jwtAuth(), user(), perm("/alert-rules"), alertRuleGets)
- pages.POST("/busi-group/:id/alert-rules", jwtAuth(), user(), perm("/alert-rules/add"), bgrw(), alertRuleAdd)
+ pages.POST("/busi-group/:id/alert-rules", jwtAuth(), user(), perm("/alert-rules/add"), bgrw(), alertRuleAddByFE)
pages.DELETE("/busi-group/:id/alert-rules", jwtAuth(), user(), perm("/alert-rules/del"), bgrw(), alertRuleDel)
pages.PUT("/busi-group/:id/alert-rules/fields", jwtAuth(), user(), perm("/alert-rules/put"), bgrw(), alertRulePutFields)
- pages.PUT("/busi-group/:id/alert-rule/:arid", jwtAuth(), user(), perm("/alert-rules/put"), alertRulePut)
+ pages.PUT("/busi-group/:id/alert-rule/:arid", jwtAuth(), user(), perm("/alert-rules/put"), alertRulePutByFE)
pages.GET("/alert-rule/:arid", jwtAuth(), user(), perm("/alert-rules"), alertRuleGet)
pages.GET("/busi-group/:id/alert-mutes", jwtAuth(), user(), perm("/alert-mutes"), bgro(), alertMuteGets)
@@ -252,11 +271,16 @@ func configRoute(r *gin.Engine, version string) {
service.POST("/users", userAddPost)
service.GET("/targets", targetGets)
- service.DELETE("/targets", targetDel)
service.GET("/targets/tags", targetGetTags)
- service.POST("/targets/tags", targetBindTags)
- service.DELETE("/targets/tags", targetUnbindTags)
- service.PUT("/targets/note", targetUpdateNote)
- service.PUT("/targets/bgid", targetUpdateBgid)
+ service.POST("/targets/tags", targetBindTagsByService)
+ service.DELETE("/targets/tags", targetUnbindTagsByService)
+ service.PUT("/targets/note", targetUpdateNoteByService)
+
+ service.GET("/alert-rules", alertRuleGets)
+ service.POST("/alert-rules", alertRuleAddByService)
+ service.DELETE("/alert-rules", alertRuleDel)
+ service.PUT("/alert-rule", alertRulePutByService)
+ service.GET("/alert-rule/:arid", alertRuleGet)
+ service.GET("/alert-rules-get-by-prod", alertRulesGetByProds)
}
}
diff --git a/src/webapi/router/router_alert_rule.go b/src/webapi/router/router_alert_rule.go
index 644ef282..ce330083 100644
--- a/src/webapi/router/router_alert_rule.go
+++ b/src/webapi/router/router_alert_rule.go
@@ -2,6 +2,7 @@ package router
import (
"net/http"
+ "strings"
"time"
"github.com/gin-gonic/gin"
@@ -24,8 +25,24 @@ func alertRuleGets(c *gin.Context) {
ginx.NewRender(c).Data(ars, err)
}
+func alertRulesGetByProds(c *gin.Context) {
+ prods := ginx.QueryStr(c, "prods", "")
+ arr := strings.Split(prods, ",")
+
+ ars, err := models.AlertRulesGetByProds(arr)
+ if err == nil {
+ cache := make(map[int64]*models.UserGroup)
+ for i := 0; i < len(ars); i++ {
+ ars[i].FillNotifyGroups(cache)
+ }
+ }
+ ginx.NewRender(c).Data(ars, err)
+}
+
// single or import
-func alertRuleAdd(c *gin.Context) {
+func alertRuleAddByFE(c *gin.Context) {
+ username := c.MustGet("username").(string)
+
var lst []models.AlertRule
ginx.BindJSON(c, &lst)
@@ -34,26 +51,48 @@ func alertRuleAdd(c *gin.Context) {
ginx.Bomb(http.StatusBadRequest, "input json is empty")
}
- username := c.MustGet("username").(string)
bgid := ginx.UrlParamInt64(c, "id")
+ reterr := alertRuleAdd(lst, username, bgid, c.GetHeader("X-Language"))
+ ginx.NewRender(c).Data(reterr, nil)
+}
+
+func alertRuleAddByService(c *gin.Context) {
+ var lst []models.AlertRule
+ ginx.BindJSON(c, &lst)
+
+ count := len(lst)
+ if count == 0 {
+ ginx.Bomb(http.StatusBadRequest, "input json is empty")
+ }
+ reterr := alertRuleAdd(lst, "", 0, c.GetHeader("X-Language"))
+ ginx.NewRender(c).Data(reterr, nil)
+}
+
+func alertRuleAdd(lst []models.AlertRule, username string, bgid int64, lang string) map[string]string {
+ count := len(lst)
// alert rule name -> error string
reterr := make(map[string]string)
for i := 0; i < count; i++ {
lst[i].Id = 0
lst[i].GroupId = bgid
- lst[i].CreateBy = username
- lst[i].UpdateBy = username
- lst[i].FE2DB()
+ if username != "" {
+ lst[i].CreateBy = username
+ lst[i].UpdateBy = username
+ }
+
+ if err := lst[i].FE2DB(); err != nil {
+ reterr[lst[i].Name] = i18n.Sprintf(lang, err.Error())
+ continue
+ }
if err := lst[i].Add(); err != nil {
- reterr[lst[i].Name] = i18n.Sprintf(c.GetHeader("X-Language"), err.Error())
+ reterr[lst[i].Name] = i18n.Sprintf(lang, err.Error())
} else {
reterr[lst[i].Name] = ""
}
}
-
- ginx.NewRender(c).Data(reterr, nil)
+ return reterr
}
func alertRuleDel(c *gin.Context) {
@@ -65,7 +104,7 @@ func alertRuleDel(c *gin.Context) {
ginx.NewRender(c).Message(models.AlertRuleDels(f.Ids, ginx.UrlParamInt64(c, "id")))
}
-func alertRulePut(c *gin.Context) {
+func alertRulePutByFE(c *gin.Context) {
var f models.AlertRule
ginx.BindJSON(c, &f)
@@ -84,6 +123,21 @@ func alertRulePut(c *gin.Context) {
ginx.NewRender(c).Message(ar.Update(f))
}
+func alertRulePutByService(c *gin.Context) {
+ var f models.AlertRule
+ ginx.BindJSON(c, &f)
+
+ arid := ginx.UrlParamInt64(c, "arid")
+ ar, err := models.AlertRuleGetById(arid)
+ ginx.Dangerous(err)
+
+ if ar == nil {
+ ginx.NewRender(c, http.StatusNotFound).Message("No such AlertRule")
+ return
+ }
+ ginx.NewRender(c).Message(ar.Update(f))
+}
+
type alertRuleFieldForm struct {
Ids []int64 `json:"ids"`
Fields map[string]interface{} `json:"fields"`
diff --git a/src/webapi/router/router_board.go b/src/webapi/router/router_board.go
new file mode 100644
index 00000000..4f799d43
--- /dev/null
+++ b/src/webapi/router/router_board.go
@@ -0,0 +1,200 @@
+package router
+
+import (
+ "net/http"
+ "time"
+
+ "github.com/didi/nightingale/v5/src/models"
+ "github.com/gin-gonic/gin"
+ "github.com/toolkits/pkg/ginx"
+)
+
+type boardForm struct {
+ Name string `json:"name"`
+ Tags string `json:"tags"`
+ Configs string `json:"configs"`
+}
+
+func boardAdd(c *gin.Context) {
+ var f boardForm
+ ginx.BindJSON(c, &f)
+
+ me := c.MustGet("user").(*models.User)
+
+ board := &models.Board{
+ GroupId: ginx.UrlParamInt64(c, "id"),
+ Name: f.Name,
+ Tags: f.Tags,
+ Configs: f.Configs,
+ CreateBy: me.Username,
+ UpdateBy: me.Username,
+ }
+
+ err := board.Add()
+ ginx.Dangerous(err)
+
+ if f.Configs != "" {
+ ginx.Dangerous(models.BoardPayloadSave(board.Id, f.Configs))
+ }
+
+ ginx.NewRender(c).Data(board, nil)
+}
+
+func boardGet(c *gin.Context) {
+ board, err := models.BoardGet("id = ?", ginx.UrlParamInt64(c, "bid"))
+ ginx.Dangerous(err)
+
+ if board == nil {
+ ginx.Bomb(http.StatusNotFound, "No such dashboard")
+ }
+
+ ginx.NewRender(c).Data(board, nil)
+}
+
+// bgrwCheck
+func boardDel(c *gin.Context) {
+ var f idsForm
+ ginx.BindJSON(c, &f)
+ f.Verify()
+
+ for i := 0; i < len(f.Ids); i++ {
+ bid := f.Ids[i]
+
+ board, err := models.BoardGet("id = ?", bid)
+ ginx.Dangerous(err)
+
+ if board == nil {
+ continue
+ }
+
+ // check permission
+ bgrwCheck(c, board.GroupId)
+
+ ginx.Dangerous(board.Del())
+ }
+
+ ginx.NewRender(c).Message(nil)
+}
+
+func Board(id int64) *models.Board {
+ obj, err := models.BoardGet("id=?", id)
+ ginx.Dangerous(err)
+
+ if obj == nil {
+ ginx.Bomb(http.StatusNotFound, "No such dashboard")
+ }
+
+ return obj
+}
+
+// bgrwCheck
+func boardPut(c *gin.Context) {
+ var f boardForm
+ ginx.BindJSON(c, &f)
+
+ me := c.MustGet("user").(*models.User)
+ bo := Board(ginx.UrlParamInt64(c, "bid"))
+
+ // check permission
+ bgrwCheck(c, bo.GroupId)
+
+ bo.Name = f.Name
+ bo.Tags = f.Tags
+ bo.UpdateBy = me.Username
+ bo.UpdateAt = time.Now().Unix()
+
+ err := bo.Update("name", "tags", "update_by", "update_at")
+ ginx.NewRender(c).Data(bo, err)
+}
+
+// bgrwCheck
+func boardPutConfigs(c *gin.Context) {
+ var f boardForm
+ ginx.BindJSON(c, &f)
+
+ me := c.MustGet("user").(*models.User)
+ bo := Board(ginx.UrlParamInt64(c, "bid"))
+
+ // check permission
+ bgrwCheck(c, bo.GroupId)
+
+ bo.UpdateBy = me.Username
+ bo.UpdateAt = time.Now().Unix()
+ ginx.Dangerous(bo.Update("update_by", "update_at"))
+
+ bo.Configs = f.Configs
+ ginx.Dangerous(models.BoardPayloadSave(bo.Id, f.Configs))
+
+ ginx.NewRender(c).Data(bo, nil)
+}
+
+func boardGets(c *gin.Context) {
+ bgid := ginx.UrlParamInt64(c, "id")
+ query := ginx.QueryStr(c, "query", "")
+
+ boards, err := models.BoardGets(bgid, query)
+ ginx.NewRender(c).Data(boards, err)
+}
+
+func boardClone(c *gin.Context) {
+ me := c.MustGet("user").(*models.User)
+ bo := Board(ginx.UrlParamInt64(c, "bid"))
+
+ newBoard := &models.Board{
+ Name: bo.Name + " Copy",
+ Tags: bo.Tags,
+ GroupId: bo.GroupId,
+ CreateBy: me.Username,
+ UpdateBy: me.Username,
+ }
+
+ ginx.Dangerous(newBoard.Add())
+
+ // clone payload
+ payload, err := models.BoardPayloadGet(bo.Id)
+ ginx.Dangerous(err)
+
+ if payload != "" {
+ ginx.Dangerous(models.BoardPayloadSave(newBoard.Id, payload))
+ }
+
+ ginx.NewRender(c).Message(nil)
+}
+
+// ---- migrate ----
+
+func migrateDashboards(c *gin.Context) {
+ lst, err := models.DashboardGetAll()
+ ginx.NewRender(c).Data(lst, err)
+}
+
+func migrateDashboardGet(c *gin.Context) {
+ dash := Dashboard(ginx.UrlParamInt64(c, "id"))
+ ginx.NewRender(c).Data(dash, nil)
+}
+
+func migrateDashboard(c *gin.Context) {
+ dash := Dashboard(ginx.UrlParamInt64(c, "id"))
+
+ var f boardForm
+ ginx.BindJSON(c, &f)
+
+ me := c.MustGet("user").(*models.User)
+
+ board := &models.Board{
+ GroupId: dash.GroupId,
+ Name: f.Name,
+ Tags: f.Tags,
+ Configs: f.Configs,
+ CreateBy: me.Username,
+ UpdateBy: me.Username,
+ }
+
+ ginx.Dangerous(board.Add())
+
+ if board.Configs != "" {
+ ginx.Dangerous(models.BoardPayloadSave(board.Id, board.Configs))
+ }
+
+ ginx.NewRender(c).Message(dash.Del())
+}
diff --git a/src/webapi/router/router_builtin.go b/src/webapi/router/router_builtin.go
index 73b627a3..578c2389 100644
--- a/src/webapi/router/router_builtin.go
+++ b/src/webapi/router/router_builtin.go
@@ -75,7 +75,11 @@ func alertRuleBuiltinImport(c *gin.Context) {
lst[i].GroupId = bgid
lst[i].CreateBy = username
lst[i].UpdateBy = username
- lst[i].FE2DB()
+
+ if err := lst[i].FE2DB(); err != nil {
+ reterr[lst[i].Name] = i18n.Sprintf(c.GetHeader("X-Language"), err.Error())
+ continue
+ }
if err := lst[i].Add(); err != nil {
reterr[lst[i].Name] = i18n.Sprintf(c.GetHeader("X-Language"), err.Error())
@@ -87,7 +91,7 @@ func alertRuleBuiltinImport(c *gin.Context) {
ginx.NewRender(c).Data(reterr, nil)
}
-func dashboardBuiltinList(c *gin.Context) {
+func builtinBoardGets(c *gin.Context) {
fp := config.C.BuiltinDashboardsDir
if fp == "" {
fp = path.Join(runner.Cwd, "etc", "dashboards")
@@ -110,6 +114,25 @@ func dashboardBuiltinList(c *gin.Context) {
ginx.NewRender(c).Data(names, nil)
}
+// read the json file content
+func builtinBoardGet(c *gin.Context) {
+ name := ginx.UrlParamStr(c, "name")
+ dirpath := config.C.BuiltinDashboardsDir
+ if dirpath == "" {
+ dirpath = path.Join(runner.Cwd, "etc", "dashboards")
+ }
+
+ jsonfile := path.Join(dirpath, name+".json")
+ if !file.IsExist(jsonfile) {
+ ginx.Bomb(http.StatusBadRequest, "%s not found", jsonfile)
+ }
+
+ body, err := file.ReadString(jsonfile)
+ ginx.NewRender(c).Data(body, err)
+}
+
+// deprecated ↓
+
type dashboardBuiltinImportForm struct {
Name string `json:"name" binding:"required"`
}
diff --git a/src/webapi/router/router_mw.go b/src/webapi/router/router_mw.go
index 2b6193ed..6b1e5e46 100644
--- a/src/webapi/router/router_mw.go
+++ b/src/webapi/router/router_mw.go
@@ -153,6 +153,20 @@ func bgrwChecks(c *gin.Context, bgids []int64) {
}
}
+func bgroCheck(c *gin.Context, bgid int64) {
+ me := c.MustGet("user").(*models.User)
+ bg := BusiGroup(bgid)
+
+ can, err := me.CanDoBusiGroup(bg, "ro")
+ ginx.Dangerous(err)
+
+ if !can {
+ ginx.Bomb(http.StatusForbidden, "forbidden")
+ }
+
+ c.Set("busi_group", bg)
+}
+
func perm(operation string) gin.HandlerFunc {
return func(c *gin.Context) {
me := c.MustGet("user").(*models.User)
diff --git a/src/webapi/router/router_target.go b/src/webapi/router/router_target.go
index 4c322964..5af9e81c 100644
--- a/src/webapi/router/router_target.go
+++ b/src/webapi/router/router_target.go
@@ -1,6 +1,7 @@
package router
import (
+ "fmt"
"net/http"
"strings"
@@ -48,7 +49,11 @@ type targetTagsForm struct {
Tags []string `json:"tags" binding:"required"`
}
-func targetBindTags(c *gin.Context) {
+func (t targetTagsForm) Verify() {
+
+}
+
+func targetBindTagsByFE(c *gin.Context) {
var f targetTagsForm
ginx.BindJSON(c, &f)
@@ -58,33 +63,49 @@ func targetBindTags(c *gin.Context) {
checkTargetPerm(c, f.Idents)
- // verify
+ ginx.NewRender(c).Message(targetBindTags(f))
+}
+
+func targetBindTagsByService(c *gin.Context) {
+ var f targetTagsForm
+ ginx.BindJSON(c, &f)
+
+ if len(f.Idents) == 0 {
+ ginx.Bomb(http.StatusBadRequest, "idents empty")
+ }
+
+ ginx.NewRender(c).Message(targetBindTags(f))
+}
+
+func targetBindTags(f targetTagsForm) error {
for i := 0; i < len(f.Tags); i++ {
arr := strings.Split(f.Tags[i], "=")
if len(arr) != 2 {
- ginx.Bomb(200, "invalid tag(%s)", f.Tags[i])
+ return fmt.Errorf("invalid tag(%s)", f.Tags[i])
}
if strings.TrimSpace(arr[0]) == "" || strings.TrimSpace(arr[1]) == "" {
- ginx.Bomb(200, "invalid tag(%s)", f.Tags[i])
+ return fmt.Errorf("invalid tag(%s)", f.Tags[i])
}
if strings.IndexByte(arr[0], '.') != -1 {
- ginx.Bomb(200, "invalid tagkey(%s): cannot contains .", arr[0])
+ return fmt.Errorf("invalid tagkey(%s): cannot contains . ", arr[0])
}
if strings.IndexByte(arr[0], '-') != -1 {
- ginx.Bomb(200, "invalid tagkey(%s): cannot contains -", arr[0])
+ return fmt.Errorf("invalid tagkey(%s): cannot contains -", arr[0])
}
if !model.LabelNameRE.MatchString(arr[0]) {
- ginx.Bomb(200, "invalid tagkey(%s)", arr[0])
+ return fmt.Errorf("invalid tagkey(%s)", arr[0])
}
}
for i := 0; i < len(f.Idents); i++ {
target, err := models.TargetGetByIdent(f.Idents[i])
- ginx.Dangerous(err)
+ if err != nil {
+ return err
+ }
if target == nil {
continue
@@ -95,18 +116,19 @@ func targetBindTags(c *gin.Context) {
tagkey := strings.Split(f.Tags[j], "=")[0]
tagkeyPrefix := tagkey + "="
if strings.HasPrefix(target.Tags, tagkeyPrefix) {
- ginx.NewRender(c).Message("duplicate tagkey(%s)", tagkey)
- return
+ return fmt.Errorf("duplicate tagkey(%s)", tagkey)
}
}
- ginx.Dangerous(target.AddTags(f.Tags))
+ err = target.AddTags(f.Tags)
+ if err != nil {
+ return err
+ }
}
-
- ginx.NewRender(c).Message(nil)
+ return nil
}
-func targetUnbindTags(c *gin.Context) {
+func targetUnbindTagsByFE(c *gin.Context) {
var f targetTagsForm
ginx.BindJSON(c, &f)
@@ -116,18 +138,37 @@ func targetUnbindTags(c *gin.Context) {
checkTargetPerm(c, f.Idents)
+ ginx.NewRender(c).Message(targetUnbindTags(f))
+}
+
+func targetUnbindTagsByService(c *gin.Context) {
+ var f targetTagsForm
+ ginx.BindJSON(c, &f)
+
+ if len(f.Idents) == 0 {
+ ginx.Bomb(http.StatusBadRequest, "idents empty")
+ }
+
+ ginx.NewRender(c).Message(targetUnbindTags(f))
+}
+
+func targetUnbindTags(f targetTagsForm) error {
for i := 0; i < len(f.Idents); i++ {
target, err := models.TargetGetByIdent(f.Idents[i])
- ginx.Dangerous(err)
+ if err != nil {
+ return err
+ }
if target == nil {
continue
}
- ginx.Dangerous(target.DelTags(f.Tags))
+ err = target.DelTags(f.Tags)
+ if err != nil {
+ return err
+ }
}
-
- ginx.NewRender(c).Message(nil)
+ return nil
}
type targetNoteForm struct {
@@ -148,6 +189,17 @@ func targetUpdateNote(c *gin.Context) {
ginx.NewRender(c).Message(models.TargetUpdateNote(f.Idents, f.Note))
}
+func targetUpdateNoteByService(c *gin.Context) {
+ var f targetNoteForm
+ ginx.BindJSON(c, &f)
+
+ if len(f.Idents) == 0 {
+ ginx.Bomb(http.StatusBadRequest, "idents empty")
+ }
+
+ ginx.NewRender(c).Message(models.TargetUpdateNote(f.Idents, f.Note))
+}
+
type targetBgidForm struct {
Idents []string `json:"idents" binding:"required"`
Bgid int64 `json:"bgid"`
diff --git a/src/webapi/webapi.go b/src/webapi/webapi.go
index 46f401d3..4c84f549 100644
--- a/src/webapi/webapi.go
+++ b/src/webapi/webapi.go
@@ -101,11 +101,7 @@ func (a Webapi) initialize() (func(), error) {
}
// init database
- if err = storage.InitDB(storage.DBConfig{
- Gorm: config.C.Gorm,
- MySQL: config.C.MySQL,
- Postgres: config.C.Postgres,
- }); err != nil {
+ if err = storage.InitDB(config.C.DB); err != nil {
return nil, err
}