Compare commits

...

12 Commits

Author SHA1 Message Date
ning
cc709540cf feat: support webhook proxy configuration 2024-12-20 14:34:56 +08:00
ning
fdd3d14871 docs: change default db type to sqlite 2024-12-06 21:04:10 +08:00
Yening Qin
e890034c19 feat: auto init db (#2345)
Co-authored-by: CRISPpp <78430796+CRISPpp@users.noreply.github.com>
2024-12-06 20:32:17 +08:00
Yening Qin
3aaab9e6ad fix: event prom eval interval (#2343) 2024-12-06 20:24:49 +08:00
CRISPpp
7f7d707cfc fix: role_operation abnormal count (#2338) 2024-12-06 16:31:47 +08:00
Xu Bin
98402e9f8a fix: quotation mark for alert rule var (#2339) 2024-12-06 16:07:47 +08:00
Xu Bin
017094fd78 fix: var support for aggregate function (#2334) 2024-12-06 11:57:51 +08:00
Yening Qin
8b6b896362 feat: redis support miniredis type (#2337)
Co-authored-by: CRISPpp <78430796+CRISPpp@users.noreply.github.com>
2024-12-06 10:46:05 +08:00
ning
acaa00cfb6 refactor: migrate add more log 2024-12-05 17:55:27 +08:00
flashbo
87f3d8595d fix: targets filter logic (#2333) 2024-12-05 14:31:57 +08:00
flashbo
42791a374d feat: targets support sorting by time (#2331) 2024-12-05 14:20:30 +08:00
kongfei605
3855c25805 chore: update dashboards for mongodb (#2332) 2024-12-04 16:19:21 +08:00
20 changed files with 3030 additions and 261 deletions

1
.gitignore vendored
View File

@@ -9,6 +9,7 @@
*.o
*.a
*.so
*.db
*.sw[po]
*.tar.gz
*.[568vq]

View File

@@ -104,9 +104,17 @@ func NewAlertRuleWorker(rule *models.AlertRule, datasourceId int64, Processor *p
Processor.ScheduleEntry = arw.Scheduler.Entry(entryID)
Processor.PromEvalInterval = getPromEvalInterval(Processor.ScheduleEntry.Schedule)
return arw
}
func getPromEvalInterval(schedule cron.Schedule) int {
now := time.Now()
next1 := schedule.Next(now)
next2 := schedule.Next(next1)
return int(next2.Sub(next1).Seconds())
}
func (arw *AlertRuleWorker) Key() string {
return common.RuleKey(arw.DatasourceId, arw.Rule.Id)
}
@@ -130,7 +138,10 @@ func (arw *AlertRuleWorker) Start() {
}
func (arw *AlertRuleWorker) Eval() {
arw.Processor.EvalStart = time.Now().Unix()
if arw.Processor.PromEvalInterval == 0 {
arw.Processor.PromEvalInterval = getPromEvalInterval(arw.Processor.ScheduleEntry.Schedule)
}
cachedRule := arw.Rule
if cachedRule == nil {
// logger.Errorf("rule_eval:%s Rule not found", arw.Key())
@@ -240,10 +251,15 @@ func (arw *AlertRuleWorker) GetPromAnomalyPoint(ruleConfig string) ([]models.Ano
readerClient := arw.PromClients.GetCli(arw.DatasourceId)
if query.VarEnabled {
anomalyPoints := arw.VarFilling(query, readerClient)
for _, v := range anomalyPoints {
lst = append(lst, v)
var anomalyPoints []models.AnomalyPoint
if hasLabelLossAggregator(query) {
// 若有聚合函数则需要先填充变量然后查询,这个方式效率较低
anomalyPoints = arw.VarFillingBeforeQuery(query, readerClient)
} else {
// 先查询再过滤变量,效率较高,但无法处理有聚合函数的情况
anomalyPoints = arw.VarFillingAfterQuery(query, readerClient)
}
lst = append(lst, anomalyPoints...)
} else {
// 无变量
promql := strings.TrimSpace(query.PromQl)
@@ -297,17 +313,18 @@ type sample struct {
Timestamp model.Time
}
// VarFilling 填充变量
// VarFillingAfterQuery 填充变量,先查询再填充变量
// 公式: mem_used_percent{host="$host"} > $val 其中 $host 为参数变量,$val 为值变量
// 实现步骤:
// 广度优先遍历,保证同一参数变量的子筛选可以覆盖上一层筛选
// 每个节点先查询无参数的 query, 即 mem_used_percent > curVal, 得到满足值变量的所有结果
// 依次遍历参数配置节点,保证同一参数变量的子筛选可以覆盖上一层筛选
// 每个节点先查询无参数的 query, 即 mem_used_percent{} > curVal, 得到满足值变量的所有结果
// 结果中有满足本节点参数变量的值,加入异常点列表
// 参数变量的值不满足的组合,需要覆盖上层筛选中产生的异常点
func (arw *AlertRuleWorker) VarFilling(query models.PromQuery, readerClient promsdk.API) map[string]models.AnomalyPoint {
func (arw *AlertRuleWorker) VarFillingAfterQuery(query models.PromQuery, readerClient promsdk.API) []models.AnomalyPoint {
varToLabel := ExtractVarMapping(query.PromQl)
fullQuery := removeVal(query.PromQl)
// 存储所有的异常点key 为参数变量的组合,可以实现子筛选对上一层筛选的覆盖
anomalyPoints := make(map[string]models.AnomalyPoint)
anomalyPointsMap := make(map[string]models.AnomalyPoint)
// 统一变量配置格式
VarConfigForCalc := &models.ChildVarConfig{
ParamVal: make([]map[string]models.ParamQuery, 1),
@@ -369,12 +386,13 @@ func (arw *AlertRuleWorker) VarFilling(query models.PromQuery, readerClient prom
curRealQuery := realQuery
var cur []string
for _, paramKey := range ParamKeys {
val := string(seqVals[i].Metric[model.LabelName(paramKey)])
val := string(seqVals[i].Metric[model.LabelName(varToLabel[paramKey])])
cur = append(cur, val)
curRealQuery = strings.Replace(curRealQuery, fmt.Sprintf("$%s", paramKey), val, -1)
curRealQuery = fillVar(curRealQuery, paramKey, val)
}
if _, ok := paramPermutation[strings.Join(cur, "-")]; ok {
anomalyPoints[strings.Join(cur, "-")] = models.AnomalyPoint{
anomalyPointsMap[strings.Join(cur, "-")] = models.AnomalyPoint{
Key: seqVals[i].Metric.String(),
Timestamp: seqVals[i].Timestamp.Unix(),
Value: float64(seqVals[i].Value),
@@ -389,12 +407,16 @@ func (arw *AlertRuleWorker) VarFilling(query models.PromQuery, readerClient prom
// 剩余的参数组合为本层筛选不产生异常点的组合,需要覆盖上层筛选中产生的异常点
for k, _ := range paramPermutation {
delete(anomalyPoints, k)
delete(anomalyPointsMap, k)
}
}
curNode = curNode.ChildVarConfigs
}
anomalyPoints := make([]models.AnomalyPoint, 0)
for _, point := range anomalyPointsMap {
anomalyPoints = append(anomalyPoints, point)
}
return anomalyPoints
}
@@ -1198,3 +1220,188 @@ func GetQueryRefAndUnit(query interface{}) (string, string, error) {
json.Unmarshal(queryBytes, &queryMap)
return queryMap.Ref, queryMap.Unit, nil
}
// VarFillingBeforeQuery 填充变量,先填充变量再查询,针对有聚合函数的情况
// 公式: avg(mem_used_percent{host="$host"}) > $val 其中 $host 为参数变量,$val 为值变量
// 实现步骤:
// 依次遍历参数配置节点,保证同一参数变量的子筛选可以覆盖上一层筛选
// 每个节点先填充参数再进行查询, 即先得到完整的 promql avg(mem_used_percent{host="127.0.0.1"}) > 5
// 再查询得到满足值变量的所有结果加入异常点列表
// 参数变量的值不满足的组合,需要覆盖上层筛选中产生的异常点
func (arw *AlertRuleWorker) VarFillingBeforeQuery(query models.PromQuery, readerClient promsdk.API) []models.AnomalyPoint {
// 存储异常点的 mapkey 为参数变量的组合,可以实现子筛选对上一层筛选的覆盖
anomalyPointsMap := sync.Map{}
// 统一变量配置格式
VarConfigForCalc := &models.ChildVarConfig{
ParamVal: make([]map[string]models.ParamQuery, 1),
ChildVarConfigs: query.VarConfig.ChildVarConfigs,
}
VarConfigForCalc.ParamVal[0] = make(map[string]models.ParamQuery)
for _, p := range query.VarConfig.ParamVal {
VarConfigForCalc.ParamVal[0][p.Name] = models.ParamQuery{
ParamType: p.ParamType,
Query: p.Query,
}
}
// 使用一个统一的参数变量顺序
var ParamKeys []string
for val, valQuery := range VarConfigForCalc.ParamVal[0] {
if valQuery.ParamType == "threshold" {
continue
}
ParamKeys = append(ParamKeys, val)
}
sort.Slice(ParamKeys, func(i, j int) bool {
return ParamKeys[i] < ParamKeys[j]
})
// 遍历变量配置链表
curNode := VarConfigForCalc
for curNode != nil {
for _, param := range curNode.ParamVal {
curPromql := query.PromQl
// 取出阈值变量
valMap := make(map[string]string)
for val, valQuery := range param {
if valQuery.ParamType == "threshold" {
valMap[val] = getString(valQuery.Query)
}
}
// 替换阈值变量
for key, val := range valMap {
curPromql = strings.Replace(curPromql, fmt.Sprintf("$%s", key), val, -1)
}
// 得到参数变量的所有组合
paramPermutation, err := arw.getParamPermutation(param, ParamKeys)
if err != nil {
logger.Errorf("rule_eval:%s, paramPermutation error:%v", arw.Key(), err)
continue
}
keyToPromql := make(map[string]string)
for paramPermutationKeys, _ := range paramPermutation {
realPromql := curPromql
split := strings.Split(paramPermutationKeys, "-")
for j := range ParamKeys {
realPromql = fillVar(realPromql, ParamKeys[j], split[j])
}
keyToPromql[paramPermutationKeys] = realPromql
}
// 并发查询
wg := sync.WaitGroup{}
semaphore := make(chan struct{}, 200)
for key, promql := range keyToPromql {
wg.Add(1)
semaphore <- struct{}{}
go func(key, promql string) {
defer func() {
<-semaphore
wg.Done()
}()
value, _, err := readerClient.Query(context.Background(), promql, time.Now())
if err != nil {
logger.Errorf("rule_eval:%s, promql:%s, error:%v", arw.Key(), promql, err)
return
}
points := models.ConvertAnomalyPoints(value)
if len(points) == 0 {
anomalyPointsMap.Delete(key)
return
}
for i := 0; i < len(points); i++ {
points[i].Severity = query.Severity
points[i].Query = promql
points[i].ValuesUnit = map[string]unit.FormattedValue{
"v": unit.ValueFormatter(query.Unit, 2, points[i].Value),
}
}
anomalyPointsMap.Store(key, points)
}(key, promql)
}
wg.Wait()
}
curNode = curNode.ChildVarConfigs
}
anomalyPoints := make([]models.AnomalyPoint, 0)
anomalyPointsMap.Range(func(key, value any) bool {
if points, ok := value.([]models.AnomalyPoint); ok {
anomalyPoints = append(anomalyPoints, points...)
}
return true
})
return anomalyPoints
}
// 判断 query 中是否有会导致标签丢失的聚合函数
func hasLabelLossAggregator(query models.PromQuery) bool {
noLabelAggregators := []string{
"sum", "min", "max", "avg",
"stddev", "stdvar",
"count", "quantile",
"group",
}
promql := strings.ToLower(query.PromQl)
for _, fn := range noLabelAggregators {
// 检查是否包含这些聚合函数,需要确保函数名后面跟着左括号
if strings.Contains(promql, fn+"(") {
return true
}
}
return false
}
// ExtractVarMapping 从 promql 中提取变量映射关系,为了在 query 之后可以将标签正确的放回 promql
// 输入: sum(rate(mem_used_percent{host="$my_host"})) by (instance) + avg(node_load1{region="$region"}) > $val
// 输出: map[string]string{"my_host":"host", "region":"region"}
func ExtractVarMapping(promql string) map[string]string {
varMapping := make(map[string]string)
// 遍历所有花括号对
for {
start := strings.Index(promql, "{")
if start == -1 {
break
}
end := strings.Index(promql, "}")
if end == -1 {
break
}
// 提取标签键值对
labels := promql[start+1 : end]
pairs := strings.Split(labels, ",")
for _, pair := range pairs {
// 分割键值对
kv := strings.Split(pair, "=")
if len(kv) != 2 {
continue
}
key := strings.TrimSpace(kv[0])
value := strings.Trim(strings.TrimSpace(kv[1]), "\"")
value = strings.Trim(value, "'")
// 检查值是否为变量(以$开头)
if strings.HasPrefix(value, "$") {
varName := value[1:] // 去掉$前缀
varMapping[varName] = key
}
}
// 继续处理剩余部分
promql = promql[end+1:]
}
return varMapping
}
func fillVar(curRealQuery string, paramKey string, val string) string {
curRealQuery = strings.Replace(curRealQuery, fmt.Sprintf("'$%s'", paramKey), fmt.Sprintf("'%s'", val), -1)
curRealQuery = strings.Replace(curRealQuery, fmt.Sprintf("\"$%s\"", paramKey), fmt.Sprintf("\"%s\"", val), -1)
return curRealQuery
}

View File

@@ -340,7 +340,7 @@ func Test_removeVal(t *testing.T) {
{
name: "removeVal7",
args: args{
promql: "mem{test1=\"test1\",test2=\"test2\",test3=\"$test3\"} > $val",
promql: "mem{test1=\"test1\",test2=\"test2\",test3='$test3'} > $val",
},
want: "mem{test1=\"test1\",test2=\"test2\"} > $val",
},
@@ -361,16 +361,16 @@ func Test_removeVal(t *testing.T) {
{
name: "removeVal10",
args: args{
promql: "mem{test1=\"test1\",test2=\"$test2\"} > $val1 and mem{test3=\"test3\",test4=\"test4\"} > $val2",
promql: "mem{test1=\"test1\",test2='$test2'} > $val1 and mem{test3=\"test3\",test4=\"test4\"} > $val2",
},
want: "mem{test1=\"test1\"} > $val1 and mem{test3=\"test3\",test4=\"test4\"} > $val2",
},
{
name: "removeVal11",
args: args{
promql: "mem{test1=\"test1\",test2=\"test2\"} > $val1 and mem{test3=\"$test3\",test4=\"test4\"} > $val2",
promql: "mem{test1='test1',test2=\"test2\"} > $val1 and mem{test3=\"$test3\",test4=\"test4\"} > $val2",
},
want: "mem{test1=\"test1\",test2=\"test2\"} > $val1 and mem{test4=\"test4\"} > $val2",
want: "mem{test1='test1',test2=\"test2\"} > $val1 and mem{test4=\"test4\"} > $val2",
},
{
name: "removeVal12",
@@ -388,3 +388,71 @@ func Test_removeVal(t *testing.T) {
})
}
}
func TestExtractVarMapping(t *testing.T) {
tests := []struct {
name string
promql string
want map[string]string
}{
{
name: "单个花括号单个变量",
promql: `mem_used_percent{host="$my_host"} > $val`,
want: map[string]string{"my_host": "host"},
},
{
name: "单个花括号多个变量",
promql: `mem_used_percent{host="$my_host",region="$region",env="prod"} > $val`,
want: map[string]string{"my_host": "host", "region": "region"},
},
{
name: "多个花括号多个变量",
promql: `sum(rate(mem_used_percent{host="$my_host"})) by (instance) + avg(node_load1{region="$region"}) > $val`,
want: map[string]string{"my_host": "host", "region": "region"},
},
{
name: "相同变量出现多次",
promql: `sum(rate(mem_used_percent{host="$my_host"})) + avg(node_load1{host="$my_host"}) > $val`,
want: map[string]string{"my_host": "host"},
},
{
name: "没有变量",
promql: `mem_used_percent{host="localhost",region="cn"} > 80`,
want: map[string]string{},
},
{
name: "没有花括号",
promql: `80 > $val`,
want: map[string]string{},
},
{
name: "格式不规范的标签",
promql: `mem_used_percent{host=$my_host,region = $region} > $val`,
want: map[string]string{"my_host": "host", "region": "region"},
},
{
name: "空花括号",
promql: `mem_used_percent{} > $val`,
want: map[string]string{},
},
{
name: "不完整的花括号",
promql: `mem_used_percent{host="$my_host"`,
want: map[string]string{},
},
{
name: "复杂表达式",
promql: `sum(rate(http_requests_total{handler="$handler",code="$code"}[5m])) by (handler) / sum(rate(http_requests_total{handler="$handler"}[5m])) by (handler) * 100 > $threshold`,
want: map[string]string{"handler": "handler", "code": "code"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ExtractVarMapping(tt.promql)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ExtractVarMapping() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -80,8 +80,8 @@ type Processor struct {
HandleRecoverEventHook HandleEventFunc
EventMuteHook EventMuteHookFunc
ScheduleEntry cron.Entry
EvalStart int64
ScheduleEntry cron.Entry
PromEvalInterval int
}
func (p *Processor) Key() string {
@@ -424,6 +424,7 @@ func (p *Processor) handleEvent(events []*models.AlertCurEvent) {
p.pendingsUseByRecover.Set(event.Hash, event)
}
event.PromEvalInterval = p.PromEvalInterval
if p.rule.PromForDuration == 0 {
fireEvents = append(fireEvents, event)
if severity > event.Severity {
@@ -442,7 +443,6 @@ func (p *Processor) handleEvent(events []*models.AlertCurEvent) {
preTriggerTime = event.TriggerTime
}
event.PromEvalInterval = int(p.ScheduleEntry.Schedule.Next(time.Unix(p.EvalStart, 0)).Unix() - p.EvalStart)
if event.LastEvalTime-preTriggerTime+int64(event.PromEvalInterval) >= int64(p.rule.PromForDuration) {
fireEvents = append(fireEvents, event)
if severity > event.Severity {

View File

@@ -13,6 +13,7 @@ import (
"github.com/ccfos/nightingale/v6/alert/astats"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/pkg/poster"
"github.com/toolkits/pkg/logger"
)
@@ -59,11 +60,17 @@ func sendWebhook(webhook *models.Webhook, event interface{}, stats *astats.Stats
if webhook != nil {
insecureSkipVerify = webhook.SkipVerify
}
transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: insecureSkipVerify},
}
if poster.UseProxy(conf.Url) {
transport.Proxy = http.ProxyFromEnvironment
}
client := http.Client{
Timeout: time.Duration(conf.Timeout) * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: insecureSkipVerify},
},
Timeout: time.Duration(conf.Timeout) * time.Second,
Transport: transport,
}
stats.AlertNotifyTotal.WithLabelValues(channel).Inc()

View File

@@ -73,14 +73,14 @@ DefaultRoles = ["Standard"]
OpenRSA = false
[DB]
# mysql postgres sqlite
DBType = "sqlite"
# postgres: host=%s port=%s user=%s dbname=%s password=%s sslmode=%s
# postgres: DSN="host=127.0.0.1 port=5432 user=root dbname=n9e_v6 password=1234 sslmode=disable"
# sqlite: DSN="/path/to/filename.db"
DSN = "root:1234@tcp(127.0.0.1:3306)/n9e_v6?charset=utf8mb4&parseTime=True&loc=Local&allowNativePasswords=true"
DSN = "n9e.db"
# enable debug mode or not
Debug = false
# mysql postgres sqlite
DBType = "mysql"
# unit: s
MaxLifetime = 7200
# max open connections
@@ -98,8 +98,8 @@ Address = "127.0.0.1:6379"
# DB = 0
# UseTLS = false
# TLSMinVersion = "1.2"
# standalone cluster sentinel
RedisType = "standalone"
# standalone cluster sentinel miniredis
RedisType = "miniredis"
# Mastername for sentinel type
# MasterName = "mymaster"
# SentinelUsername = ""

7
go.mod
View File

@@ -45,10 +45,15 @@ require (
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde
)
require github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
require (
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect
)
require (
github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect
github.com/alicebob/miniredis/v2 v2.33.0
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect

6
go.sum
View File

@@ -15,6 +15,10 @@ github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030I
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA=
github.com/alicebob/miniredis/v2 v2.33.0/go.mod h1:MhP4a3EU7aENRi9aO+tHfTBZicLqQevyi/DJpoj6mi0=
github.com/aws/aws-sdk-go v1.44.302 h1:ST3ko6GrJKn3Xi+nAvxjG3uk/V1pW8KC52WLeIxqqNk=
github.com/aws/aws-sdk-go v1.44.302/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -328,6 +332,8 @@ github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZ
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=

View File

@@ -1,5 +1,5 @@
{
"name": "MongoDB by instance",
"name": "MongoDB Overview by exporter",
"tags": "Prometheus MongoDB",
"ident": "",
"configs": {
@@ -9,11 +9,11 @@
"id": "939298f2-b21f-4e2f-9142-c10946cc4032",
"layout": {
"h": 1,
"i": "939298f2-b21f-4e2f-9142-c10946cc4032",
"isResizable": false,
"w": 24,
"x": 0,
"y": 0,
"i": "939298f2-b21f-4e2f-9142-c10946cc4032",
"isResizable": false
"y": 0
},
"name": "Basic Info",
"type": "row"
@@ -32,12 +32,12 @@
"description": "instance count",
"id": "91970d24-3f04-4424-a1ed-73e7d28f5706",
"layout": {
"h": 4,
"h": 7,
"i": "91970d24-3f04-4424-a1ed-73e7d28f5706",
"isResizable": true,
"w": 6,
"x": 0,
"y": 1,
"i": "91970d24-3f04-4424-a1ed-73e7d28f5706",
"isResizable": true
"y": 1
},
"name": "Up",
"options": {
@@ -77,54 +77,39 @@
"version": "2.0.0"
},
{
"type": "stat",
"id": "c7b52e8e-b417-4c61-a15e-e2f186fccd67",
"layout": {
"h": 4,
"w": 6,
"x": 6,
"y": 1,
"i": "c7b52e8e-b417-4c61-a15e-e2f186fccd67",
"isResizable": true
},
"version": "3.0.0",
"datasourceCate": "prometheus",
"datasourceValue": "${prom}",
"targets": [
{
"expr": "mongodb_ss_uptime{instance=\"$instance\"}",
"refId": "A",
"maxDataPoints": 240
}
],
"transformations": [
{
"id": "organize",
"options": {}
}
],
"name": "Uptime",
"description": "Uptime",
"maxPerRow": 4,
"custom": {
"textMode": "value",
"graphMode": "none",
"colorMode": "value",
"calc": "lastNotNull",
"valueField": "Value",
"colSpan": 1,
"colorMode": "value",
"textMode": "value",
"textSize": {
"title": null
},
"orientation": "auto"
"valueField": "Value"
},
"datasourceCate": "prometheus",
"datasourceValue": "${prom}",
"description": "Uptime",
"id": "c7b52e8e-b417-4c61-a15e-e2f186fccd67",
"layout": {
"h": 7,
"i": "c7b52e8e-b417-4c61-a15e-e2f186fccd67",
"isResizable": true,
"w": 6,
"x": 6,
"y": 1
},
"name": "Uptime",
"options": {
"standardOptions": {
"util": "humantimeSeconds"
},
"thresholds": {
"steps": [
{
"color": "#634CD9",
"value": null,
"type": "base"
"type": "base",
"value": null
}
]
},
@@ -147,51 +132,34 @@
},
"type": "range"
}
],
"standardOptions": {
"util": "seconds",
"decimals": 2
}
]
},
"overrides": [
"targets": [
{
"matcher": {
"id": "byFrameRefID"
},
"properties": {
"thresholds": {
"steps": [
{
"color": "#6C53B1",
"value": null,
"type": "base"
}
]
},
"standardOptions": {
"decimals": 0
}
}
"expr": "mongodb_ss_uptime{instance=\"$instance\"}",
"refId": "A"
}
]
],
"type": "stat",
"version": "2.0.0"
},
{
"type": "timeseries",
"id": "8446dded-9e11-4ee9-bdad-769b193ddf3e",
"layout": {
"h": 4,
"h": 7,
"i": "8446dded-9e11-4ee9-bdad-769b193ddf3e",
"isResizable": true,
"w": 6,
"x": 12,
"y": 1,
"i": "8446dded-9e11-4ee9-bdad-769b193ddf3e",
"isResizable": true
"y": 1
},
"version": "3.0.0",
"datasourceCate": "prometheus",
"datasourceValue": "${prom}",
"targets": [
{
"expr": "mongodb_ss_mem_resident * 1024 * 1024",
"expr": "mongodb_ss_mem_resident{instance='$instance'} * 1024 * 1024",
"legend": "{{type}}",
"refId": "A",
"maxDataPoints": 240
@@ -219,8 +187,7 @@
"selectMode": "single"
},
"standardOptions": {
"util": "bytesIEC",
"decimals": 2
"util": "bytesIEC"
},
"thresholds": {
"steps": [
@@ -275,12 +242,12 @@
"description": "Page faults indicate that requests are processed from disk either because an index is missing or there is not enough memory for the data set. Consider increasing memory or sharding out.",
"id": "3eda28e7-2480-4ddc-b346-89ced1c33034",
"layout": {
"h": 4,
"h": 7,
"i": "3eda28e7-2480-4ddc-b346-89ced1c33034",
"isResizable": true,
"w": 6,
"x": 18,
"y": 1,
"i": "3eda28e7-2480-4ddc-b346-89ced1c33034",
"isResizable": true
"y": 1
},
"name": "Page Faults",
"options": {
@@ -333,12 +300,12 @@
"description": "Network traffic (bytes)",
"id": "528d0485-f947-470d-95f3-59eae157ebb6",
"layout": {
"h": 4,
"h": 7,
"i": "528d0485-f947-470d-95f3-59eae157ebb6",
"isResizable": true,
"w": 6,
"x": 0,
"y": 5,
"i": "528d0485-f947-470d-95f3-59eae157ebb6",
"isResizable": true
"y": 8
},
"name": "Network I/O",
"options": {
@@ -395,12 +362,12 @@
"description": "Number of connections Keep in mind the hard limit on the maximum number of connections set by your distribution.",
"id": "067e97c3-4e57-447f-a9dc-a49627b6ce18",
"layout": {
"h": 4,
"h": 7,
"i": "067e97c3-4e57-447f-a9dc-a49627b6ce18",
"isResizable": true,
"w": 6,
"x": 6,
"y": 5,
"i": "067e97c3-4e57-447f-a9dc-a49627b6ce18",
"isResizable": true
"y": 8
},
"name": "Connections",
"options": {
@@ -450,12 +417,12 @@
"description": "Number of assertion errors, Asserts are not important by themselves, but you can correlate spikes with other graphs.",
"id": "9e9b7356-cf0e-4e5f-95f5-00258c576bf4",
"layout": {
"h": 4,
"h": 7,
"i": "9e9b7356-cf0e-4e5f-95f5-00258c576bf4",
"isResizable": true,
"w": 6,
"x": 12,
"y": 5,
"i": "9e9b7356-cf0e-4e5f-95f5-00258c576bf4",
"isResizable": true
"y": 8
},
"name": "Assert Events",
"options": {
@@ -505,12 +472,12 @@
"description": "Number of operations waiting to acquire locks, Any number of queued operations for long periods of time is an indication of possible issues. Find the cause and fix it before requests get stuck in the queue.",
"id": "2698f0f8-a76a-499b-99cf-30504f0f4db6",
"layout": {
"h": 4,
"h": 7,
"i": "2698f0f8-a76a-499b-99cf-30504f0f4db6",
"isResizable": true,
"w": 6,
"x": 18,
"y": 5,
"i": "2698f0f8-a76a-499b-99cf-30504f0f4db6",
"isResizable": true
"y": 8
},
"name": "Lock Queue",
"options": {
@@ -547,11 +514,11 @@
"id": "2bdb8cc9-92f4-449e-8f70-a4c470a21604",
"layout": {
"h": 1,
"i": "2bdb8cc9-92f4-449e-8f70-a4c470a21604",
"isResizable": false,
"w": 24,
"x": 0,
"y": 9,
"i": "2bdb8cc9-92f4-449e-8f70-a4c470a21604",
"isResizable": false
"y": 15
},
"name": "Operation Info",
"type": "row"
@@ -574,12 +541,12 @@
"description": "Number of requests received Shows how many times a command is executed per second on average during the selected interval.",
"id": "c2819508-95e7-4c63-aeae-ce19f92469cd",
"layout": {
"h": 5,
"h": 7,
"i": "c2819508-95e7-4c63-aeae-ce19f92469cd",
"isResizable": true,
"w": 12,
"x": 0,
"y": 10,
"i": "c2819508-95e7-4c63-aeae-ce19f92469cd",
"isResizable": true
"y": 16
},
"name": "Command Operations",
"options": {
@@ -625,12 +592,12 @@
"type": "timeseries",
"id": "7030d97a-d69f-4916-a415-ec57503ab1ed",
"layout": {
"h": 5,
"h": 7,
"i": "7030d97a-d69f-4916-a415-ec57503ab1ed",
"isResizable": true,
"w": 12,
"x": 12,
"y": 10,
"i": "7030d97a-d69f-4916-a415-ec57503ab1ed",
"isResizable": true
"y": 16
},
"version": "3.0.0",
"datasourceCate": "prometheus",
@@ -704,19 +671,19 @@
"type": "timeseries",
"id": "1c3b73d5-c25c-449f-995d-26acc9c621e1",
"layout": {
"h": 5,
"h": 7,
"i": "1c3b73d5-c25c-449f-995d-26acc9c621e1",
"isResizable": true,
"w": 8,
"x": 0,
"y": 15,
"i": "1c3b73d5-c25c-449f-995d-26acc9c621e1",
"isResizable": true
"y": 23
},
"version": "3.0.0",
"datasourceCate": "prometheus",
"datasourceValue": "${prom}",
"targets": [
{
"expr": "rate(mongodb_ss_opLatencies_latency{}[5m]) / rate(mongodb_ss_opLatencies_latency{}[5m]) / 1000",
"expr": "rate(mongodb_ss_opLatencies_latency{instance='$instance'}[5m]) / rate(mongodb_ss_opLatencies_latency{instance='$instance'}[5m]) / 1000",
"legend": "{{op_type}}",
"refId": "A",
"maxDataPoints": 240
@@ -799,12 +766,12 @@
"description": "",
"id": "e642183c-8ba2-4f60-abc6-c65de49e7577",
"layout": {
"h": 5,
"h": 7,
"i": "e642183c-8ba2-4f60-abc6-c65de49e7577",
"isResizable": true,
"w": 8,
"x": 8,
"y": 15,
"i": "e642183c-8ba2-4f60-abc6-c65de49e7577",
"isResizable": true
"y": 23
},
"name": "Query Efficiency",
"options": {
@@ -861,12 +828,12 @@
"description": "number of cursors Helps identify why connections are increasing. Shows active cursors compared to cursors being automatically killed after 10 minutes due to an application not closing the connection.",
"id": "8b5a4f44-3291-4822-ab73-f56be6c62674",
"layout": {
"h": 5,
"h": 7,
"i": "8b5a4f44-3291-4822-ab73-f56be6c62674",
"isResizable": true,
"w": 8,
"x": 16,
"y": 15,
"i": "8b5a4f44-3291-4822-ab73-f56be6c62674",
"isResizable": true
"y": 23
},
"name": "Cursors",
"options": {
@@ -903,11 +870,11 @@
"id": "06946b19-94b4-4f72-bd87-70f87989257d",
"layout": {
"h": 1,
"i": "06946b19-94b4-4f72-bd87-70f87989257d",
"isResizable": false,
"w": 24,
"x": 0,
"y": 20,
"i": "06946b19-94b4-4f72-bd87-70f87989257d",
"isResizable": false
"y": 30
},
"name": "Cache Info",
"panels": [],
@@ -917,19 +884,19 @@
"type": "timeseries",
"id": "bb0ae571-43a1-430b-8f63-256f6f1ebee6",
"layout": {
"h": 5,
"h": 7,
"i": "bb0ae571-43a1-430b-8f63-256f6f1ebee6",
"isResizable": true,
"w": 6,
"x": 0,
"y": 21,
"i": "bb0ae571-43a1-430b-8f63-256f6f1ebee6",
"isResizable": true
"y": 31
},
"version": "3.0.0",
"datasourceCate": "prometheus",
"datasourceValue": "${prom}",
"targets": [
{
"expr": "mongodb_ss_wt_cache_bytes_currently_in_the_cache{}",
"expr": "mongodb_ss_wt_cache_bytes_currently_in_the_cache{instance='$instance'}",
"legend": "total",
"refId": "A",
"maxDataPoints": 240
@@ -975,8 +942,7 @@
"selectMode": "single"
},
"standardOptions": {
"util": "bytesIEC",
"decimals": 2
"util": "bytesIEC"
},
"thresholds": {
"steps": [
@@ -1017,19 +983,19 @@
"type": "timeseries",
"id": "f1ffd169-2a1a-42bc-9647-0e6621be0fef",
"layout": {
"h": 5,
"h": 7,
"i": "f1ffd169-2a1a-42bc-9647-0e6621be0fef",
"isResizable": true,
"w": 6,
"x": 6,
"y": 21,
"i": "f1ffd169-2a1a-42bc-9647-0e6621be0fef",
"isResizable": true
"y": 31
},
"version": "3.0.0",
"datasourceCate": "prometheus",
"datasourceValue": "${prom}",
"targets": [
{
"expr": "rate(mongodb_ss_wt_cache_bytes_read_into_cache{}[5m])",
"expr": "rate(mongodb_ss_wt_cache_bytes_read_into_cache{instance='$instance'}[5m])",
"legend": "read",
"refId": "A",
"maxDataPoints": 240
@@ -1104,19 +1070,19 @@
"type": "timeseries",
"id": "43ee140d-ae6d-474a-9892-fa4743d7f97e",
"layout": {
"h": 5,
"h": 7,
"i": "43ee140d-ae6d-474a-9892-fa4743d7f97e",
"isResizable": true,
"w": 6,
"x": 12,
"y": 21,
"i": "43ee140d-ae6d-474a-9892-fa4743d7f97e",
"isResizable": true
"y": 31
},
"version": "3.0.0",
"datasourceCate": "prometheus",
"datasourceValue": "${prom}",
"targets": [
{
"expr": "100 * sum(mongodb_ss_wt_cache_tracked_dirty_pages_in_the_cache{}) / sum(mongodb_ss_wt_cache_pages_currently_held_in_the_cache{})",
"expr": "100 * sum(mongodb_ss_wt_cache_tracked_dirty_pages_in_the_cache{instance='$instance'}) / sum(mongodb_ss_wt_cache_pages_currently_held_in_the_cache{instance='$instance'})",
"legend": "dirty rate",
"refId": "A",
"maxDataPoints": 240
@@ -1185,19 +1151,19 @@
"type": "timeseries",
"id": "1a22c31a-859a-400c-af2a-ae83c308d0f2",
"layout": {
"h": 5,
"h": 7,
"i": "1a22c31a-859a-400c-af2a-ae83c308d0f2",
"isResizable": true,
"w": 6,
"x": 18,
"y": 21,
"i": "1a22c31a-859a-400c-af2a-ae83c308d0f2",
"isResizable": true
"y": 31
},
"version": "3.0.0",
"datasourceCate": "prometheus",
"datasourceValue": "${prom}",
"targets": [
{
"expr": "rate(mongodb_mongod_wiredtiger_cache_evicted_total{}[5m])",
"expr": "rate(mongodb_mongod_wiredtiger_cache_evicted_total{instance='$instance'}[5m])",
"legend": "evicted pages",
"refId": "A",
"maxDataPoints": 240
@@ -1265,95 +1231,125 @@
"id": "b0016f4a-c565-4276-a08d-bacdf94b6b5a",
"layout": {
"h": 1,
"i": "b0016f4a-c565-4276-a08d-bacdf94b6b5a",
"isResizable": false,
"w": 24,
"x": 0,
"y": 26,
"i": "b0016f4a-c565-4276-a08d-bacdf94b6b5a",
"isResizable": false
"y": 45
},
"name": "ReplSet Info",
"type": "row"
},
{
"type": "timeseries",
"id": "f73fd0cd-ecbe-41f0-a2dc-4e02f7eaef1c",
"layout": {
"h": 5,
"w": 12,
"x": 0,
"y": 27,
"i": "f73fd0cd-ecbe-41f0-a2dc-4e02f7eaef1c",
"isResizable": true
"custom": {
"calc": "lastNotNull",
"colSpan": 1,
"colorMode": "value",
"textMode": "value",
"textSize": {},
"valueField": "Value"
},
"version": "3.0.0",
"datasourceCate": "prometheus",
"datasourceValue": "${prom}",
"targets": [
{
"expr": "mongodb_mongod_replset_member_replication_lag{instance=\"$instance\"}",
"legend": "",
"refId": "A",
"maxDataPoints": 240
}
],
"transformations": [
{
"id": "organize",
"options": {}
}
],
"name": "Replset Lag Seconds",
"description": "replica set member master-slave synchronization delay",
"maxPerRow": 4,
"description": "",
"id": "6187ceee-7c25-43f2-be1b-c44ad612ab52",
"layout": {
"h": 7,
"i": "6187ceee-7c25-43f2-be1b-c44ad612ab52",
"isResizable": true,
"w": 12,
"x": 0,
"y": 46
},
"name": "Replset Election",
"options": {
"tooltip": {
"mode": "all",
"sort": "none"
},
"legend": {
"displayMode": "hidden",
"heightInPercentage": 30,
"placement": "bottom",
"behaviour": "showItem",
"selectMode": "single"
},
"standardOptions": {
"decimals": 1,
"util": "seconds"
},
"thresholds": {
"steps": [
{
"color": "#6C53B1",
"value": null,
"type": "base"
"color": "#634CD9",
"type": "base",
"value": null
}
]
}
},
"valueMappings": [
{
"match": {
"to": 1800
},
"result": {
"color": "#f24526"
},
"type": "range"
},
{
"match": {
"from": 1800
},
"result": {
"color": "#53b503"
},
"type": "range"
}
]
},
"targets": [
{
"expr": "time() - mongodb_mongod_replset_member_election_date",
"refId": "A"
}
],
"type": "stat",
"version": "2.0.0"
},
{
"custom": {
"drawStyle": "lines",
"lineInterpolation": "smooth",
"spanNulls": false,
"lineWidth": 2,
"fillOpacity": 0.3,
"gradientMode": "opacity",
"stack": "off",
"scaleDistribution": {
"type": "linear"
},
"showPoints": "none",
"pointSize": 5
"lineInterpolation": "smooth",
"lineWidth": 2,
"stack": "off"
},
"overrides": [
{
"matcher": {
"id": "byFrameRefID"
},
"properties": {
"rightYAxisDisplay": "off"
}
"datasourceCate": "prometheus",
"datasourceValue": "${prom}",
"description": "replica set member master-slave synchronization delay",
"id": "f73fd0cd-ecbe-41f0-a2dc-4e02f7eaef1c",
"layout": {
"h": 7,
"i": "f73fd0cd-ecbe-41f0-a2dc-4e02f7eaef1c",
"isResizable": true,
"w": 12,
"x": 12,
"y": 46
},
"name": "Replset Lag Seconds",
"options": {
"legend": {
"displayMode": "hidden"
},
"standardOptions": {
"util": "seconds"
},
"thresholds": {},
"tooltip": {
"mode": "all",
"sort": "none"
}
]
},
"targets": [
{
"expr": "mongodb_mongod_replset_member_replication_lag{instance=\"$instance\"}",
"legend": "lag",
"refId": "A"
}
],
"type": "timeseries",
"version": "2.0.0"
}
],
"var": [
@@ -1375,4 +1371,4 @@
"version": "3.0.0"
},
"uuid": 1717556328065329000
}
}

View File

@@ -372,12 +372,14 @@ func GetHostsQuery(queries []HostQuery) []map[string]interface{} {
blank += " "
}
} else {
blank := " "
var args []interface{}
var query []string
for _, tag := range lst {
m["tags not like ?"+blank] = "%" + tag + "%"
m["host_tags not like ?"+blank] = "%" + tag + "%"
blank += " "
query = append(query, "tags not like ?",
"(host_tags not like ? or host_tags is null)")
args = append(args, "%"+tag+"%", "%"+tag+"%")
}
m[strings.Join(query, " and ")] = args
}
case "hosts":
lst := []string{}
@@ -398,11 +400,13 @@ func GetHostsQuery(queries []HostQuery) []map[string]interface{} {
blank += " "
}
} else if q.Op == "!~" {
blank := " "
var args []interface{}
var query []string
for _, host := range lst {
m["ident not like ?"+blank] = strings.ReplaceAll(host, "*", "%")
blank += " "
query = append(query, "ident not like ?")
args = append(args, strings.ReplaceAll(host, "*", "%"))
}
m[strings.Join(query, " and ")] = args
}
}
query = append(query, m)

View File

@@ -20,6 +20,17 @@ type BuiltinComponent struct {
UpdatedBy string `json:"updated_by" gorm:"type:varchar(191);not null;default:'';comment:'updater'"`
}
type PostgresBuiltinComponent struct {
ID uint64 `json:"id" gorm:"primaryKey;type:bigint;autoIncrement;comment:'unique identifier'"`
Ident string `json:"ident" gorm:"type:varchar(191);not null;uniqueIndex:idx_ident,sort:asc;comment:'identifier of component'"`
Logo string `json:"logo" gorm:"type:text;comment:'logo of component'"`
Readme string `json:"readme" gorm:"type:text;not null;comment:'readme of component'"`
CreatedAt int64 `json:"created_at" gorm:"type:bigint;not null;default:0;comment:'create time'"`
CreatedBy string `json:"created_by" gorm:"type:varchar(191);not null;default:'';comment:'creator'"`
UpdatedAt int64 `json:"updated_at" gorm:"type:bigint;not null;default:0;comment:'update time'"`
UpdatedBy string `json:"updated_by" gorm:"type:varchar(191);not null;default:'';comment:'updater'"`
}
func (bc *BuiltinComponent) TableName() string {
return "builtin_components"
}

View File

@@ -38,13 +38,22 @@ func MigrateIbexTables(db *gorm.DB) {
for i := 0; i < 100; i++ {
tableName := fmt.Sprintf("task_host_%d", i)
err := db.Table(tableName).AutoMigrate(&imodels.TaskHost{})
if err != nil {
logger.Errorf("failed to migrate table:%s %v", tableName, err)
exists := db.Migrator().HasTable(tableName)
if exists {
continue
} else {
err := db.Table(tableName).AutoMigrate(&imodels.TaskHost{})
if err != nil {
logger.Errorf("failed to migrate table:%s %v", tableName, err)
}
}
}
}
func isPostgres(db *gorm.DB) bool {
dialect := db.Dialector.Name()
return dialect == "postgres"
}
func MigrateTables(db *gorm.DB) error {
var tableOptions string
switch db.Dialector.(type) {
@@ -54,13 +63,18 @@ func MigrateTables(db *gorm.DB) error {
if tableOptions != "" {
db = db.Set("gorm:table_options", tableOptions)
}
dts := []interface{}{&RecordingRule{}, &AlertRule{}, &AlertSubscribe{}, &AlertMute{},
&TaskRecord{}, &ChartShare{}, &Target{}, &Configs{}, &Datasource{}, &NotifyTpl{},
&Board{}, &BoardBusigroup{}, &Users{}, &SsoConfig{}, &models.BuiltinMetric{},
&models.MetricFilter{}, &models.BuiltinComponent{}, &models.NotificaitonRecord{},
&models.MetricFilter{}, &models.NotificaitonRecord{},
&models.TargetBusiGroup{}}
if isPostgres(db) {
dts = append(dts, &models.PostgresBuiltinComponent{})
} else {
dts = append(dts, &models.BuiltinComponent{})
}
if !db.Migrator().HasColumn(&imodels.TaskSchedulerHealth{}, "scheduler") {
dts = append(dts, &imodels.TaskSchedulerHealth{})
}
@@ -78,7 +92,7 @@ func MigrateTables(db *gorm.DB) error {
for _, dt := range asyncDts {
if err := db.AutoMigrate(dt); err != nil {
logger.Errorf("failed to migrate table: %v", err)
logger.Errorf("failed to migrate table %+v err:%v", dt, err)
}
}
}()
@@ -174,14 +188,20 @@ func InsertPermPoints(db *gorm.DB) {
})
for _, op := range ops {
exists, err := models.Exists(db.Model(&models.RoleOperation{}).Where("operation = ? and role_name = ?", op.Operation, op.RoleName))
var count int64
err := db.Raw("SELECT COUNT(*) FROM role_operation WHERE operation = ? AND role_name = ?",
op.Operation, op.RoleName).Scan(&count).Error
if err != nil {
logger.Errorf("check role operation exists failed, %v", err)
continue
}
if exists {
if count > 0 {
continue
}
err = db.Create(&op).Error
if err != nil {
logger.Errorf("insert role operation failed, %v", err)

View File

@@ -0,0 +1,69 @@
package migrate
import (
"fmt"
"testing"
"github.com/ccfos/nightingale/v6/models"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
func TestInsertPermPoints(t *testing.T) {
db, err := gorm.Open(mysql.Open("root:1234@tcp(127.0.0.1:3306)/n9e_v6?charset=utf8mb4&parseTime=True&loc=Local&allowNativePasswords=true"), &gorm.Config{NamingStrategy: schema.NamingStrategy{
SingularTable: true,
}})
if err != nil {
fmt.Printf("failed to connect database: %v", err)
}
var ops []models.RoleOperation
ops = append(ops, models.RoleOperation{
RoleName: "Standard",
Operation: "/alert-mutes/put",
})
ops = append(ops, models.RoleOperation{
RoleName: "Standard",
Operation: "/log/index-patterns",
})
ops = append(ops, models.RoleOperation{
RoleName: "Standard",
Operation: "/help/variable-configs",
})
ops = append(ops, models.RoleOperation{
RoleName: "Admin",
Operation: "/permissions",
})
ops = append(ops, models.RoleOperation{
RoleName: "Standard",
Operation: "/ibex-settings",
})
db = db.Debug()
for _, op := range ops {
var count int64
err := db.Raw("SELECT COUNT(*) FROM role_operation WHERE operation = ? AND role_name = ?",
op.Operation, op.RoleName).Scan(&count).Error
fmt.Printf("count: %d\n", count)
if err != nil {
fmt.Printf("check role operation exists failed, %v", err)
continue
}
if count > 0 {
continue
}
err = db.Create(&op).Error
if err != nil {
fmt.Printf("insert role operation failed, %v", err)
}
}
}

View File

@@ -185,8 +185,16 @@ func BuildTargetWhereWithQuery(query string) BuildTargetWhereOption {
if query != "" {
arr := strings.Fields(query)
for i := 0; i < len(arr); i++ {
q := "%" + arr[i] + "%"
session = session.Where("ident like ? or host_ip like ? or note like ? or tags like ? or host_tags like ? or os like ?", q, q, q, q, q, q)
if strings.HasPrefix(arr[i], "-") {
q := "%" + arr[i][1:] + "%"
session = session.Where("ident not like ? and host_ip not like ? and "+
"note not like ? and tags not like ? and (host_tags not like ? or "+
"host_tags is null) and os not like ?", q, q, q, q, q, q)
} else {
q := "%" + arr[i] + "%"
session = session.Where("ident like ? or host_ip like ? or note like ? or "+
"tags like ? or host_tags like ? or os like ?", q, q, q, q, q, q)
}
}
}
return session
@@ -197,6 +205,8 @@ func BuildTargetWhereWithDowntime(downtime int64) BuildTargetWhereOption {
return func(session *gorm.DB) *gorm.DB {
if downtime > 0 {
session = session.Where("target.update_at < ?", time.Now().Unix()-downtime)
} else if downtime < 0 {
session = session.Where("target.update_at > ?", time.Now().Unix()+downtime)
}
return session
}
@@ -270,7 +280,11 @@ func TargetFilterQueryBuild(ctx *ctx.Context, query []map[string]interface{}, li
for _, q := range query {
tx := DB(ctx).Model(&Target{})
for k, v := range q {
tx = tx.Or(k, v)
if strings.Count(k, "?") > 1 {
tx = tx.Or(k, v.([]interface{})...)
} else {
tx = tx.Or(k, v)
}
}
sub = sub.Where(tx)
}

1995
pkg/ormx/database_init.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,60 @@
package ormx
import (
"testing"
"github.com/stretchr/testify/assert"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func TestDataBaseInit(t *testing.T) {
tests := []struct {
name string
config DBConfig
}{
{
name: "MySQL",
config: DBConfig{
DBType: "mysql",
DSN: "root:1234@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local&allowNativePasswords=true",
},
},
{
name: "Postgres",
config: DBConfig{
DBType: "postgres",
DSN: "host=127.0.0.1 port=5432 user=postgres dbname=test password=1234 sslmode=disable",
},
},
{
name: "SQLite",
config: DBConfig{
DBType: "sqlite",
DSN: "./test.db",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := createDatabase(tt.config, &gorm.Config{})
assert.NoError(t, err)
var dialector gorm.Dialector
switch tt.config.DBType {
case "mysql":
dialector = mysql.Open(tt.config.DSN)
case "postgres":
dialector = postgres.Open(tt.config.DSN)
case "sqlite":
dialector = sqlite.Open(tt.config.DSN)
}
db, err := gorm.Open(dialector, &gorm.Config{})
assert.NoError(t, err)
err = DataBaseInit(tt.config, db)
assert.NoError(t, err)
})
}
}

View File

@@ -2,6 +2,7 @@ package ormx
import (
"fmt"
"os"
"reflect"
"strings"
"time"
@@ -70,6 +71,234 @@ func (l *TKitLogger) Printf(s string, i ...interface{}) {
}
}
func createDatabase(c DBConfig, gconfig *gorm.Config) error {
switch strings.ToLower(c.DBType) {
case "mysql":
return createMysqlDatabase(c.DSN, gconfig)
case "postgres":
return createPostgresDatabase(c.DSN, gconfig)
case "sqlite":
return createSqliteDatabase(c.DSN, gconfig)
default:
return fmt.Errorf("dialector(%s) not supported", c.DBType)
}
}
func createSqliteDatabase(dsn string, gconfig *gorm.Config) error {
tempDialector := sqlite.Open(dsn)
_, err := gorm.Open(tempDialector, gconfig)
if err != nil {
return fmt.Errorf("failed to open temporary connection: %v", err)
}
fmt.Println("sqlite file created")
return nil
}
func createPostgresDatabase(dsn string, gconfig *gorm.Config) error {
dsnParts := strings.Split(dsn, " ")
dbName := ""
connectionWithoutDB := ""
for _, part := range dsnParts {
if strings.HasPrefix(part, "dbname=") {
dbName = part[strings.Index(part, "=")+1:]
} else {
connectionWithoutDB += part
connectionWithoutDB += " "
}
}
createDBQuery := fmt.Sprintf("CREATE DATABASE %s ENCODING='UTF8' LC_COLLATE='en_US.UTF-8' LC_CTYPE='en_US.UTF-8';", dbName)
tempDialector := postgres.Open(connectionWithoutDB)
tempDB, err := gorm.Open(tempDialector, gconfig)
if err != nil {
return fmt.Errorf("failed to open temporary connection: %v", err)
}
result := tempDB.Exec(createDBQuery)
if result.Error != nil {
return fmt.Errorf("failed to execute create database query: %v", result.Error)
}
return nil
}
func createMysqlDatabase(dsn string, gconfig *gorm.Config) error {
dsnParts := strings.SplitN(dsn, "/", 2)
if len(dsnParts) != 2 {
return fmt.Errorf("failed to parse DSN: %s", dsn)
}
connectionInfo := dsnParts[0]
dbInfo := dsnParts[1]
dbName := dbInfo
queryIndex := strings.Index(dbInfo, "?")
if queryIndex != -1 {
dbName = dbInfo[:queryIndex]
} else {
return fmt.Errorf("failed to parse database name from DSN: %s", dsn)
}
connectionWithoutDB := connectionInfo + "/?" + dbInfo[queryIndex+1:]
createDBQuery := fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s CHARACTER SET utf8mb4", dbName)
tempDialector := mysql.Open(connectionWithoutDB)
tempDB, err := gorm.Open(tempDialector, gconfig)
if err != nil {
return fmt.Errorf("failed to open temporary connection: %v", err)
}
result := tempDB.Exec(createDBQuery)
if result.Error != nil {
return fmt.Errorf("failed to execute create database query: %v", result.Error)
}
return nil
}
func checkDatabaseExist(c DBConfig) (bool, error) {
switch strings.ToLower(c.DBType) {
case "mysql":
return checkMysqlDatabaseExist(c)
case "postgres":
return checkPostgresDatabaseExist(c)
case "sqlite":
return checkSqliteDatabaseExist(c)
default:
return false, fmt.Errorf("dialector(%s) not supported", c.DBType)
}
}
func checkSqliteDatabaseExist(c DBConfig) (bool, error) {
if _, err := os.Stat(c.DSN); os.IsNotExist(err) {
fmt.Printf("sqlite file not exists: %s\n", c.DSN)
return false, nil
} else {
return true, nil
}
}
func checkPostgresDatabaseExist(c DBConfig) (bool, error) {
dsnParts := strings.Split(c.DSN, " ")
dbName := ""
connectionWithoutDB := ""
for _, part := range dsnParts {
if strings.HasPrefix(part, "dbname=") {
dbName = part[strings.Index(part, "=")+1:]
} else {
connectionWithoutDB += part
connectionWithoutDB += " "
}
}
dialector := postgres.Open(connectionWithoutDB)
gconfig := &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: c.TablePrefix,
SingularTable: true,
},
Logger: gormLogger,
}
db, err := gorm.Open(dialector, gconfig)
if err != nil {
return false, fmt.Errorf("failed to open database: %v", err)
}
var databases []string
query := genQuery(c)
if err := db.Raw(query).Scan(&databases).Error; err != nil {
return false, fmt.Errorf("failed to query: %v", err)
}
for _, database := range databases {
if database == dbName {
fmt.Println("Database exist")
return true, nil
}
}
return false, nil
}
func checkMysqlDatabaseExist(c DBConfig) (bool, error) {
dsnParts := strings.SplitN(c.DSN, "/", 2)
if len(dsnParts) != 2 {
return false, fmt.Errorf("failed to parse DSN: %s", c.DSN)
}
connectionInfo := dsnParts[0]
dbInfo := dsnParts[1]
dbName := dbInfo
queryIndex := strings.Index(dbInfo, "?")
if queryIndex != -1 {
dbName = dbInfo[:queryIndex]
} else {
return false, fmt.Errorf("failed to parse database name from DSN: %s", c.DSN)
}
connectionWithoutDB := connectionInfo + "/?" + dbInfo[queryIndex+1:]
var dialector gorm.Dialector
switch strings.ToLower(c.DBType) {
case "mysql":
dialector = mysql.Open(connectionWithoutDB)
case "postgres":
dialector = postgres.Open(connectionWithoutDB)
default:
return false, fmt.Errorf("unsupported database type: %s", c.DBType)
}
gconfig := &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: c.TablePrefix,
SingularTable: true,
},
Logger: gormLogger,
}
db, err := gorm.Open(dialector, gconfig)
if err != nil {
return false, fmt.Errorf("failed to open database: %v", err)
}
var databases []string
query := genQuery(c)
if err := db.Raw(query).Scan(&databases).Error; err != nil {
return false, fmt.Errorf("failed to query: %v", err)
}
for _, database := range databases {
if database == dbName {
return true, nil
}
}
return false, nil
}
func genQuery(c DBConfig) string {
switch strings.ToLower(c.DBType) {
case "mysql":
return "SHOW DATABASES"
case "postgres":
return "SELECT datname FROM pg_database"
case "sqlite":
return ""
default:
return ""
}
}
// New Create gorm.DB instance
func New(c DBConfig) (*gorm.DB, error) {
var dialector gorm.Dialector
@@ -95,9 +324,30 @@ func New(c DBConfig) (*gorm.DB, error) {
Logger: gormLogger,
}
dbExist, checkErr := checkDatabaseExist(c)
if checkErr != nil {
return nil, checkErr
}
if !dbExist {
fmt.Println("Database not exist, trying to create it")
createErr := createDatabase(c, gconfig)
if createErr != nil {
return nil, fmt.Errorf("failed to create database: %v", createErr)
}
db, err := gorm.Open(dialector, gconfig)
if err != nil {
return nil, fmt.Errorf("failed to reopen database after creation: %v", err)
}
err = DataBaseInit(c, db)
if err != nil {
return nil, fmt.Errorf("failed to init database: %v", err)
}
}
db, err := gorm.Open(dialector, gconfig)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to open database: %v", err)
}
if c.Debug {

View File

@@ -63,7 +63,7 @@ func GetByUrl[T any](url string, cfg conf.CenterApi) (T, error) {
Timeout: time.Duration(cfg.Timeout) * time.Millisecond,
}
if useProxy(url) {
if UseProxy(url) {
client.Transport = ProxyTransporter
}
@@ -147,7 +147,7 @@ func PostByUrl[T any](url string, cfg conf.CenterApi, v interface{}) (t T, err e
Timeout: time.Duration(cfg.Timeout) * time.Millisecond,
}
if useProxy(url) {
if UseProxy(url) {
client.Transport = ProxyTransporter
}
@@ -195,7 +195,7 @@ var ProxyTransporter = &http.Transport{
Proxy: http.ProxyFromEnvironment,
}
func useProxy(url string) bool {
func UseProxy(url string) bool {
// N9E_PROXY_URL=oapi.dingtalk.com,feishu.com
patterns := os.Getenv("N9E_PROXY_URL")
if patterns != "" {
@@ -228,7 +228,7 @@ func PostJSON(url string, timeout time.Duration, v interface{}, retries ...int)
Timeout: timeout,
}
if useProxy(url) {
if UseProxy(url) {
client.Transport = ProxyTransporter
}

View File

@@ -7,6 +7,7 @@ import (
"os"
"strings"
"github.com/alicebob/miniredis/v2"
"github.com/ccfos/nightingale/v6/pkg/tlsx"
"github.com/redis/go-redis/v9"
"github.com/toolkits/pkg/logger"
@@ -28,6 +29,7 @@ type Redis redis.Cmdable
func NewRedis(cfg RedisConfig) (Redis, error) {
var redisClient Redis
switch cfg.RedisType {
case "standalone", "":
redisOptions := &redis.Options{
@@ -88,6 +90,16 @@ func NewRedis(cfg RedisConfig) (Redis, error) {
redisClient = redis.NewFailoverClient(redisOptions)
case "miniredis":
s, err := miniredis.Run()
if err != nil {
fmt.Println("failed to init miniredis:", err)
os.Exit(1)
}
redisClient = redis.NewClient(&redis.Options{
Addr: s.Addr(),
})
default:
fmt.Println("failed to init redis , redis type is illegal:", cfg.RedisType)
os.Exit(1)

44
storage/redis_test.go Normal file
View File

@@ -0,0 +1,44 @@
package storage
import (
"context"
"testing"
"github.com/alicebob/miniredis/v2"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/assert"
)
func TestMiniRedisMGet(t *testing.T) {
s, err := miniredis.Run()
if err != nil {
t.Fatalf("failed to start miniredis: %v", err)
}
defer s.Close()
rdb := redis.NewClient(&redis.Options{
Addr: s.Addr(),
})
err = rdb.Ping(context.Background()).Err()
if err != nil {
t.Fatalf("failed to ping miniredis: %v", err)
}
mp := make(map[string]interface{})
mp["key1"] = "value1"
mp["key2"] = "value2"
mp["key3"] = "value3"
err = MSet(context.Background(), rdb, mp)
if err != nil {
t.Fatalf("failed to set miniredis value: %v", err)
}
ctx := context.Background()
keys := []string{"key1", "key2", "key3", "key4"}
vals := MGet(ctx, rdb, keys)
expected := [][]byte{[]byte("value1"), []byte("value2"), []byte("value3")}
assert.Equal(t, expected, vals)
}