Compare commits

...

22 Commits

Author SHA1 Message Date
ning
b4c16b98f3 fix: edge eval with var 2025-03-17 15:51:11 +08:00
Yening Qin
30519fe909 refactor: optimize post func (#2455) 2025-02-08 18:27:56 +08:00
ning
5b8fb144ac refactor: print log 2025-01-22 20:17:22 +08:00
Yening Qin
696f058975 feat: es alert support nodata alert (#2446) 2025-01-21 18:03:47 +08:00
Yening Qin
6195904715 refactor: optimize data recv and push 2025-01-16 15:52:57 +08:00
Yening Qin
47ace00ffa refactor data recv limit (#2430) 2025-01-14 16:17:27 +08:00
Yening Qin
d8feeec345 refactor: change metric type filter (#2426) 2025-01-10 17:32:57 +08:00
ning
6a31521b62 fix: es group by int 2025-01-09 12:09:48 +08:00
ning
61512857a5 refactor: builtin_components 2025-01-08 20:48:37 +08:00
ning
cb56037ef8 refactor: change eval sleep 2025-01-08 19:40:04 +08:00
ning
2ebd64dfa0 Merge branch 'main' of github.com:ccfos/nightingale 2025-01-08 11:28:58 +08:00
ning
4d2ffdf096 refactor: change builtin component model 2025-01-08 11:24:50 +08:00
kongfei605
1915701ce0 chore: default interval for cloudwatch collecting (#2418) 2025-01-07 10:35:53 +08:00
kongfei605
7fd9cd5a3d chore: update tpl and readme for cloudwatch (#2417) 2025-01-06 19:13:45 +08:00
ning
0e2f386419 refactor: change tdengine 2025-01-06 17:29:58 +08:00
ning
b96b08fb9e refactor: change tdengine 2025-01-06 17:22:22 +08:00
ning
eebd1021de refactor: change tdengine 2025-01-06 17:04:46 +08:00
ning
ef61a4cfa7 refactor: change tdengine 2025-01-06 16:50:29 +08:00
ning
2563d2891d feat: add cron pattern validation for alert rule 2025-01-06 11:45:43 +08:00
ning
6ae8ef0d9f feat: add random delay before starting alert rule worker 2025-01-06 11:21:26 +08:00
Yening Qin
38adbefe9c feat: dashboard support add annotations (#2416) 2025-01-05 17:46:17 +08:00
Yening Qin
3f5e0c056d feat: support elasticsearch alert (#2400)
Co-authored-by: Xu Bin <140785332+Reditiny@users.noreply.github.com>
2025-01-05 17:44:17 +08:00
66 changed files with 4619 additions and 1244 deletions

2
.gitignore vendored
View File

@@ -65,3 +65,5 @@ queries.active
/n9e-*
n9e.sql
!/datasource

View File

@@ -4,6 +4,8 @@ import (
"context"
"fmt"
"github.com/ccfos/nightingale/v6/dscache"
"github.com/ccfos/nightingale/v6/alert/aconf"
"github.com/ccfos/nightingale/v6/alert/astats"
"github.com/ccfos/nightingale/v6/alert/dispatch"
@@ -21,12 +23,11 @@ import (
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/pkg/httpx"
"github.com/ccfos/nightingale/v6/pkg/logx"
"github.com/ccfos/nightingale/v6/pkg/macros"
"github.com/ccfos/nightingale/v6/prom"
"github.com/ccfos/nightingale/v6/pushgw/pconf"
"github.com/ccfos/nightingale/v6/pushgw/writer"
"github.com/ccfos/nightingale/v6/storage"
"github.com/ccfos/nightingale/v6/tdengine"
"github.com/flashcatcloud/ibex/src/cmd/ibex"
)
@@ -66,11 +67,12 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
promClients := prom.NewPromClient(ctx)
dispatch.InitRegisterQueryFunc(promClients)
tdengineClients := tdengine.NewTdengineClient(ctx, config.Alert.Heartbeat)
externalProcessors := process.NewExternalProcessors()
Start(config.Alert, config.Pushgw, syncStats, alertStats, externalProcessors, targetCache, busiGroupCache, alertMuteCache, alertRuleCache, notifyConfigCache, taskTplsCache, dsCache, ctx, promClients, tdengineClients, userCache, userGroupCache)
macros.RegisterMacro(macros.MacroInVain)
dscache.Init(ctx, false)
Start(config.Alert, config.Pushgw, syncStats, alertStats, externalProcessors, targetCache, busiGroupCache, alertMuteCache, alertRuleCache, notifyConfigCache, taskTplsCache, dsCache, ctx, promClients, userCache, userGroupCache)
r := httpx.GinEngine(config.Global.RunMode, config.HTTP,
configCvalCache.PrintBodyPaths, configCvalCache.PrintAccessLog)
@@ -93,7 +95,7 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
func Start(alertc aconf.Alert, pushgwc pconf.Pushgw, syncStats *memsto.Stats, alertStats *astats.Stats, externalProcessors *process.ExternalProcessorsType, targetCache *memsto.TargetCacheType, busiGroupCache *memsto.BusiGroupCacheType,
alertMuteCache *memsto.AlertMuteCacheType, alertRuleCache *memsto.AlertRuleCacheType, notifyConfigCache *memsto.NotifyConfigCacheType, taskTplsCache *memsto.TaskTplCache, datasourceCache *memsto.DatasourceCacheType, ctx *ctx.Context,
promClients *prom.PromClientMap, tdendgineClients *tdengine.TdengineClientMap, userCache *memsto.UserCacheType, userGroupCache *memsto.UserGroupCacheType) {
promClients *prom.PromClientMap, userCache *memsto.UserCacheType, userGroupCache *memsto.UserGroupCacheType) {
alertSubscribeCache := memsto.NewAlertSubscribeCache(ctx, syncStats)
recordingRuleCache := memsto.NewRecordingRuleCache(ctx, syncStats)
targetsOfAlertRulesCache := memsto.NewTargetOfAlertRuleCache(ctx, alertc.Heartbeat.EngineName, syncStats)
@@ -106,7 +108,7 @@ func Start(alertc aconf.Alert, pushgwc pconf.Pushgw, syncStats *memsto.Stats, al
record.NewScheduler(alertc, recordingRuleCache, promClients, writers, alertStats, datasourceCache)
eval.NewScheduler(alertc, externalProcessors, alertRuleCache, targetCache, targetsOfAlertRulesCache,
busiGroupCache, alertMuteCache, datasourceCache, promClients, tdendgineClients, naming, ctx, alertStats)
busiGroupCache, alertMuteCache, datasourceCache, promClients, naming, ctx, alertStats)
dp := dispatch.NewDispatch(alertRuleCache, userCache, userGroupCache, alertSubscribeCache, targetCache, notifyConfigCache, taskTplsCache, alertc.Alerting, ctx, alertStats)
consumer := dispatch.NewConsumer(alertc.Alerting, ctx, dp, promClients)

View File

@@ -13,8 +13,6 @@ import (
"github.com/ccfos/nightingale/v6/memsto"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/prom"
"github.com/ccfos/nightingale/v6/tdengine"
"github.com/toolkits/pkg/logger"
)
@@ -33,8 +31,7 @@ type Scheduler struct {
alertMuteCache *memsto.AlertMuteCacheType
datasourceCache *memsto.DatasourceCacheType
promClients *prom.PromClientMap
tdengineClients *tdengine.TdengineClientMap
promClients *prom.PromClientMap
naming *naming.Naming
@@ -45,7 +42,7 @@ type Scheduler struct {
func NewScheduler(aconf aconf.Alert, externalProcessors *process.ExternalProcessorsType, arc *memsto.AlertRuleCacheType,
targetCache *memsto.TargetCacheType, toarc *memsto.TargetsOfAlertRuleCacheType,
busiGroupCache *memsto.BusiGroupCacheType, alertMuteCache *memsto.AlertMuteCacheType, datasourceCache *memsto.DatasourceCacheType,
promClients *prom.PromClientMap, tdengineClients *tdengine.TdengineClientMap, naming *naming.Naming, ctx *ctx.Context, stats *astats.Stats) *Scheduler {
promClients *prom.PromClientMap, naming *naming.Naming, ctx *ctx.Context, stats *astats.Stats) *Scheduler {
scheduler := &Scheduler{
aconf: aconf,
alertRules: make(map[string]*AlertRuleWorker),
@@ -59,9 +56,8 @@ func NewScheduler(aconf aconf.Alert, externalProcessors *process.ExternalProcess
alertMuteCache: alertMuteCache,
datasourceCache: datasourceCache,
promClients: promClients,
tdengineClients: tdengineClients,
naming: naming,
promClients: promClients,
naming: naming,
ctx: ctx,
stats: stats,
@@ -95,7 +91,7 @@ func (s *Scheduler) syncAlertRules() {
}
ruleType := rule.GetRuleType()
if rule.IsPrometheusRule() || rule.IsLokiRule() || rule.IsTdengineRule() {
if rule.IsPrometheusRule() || rule.IsLokiRule() || rule.IsTdengineRule() || rule.IsClickHouseRule() || rule.IsElasticSearch() {
datasourceIds := s.datasourceCache.GetIDsByDsCateAndQueries(rule.Cate, rule.DatasourceQueries)
for _, dsId := range datasourceIds {
if !naming.DatasourceHashRing.IsHit(strconv.FormatInt(dsId, 10), fmt.Sprintf("%d", rule.Id), s.aconf.Heartbeat.Endpoint) {
@@ -118,7 +114,7 @@ func (s *Scheduler) syncAlertRules() {
}
processor := process.NewProcessor(s.aconf.Heartbeat.EngineName, rule, dsId, s.alertRuleCache, s.targetCache, s.targetsOfAlertRuleCache, s.busiGroupCache, s.alertMuteCache, s.datasourceCache, s.ctx, s.stats)
alertRule := NewAlertRuleWorker(rule, dsId, processor, s.promClients, s.tdengineClients, s.ctx)
alertRule := NewAlertRuleWorker(rule, dsId, processor, s.promClients, s.ctx)
alertRuleWorkers[alertRule.Hash()] = alertRule
}
} else if rule.IsHostRule() {
@@ -127,7 +123,7 @@ func (s *Scheduler) syncAlertRules() {
continue
}
processor := process.NewProcessor(s.aconf.Heartbeat.EngineName, rule, 0, s.alertRuleCache, s.targetCache, s.targetsOfAlertRuleCache, s.busiGroupCache, s.alertMuteCache, s.datasourceCache, s.ctx, s.stats)
alertRule := NewAlertRuleWorker(rule, 0, processor, s.promClients, s.tdengineClients, s.ctx)
alertRule := NewAlertRuleWorker(rule, 0, processor, s.promClients, s.ctx)
alertRuleWorkers[alertRule.Hash()] = alertRule
} else {
// 如果 rule 不是通过 prometheus engine 来告警的,则创建为 externalRule
@@ -153,6 +149,7 @@ func (s *Scheduler) syncAlertRules() {
for hash, rule := range alertRuleWorkers {
if _, has := s.alertRules[hash]; !has {
rule.Prepare()
time.Sleep(time.Duration(20) * time.Millisecond)
rule.Start()
s.alertRules[hash] = rule
}

View File

@@ -15,15 +15,16 @@ import (
"github.com/ccfos/nightingale/v6/alert/common"
"github.com/ccfos/nightingale/v6/alert/process"
"github.com/ccfos/nightingale/v6/dscache"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/pkg/hash"
"github.com/ccfos/nightingale/v6/pkg/parser"
"github.com/ccfos/nightingale/v6/pkg/poster"
promsdk "github.com/ccfos/nightingale/v6/pkg/prom"
promql2 "github.com/ccfos/nightingale/v6/pkg/promql"
"github.com/ccfos/nightingale/v6/pkg/unit"
"github.com/ccfos/nightingale/v6/prom"
"github.com/ccfos/nightingale/v6/tdengine"
"github.com/prometheus/common/model"
"github.com/robfig/cron/v3"
@@ -41,14 +42,15 @@ type AlertRuleWorker struct {
Processor *process.Processor
PromClients *prom.PromClientMap
TdengineClients *tdengine.TdengineClientMap
Ctx *ctx.Context
PromClients *prom.PromClientMap
Ctx *ctx.Context
Scheduler *cron.Cron
HostAndDeviceIdentCache sync.Map
LastSeriesStore map[uint64]models.DataResp
DeviceIdentHook func(arw *AlertRuleWorker, paramQuery models.ParamQuery) ([]string, error)
}
@@ -72,7 +74,7 @@ const (
Inner JoinType = "inner"
)
func NewAlertRuleWorker(rule *models.AlertRule, datasourceId int64, Processor *process.Processor, promClients *prom.PromClientMap, tdengineClients *tdengine.TdengineClientMap, ctx *ctx.Context) *AlertRuleWorker {
func NewAlertRuleWorker(rule *models.AlertRule, datasourceId int64, Processor *process.Processor, promClients *prom.PromClientMap, ctx *ctx.Context) *AlertRuleWorker {
arw := &AlertRuleWorker{
DatasourceId: datasourceId,
Quit: make(chan struct{}),
@@ -80,12 +82,12 @@ func NewAlertRuleWorker(rule *models.AlertRule, datasourceId int64, Processor *p
Processor: Processor,
PromClients: promClients,
TdengineClients: tdengineClients,
Ctx: ctx,
HostAndDeviceIdentCache: sync.Map{},
DeviceIdentHook: func(arw *AlertRuleWorker, paramQuery models.ParamQuery) ([]string, error) {
return nil, nil
},
LastSeriesStore: make(map[uint64]models.DataResp),
}
interval := rule.PromEvalInterval
@@ -138,11 +140,11 @@ func (arw *AlertRuleWorker) Prepare() {
}
func (arw *AlertRuleWorker) Start() {
logger.Infof("eval:%s started", arw.Key())
arw.Scheduler.Start()
}
func (arw *AlertRuleWorker) Eval() {
logger.Infof("eval:%s started", arw.Key())
if arw.Processor.PromEvalInterval == 0 {
arw.Processor.PromEvalInterval = getPromEvalInterval(arw.Processor.ScheduleEntry.Schedule)
}
@@ -167,12 +169,10 @@ func (arw *AlertRuleWorker) Eval() {
anomalyPoints, err = arw.GetPromAnomalyPoint(cachedRule.RuleConfig)
case models.HOST:
anomalyPoints, err = arw.GetHostAnomalyPoint(cachedRule.RuleConfig)
case models.TDENGINE:
anomalyPoints, recoverPoints, err = arw.GetTdengineAnomalyPoint(cachedRule, arw.Processor.DatasourceId())
case models.LOKI:
anomalyPoints, err = arw.GetPromAnomalyPoint(cachedRule.RuleConfig)
default:
return
anomalyPoints, recoverPoints = arw.GetAnomalyPoint(cachedRule, arw.Processor.DatasourceId())
}
if err != nil {
@@ -537,7 +537,7 @@ func (arw *AlertRuleWorker) getParamPermutation(paramVal map[string]models.Param
if len(query) == 0 {
paramsKeyAllLabel, err := getParamKeyAllLabel(varToLabel[paramKey], originPromql, readerClient)
if err != nil {
logger.Errorf("rule_eval:%s, fail to getParamKeyAllLabel, error:%v", arw.Key(), paramQuery.Query, err)
logger.Errorf("rule_eval:%s, fail to getParamKeyAllLabel, error:%v query:%s", arw.Key(), err, paramQuery.Query)
}
params = paramsKeyAllLabel
} else {
@@ -634,17 +634,27 @@ func (arw *AlertRuleWorker) getHostIdents(paramQuery models.ParamQuery) ([]strin
return nil, err
}
hostsQuery := models.GetHostsQuery(queries)
session := models.TargetFilterQueryBuild(arw.Ctx, hostsQuery, 0, 0)
var lst []*models.Target
err = session.Find(&lst).Error
if err != nil {
return nil, err
if !arw.Ctx.IsCenter {
lst, err := poster.PostByUrlsWithResp[[]*models.Target](arw.Ctx, "/v1/n9e/targets-of-host-query", queries)
if err != nil {
return nil, err
}
for i := range lst {
params = append(params, lst[i].Ident)
}
} else {
hostsQuery := models.GetHostsQuery(queries)
session := models.TargetFilterQueryBuild(arw.Ctx, hostsQuery, 0, 0)
var lst []*models.Target
err = session.Find(&lst).Error
if err != nil {
return nil, err
}
for i := range lst {
params = append(params, lst[i].Ident)
}
}
for i := range lst {
params = append(params, lst[i].Ident)
}
arw.HostAndDeviceIdentCache.Store(cacheKey, params)
return params, nil
}
@@ -681,69 +691,6 @@ func combine(paramKeys []string, paraMap map[string][]string, index int, current
}
}
func (arw *AlertRuleWorker) GetTdengineAnomalyPoint(rule *models.AlertRule, dsId int64) ([]models.AnomalyPoint, []models.AnomalyPoint, error) {
// 获取查询和规则判断条件
points := []models.AnomalyPoint{}
recoverPoints := []models.AnomalyPoint{}
ruleConfig := strings.TrimSpace(rule.RuleConfig)
if ruleConfig == "" {
logger.Warningf("rule_eval:%d promql is blank", rule.Id)
arw.Processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.Processor.DatasourceId()), GET_RULE_CONFIG, arw.Processor.BusiGroupCache.GetNameByBusiGroupId(arw.Rule.GroupId), fmt.Sprintf("%v", arw.Rule.Id)).Inc()
return points, recoverPoints, errors.New("rule config is nil")
}
var ruleQuery models.RuleQuery
err := json.Unmarshal([]byte(ruleConfig), &ruleQuery)
if err != nil {
logger.Warningf("rule_eval:%d promql parse error:%s", rule.Id, err.Error())
arw.Processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.Processor.DatasourceId()), GET_RULE_CONFIG, arw.Processor.BusiGroupCache.GetNameByBusiGroupId(arw.Rule.GroupId), fmt.Sprintf("%v", arw.Rule.Id)).Inc()
return points, recoverPoints, err
}
arw.Inhibit = ruleQuery.Inhibit
if len(ruleQuery.Queries) > 0 {
seriesStore := make(map[uint64]models.DataResp)
// 将不同查询的 hash 索引分组存放
seriesTagIndexes := make(map[string]map[uint64][]uint64)
for _, query := range ruleQuery.Queries {
seriesTagIndex := make(map[uint64][]uint64)
arw.Processor.Stats.CounterQueryDataTotal.WithLabelValues(fmt.Sprintf("%d", arw.DatasourceId)).Inc()
cli := arw.TdengineClients.GetCli(dsId)
if cli == nil {
logger.Warningf("rule_eval:%d tdengine client is nil", rule.Id)
arw.Processor.Stats.CounterQueryDataErrorTotal.WithLabelValues(fmt.Sprintf("%d", arw.DatasourceId)).Inc()
arw.Processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.Processor.DatasourceId()), GET_CLIENT, arw.Processor.BusiGroupCache.GetNameByBusiGroupId(arw.Rule.GroupId), fmt.Sprintf("%v", arw.Rule.Id)).Inc()
continue
}
series, err := cli.Query(query)
arw.Processor.Stats.CounterQueryDataTotal.WithLabelValues(fmt.Sprintf("%d", arw.DatasourceId)).Inc()
if err != nil {
logger.Warningf("rule_eval rid:%d query data error: %v", rule.Id, err)
arw.Processor.Stats.CounterQueryDataErrorTotal.WithLabelValues(fmt.Sprintf("%d", arw.DatasourceId)).Inc()
arw.Processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.Processor.DatasourceId()), QUERY_DATA, arw.Processor.BusiGroupCache.GetNameByBusiGroupId(arw.Rule.GroupId), fmt.Sprintf("%v", arw.Rule.Id)).Inc()
return points, recoverPoints, err
}
// 此条日志很重要,是告警判断的现场值
logger.Debugf("rule_eval rid:%d req:%+v resp:%+v", rule.Id, query, series)
MakeSeriesMap(series, seriesTagIndex, seriesStore)
ref, err := GetQueryRef(query)
if err != nil {
logger.Warningf("rule_eval rid:%d query ref error: %v query:%+v", rule.Id, err, query)
arw.Processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.Processor.DatasourceId()), GET_RULE_CONFIG, arw.Processor.BusiGroupCache.GetNameByBusiGroupId(arw.Rule.GroupId), fmt.Sprintf("%v", arw.Rule.Id)).Inc()
continue
}
seriesTagIndexes[ref] = seriesTagIndex
}
points, recoverPoints = GetAnomalyPoint(rule.Id, ruleQuery, seriesTagIndexes, seriesStore)
}
return points, recoverPoints, nil
}
func (arw *AlertRuleWorker) GetHostAnomalyPoint(ruleConfig string) ([]models.AnomalyPoint, error) {
var lst []models.AnomalyPoint
var severity int
@@ -886,122 +833,6 @@ func (arw *AlertRuleWorker) GetHostAnomalyPoint(ruleConfig string) ([]models.Ano
return lst, nil
}
func GetAnomalyPoint(ruleId int64, ruleQuery models.RuleQuery, seriesTagIndexes map[string]map[uint64][]uint64, seriesStore map[uint64]models.DataResp) ([]models.AnomalyPoint, []models.AnomalyPoint) {
points := []models.AnomalyPoint{}
recoverPoints := []models.AnomalyPoint{}
if len(ruleQuery.Triggers) == 0 {
return points, recoverPoints
}
if len(seriesTagIndexes) == 0 {
return points, recoverPoints
}
unitMap := make(map[string]string)
for _, query := range ruleQuery.Queries {
ref, unit, err := GetQueryRefAndUnit(query)
if err != nil {
continue
}
unitMap[ref] = unit
}
for _, trigger := range ruleQuery.Triggers {
// seriesTagIndex 的 key 仅做分组使用value 为每组 series 的 hash
seriesTagIndex := ProcessJoins(ruleId, trigger, seriesTagIndexes, seriesStore)
for _, seriesHash := range seriesTagIndex {
valuesUnitMap := make(map[string]unit.FormattedValue)
sort.Slice(seriesHash, func(i, j int) bool {
return seriesHash[i] < seriesHash[j]
})
m := make(map[string]interface{})
var ts int64
var sample models.DataResp
var value float64
for _, serieHash := range seriesHash {
series, exists := seriesStore[serieHash]
if !exists {
logger.Warningf("rule_eval rid:%d series:%+v not found", ruleId, series)
continue
}
t, v, exists := series.Last()
if !exists {
logger.Warningf("rule_eval rid:%d series:%+v value not found", ruleId, series)
continue
}
if !strings.Contains(trigger.Exp, "$"+series.Ref) {
// 表达式中不包含该变量
continue
}
if u, exists := unitMap[series.Ref]; exists {
valuesUnitMap[series.Ref] = unit.ValueFormatter(u, 2, v)
}
m["$"+series.Ref] = v
m["$"+series.Ref+"."+series.MetricName()] = v
ts = int64(t)
sample = series
value = v
}
isTriggered := parser.Calc(trigger.Exp, m)
// 此条日志很重要,是告警判断的现场值
logger.Infof("rule_eval rid:%d trigger:%+v exp:%s res:%v m:%v", ruleId, trigger, trigger.Exp, isTriggered, m)
var values string
for k, v := range m {
if !strings.Contains(k, ".") {
continue
}
values += fmt.Sprintf("%s:%v ", k, v)
}
point := models.AnomalyPoint{
Key: sample.MetricName(),
Labels: sample.Metric,
Timestamp: int64(ts),
Value: value,
Values: values,
Severity: trigger.Severity,
Triggered: isTriggered,
Query: fmt.Sprintf("query:%+v trigger:%+v", ruleQuery.Queries, trigger),
RecoverConfig: trigger.RecoverConfig,
ValuesUnit: valuesUnitMap,
}
if sample.Query != "" {
point.Query = sample.Query
}
// 恢复条件判断经过讨论是只在表达式模式下支持,表达式模式会通过 isTriggered 判断是告警点还是恢复点
// 1. 不设置恢复判断,满足恢复条件产生 recoverPoint 恢复,无数据不产生 anomalyPoint 恢复
// 2. 设置满足条件才恢复,仅可通过产生 recoverPoint 恢复,不能通过不产生 anomalyPoint 恢复
// 3. 设置无数据不恢复,仅可通过产生 recoverPoint 恢复,不产生 anomalyPoint 恢复
if isTriggered {
points = append(points, point)
} else {
switch trigger.RecoverConfig.JudgeType {
case models.Origin:
// 对齐原实现 do nothing
case models.RecoverOnCondition:
// 额外判断恢复条件,满足才恢复
fulfill := parser.Calc(trigger.RecoverConfig.RecoverExp, m)
if !fulfill {
continue
}
}
recoverPoints = append(recoverPoints, point)
}
}
}
return points, recoverPoints
}
func flatten(rehashed map[uint64][][]uint64) map[uint64][]uint64 {
seriesTagIndex := make(map[uint64][]uint64)
var i uint64
@@ -1481,3 +1312,233 @@ func fillVar(curRealQuery string, paramKey string, val string) string {
curRealQuery = strings.Replace(curRealQuery, fmt.Sprintf("\"$%s\"", paramKey), fmt.Sprintf("\"%s\"", val), -1)
return curRealQuery
}
func (arw *AlertRuleWorker) GetAnomalyPoint(rule *models.AlertRule, dsId int64) ([]models.AnomalyPoint, []models.AnomalyPoint) {
// 获取查询和规则判断条件
points := []models.AnomalyPoint{}
recoverPoints := []models.AnomalyPoint{}
ruleConfig := strings.TrimSpace(rule.RuleConfig)
if ruleConfig == "" {
logger.Warningf("rule_eval:%d promql is blank", rule.Id)
arw.Processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.Processor.DatasourceId()), GET_RULE_CONFIG, arw.Processor.BusiGroupCache.GetNameByBusiGroupId(arw.Rule.GroupId), fmt.Sprintf("%v", arw.Rule.Id)).Inc()
return points, recoverPoints
}
var ruleQuery models.RuleQuery
err := json.Unmarshal([]byte(ruleConfig), &ruleQuery)
if err != nil {
logger.Warningf("rule_eval:%d promql parse error:%s", rule.Id, err.Error())
arw.Processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.Processor.DatasourceId()), GET_RULE_CONFIG, arw.Processor.BusiGroupCache.GetNameByBusiGroupId(arw.Rule.GroupId), fmt.Sprintf("%v", arw.Rule.Id)).Inc()
return points, recoverPoints
}
arw.Inhibit = ruleQuery.Inhibit
if len(ruleQuery.Queries) > 0 {
seriesStore := make(map[uint64]models.DataResp)
seriesTagIndexes := make(map[string]map[uint64][]uint64, 0)
for _, query := range ruleQuery.Queries {
seriesTagIndex := make(map[uint64][]uint64)
plug, exists := dscache.DsCache.Get(rule.Cate, dsId)
if !exists {
logger.Warningf("rule_eval rid:%d datasource:%d not exists", rule.Id, dsId)
arw.Processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.Processor.DatasourceId()), GET_CLIENT, arw.Processor.BusiGroupCache.GetNameByBusiGroupId(arw.Rule.GroupId), fmt.Sprintf("%v", arw.Rule.Id)).Inc()
continue
}
series, err := plug.QueryData(context.Background(), query)
arw.Processor.Stats.CounterQueryDataTotal.WithLabelValues(fmt.Sprintf("%d", arw.DatasourceId)).Inc()
if err != nil {
logger.Warningf("rule_eval rid:%d query data error: %v", rule.Id, err)
arw.Processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.Processor.DatasourceId()), GET_CLIENT, arw.Processor.BusiGroupCache.GetNameByBusiGroupId(arw.Rule.GroupId), fmt.Sprintf("%v", arw.Rule.Id)).Inc()
continue
}
// 此条日志很重要,是告警判断的现场值
logger.Infof("rule_eval rid:%d req:%+v resp:%v", rule.Id, query, series)
for i := 0; i < len(series); i++ {
serieHash := hash.GetHash(series[i].Metric, series[i].Ref)
tagHash := hash.GetTagHash(series[i].Metric)
seriesStore[serieHash] = series[i]
// 将曲线按照相同的 tag 分组
if _, exists := seriesTagIndex[tagHash]; !exists {
seriesTagIndex[tagHash] = make([]uint64, 0)
}
seriesTagIndex[tagHash] = append(seriesTagIndex[tagHash], serieHash)
}
ref, err := GetQueryRef(query)
if err != nil {
logger.Warningf("rule_eval rid:%d query:%+v get ref error:%s", rule.Id, query, err.Error())
continue
}
seriesTagIndexes[ref] = seriesTagIndex
}
unitMap := make(map[string]string)
for _, query := range ruleQuery.Queries {
ref, unit, err := GetQueryRefAndUnit(query)
if err != nil {
continue
}
unitMap[ref] = unit
}
if !ruleQuery.ExpTriggerDisable {
for _, trigger := range ruleQuery.Triggers {
seriesTagIndex := ProcessJoins(rule.Id, trigger, seriesTagIndexes, seriesStore)
for _, seriesHash := range seriesTagIndex {
valuesUnitMap := make(map[string]unit.FormattedValue)
sort.Slice(seriesHash, func(i, j int) bool {
return seriesHash[i] < seriesHash[j]
})
m := make(map[string]interface{})
var ts int64
var sample models.DataResp
var value float64
for _, serieHash := range seriesHash {
series, exists := seriesStore[serieHash]
if !exists {
logger.Warningf("rule_eval rid:%d series:%+v not found", rule.Id, series)
continue
}
t, v, exists := series.Last()
if !exists {
logger.Warningf("rule_eval rid:%d series:%+v value not found", rule.Id, series)
continue
}
if !strings.Contains(trigger.Exp, "$"+series.Ref) {
// 表达式中不包含该变量
continue
}
m["$"+series.Ref] = v
m["$"+series.Ref+"."+series.MetricName()] = v
for k, v := range series.Metric {
if k == "__name__" {
continue
}
if !strings.Contains(trigger.Exp, "$"+series.Ref+"."+string(k)) {
// 过滤掉表达式中不包含的标签
continue
}
m["$"+series.Ref+"."+string(k)] = string(v)
}
if u, exists := unitMap[series.Ref]; exists {
valuesUnitMap["$"+series.Ref+"."+series.MetricName()] = unit.ValueFormatter(u, 2, v)
}
ts = int64(t)
sample = series
value = v
logger.Infof("rule_eval rid:%d origin series labels:%+v", rule.Id, series.Metric)
}
isTriggered := parser.CalcWithRid(trigger.Exp, m, rule.Id)
// 此条日志很重要,是告警判断的现场值
logger.Infof("rule_eval rid:%d trigger:%+v exp:%s res:%v m:%v", rule.Id, trigger, trigger.Exp, isTriggered, m)
var values string
for k, v := range m {
if !strings.Contains(k, ".") {
continue
}
switch v.(type) {
case float64:
values += fmt.Sprintf("%s:%.3f ", k, v)
case string:
values += fmt.Sprintf("%s:%s ", k, v)
}
}
point := models.AnomalyPoint{
Key: sample.MetricName(),
Labels: sample.Metric,
Timestamp: int64(ts),
Value: value,
Values: values,
Severity: trigger.Severity,
Triggered: isTriggered,
Query: fmt.Sprintf("query:%+v trigger:%+v", ruleQuery.Queries, trigger),
RecoverConfig: trigger.RecoverConfig,
ValuesUnit: valuesUnitMap,
}
if isTriggered {
points = append(points, point)
} else {
switch trigger.RecoverConfig.JudgeType {
case models.Origin:
// do nothing
case models.RecoverOnCondition:
fulfill := parser.CalcWithRid(trigger.RecoverConfig.RecoverExp, m, rule.Id)
if !fulfill {
continue
}
}
recoverPoints = append(recoverPoints, point)
}
}
}
}
if ruleQuery.NodataTrigger.Enable {
now := time.Now().Unix()
// 使用 arw.LastSeriesStore 检查上次查询结果
if len(arw.LastSeriesStore) > 0 {
// 遍历上次的曲线数据
for hash, lastSeries := range arw.LastSeriesStore {
if ruleQuery.NodataTrigger.ResolveAfterEnable {
lastTs, _, exists := lastSeries.Last()
if !exists {
continue
}
// 检查是否超过 resolve_after 时间
if now-int64(lastTs) > int64(ruleQuery.NodataTrigger.ResolveAfter) {
logger.Infof("rule_eval rid:%d series:%+v resolve after %d seconds now:%d lastTs:%d", rule.Id, lastSeries, ruleQuery.NodataTrigger.ResolveAfter, now, int64(lastTs))
delete(arw.LastSeriesStore, hash)
continue
}
}
// 检查是否在本次查询结果中存在
if _, exists := seriesStore[hash]; !exists {
// 生成无数据告警点
point := models.AnomalyPoint{
Key: lastSeries.MetricName(),
Labels: lastSeries.Metric,
Timestamp: now,
Value: 0,
Values: fmt.Sprintf("nodata since %v", time.Unix(now, 0).Format("2006-01-02 15:04:05")),
Severity: ruleQuery.NodataTrigger.Severity,
Triggered: true,
Query: fmt.Sprintf("nodata check for %s", lastSeries.LabelsString()),
TriggerType: models.TriggerTypeNodata,
}
points = append(points, point)
logger.Infof("rule_eval rid:%d nodata point:%+v", rule.Id, point)
}
}
}
// 更新 arw.LastSeriesStore
for hash, series := range seriesStore {
arw.LastSeriesStore[hash] = series
}
}
}
return points, recoverPoints
}

View File

@@ -224,6 +224,15 @@ func (p *Processor) BuildEvent(anomalyPoint models.AnomalyPoint, from string, no
event.RecoverConfig = anomalyPoint.RecoverConfig
event.RuleHash = ruleHash
if anomalyPoint.TriggerType == models.TriggerTypeNodata {
event.TriggerValue = "nodata"
ruleConfig := models.RuleQuery{}
json.Unmarshal([]byte(p.rule.RuleConfig), &ruleConfig)
ruleConfig.TriggerType = anomalyPoint.TriggerType
b, _ := json.Marshal(ruleConfig)
event.RuleConfig = string(b)
}
if err := json.Unmarshal([]byte(p.rule.Annotations), &event.AnnotationsJSON); err != nil {
event.AnnotationsJSON = make(map[string]string) // 解析失败时使用空 map
logger.Warningf("unmarshal annotations json failed: %v, rule: %d", err, p.rule.Id)

View File

@@ -4,6 +4,8 @@ import (
"context"
"fmt"
"github.com/ccfos/nightingale/v6/dscache"
"github.com/ccfos/nightingale/v6/alert"
"github.com/ccfos/nightingale/v6/alert/astats"
"github.com/ccfos/nightingale/v6/alert/dispatch"
@@ -27,14 +29,13 @@ import (
"github.com/ccfos/nightingale/v6/pkg/httpx"
"github.com/ccfos/nightingale/v6/pkg/i18nx"
"github.com/ccfos/nightingale/v6/pkg/logx"
"github.com/ccfos/nightingale/v6/pkg/macros"
"github.com/ccfos/nightingale/v6/pkg/version"
"github.com/ccfos/nightingale/v6/prom"
"github.com/ccfos/nightingale/v6/pushgw/idents"
pushgwrt "github.com/ccfos/nightingale/v6/pushgw/router"
"github.com/ccfos/nightingale/v6/pushgw/writer"
"github.com/ccfos/nightingale/v6/storage"
"github.com/ccfos/nightingale/v6/tdengine"
"github.com/flashcatcloud/ibex/src/cmd/ibex"
)
@@ -107,10 +108,11 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
dispatch.InitRegisterQueryFunc(promClients)
tdengineClients := tdengine.NewTdengineClient(ctx, config.Alert.Heartbeat)
externalProcessors := process.NewExternalProcessors()
alert.Start(config.Alert, config.Pushgw, syncStats, alertStats, externalProcessors, targetCache, busiGroupCache, alertMuteCache, alertRuleCache, notifyConfigCache, taskTplCache, dsCache, ctx, promClients, tdengineClients, userCache, userGroupCache)
macros.RegisterMacro(macros.MacroInVain)
dscache.Init(ctx, false)
alert.Start(config.Alert, config.Pushgw, syncStats, alertStats, externalProcessors, targetCache, busiGroupCache, alertMuteCache, alertRuleCache, notifyConfigCache, taskTplCache, dsCache, ctx, promClients, userCache, userGroupCache)
writers := writer.NewWriters(config.Pushgw)
@@ -120,7 +122,7 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
alertrtRouter := alertrt.New(config.HTTP, config.Alert, alertMuteCache, targetCache, busiGroupCache, alertStats, ctx, externalProcessors)
centerRouter := centerrt.New(config.HTTP, config.Center, config.Alert, config.Ibex,
cconf.Operations, dsCache, notifyConfigCache, promClients, tdengineClients,
cconf.Operations, dsCache, notifyConfigCache, promClients,
redis, sso, ctx, metas, idents, targetCache, userCache, userGroupCache)
pushgwRouter := pushgwrt.New(config.HTTP, config.Pushgw, config.Alert, targetCache, busiGroupCache, idents, metas, writers, ctx)

View File

@@ -92,7 +92,6 @@ func (s *Set) updateMeta(items map[string]models.HostMeta) {
func (s *Set) updateTargets(m map[string]models.HostMeta) error {
if s.redis == nil {
logger.Warningf("redis is nil")
return nil
}

View File

@@ -24,7 +24,6 @@ import (
"github.com/ccfos/nightingale/v6/prom"
"github.com/ccfos/nightingale/v6/pushgw/idents"
"github.com/ccfos/nightingale/v6/storage"
"github.com/ccfos/nightingale/v6/tdengine"
"github.com/gin-gonic/gin"
"github.com/rakyll/statik/fs"
@@ -42,7 +41,6 @@ type Router struct {
DatasourceCache *memsto.DatasourceCacheType
NotifyConfigCache *memsto.NotifyConfigCacheType
PromClients *prom.PromClientMap
TdendgineClients *tdengine.TdengineClientMap
Redis storage.Redis
MetaSet *metas.Set
IdentSet *idents.Set
@@ -57,7 +55,7 @@ type Router struct {
func New(httpConfig httpx.Config, center cconf.Center, alert aconf.Alert, ibex conf.Ibex,
operations cconf.Operation, ds *memsto.DatasourceCacheType, ncc *memsto.NotifyConfigCacheType,
pc *prom.PromClientMap, tdendgineClients *tdengine.TdengineClientMap, redis storage.Redis,
pc *prom.PromClientMap, redis storage.Redis,
sso *sso.SsoClient, ctx *ctx.Context, metaSet *metas.Set, idents *idents.Set,
tc *memsto.TargetCacheType, uc *memsto.UserCacheType, ugc *memsto.UserGroupCacheType) *Router {
return &Router{
@@ -69,7 +67,6 @@ func New(httpConfig httpx.Config, center cconf.Center, alert aconf.Alert, ibex c
DatasourceCache: ds,
NotifyConfigCache: ncc,
PromClients: pc,
TdendgineClients: tdendgineClients,
Redis: redis,
MetaSet: metaSet,
IdentSet: idents,
@@ -187,12 +184,24 @@ func (rt *Router) Config(r *gin.Engine) {
pages.POST("/datasource/query", rt.datasourceQuery)
pages.POST("/ds-query", rt.QueryData)
pages.POST("/logs-query", rt.QueryLog)
pages.POST("/logs-query", rt.QueryLogV2)
pages.POST("/tdengine-databases", rt.tdengineDatabases)
pages.POST("/tdengine-tables", rt.tdengineTables)
pages.POST("/tdengine-columns", rt.tdengineColumns)
pages.POST("/log-query-batch", rt.QueryLogBatch)
// 数据库元数据接口
pages.POST("/db-databases", rt.ShowDatabases)
pages.POST("/db-tables", rt.ShowTables)
pages.POST("/db-desc-table", rt.DescribeTable)
// es 专用接口
pages.POST("/indices", rt.auth(), rt.user(), rt.QueryIndices)
pages.POST("/es-variable", rt.auth(), rt.user(), rt.QueryESVariable)
pages.POST("/fields", rt.auth(), rt.user(), rt.QueryFields)
pages.POST("/log-query", rt.auth(), rt.user(), rt.QueryLog)
} else {
pages.Any("/proxy/:id/*url", rt.auth(), rt.dsProxy)
pages.POST("/query-range-batch", rt.auth(), rt.promBatchQueryRange)
@@ -201,11 +210,24 @@ func (rt *Router) Config(r *gin.Engine) {
pages.POST("/datasource/query", rt.auth(), rt.user(), rt.datasourceQuery)
pages.POST("/ds-query", rt.auth(), rt.QueryData)
pages.POST("/logs-query", rt.auth(), rt.QueryLog)
pages.POST("/logs-query", rt.auth(), rt.QueryLogV2)
pages.POST("/tdengine-databases", rt.auth(), rt.tdengineDatabases)
pages.POST("/tdengine-tables", rt.auth(), rt.tdengineTables)
pages.POST("/tdengine-columns", rt.auth(), rt.tdengineColumns)
pages.POST("/log-query-batch", rt.auth(), rt.user(), rt.QueryLogBatch)
// 数据库元数据接口
pages.POST("/db-databases", rt.auth(), rt.user(), rt.ShowDatabases)
pages.POST("/db-tables", rt.auth(), rt.user(), rt.ShowTables)
pages.POST("/db-desc-table", rt.auth(), rt.user(), rt.DescribeTable)
// es 专用接口
pages.POST("/indices", rt.auth(), rt.user(), rt.QueryIndices)
pages.POST("/es-variable", rt.QueryESVariable)
pages.POST("/fields", rt.QueryFields)
pages.POST("/log-query", rt.QueryLog)
}
pages.GET("/sql-template", rt.QuerySqlTemplate)
@@ -322,6 +344,11 @@ func (rt *Router) Config(r *gin.Engine) {
pages.GET("/share-charts", rt.chartShareGets)
pages.POST("/share-charts", rt.auth(), rt.chartShareAdd)
pages.POST("/dashboard-annotations", rt.auth(), rt.user(), rt.perm("/dashboards/put"), rt.dashAnnotationAdd)
pages.GET("/dashboard-annotations", rt.dashAnnotationGets)
pages.PUT("/dashboard-annotation/:id", rt.auth(), rt.user(), rt.perm("/dashboards/put"), rt.dashAnnotationPut)
pages.DELETE("/dashboard-annotation/:id", rt.auth(), rt.user(), rt.perm("/dashboards/del"), rt.dashAnnotationDel)
// pages.GET("/alert-rules/builtin/alerts-cates", rt.auth(), rt.user(), rt.builtinAlertCateGets)
// pages.GET("/alert-rules/builtin/list", rt.auth(), rt.user(), rt.builtinAlertRules)
pages.GET("/alert-rules/callbacks", rt.auth(), rt.user(), rt.alertRuleCallbacks)
@@ -479,6 +506,7 @@ func (rt *Router) Config(r *gin.Engine) {
pages.PUT("/builtin-payloads", rt.auth(), rt.user(), rt.perm("/built-in-components/put"), rt.builtinPayloadsPut)
pages.DELETE("/builtin-payloads", rt.auth(), rt.user(), rt.perm("/built-in-components/del"), rt.builtinPayloadsDel)
pages.GET("/builtin-payload", rt.auth(), rt.user(), rt.builtinPayloadsGetByUUIDOrID)
}
r.GET("/api/n9e/versions", func(c *gin.Context) {
@@ -521,6 +549,8 @@ func (rt *Router) Config(r *gin.Engine) {
service.PUT("/targets/note", rt.targetUpdateNoteByService)
service.PUT("/targets/bgid", rt.targetUpdateBgidByService)
service.POST("/targets-of-host-query", rt.targetsOfHostQuery)
service.POST("/alert-rules", rt.alertRuleAddByService)
service.POST("/alert-rule-add", rt.alertRuleAddOneByService)
service.DELETE("/alert-rules", rt.alertRuleDelByService)

View File

@@ -102,9 +102,29 @@ func (rt *Router) builtinMetricsDefaultTypes(c *gin.Context) {
func (rt *Router) builtinMetricsTypes(c *gin.Context) {
collector := ginx.QueryStr(c, "collector", "")
query := ginx.QueryStr(c, "query", "")
disabled := ginx.QueryInt(c, "disabled", -1)
lang := c.GetHeader("X-Language")
ginx.NewRender(c).Data(models.BuiltinMetricTypes(rt.Ctx, lang, collector, query))
metricTypeList, err := models.BuiltinMetricTypes(rt.Ctx, lang, collector, query)
ginx.Dangerous(err)
componentList, err := models.BuiltinComponentGets(rt.Ctx, "", disabled)
ginx.Dangerous(err)
// 创建一个 map 来存储 componentList 中的类型
componentTypes := make(map[string]struct{})
for _, comp := range componentList {
componentTypes[comp.Ident] = struct{}{}
}
filteredMetricTypeList := make([]string, 0)
for _, metricType := range metricTypeList {
if _, exists := componentTypes[metricType]; exists {
filteredMetricTypeList = append(filteredMetricTypeList, metricType)
}
}
ginx.NewRender(c).Data(filteredMetricTypeList, nil)
}
func (rt *Router) builtinMetricsCollectors(c *gin.Context) {

View File

@@ -0,0 +1,99 @@
package router
import (
"fmt"
"net/http"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
)
func checkAnnotationPermission(c *gin.Context, ctx *ctx.Context, dashboardId int64) {
dashboard, err := models.BoardGetByID(ctx, dashboardId)
if err != nil {
ginx.Bomb(http.StatusInternalServerError, "failed to get dashboard: %v", err)
}
if dashboard == nil {
ginx.Bomb(http.StatusNotFound, "dashboard not found")
}
bg := BusiGroup(ctx, dashboard.GroupId)
me := c.MustGet("user").(*models.User)
can, err := me.CanDoBusiGroup(ctx, bg, "rw")
ginx.Dangerous(err)
if !can {
ginx.Bomb(http.StatusForbidden, "forbidden")
}
}
func (rt *Router) dashAnnotationAdd(c *gin.Context) {
var f models.DashAnnotation
ginx.BindJSON(c, &f)
username := c.MustGet("username").(string)
now := time.Now().Unix()
checkAnnotationPermission(c, rt.Ctx, f.DashboardId)
f.CreateBy = username
f.CreateAt = now
f.UpdateBy = username
f.UpdateAt = now
ginx.NewRender(c).Data(f.Id, f.Add(rt.Ctx))
}
func (rt *Router) dashAnnotationGets(c *gin.Context) {
dashboardId := ginx.QueryInt64(c, "dashboard_id")
from := ginx.QueryInt64(c, "from")
to := ginx.QueryInt64(c, "to")
limit := ginx.QueryInt(c, "limit", 100)
lst, err := models.DashAnnotationGets(rt.Ctx, dashboardId, from, to, limit)
ginx.NewRender(c).Data(lst, err)
}
func (rt *Router) dashAnnotationPut(c *gin.Context) {
var f models.DashAnnotation
ginx.BindJSON(c, &f)
id := ginx.UrlParamInt64(c, "id")
annotation, err := getAnnotationById(rt.Ctx, id)
ginx.Dangerous(err)
checkAnnotationPermission(c, rt.Ctx, annotation.DashboardId)
f.Id = id
f.UpdateAt = time.Now().Unix()
f.UpdateBy = c.MustGet("username").(string)
ginx.NewRender(c).Message(f.Update(rt.Ctx))
}
func (rt *Router) dashAnnotationDel(c *gin.Context) {
id := ginx.UrlParamInt64(c, "id")
annotation, err := getAnnotationById(rt.Ctx, id)
ginx.Dangerous(err)
checkAnnotationPermission(c, rt.Ctx, annotation.DashboardId)
ginx.NewRender(c).Message(models.DashAnnotationDel(rt.Ctx, id))
}
// 可以提取获取注释的通用方法
func getAnnotationById(ctx *ctx.Context, id int64) (*models.DashAnnotation, error) {
annotation, err := models.DashAnnotationGet(ctx, "id=?", id)
if err != nil {
return nil, err
}
if annotation == nil {
return nil, fmt.Errorf("annotation not found")
}
return annotation, nil
}

View File

@@ -93,10 +93,12 @@ func (rt *Router) datasourceUpsert(c *gin.Context) {
var count int64
if !req.ForceSave {
err = DatasourceCheck(req)
if err != nil {
Dangerous(c, err)
return
if req.PluginType == models.PROMETHEUS || req.PluginType == models.LOKI || req.PluginType == models.TDENGINE {
err = DatasourceCheck(req)
if err != nil {
Dangerous(c, err)
return
}
}
}
@@ -122,7 +124,7 @@ func (rt *Router) datasourceUpsert(c *gin.Context) {
}
func DatasourceCheck(ds models.Datasource) error {
if ds.PluginType != models.ELASTICSEARCH {
if ds.PluginType == models.PROMETHEUS || ds.PluginType == models.LOKI || ds.PluginType == models.TDENGINE {
if ds.HTTPJson.Url == "" {
return fmt.Errorf("url is empty")
}

View File

@@ -0,0 +1,101 @@
package router
import (
"context"
"github.com/ccfos/nightingale/v6/dscache"
"github.com/ccfos/nightingale/v6/dskit/types"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/logger"
)
func (rt *Router) ShowDatabases(c *gin.Context) {
var f models.QueryParam
ginx.BindJSON(c, &f)
plug, exists := dscache.DsCache.Get(f.Cate, f.DatasourceId)
if !exists {
logger.Warningf("cluster:%d not exists", f.DatasourceId)
ginx.Bomb(200, "cluster not exists")
}
var databases []string
var err error
type DatabaseShower interface {
ShowDatabases(context.Context) ([]string, error)
}
switch plug.(type) {
case DatabaseShower:
databases, err = plug.(DatabaseShower).ShowDatabases(c.Request.Context())
ginx.Dangerous(err)
default:
ginx.Bomb(200, "datasource not exists")
}
if len(databases) == 0 {
databases = make([]string, 0)
}
ginx.NewRender(c).Data(databases, nil)
}
func (rt *Router) ShowTables(c *gin.Context) {
var f models.QueryParam
ginx.BindJSON(c, &f)
plug, exists := dscache.DsCache.Get(f.Cate, f.DatasourceId)
if !exists {
logger.Warningf("cluster:%d not exists", f.DatasourceId)
ginx.Bomb(200, "cluster not exists")
}
// 只接受一个入参
tables := make([]string, 0)
var err error
type TableShower interface {
ShowTables(ctx context.Context, database string) ([]string, error)
}
switch plug.(type) {
case TableShower:
if len(f.Querys) > 0 {
database, ok := f.Querys[0].(string)
if ok {
tables, err = plug.(TableShower).ShowTables(c.Request.Context(), database)
}
}
default:
ginx.Bomb(200, "datasource not exists")
}
ginx.NewRender(c).Data(tables, err)
}
func (rt *Router) DescribeTable(c *gin.Context) {
var f models.QueryParam
ginx.BindJSON(c, &f)
plug, exists := dscache.DsCache.Get(f.Cate, f.DatasourceId)
if !exists {
logger.Warningf("cluster:%d not exists", f.DatasourceId)
ginx.Bomb(200, "cluster not exists")
}
// 只接受一个入参
columns := make([]*types.ColumnProperty, 0)
var err error
type TableDescriber interface {
DescribeTable(context.Context, interface{}) ([]*types.ColumnProperty, error)
}
switch plug.(type) {
case TableDescriber:
client := plug.(TableDescriber)
if len(f.Querys) > 0 {
columns, err = client.DescribeTable(c.Request.Context(), f.Querys[0])
}
default:
ginx.Bomb(200, "datasource not exists")
}
ginx.NewRender(c).Data(columns, err)
}

View File

@@ -0,0 +1,77 @@
package router
import (
"github.com/ccfos/nightingale/v6/datasource/es"
"github.com/ccfos/nightingale/v6/dscache"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/logger"
)
type IndexReq struct {
Cate string `json:"cate"`
DatasourceId int64 `json:"datasource_id"`
Index string `json:"index"`
}
type FieldValueReq struct {
Cate string `json:"cate"`
DatasourceId int64 `json:"datasource_id"`
Index string `json:"index"`
Query FieldObj `json:"query"`
}
type FieldObj struct {
Find string `json:"find"`
Field string `json:"field"`
Query string `json:"query"`
}
func (rt *Router) QueryIndices(c *gin.Context) {
var f IndexReq
ginx.BindJSON(c, &f)
plug, exists := dscache.DsCache.Get(f.Cate, f.DatasourceId)
if !exists {
logger.Warningf("cluster:%d not exists", f.DatasourceId)
ginx.Bomb(200, "cluster not exists")
}
indices, err := plug.(*es.Elasticsearch).QueryIndices()
ginx.Dangerous(err)
ginx.NewRender(c).Data(indices, nil)
}
func (rt *Router) QueryFields(c *gin.Context) {
var f IndexReq
ginx.BindJSON(c, &f)
plug, exists := dscache.DsCache.Get(f.Cate, f.DatasourceId)
if !exists {
logger.Warningf("cluster:%d not exists", f.DatasourceId)
ginx.Bomb(200, "cluster not exists")
}
fields, err := plug.(*es.Elasticsearch).QueryFields([]string{f.Index})
ginx.Dangerous(err)
ginx.NewRender(c).Data(fields, nil)
}
func (rt *Router) QueryESVariable(c *gin.Context) {
var f FieldValueReq
ginx.BindJSON(c, &f)
plug, exists := dscache.DsCache.Get(f.Cate, f.DatasourceId)
if !exists {
logger.Warningf("cluster:%d not exists", f.DatasourceId)
ginx.Bomb(200, "cluster not exists")
}
fields, err := plug.(*es.Elasticsearch).QueryFieldValue([]string{f.Index}, f.Query.Field, f.Query.Query)
ginx.Dangerous(err)
ginx.NewRender(c).Data(fields, nil)
}

View File

@@ -0,0 +1,181 @@
package router
import (
"fmt"
"sort"
"github.com/ccfos/nightingale/v6/dscache"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/logger"
)
func CheckDsPerm(c *gin.Context, dsId int64, cate string, q interface{}) bool {
// todo: 后续需要根据 cate 判断是否需要权限
return true
}
type QueryFrom struct {
Queries []Query `json:"queries"`
Exps []Exp `json:"exps"`
}
type Query struct {
Ref string `json:"ref"`
Did int64 `json:"ds_id"`
DsCate string `json:"ds_cate"`
Query interface{} `json:"query"`
}
type Exp struct {
Exp string `json:"exp"`
Ref string `json:"ref"`
}
type LogResp struct {
Total int64 `json:"total"`
List []interface{} `json:"list"`
}
func (rt *Router) QueryLogBatch(c *gin.Context) {
var f QueryFrom
ginx.BindJSON(c, &f)
var resp LogResp
var errMsg string
for _, q := range f.Queries {
if !rt.Center.AnonymousAccess.PromQuerier && !CheckDsPerm(c, q.Did, q.DsCate, q) {
ginx.Bomb(200, "no permission")
}
plug, exists := dscache.DsCache.Get(q.DsCate, q.Did)
if !exists {
logger.Warningf("cluster:%d not exists query:%+v", q.Did, q)
ginx.Bomb(200, "cluster not exists")
}
data, total, err := plug.QueryLog(c.Request.Context(), q.Query)
if err != nil {
errMsg += fmt.Sprintf("query data error: %v query:%v\n ", err, q)
logger.Warningf("query data error: %v query:%v", err, q)
continue
}
m := make(map[string]interface{})
m["ref"] = q.Ref
m["ds_id"] = q.Did
m["ds_cate"] = q.DsCate
m["data"] = data
resp.List = append(resp.List, m)
resp.Total += total
}
if errMsg != "" || len(resp.List) == 0 {
ginx.Bomb(200, errMsg)
}
ginx.NewRender(c).Data(resp, nil)
}
func (rt *Router) QueryData(c *gin.Context) {
var f models.QueryParam
ginx.BindJSON(c, &f)
var resp []models.DataResp
var err error
for _, q := range f.Querys {
if !rt.Center.AnonymousAccess.PromQuerier && !CheckDsPerm(c, f.DatasourceId, f.Cate, q) {
ginx.Bomb(403, "no permission")
}
plug, exists := dscache.DsCache.Get(f.Cate, f.DatasourceId)
if !exists {
logger.Warningf("cluster:%d not exists", f.DatasourceId)
ginx.Bomb(200, "cluster not exists")
}
var datas []models.DataResp
datas, err = plug.QueryData(c.Request.Context(), q)
if err != nil {
logger.Warningf("query data error: req:%+v err:%v", q, err)
ginx.Bomb(200, "err:%v", err)
}
logger.Debugf("query data: req:%+v resp:%+v", q, datas)
resp = append(resp, datas...)
}
// 面向API的统一处理
// 按照 .Metric 排序
// 确保仪表盘中相同图例的曲线颜色相同
if len(resp) > 1 {
sort.Slice(resp, func(i, j int) bool {
if resp[i].Metric != nil && resp[j].Metric != nil {
return resp[i].Metric.String() < resp[j].Metric.String()
}
return false
})
}
ginx.NewRender(c).Data(resp, err)
}
func (rt *Router) QueryLogV2(c *gin.Context) {
var f models.QueryParam
ginx.BindJSON(c, &f)
var resp LogResp
var errMsg string
for _, q := range f.Querys {
if !rt.Center.AnonymousAccess.PromQuerier && !CheckDsPerm(c, f.DatasourceId, f.Cate, q) {
ginx.Bomb(200, "no permission")
}
plug, exists := dscache.DsCache.Get(f.Cate, f.DatasourceId)
if !exists {
logger.Warningf("cluster:%d not exists query:%+v", f.DatasourceId, f)
ginx.Bomb(200, "cluster not exists")
}
data, total, err := plug.QueryLog(c.Request.Context(), q)
if err != nil {
errMsg += fmt.Sprintf("query data error: %v query:%v\n ", err, q)
logger.Warningf("query data error: %v query:%v", err, q)
continue
}
resp.List = append(resp.List, data...)
resp.Total += total
}
if errMsg != "" || len(resp.List) == 0 {
ginx.Bomb(200, errMsg)
}
ginx.NewRender(c).Data(resp, nil)
}
func (rt *Router) QueryLog(c *gin.Context) {
var f models.QueryParam
ginx.BindJSON(c, &f)
var resp []interface{}
for _, q := range f.Querys {
if !rt.Center.AnonymousAccess.PromQuerier && !CheckDsPerm(c, f.DatasourceId, f.Cate, q) {
ginx.Bomb(200, "no permission")
}
plug, exists := dscache.DsCache.Get("elasticsearch", f.DatasourceId)
if !exists {
logger.Warningf("cluster:%d not exists", f.DatasourceId)
ginx.Bomb(200, "cluster not exists")
}
data, _, err := plug.QueryLog(c.Request.Context(), q)
if err != nil {
logger.Warningf("query data error: %v", err)
ginx.Bomb(200, "err:%v", err)
continue
}
resp = append(resp, data...)
}
ginx.NewRender(c).Data(resp, nil)
}

View File

@@ -406,16 +406,15 @@ func haveNeverGroupedIdent(ctx *ctx.Context, idents []string) (bool, error) {
if err != nil {
return false, err
}
if len(bgids) <= 0 {
return true, nil
}
}
}
return false, nil
}
func (rt *Router) targetBindBgids(c *gin.Context) {
var f targetBgidsForm
var err error
@@ -571,3 +570,18 @@ func (rt *Router) targetsOfAlertRule(c *gin.Context) {
ginx.NewRender(c).Data(ret, err)
}
func (rt *Router) targetsOfHostQuery(c *gin.Context) {
var queries []models.HostQuery
ginx.BindJSON(c, &queries)
hostsQuery := models.GetHostsQuery(queries)
session := models.TargetFilterQueryBuild(rt.Ctx, hostsQuery, 0, 0)
var lst []*models.Target
err := session.Find(&lst).Error
if err != nil {
ginx.Bomb(http.StatusInternalServerError, err.Error())
}
ginx.NewRender(c).Data(lst, nil)
}

View File

@@ -1,14 +1,14 @@
package router
import (
"net/http"
"fmt"
"github.com/ccfos/nightingale/v6/center/cconf"
"github.com/ccfos/nightingale/v6/datasource/tdengine"
"github.com/ccfos/nightingale/v6/dscache"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/logger"
"net/http"
)
type databasesQueryForm struct {
@@ -20,13 +20,13 @@ func (rt *Router) tdengineDatabases(c *gin.Context) {
var f databasesQueryForm
ginx.BindJSON(c, &f)
tdClient := rt.TdendgineClients.GetCli(f.DatasourceId)
if tdClient == nil {
datasource, hit := dscache.DsCache.Get(f.Cate, f.DatasourceId)
if _, ok := datasource.(*tdengine.TDengine); !hit || !ok {
ginx.NewRender(c, http.StatusNotFound).Message("No such datasource")
return
}
databases, err := tdClient.GetDatabases()
databases, err := datasource.(*tdengine.TDengine).ShowDatabases(rt.Ctx.Ctx)
ginx.NewRender(c).Data(databases, err)
}
@@ -37,18 +37,29 @@ type tablesQueryForm struct {
IsStable bool `json:"is_stable"`
}
type Column struct {
Name string `json:"name"`
Type string `json:"type"`
Size int `json:"size"`
}
// get tdengine tables
func (rt *Router) tdengineTables(c *gin.Context) {
var f tablesQueryForm
ginx.BindJSON(c, &f)
tdClient := rt.TdendgineClients.GetCli(f.DatasourceId)
if tdClient == nil {
datasource, hit := dscache.DsCache.Get(f.Cate, f.DatasourceId)
if _, ok := datasource.(*tdengine.TDengine); !hit || !ok {
ginx.NewRender(c, http.StatusNotFound).Message("No such datasource")
return
}
tables, err := tdClient.GetTables(f.Database, f.IsStable)
database := fmt.Sprintf("%s.tables", f.Database)
if f.IsStable {
database = fmt.Sprintf("%s.stables", f.Database)
}
tables, err := datasource.(*tdengine.TDengine).ShowTables(rt.Ctx.Ctx, database)
ginx.NewRender(c).Data(tables, err)
}
@@ -59,50 +70,31 @@ type columnsQueryForm struct {
Table string `json:"table"`
}
// get tdengine columns
func (rt *Router) tdengineColumns(c *gin.Context) {
var f columnsQueryForm
ginx.BindJSON(c, &f)
tdClient := rt.TdendgineClients.GetCli(f.DatasourceId)
if tdClient == nil {
datasource, hit := dscache.DsCache.Get(f.Cate, f.DatasourceId)
if _, ok := datasource.(*tdengine.TDengine); !hit || !ok {
ginx.NewRender(c, http.StatusNotFound).Message("No such datasource")
return
}
columns, err := tdClient.GetColumns(f.Database, f.Table)
ginx.NewRender(c).Data(columns, err)
}
func (rt *Router) QueryData(c *gin.Context) {
var f models.QueryParam
ginx.BindJSON(c, &f)
var resp []models.DataResp
var err error
tdClient := rt.TdendgineClients.GetCli(f.DatasourceId)
for _, q := range f.Querys {
datas, err := tdClient.Query(q)
ginx.Dangerous(err)
resp = append(resp, datas...)
query := map[string]string{
"database": f.Database,
"table": f.Table,
}
ginx.NewRender(c).Data(resp, err)
}
func (rt *Router) QueryLog(c *gin.Context) {
var f models.QueryParam
ginx.BindJSON(c, &f)
tdClient := rt.TdendgineClients.GetCli(f.DatasourceId)
if len(f.Querys) == 0 {
ginx.Bomb(200, "querys is empty")
return
columns, err := datasource.(*tdengine.TDengine).DescribeTable(rt.Ctx.Ctx, query)
// 对齐前端,后续可以将 tdEngine 的查数据的接口都统一
tdColumns := make([]Column, len(columns))
for i, column := range columns {
tdColumns[i] = Column{
Name: column.Field,
Type: column.Type,
}
}
data, err := tdClient.QueryLog(f.Querys[0])
logger.Debugf("tdengine query:%s result: %+v", f.Querys[0], data)
ginx.NewRender(c).Data(data, err)
ginx.NewRender(c).Data(tdColumns, err)
}
// query sql template

View File

@@ -5,6 +5,8 @@ import (
"errors"
"fmt"
"github.com/ccfos/nightingale/v6/dscache"
"github.com/ccfos/nightingale/v6/alert"
"github.com/ccfos/nightingale/v6/alert/astats"
"github.com/ccfos/nightingale/v6/alert/dispatch"
@@ -17,13 +19,12 @@ import (
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/pkg/httpx"
"github.com/ccfos/nightingale/v6/pkg/logx"
"github.com/ccfos/nightingale/v6/pkg/macros"
"github.com/ccfos/nightingale/v6/prom"
"github.com/ccfos/nightingale/v6/pushgw/idents"
pushgwrt "github.com/ccfos/nightingale/v6/pushgw/router"
"github.com/ccfos/nightingale/v6/pushgw/writer"
"github.com/ccfos/nightingale/v6/storage"
"github.com/ccfos/nightingale/v6/tdengine"
"github.com/flashcatcloud/ibex/src/cmd/ibex"
)
@@ -61,6 +62,8 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
r := httpx.GinEngine(config.Global.RunMode, config.HTTP, configCvalCache.PrintBodyPaths, configCvalCache.PrintAccessLog)
pushgwRouter.Config(r)
macros.RegisterMacro(macros.MacroInVain)
dscache.Init(ctx, false)
if !config.Alert.Disable {
configCache := memsto.NewConfigCache(ctx, syncStats, nil, "")
@@ -77,11 +80,10 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
dispatch.InitRegisterQueryFunc(promClients)
tdengineClients := tdengine.NewTdengineClient(ctx, config.Alert.Heartbeat)
externalProcessors := process.NewExternalProcessors()
alert.Start(config.Alert, config.Pushgw, syncStats, alertStats, externalProcessors, targetCache, busiGroupCache, alertMuteCache,
alertRuleCache, notifyConfigCache, taskTplsCache, dsCache, ctx, promClients, tdengineClients, userCache, userGroupCache)
alertRuleCache, notifyConfigCache, taskTplsCache, dsCache, ctx, promClients, userCache, userGroupCache)
alertrtRouter := alertrt.New(config.HTTP, config.Alert, alertMuteCache, targetCache, busiGroupCache, alertStats, ctx, externalProcessors)

241
datasource/ck/clickhouse.go Normal file
View File

@@ -0,0 +1,241 @@
package ck
import (
"context"
"fmt"
"strings"
"github.com/ccfos/nightingale/v6/datasource"
ck "github.com/ccfos/nightingale/v6/dskit/clickhouse"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/macros"
"github.com/mitchellh/mapstructure"
"github.com/toolkits/pkg/logger"
)
const (
CKType = "ck"
TimeFieldFormatEpochMilli = "epoch_millis"
TimeFieldFormatEpochSecond = "epoch_second"
DefaultLimit = 500
)
var (
ckPrivBanned = []string{
"INSERT",
"CREATE",
"DROP",
"DELETE",
"UPDATE",
"ALL",
}
ckBannedOp = map[string]struct{}{
"CREATE": {},
"INSERT": {},
"ALTER": {},
"REVOKE": {},
"DROP": {},
"RENAME": {},
"ATTACH": {},
"DETACH": {},
"OPTIMIZE": {},
"TRUNCATE": {},
"SET": {},
}
)
func init() {
datasource.RegisterDatasource(CKType, new(Clickhouse))
}
type CKShard struct {
Addr string `json:"ck.addr" mapstructure:"ck.addr"`
User string `json:"ck.user" mapstructure:"ck.user"`
Password string `json:"ck.password" mapstructure:"ck.password"`
Database string `json:"ck.db" mapstructure:"ck.db"`
IsEncrypted bool `json:"ck.is_encrypt" mapstructure:"ck.is_encrypt"`
}
type QueryParam struct {
Limit int `json:"limit" mapstructure:"limit"`
Sql string `json:"sql" mapstructure:"sql"`
Ref string `json:"ref" mapstructure:"ref"`
From int64 `json:"from" mapstructure:"from"`
To int64 `json:"to" mapstructure:"to"`
TimeField string `json:"time_field" mapstructure:"time_field"`
TimeFormat string `json:"time_format" mapstructure:"time_format"`
Keys datasource.Keys `json:"keys" mapstructure:"keys"`
Database string `json:"database" mapstructure:"database"`
Table string `json:"table" mapstructure:"table"`
}
type Clickhouse struct {
ck.Clickhouse `json:",inline" mapstructure:",squash"`
}
func (c *Clickhouse) Init(settings map[string]interface{}) (datasource.Datasource, error) {
newest := new(Clickhouse)
err := mapstructure.Decode(settings, newest)
return newest, err
}
func (c *Clickhouse) InitClient() error {
return c.InitCli()
}
func (c *Clickhouse) Validate(ctx context.Context) error {
if len(c.Nodes) == 0 {
return fmt.Errorf("ck shard is invalid, please check datasource setting")
}
addr := c.Nodes[0]
if len(strings.Trim(c.User, " ")) == 0 {
return fmt.Errorf("ck shard user is invalid, please check datasource setting")
}
if len(strings.Trim(addr, " ")) == 0 {
return fmt.Errorf("ck shard addr is invalid, please check datasource setting")
}
// if len(strings.Trim(shard.Password, " ")) == 0 {
// return fmt.Errorf("ck shard password is empty, please check datasource setting or set password for user")
// }
return nil
}
// Equal compares whether two objects are the same, used for caching
func (c *Clickhouse) Equal(p datasource.Datasource) bool {
plg, ok := p.(*Clickhouse)
if !ok {
logger.Errorf("unexpected plugin type, expected is ck")
return false
}
// only compare first shard
if len(c.Nodes) == 0 {
logger.Errorf("ck shard is empty")
return false
}
addr := c.Nodes[0]
if len(plg.Nodes) == 0 {
logger.Errorf("new ck plugin obj shard is empty")
return false
}
newAddr := plg.Nodes[0]
if c.User != plg.User {
return false
}
if addr != newAddr {
return false
}
if c.Password != plg.Password {
return false
}
return true
}
func (c *Clickhouse) MakeLogQuery(ctx context.Context, query interface{}, eventTags []string, start, end int64) (interface{}, error) {
return nil, nil
}
func (c *Clickhouse) MakeTSQuery(ctx context.Context, query interface{}, eventTags []string, start, end int64) (interface{}, error) {
return nil, nil
}
func (c *Clickhouse) QueryMapData(ctx context.Context, query interface{}) ([]map[string]string, error) {
return nil, nil
}
func (c *Clickhouse) QueryData(ctx context.Context, query interface{}) ([]models.DataResp, error) {
ckQueryParam := new(ck.QueryParam)
if err := mapstructure.Decode(query, ckQueryParam); err != nil {
return nil, err
}
if strings.Contains(ckQueryParam.Sql, "$__") {
var err error
ckQueryParam.Sql, err = macros.Macro(ckQueryParam.Sql, ckQueryParam.From, ckQueryParam.To)
if err != nil {
return nil, err
}
}
if ckQueryParam.Keys.ValueKey == "" {
return nil, fmt.Errorf("valueKey is required")
}
rows, err := c.QueryTimeseries(ctx, ckQueryParam)
if err != nil {
logger.Warningf("query:%+v get data err:%v", ckQueryParam, err)
return nil, err
}
if err != nil {
logger.Warningf("query:%+v get data err:%v", ckQueryParam, err)
return []models.DataResp{}, err
}
data := make([]models.DataResp, 0)
for i := range rows {
data = append(data, models.DataResp{
Ref: ckQueryParam.Ref,
Metric: rows[i].Metric,
Values: rows[i].Values,
})
}
return data, nil
}
func (c *Clickhouse) QueryLog(ctx context.Context, query interface{}) ([]interface{}, int64, error) {
ckQueryParam := new(QueryParam)
if err := mapstructure.Decode(query, ckQueryParam); err != nil {
return nil, 0, err
}
if strings.Contains(ckQueryParam.Sql, "$__") {
var err error
ckQueryParam.Sql, err = macros.Macro(ckQueryParam.Sql, ckQueryParam.From, ckQueryParam.To)
if err != nil {
return nil, 0, err
}
}
rows, err := c.Query(ctx, ckQueryParam)
if err != nil {
logger.Warningf("query:%+v get data err:%v", ckQueryParam, err)
return nil, 0, err
}
limit := getLimit(len(rows), ckQueryParam.Limit)
logs := make([]interface{}, 0)
for i := 0; i < limit; i++ {
logs = append(logs, rows[i])
}
return logs, int64(limit), nil
}
func getLimit(rowLen, pLimit int) int {
limit := DefaultLimit
if pLimit > 0 {
limit = pLimit
}
if rowLen > limit {
return limit
}
return rowLen
}

View File

@@ -0,0 +1,603 @@
package eslike
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"github.com/araddon/dateparse"
"github.com/bitly/go-simplejson"
"github.com/ccfos/nightingale/v6/models"
"github.com/mitchellh/mapstructure"
"github.com/olivere/elastic/v7"
"github.com/prometheus/common/model"
"github.com/toolkits/pkg/logger"
)
type Query struct {
Ref string `json:"ref" mapstructure:"ref"`
Index string `json:"index" mapstructure:"index"`
Filter string `json:"filter" mapstructure:"filter"`
MetricAggr MetricAggr `json:"value" mapstructure:"value"`
GroupBy []GroupBy `json:"group_by" mapstructure:"group_by"`
DateField string `json:"date_field" mapstructure:"date_field"`
Interval int64 `json:"interval" mapstructure:"interval"`
Start int64 `json:"start" mapstructure:"start"`
End int64 `json:"end" mapstructure:"end"`
P int `json:"page" mapstructure:"page"` // 页码
Limit int `json:"limit" mapstructure:"limit"` // 每页个数
Ascending bool `json:"ascending" mapstructure:"ascending"` // 按照DataField排序
Timeout int `json:"timeout" mapstructure:"timeout"`
MaxShard int `json:"max_shard" mapstructure:"max_shard"`
}
type MetricAggr struct {
Field string `json:"field" mapstructure:"field"`
Func string `json:"func" mapstructure:"func"`
Ref string `json:"ref" mapstructure:"ref"` // 变量名A B C
}
type GroupBy struct {
Cate GroupByCate `json:"cate" mapstructure:"cate"` // 分组类型
Field string `json:"field" mapstructure:"field"`
MinDocCount int64 `json:"min_doc_count" mapstructure:"min_doc_count"`
Order string `json:"order" mapstructure:"order"`
OrderBy string `json:"order_by" mapstructure:"order_by"`
Size int `json:"size" mapstructure:"size"`
Params []Param `json:"params" mapstructure:"params"` // 类型是 filter 时使用
Interval int64 `json:"interval" mapstructure:"interval"` // 分组间隔
}
type SearchFunc func(ctx context.Context, indices []string, source interface{}, timeout int, maxShard int) (*elastic.SearchResult, error)
type QueryFieldsFunc func(indices []string) ([]string, error)
// 分组类型
type GroupByCate string
const (
Filters GroupByCate = "filters"
Histgram GroupByCate = "histgram"
Terms GroupByCate = "terms"
)
// 参数
type Param struct {
Alias string `json:"alias,omitempty"` // 别名a=b的形式filter 特有
Query string `json:"query,omitempty"` // 查询条件filter 特有
}
type MetricPtr struct {
Data map[string][][]float64
}
func IterGetMap(m, ret map[string]interface{}, prefixKey string) {
for k, v := range m {
switch v.(type) {
case map[string]interface{}:
var key string
if prefixKey != "" {
key = fmt.Sprintf("%s.%s", prefixKey, k)
} else {
key = k
}
IterGetMap(v.(map[string]interface{}), ret, key)
default:
ret[prefixKey+"."+k] = []interface{}{v}
}
}
}
func TransferData(metric, ref string, m map[string][][]float64) []models.DataResp {
var datas []models.DataResp
for k, v := range m {
data := models.DataResp{
Ref: ref,
Metric: make(model.Metric),
Labels: k,
Values: v,
}
data.Metric["__name__"] = model.LabelValue(metric)
labels := strings.Split(k, "--")
for _, label := range labels {
arr := strings.SplitN(label, "=", 2)
if len(arr) == 2 {
data.Metric[model.LabelName(arr[0])] = model.LabelValue(arr[1])
}
}
datas = append(datas, data)
}
for i := 0; i < len(datas); i++ {
for k, v := range datas[i].Metric {
if k == "__name__" {
datas[i].Metric[k] = model.LabelValue(ref) + "_" + v
}
}
}
return datas
}
func GetQueryString(filter string, q *elastic.RangeQuery) *elastic.BoolQuery {
var queryString *elastic.BoolQuery
if filter != "" {
if strings.Contains(filter, ":") || strings.Contains(filter, "AND") || strings.Contains(filter, "OR") || strings.Contains(filter, "NOT") {
queryString = elastic.NewBoolQuery().Must(elastic.NewQueryStringQuery(filter)).Filter(q)
} else {
queryString = elastic.NewBoolQuery().Filter(elastic.NewMultiMatchQuery(filter).Lenient(true).Type("phrase")).Filter(q)
}
} else {
queryString = elastic.NewBoolQuery().Should(q)
}
return queryString
}
func getUnixTs(timeStr string) int64 {
ts, err := strconv.ParseInt(timeStr, 10, 64)
if err == nil {
return ts
}
parsedTime, err := dateparse.ParseAny(timeStr)
if err != nil {
logger.Error("failed to ParseAny: ", err)
return 0
}
return parsedTime.UnixMilli()
}
func GetBuckts(labelKey string, keys []string, arr []interface{}, metrics *MetricPtr, labels string, ts int64, f string) {
var err error
bucketsKey := ""
if len(keys) > 0 {
bucketsKey = keys[0]
}
newlabels := ""
for i := 0; i < len(arr); i++ {
tmp := arr[i].(map[string]interface{})
keyAsString, getTs := tmp["key_as_string"]
if getTs {
ts = getUnixTs(keyAsString.(string))
}
keyValue := tmp["key"]
switch keyValue.(type) {
case json.Number, string:
if !getTs {
if labels != "" {
newlabels = fmt.Sprintf("%s--%s=%v", labels, labelKey, keyValue)
} else {
newlabels = fmt.Sprintf("%s=%v", labelKey, keyValue)
}
}
default:
continue
}
var finalValue float64
if len(keys) == 0 { // 计算 doc_count 的情况
count := tmp["doc_count"]
finalValue, err = count.(json.Number).Float64()
if err != nil {
logger.Warningf("labelKey:%s get value error:%v", labelKey, err)
}
newValues := []float64{float64(ts / 1000), finalValue}
metrics.Data[newlabels] = append(metrics.Data[newlabels], newValues)
continue
}
innerBuckets, exists := tmp[bucketsKey]
if !exists {
continue
}
nextBucketsArr, exists := innerBuckets.(map[string]interface{})["buckets"]
if exists {
if len(keys[1:]) >= 1 {
GetBuckts(bucketsKey, keys[1:], nextBucketsArr.([]interface{}), metrics, newlabels, ts, f)
} else {
GetBuckts(bucketsKey, []string{}, nextBucketsArr.([]interface{}), metrics, newlabels, ts, f)
}
} else {
// doc_count
if f == "count" || f == "nodata" {
count := tmp["doc_count"]
finalValue, err = count.(json.Number).Float64()
if err != nil {
logger.Warningf("get %v value error:%v", count, err)
}
} else {
values, exists := innerBuckets.(map[string]interface{})["value"]
if exists {
switch values.(type) {
case json.Number:
value, err := values.(json.Number).Float64()
if err != nil {
logger.Warningf("labelKey:%s get value error:%v", labelKey, err)
}
finalValue = value
}
} else {
switch values.(type) {
case map[string]interface{}:
var err error
values := innerBuckets.(map[string]interface{})["values"]
for _, v := range values.(map[string]interface{}) {
finalValue, err = v.(json.Number).Float64()
if err != nil {
logger.Warningf("labelKey:%s get value error:%v", labelKey, err)
}
}
default:
values := innerBuckets.(map[string]interface{})["values"]
for _, v := range values.(map[string]interface{}) {
// Todo 修复 v is nil 导致 panic 情况
finalValue, err = v.(json.Number).Float64()
if err != nil {
logger.Warningf("labelKey:%s get value error:%v", labelKey, err)
}
}
}
}
}
if _, exists := metrics.Data[newlabels]; !exists {
metrics.Data[newlabels] = [][]float64{}
}
newValues := []float64{float64(ts / 1000), finalValue}
metrics.Data[newlabels] = append(metrics.Data[newlabels], newValues)
}
}
}
func MakeLogQuery(ctx context.Context, query interface{}, eventTags []string, start, end int64) (interface{}, error) {
param := new(Query)
if err := mapstructure.Decode(query, param); err != nil {
return nil, err
}
for i := 0; i < len(eventTags); i++ {
eventTags[i] = strings.Replace(eventTags[i], "=", ":", 1)
}
if len(eventTags) > 0 {
if param.Filter == "" {
param.Filter = strings.Join(eventTags, " AND ")
} else {
param.Filter = param.Filter + " AND " + strings.Join(eventTags, " AND ")
}
}
param.Start = start
param.End = end
return param, nil
}
func MakeTSQuery(ctx context.Context, query interface{}, eventTags []string, start, end int64) (interface{}, error) {
param := new(Query)
if err := mapstructure.Decode(query, param); err != nil {
return nil, err
}
for i := 0; i < len(eventTags); i++ {
eventTags[i] = strings.Replace(eventTags[i], "=", ":", 1)
}
if len(eventTags) > 0 {
if param.Filter == "" {
param.Filter = strings.Join(eventTags, " AND ")
} else {
param.Filter = param.Filter + " AND " + strings.Join(eventTags, " AND ")
}
}
param.Start = start
param.End = end
return param, nil
}
func QueryData(ctx context.Context, queryParam interface{}, cliTimeout int64, version string, search SearchFunc) ([]models.DataResp, error) {
param := new(Query)
if err := mapstructure.Decode(queryParam, param); err != nil {
return nil, err
}
if param.Timeout == 0 {
param.Timeout = int(cliTimeout) / 1000
}
if param.Interval == 0 {
param.Interval = 60
}
if param.MaxShard < 1 {
param.MaxShard = 5
}
if param.DateField == "" {
param.DateField = "@timestamp"
}
indexArr := strings.Split(param.Index, ",")
q := elastic.NewRangeQuery(param.DateField)
now := time.Now().Unix()
var start, end int64
if param.End != 0 && param.Start != 0 {
end = param.End - param.End%param.Interval
start = param.Start - param.Start%param.Interval
} else {
end = now
start = end - param.Interval
}
delay, ok := ctx.Value("delay").(int64)
if ok && delay != 0 {
end = end - delay
start = start - delay
}
q.Gte(time.Unix(start, 0).UnixMilli())
q.Lte(time.Unix(end, 0).UnixMilli())
q.Format("epoch_millis")
field := param.MetricAggr.Field
groupBys := param.GroupBy
queryString := GetQueryString(param.Filter, q)
var aggr elastic.Aggregation
switch param.MetricAggr.Func {
case "avg":
aggr = elastic.NewAvgAggregation().Field(field)
case "max":
aggr = elastic.NewMaxAggregation().Field(field)
case "min":
aggr = elastic.NewMinAggregation().Field(field)
case "sum":
aggr = elastic.NewSumAggregation().Field(field)
case "count":
aggr = elastic.NewValueCountAggregation().Field(field)
case "p90":
aggr = elastic.NewPercentilesAggregation().Percentiles(90).Field(field)
case "p95":
aggr = elastic.NewPercentilesAggregation().Percentiles(95).Field(field)
case "p99":
aggr = elastic.NewPercentilesAggregation().Percentiles(99).Field(field)
case "median":
aggr = elastic.NewPercentilesAggregation().Percentiles(50).Field(field)
default:
return nil, fmt.Errorf("func %s not support", param.MetricAggr.Func)
}
tsAggr := elastic.NewDateHistogramAggregation().
Field(param.DateField).
MinDocCount(1)
if strings.HasPrefix(version, "7") {
tsAggr.FixedInterval(fmt.Sprintf("%ds", param.Interval))
} else {
// 兼容 7.0 以下的版本
// OpenSearch 也使用这个字段
tsAggr.Interval(fmt.Sprintf("%ds", param.Interval))
}
// group by
var groupByAggregation elastic.Aggregation
if len(groupBys) > 0 {
groupBy := groupBys[0]
if groupBy.MinDocCount == 0 {
groupBy.MinDocCount = 1
}
if groupBy.Size == 0 {
groupBy.Size = 300
}
switch groupBy.Cate {
case Terms:
if param.MetricAggr.Func != "count" {
groupByAggregation = elastic.NewTermsAggregation().Field(groupBy.Field).SubAggregation(field, aggr).OrderByKeyDesc().Size(groupBy.Size).MinDocCount(int(groupBy.MinDocCount))
} else {
groupByAggregation = elastic.NewTermsAggregation().Field(groupBy.Field).OrderByKeyDesc().Size(groupBy.Size).MinDocCount(int(groupBy.MinDocCount))
}
case Histgram:
if param.MetricAggr.Func != "count" {
groupByAggregation = elastic.NewHistogramAggregation().Field(groupBy.Field).Interval(float64(groupBy.Interval)).SubAggregation(field, aggr)
} else {
groupByAggregation = elastic.NewHistogramAggregation().Field(groupBy.Field).Interval(float64(groupBy.Interval))
}
case Filters:
for _, filterParam := range groupBy.Params {
if param.MetricAggr.Func != "count" {
groupByAggregation = elastic.NewFilterAggregation().Filter(elastic.NewTermQuery(filterParam.Query, "true")).SubAggregation(field, aggr)
} else {
groupByAggregation = elastic.NewFilterAggregation().Filter(elastic.NewTermQuery(filterParam.Query, "true"))
}
}
}
for i := 1; i < len(groupBys); i++ {
groupBy := groupBys[i]
if groupBy.MinDocCount == 0 {
groupBy.MinDocCount = 1
}
if groupBy.Size == 0 {
groupBy.Size = 300
}
switch groupBy.Cate {
case Terms:
groupByAggregation = elastic.NewTermsAggregation().Field(groupBy.Field).SubAggregation(groupBys[i-1].Field, groupByAggregation).OrderByKeyDesc().Size(groupBy.Size).MinDocCount(int(groupBy.MinDocCount))
case Histgram:
groupByAggregation = elastic.NewHistogramAggregation().Field(groupBy.Field).Interval(float64(groupBy.Interval)).SubAggregation(groupBys[i-1].Field, groupByAggregation)
case Filters:
for _, filterParam := range groupBy.Params {
groupByAggregation = elastic.NewFilterAggregation().Filter(elastic.NewTermQuery(filterParam.Query, "true")).SubAggregation(groupBys[i-1].Field, groupByAggregation)
}
}
}
tsAggr.SubAggregation(groupBys[len(groupBys)-1].Field, groupByAggregation)
} else if param.MetricAggr.Func != "count" {
tsAggr.SubAggregation(field, aggr)
}
source, _ := queryString.Source()
b, _ := json.Marshal(source)
logger.Debugf("query_data q:%+v tsAggr:%+v query_string:%s", param, tsAggr, string(b))
searchSource := elastic.NewSearchSource().
Query(queryString).
Aggregation("ts", tsAggr)
searchSourceString, err := searchSource.Source()
if err != nil {
logger.Warningf("query_data searchSource:%s to string error:%v", searchSourceString, err)
}
jsonSearchSource, err := json.Marshal(searchSourceString)
if err != nil {
logger.Warningf("query_data searchSource:%s to json error:%v", searchSourceString, err)
}
result, err := search(ctx, indexArr, searchSource, param.Timeout, param.MaxShard)
if err != nil {
logger.Warningf("query_data searchSource:%s query_data error:%v", searchSourceString, err)
return nil, err
}
logger.Debugf("query_data searchSource:%s resp:%s", string(jsonSearchSource), string(result.Aggregations["ts"]))
js, err := simplejson.NewJson(result.Aggregations["ts"])
if err != nil {
return nil, err
}
bucketsData, err := js.Get("buckets").Array()
if err != nil {
return nil, err
}
var keys []string
for i := len(groupBys) - 1; i >= 0; i-- {
keys = append(keys, groupBys[i].Field)
}
if param.MetricAggr.Func != "count" {
keys = append(keys, field)
}
metrics := &MetricPtr{Data: make(map[string][][]float64)}
GetBuckts("", keys, bucketsData, metrics, "", 0, param.MetricAggr.Func)
return TransferData(fmt.Sprintf("%s_%s", field, param.MetricAggr.Func), param.Ref, metrics.Data), nil
}
func HitFilter(typ string) bool {
switch typ {
case "keyword", "date", "long", "integer", "short", "byte", "double", "float", "half_float", "scaled_float", "unsigned_long":
return false
default:
return true
}
}
func QueryLog(ctx context.Context, queryParam interface{}, timeout int64, version string, maxShard int, search SearchFunc) ([]interface{}, int64, error) {
param := new(Query)
if err := mapstructure.Decode(queryParam, param); err != nil {
return nil, 0, err
}
if param.Timeout == 0 {
param.Timeout = int(timeout)
}
indexArr := strings.Split(param.Index, ",")
now := time.Now().Unix()
var start, end int64
if param.End != 0 && param.Start != 0 {
end = param.End - param.End%param.Interval
start = param.Start - param.Start%param.Interval
} else {
end = now
start = end - param.Interval
}
q := elastic.NewRangeQuery(param.DateField)
q.Gte(time.Unix(start, 0).UnixMilli())
q.Lte(time.Unix(end, 0).UnixMilli())
q.Format("epoch_millis")
queryString := GetQueryString(param.Filter, q)
if param.Limit <= 0 {
param.Limit = 10
}
if param.MaxShard < 1 {
param.MaxShard = maxShard
}
source := elastic.NewSearchSource().
TrackTotalHits(true).
Query(queryString).
From(param.P).
Size(param.Limit).
Sort(param.DateField, param.Ascending)
result, err := search(ctx, indexArr, source, param.Timeout, param.MaxShard)
if err != nil {
logger.Warningf("query data error:%v", err)
return nil, 0, err
}
total := result.TotalHits()
var ret []interface{}
b, _ := json.Marshal(source)
logger.Debugf("query data result query source:%s len:%d total:%d", string(b), len(result.Hits.Hits), total)
resultBytes, _ := json.Marshal(result)
logger.Debugf("query data result query source:%s result:%s", string(b), string(resultBytes))
if strings.HasPrefix(version, "6") {
for i := 0; i < len(result.Hits.Hits); i++ {
var x map[string]interface{}
err := json.Unmarshal(result.Hits.Hits[i].Source, &x)
if err != nil {
logger.Warningf("Unmarshal soruce error:%v", err)
continue
}
if result.Hits.Hits[i].Fields == nil {
result.Hits.Hits[i].Fields = make(map[string]interface{})
}
IterGetMap(x, result.Hits.Hits[i].Fields, "")
ret = append(ret, result.Hits.Hits[i])
}
} else {
for _, hit := range result.Hits.Hits {
ret = append(ret, hit)
}
}
return ret, total, nil
}

114
datasource/datasource.go Normal file
View File

@@ -0,0 +1,114 @@
package datasource
import (
"context"
"fmt"
"strings"
"github.com/ccfos/nightingale/v6/models"
)
type DatasourceType struct {
Id int64 `json:"id"`
Category string `json:"category"`
PluginType string `json:"type"`
PluginTypeName string `json:"type_name"`
}
type Keys struct {
ValueKey string `json:"valueKey" mapstructure:"valueKey"` // 多个用空格分隔
LabelKey string `json:"labelKey" mapstructure:"labelKey"` // 多个用空格分隔
TimeKey string `json:"timeKey" mapstructure:"timeKey"`
TimeFormat string `json:"timeFormat" mapstructure:"timeFormat"`
}
var DatasourceTypes map[int64]DatasourceType
func init() {
DatasourceTypes = make(map[int64]DatasourceType)
DatasourceTypes[1] = DatasourceType{
Id: 1,
Category: "timeseries",
PluginType: "prometheus",
PluginTypeName: "Prometheus Like",
}
DatasourceTypes[2] = DatasourceType{
Id: 2,
Category: "logging",
PluginType: "elasticsearch",
PluginTypeName: "Elasticsearch",
}
DatasourceTypes[3] = DatasourceType{
Id: 3,
Category: "logging",
PluginType: "aliyun-sls",
PluginTypeName: "SLS",
}
DatasourceTypes[4] = DatasourceType{
Id: 4,
Category: "timeseries",
PluginType: "ck",
PluginTypeName: "ClickHouse",
}
}
type NewDatasrouceFn func(settings map[string]interface{}) (Datasource, error)
var datasourceRegister = map[string]NewDatasrouceFn{}
type Datasource interface {
Init(settings map[string]interface{}) (Datasource, error) // 初始化配置
InitClient() error // 初始化客户端
Validate(ctx context.Context) error // 参数验证
Equal(p Datasource) bool // 验证是否相等
MakeLogQuery(ctx context.Context, query interface{}, eventTags []string, start, end int64) (interface{}, error)
MakeTSQuery(ctx context.Context, query interface{}, eventTags []string, start, end int64) (interface{}, error)
QueryData(ctx context.Context, query interface{}) ([]models.DataResp, error)
QueryLog(ctx context.Context, query interface{}) ([]interface{}, int64, error)
// 在生成告警事件时,会调用该方法,用于获取额外的数据
QueryMapData(ctx context.Context, query interface{}) ([]map[string]string, error)
}
func RegisterDatasource(typ string, p Datasource) {
if _, found := datasourceRegister[typ]; found {
return
}
datasourceRegister[typ] = p.Init
}
func GetDatasourceByType(typ string, settings map[string]interface{}) (Datasource, error) {
typ = strings.ReplaceAll(typ, ".logging", "")
fn, found := datasourceRegister[typ]
if !found {
return nil, fmt.Errorf("plugin type %s not found", typ)
}
plug, err := fn(settings)
if err != nil {
return nil, err
}
return plug, nil
}
type DatasourceInfo struct {
Id int64 `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
ClusterName string `json:"cluster_name"`
Category string `json:"category"`
PluginId int64 `json:"plugin_id"`
Type string `json:"plugin_type"`
PluginTypeName string `json:"plugin_type_name"`
Settings map[string]interface{} `json:"settings"`
HTTPJson models.HTTP `json:"http"`
AuthJson models.Auth `json:"auth"`
Status string `json:"status"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
IsDefault bool `json:"is_default"`
}

405
datasource/es/es.go Normal file
View File

@@ -0,0 +1,405 @@
package es
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/url"
"reflect"
"sort"
"strings"
"time"
"github.com/ccfos/nightingale/v6/datasource"
"github.com/ccfos/nightingale/v6/datasource/commons/eslike"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/tlsx"
"github.com/mitchellh/mapstructure"
"github.com/olivere/elastic/v7"
"github.com/toolkits/pkg/logger"
)
const (
ESType = "elasticsearch"
)
type Elasticsearch struct {
Addr string `json:"es.addr" mapstructure:"es.addr"`
Nodes []string `json:"es.nodes" mapstructure:"es.nodes"`
Timeout int64 `json:"es.timeout" mapstructure:"es.timeout"` // millis
Basic BasicAuth `json:"es.basic" mapstructure:"es.basic"`
TLS TLS `json:"es.tls" mapstructure:"es.tls"`
Version string `json:"es.version" mapstructure:"es.version"`
Headers map[string]string `json:"es.headers" mapstructure:"es.headers"`
MinInterval int `json:"es.min_interval" mapstructure:"es.min_interval"` // seconds
MaxShard int `json:"es.max_shard" mapstructure:"es.max_shard"`
ClusterName string `json:"es.cluster_name" mapstructure:"es.cluster_name"`
EnableWrite bool `json:"es.enable_write" mapstructure:"es.enable_write"` // 允许写操作
Client *elastic.Client `json:"es.client" mapstructure:"es.client"`
}
type TLS struct {
SkipTlsVerify bool `json:"es.tls.skip_tls_verify" mapstructure:"es.tls.skip_tls_verify"`
}
type BasicAuth struct {
Enable bool `json:"es.auth.enable" mapstructure:"es.auth.enable"`
Username string `json:"es.user" mapstructure:"es.user"`
Password string `json:"es.password" mapstructure:"es.password"`
}
func init() {
datasource.RegisterDatasource(ESType, new(Elasticsearch))
}
func (e *Elasticsearch) Init(settings map[string]interface{}) (datasource.Datasource, error) {
newest := new(Elasticsearch)
err := mapstructure.Decode(settings, newest)
return newest, err
}
func (e *Elasticsearch) InitClient() error {
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: time.Duration(e.Timeout) * time.Millisecond,
}).DialContext,
ResponseHeaderTimeout: time.Duration(e.Timeout) * time.Millisecond,
}
if len(e.Nodes) > 0 {
e.Addr = e.Nodes[0]
}
if strings.Contains(e.Addr, "https") {
tlsConfig := tlsx.ClientConfig{
InsecureSkipVerify: e.TLS.SkipTlsVerify,
UseTLS: true,
}
cfg, err := tlsConfig.TLSConfig()
if err != nil {
return err
}
transport.TLSClientConfig = cfg
}
var err error
options := []elastic.ClientOptionFunc{
elastic.SetURL(e.Nodes...),
}
if e.Basic.Username != "" {
options = append(options, elastic.SetBasicAuth(e.Basic.Username, e.Basic.Password))
}
headers := http.Header{}
for k, v := range e.Headers {
headers[k] = []string{v}
}
options = append(options, elastic.SetHeaders(headers))
options = append(options, elastic.SetHttpClient(&http.Client{Transport: transport}))
options = append(options, elastic.SetSniff(false))
options = append(options, elastic.SetHealthcheck(false))
e.Client, err = elastic.NewClient(options...)
return err
}
func (e *Elasticsearch) Equal(other datasource.Datasource) bool {
sort.Strings(e.Nodes)
sort.Strings(other.(*Elasticsearch).Nodes)
if strings.Join(e.Nodes, ",") != strings.Join(other.(*Elasticsearch).Nodes, ",") {
return false
}
if e.Basic.Username != other.(*Elasticsearch).Basic.Username {
return false
}
if e.Basic.Password != other.(*Elasticsearch).Basic.Password {
return false
}
if e.TLS.SkipTlsVerify != other.(*Elasticsearch).TLS.SkipTlsVerify {
return false
}
if e.EnableWrite != other.(*Elasticsearch).EnableWrite {
return false
}
if !reflect.DeepEqual(e.Headers, other.(*Elasticsearch).Headers) {
return false
}
return true
}
func (e *Elasticsearch) Validate(ctx context.Context) (err error) {
if len(e.Nodes) == 0 {
return fmt.Errorf("need a valid addr")
}
for _, addr := range e.Nodes {
_, err = url.Parse(addr)
if err != nil {
return fmt.Errorf("parse addr error: %v", err)
}
}
if e.Basic.Enable && (len(e.Basic.Username) == 0 || len(e.Basic.Password) == 0) {
return fmt.Errorf("need a valid user, password")
}
if e.MaxShard == 0 {
e.MaxShard = 5
}
if e.MinInterval < 10 {
e.MinInterval = 10
}
if e.Timeout == 0 {
e.Timeout = 60000
}
if !strings.HasPrefix(e.Version, "6") && !strings.HasPrefix(e.Version, "7") {
return fmt.Errorf("version must be 6.0+ or 7.0+")
}
return nil
}
func (e *Elasticsearch) MakeLogQuery(ctx context.Context, query interface{}, eventTags []string, start, end int64) (interface{}, error) {
return eslike.MakeLogQuery(ctx, query, eventTags, start, end)
}
func (e *Elasticsearch) MakeTSQuery(ctx context.Context, query interface{}, eventTags []string, start, end int64) (interface{}, error) {
return eslike.MakeTSQuery(ctx, query, eventTags, start, end)
}
func (e *Elasticsearch) QueryData(ctx context.Context, queryParam interface{}) ([]models.DataResp, error) {
search := func(ctx context.Context, indices []string, source interface{}, timeout int, maxShard int) (*elastic.SearchResult, error) {
return e.Client.Search().
Index(indices...).
Source(source).
Timeout(fmt.Sprintf("%ds", timeout)).
MaxConcurrentShardRequests(maxShard).
Do(ctx)
}
return eslike.QueryData(ctx, queryParam, e.Timeout, e.Version, search)
}
func (e *Elasticsearch) QueryIndices() ([]string, error) {
result, err := e.Client.IndexNames()
return result, err
}
func (e *Elasticsearch) QueryFields(indexs []string) ([]string, error) {
var fields []string
result, err := elastic.NewGetFieldMappingService(e.Client).Index(indexs...).Do(context.Background())
if err != nil {
return fields, err
}
fieldMap := make(map[string]struct{})
for _, indexMap := range result {
if m, exists := indexMap.(map[string]interface{})["mappings"]; exists {
for k, v := range m.(map[string]interface{}) {
// 兼容 es6 版本
if k == "doc" && strings.HasPrefix(e.Version, "6") {
// if k == "doc" {
for kk, vv := range v.(map[string]interface{}) {
typ := getFieldType(kk, vv.(map[string]interface{}))
if eslike.HitFilter(typ) {
continue
}
if _, exsits := fieldMap[kk]; !exsits {
fieldMap[kk] = struct{}{}
fields = append(fields, kk)
}
}
} else {
// es7 版本
typ := getFieldType(k, v.(map[string]interface{}))
if eslike.HitFilter(typ) {
continue
}
if _, exsits := fieldMap[k]; !exsits {
fieldMap[k] = struct{}{}
fields = append(fields, k)
}
}
}
}
}
sort.Strings(fields)
return fields, nil
}
func (e *Elasticsearch) QueryLog(ctx context.Context, queryParam interface{}) ([]interface{}, int64, error) {
search := func(ctx context.Context, indices []string, source interface{}, timeout int, maxShard int) (*elastic.SearchResult, error) {
// 应该是之前为了获取 fields 字段,做的这个兼容
// fields, err := e.QueryFields(indices)
// if err != nil {
// logger.Warningf("query data error:%v", err)
// return nil, err
// }
// if source != nil && strings.HasPrefix(e.Version, "7") {
// source = source.(*elastic.SearchSource).DocvalueFields(fields...)
// }
return e.Client.Search().
Index(indices...).
MaxConcurrentShardRequests(maxShard).
Source(source).
Timeout(fmt.Sprintf("%ds", timeout)).
Do(ctx)
}
return eslike.QueryLog(ctx, queryParam, e.Timeout, e.Version, e.MaxShard, search)
}
func (e *Elasticsearch) QueryFieldValue(indexs []string, field string, query string) ([]string, error) {
var values []string
search := e.Client.Search().
Index(indexs...).
Size(0)
if query != "" {
search = search.Query(elastic.NewBoolQuery().Must(elastic.NewQueryStringQuery(query)))
}
search = search.Aggregation("distinct", elastic.NewTermsAggregation().Field(field).Size(10000))
result, err := search.Do(context.Background())
if err != nil {
return values, err
}
agg, found := result.Aggregations.Terms("distinct")
if !found {
return values, nil
}
for _, bucket := range agg.Buckets {
values = append(values, bucket.Key.(string))
}
return values, nil
}
func (e *Elasticsearch) Test(ctx context.Context) (err error) {
err = e.Validate(ctx)
if err != nil {
return err
}
if e.Addr == "" {
return fmt.Errorf("addr is invalid")
}
if e.Version == "7.10+" {
options := []elastic.ClientOptionFunc{
elastic.SetURL(e.Addr),
}
if e.Basic.Enable {
options = append(options, elastic.SetBasicAuth(e.Basic.Username, e.Basic.Password))
}
client, err := elastic.NewClient(options...)
if err != nil {
return fmt.Errorf("config is invalid:%v", err)
}
_, err = client.ElasticsearchVersion(e.Addr)
if err != nil {
return fmt.Errorf("config is invalid:%v", err)
}
} else {
return fmt.Errorf("version must be 7.10+")
}
return nil
}
func getFieldType(key string, m map[string]interface{}) string {
if innerMap, exists := m["mapping"]; exists {
if innerM, exists := innerMap.(map[string]interface{})[key]; exists {
if typ, exists := innerM.(map[string]interface{})["type"]; exists {
return typ.(string)
}
} else {
arr := strings.Split(key, ".")
if innerM, exists := innerMap.(map[string]interface{})[arr[len(arr)-1]]; exists {
if typ, exists := innerM.(map[string]interface{})["type"]; exists {
return typ.(string)
}
}
}
}
return ""
}
func (e *Elasticsearch) QueryMapData(ctx context.Context, query interface{}) ([]map[string]string, error) {
search := func(ctx context.Context, indices []string, source interface{}, timeout int, maxShard int) (*elastic.SearchResult, error) {
return e.Client.Search().
Index(indices...).
Source(source).
Timeout(fmt.Sprintf("%ds", timeout)).
Do(ctx)
}
param := new(eslike.Query)
if err := mapstructure.Decode(query, param); err != nil {
return nil, err
}
// 扩大查询范围, 解决上一次查询消耗时间太多,导致本次查询时间范围起止时间,滞后问题
param.Interval += 30
res, _, err := eslike.QueryLog(ctx, param, e.Timeout, e.Version, e.MaxShard, search)
if err != nil {
return nil, err
}
var result []map[string]string
for _, item := range res {
logger.Debugf("query:%v item:%v", query, item)
if itemMap, ok := item.(*elastic.SearchHit); ok {
mItem := make(map[string]string)
// 遍历 fields 字段的每个键值对
sourceMap := make(map[string]interface{})
err := json.Unmarshal(itemMap.Source, &sourceMap)
if err != nil {
logger.Warningf("unmarshal source%s error:%v", string(itemMap.Source), err)
continue
}
for k, v := range sourceMap {
mItem[k] = fmt.Sprintf("%v", v)
}
// 将处理好的 map 添加到 m 切片中
result = append(result, mItem)
// 只取第一条数据
break
}
}
return result, nil
}

15
datasource/prom/prom.go Normal file
View File

@@ -0,0 +1,15 @@
package prom
type Prometheus struct {
PrometheusAddr string `json:"prometheus.addr"`
PrometheusBasic struct {
PrometheusUser string `json:"prometheus.user"`
PrometheusPass string `json:"prometheus.password"`
} `json:"prometheus.basic"`
Headers map[string]string `json:"prometheus.headers"`
PrometheusTimeout int64 `json:"prometheus.timeout"`
ClusterName string `json:"prometheus.cluster_name"`
WriteAddr string `json:"prometheus.write_addr"`
TsdbType string `json:"prometheus.tsdb_type"`
InternalAddr string `json:"prometheus.internal_addr"`
}

View File

@@ -1,24 +1,32 @@
package tdengine
import (
"encoding/base64"
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"reflect"
"strconv"
"strings"
"sync"
"time"
"github.com/ccfos/nightingale/v6/alert/aconf"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/prometheus/common/model"
"github.com/toolkits/pkg/logger"
"github.com/ccfos/nightingale/v6/datasource"
td "github.com/ccfos/nightingale/v6/dskit/tdengine"
"github.com/ccfos/nightingale/v6/models"
"github.com/mitchellh/mapstructure"
)
const (
TDEngineType = "tdengine"
)
type TDengine struct {
td.Tdengine `json:",inline" mapstructure:",squash"`
}
type TdengineQuery struct {
From string `json:"from"`
Interval int64 `json:"interval"`
@@ -34,162 +42,94 @@ type Keys struct {
TimeFormat string `json:"timeFormat"`
}
type TdengineClientMap struct {
sync.RWMutex
ctx *ctx.Context
heartbeat aconf.HeartbeatConfig
ReaderClients map[int64]*tdengineClient
func init() {
datasource.RegisterDatasource(TDEngineType, new(TDengine))
}
func (pc *TdengineClientMap) Set(datasourceId int64, r *tdengineClient) {
if r == nil {
return
}
pc.Lock()
defer pc.Unlock()
pc.ReaderClients[datasourceId] = r
func (td *TDengine) Init(settings map[string]interface{}) (datasource.Datasource, error) {
newest := new(TDengine)
err := mapstructure.Decode(settings, newest)
return newest, err
}
func (pc *TdengineClientMap) GetDatasourceIds() []int64 {
pc.RLock()
defer pc.RUnlock()
var datasourceIds []int64
for k := range pc.ReaderClients {
datasourceIds = append(datasourceIds, k)
func (td *TDengine) InitClient() error {
td.InitCli()
return nil
}
func (td *TDengine) Equal(other datasource.Datasource) bool {
otherTD, ok := other.(*TDengine)
if !ok {
return false
}
return datasourceIds
}
func (pc *TdengineClientMap) GetCli(datasourceId int64) *tdengineClient {
pc.RLock()
defer pc.RUnlock()
c := pc.ReaderClients[datasourceId]
return c
}
func (pc *TdengineClientMap) IsNil(datasourceId int64) bool {
pc.RLock()
defer pc.RUnlock()
c, exists := pc.ReaderClients[datasourceId]
if !exists {
return true
if td.Addr != otherTD.Addr {
return false
}
return c == nil
}
func (pc *TdengineClientMap) Reset() {
pc.Lock()
defer pc.Unlock()
pc.ReaderClients = make(map[int64]*tdengineClient)
}
func (pc *TdengineClientMap) Del(datasourceId int64) {
pc.Lock()
defer pc.Unlock()
delete(pc.ReaderClients, datasourceId)
}
type tdengineClient struct {
url string
client *http.Client
header map[string][]string
}
func newTdengine(po TdengineOption) *tdengineClient {
tc := &tdengineClient{
url: po.Url,
}
tc.client = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
DisableCompression: true,
},
}
tc.header = map[string][]string{
"Connection": {"keep-alive"},
}
for _, v := range po.Headers {
kv := strings.Split(v, ":")
if len(kv) != 2 {
continue
if td.Basic != nil && otherTD.Basic != nil {
if td.Basic.User != otherTD.Basic.User {
return false
}
tc.header[kv[0]] = []string{kv[1]}
}
if po.BasicAuthUser != "" {
basic := base64.StdEncoding.EncodeToString([]byte(po.BasicAuthUser + ":" + po.BasicAuthPass))
tc.header["Authorization"] = []string{fmt.Sprintf("Basic %s", basic)}
}
return tc
}
type APIResponse struct {
Code int `json:"code"`
ColumnMeta [][]interface{} `json:"column_meta"`
Data [][]interface{} `json:"data"`
Rows int `json:"rows"`
}
func (tc *tdengineClient) QueryTable(query string) (APIResponse, error) {
var apiResp APIResponse
req, err := http.NewRequest("POST", tc.url+"/rest/sql", strings.NewReader(query))
if err != nil {
return apiResp, err
}
for k, v := range tc.header {
req.Header[k] = v
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := tc.client.Do(req)
if err != nil {
return apiResp, err
}
defer resp.Body.Close()
// 限制响应体大小为10MB
maxSize := int64(10 * 1024 * 1024) // 10MB
limitedReader := http.MaxBytesReader(nil, resp.Body, maxSize)
if resp.StatusCode != http.StatusOK {
return apiResp, fmt.Errorf("HTTP error, status: %s", resp.Status)
}
err = json.NewDecoder(limitedReader).Decode(&apiResp)
if err != nil {
if strings.Contains(err.Error(), "http: request body too large") {
return apiResp, fmt.Errorf("response body exceeds 10MB limit")
if td.Basic.Password != otherTD.Basic.Password {
return false
}
return apiResp, err
}
return apiResp, nil
if td.Token != otherTD.Token {
return false
}
if td.Timeout != otherTD.Timeout {
return false
}
if td.DialTimeout != otherTD.DialTimeout {
return false
}
if td.MaxIdleConnsPerHost != otherTD.MaxIdleConnsPerHost {
return false
}
if len(td.Headers) != len(otherTD.Headers) {
return false
}
for k, v := range td.Headers {
if otherV, ok := otherTD.Headers[k]; !ok || v != otherV {
return false
}
}
return true
}
func (tc *tdengineClient) QueryLog(query interface{}) (APIResponse, error) {
b, err := json.Marshal(query)
func (td *TDengine) Validate(ctx context.Context) (err error) {
return nil
}
func (td *TDengine) MakeLogQuery(ctx context.Context, query interface{}, eventTags []string, start, end int64) (interface{}, error) {
return nil, nil
}
func (td *TDengine) MakeTSQuery(ctx context.Context, query interface{}, eventTags []string, start, end int64) (interface{}, error) {
return nil, nil
}
func (td *TDengine) QueryData(ctx context.Context, queryParam interface{}) ([]models.DataResp, error) {
return td.Query(queryParam, 0)
}
func (td *TDengine) QueryLog(ctx context.Context, queryParam interface{}) ([]interface{}, int64, error) {
b, err := json.Marshal(queryParam)
if err != nil {
return APIResponse{}, err
return nil, 0, err
}
var q TdengineQuery
err = json.Unmarshal(b, &q)
if err != nil {
return APIResponse{}, err
return nil, 0, err
}
if q.Interval == 0 {
@@ -218,135 +158,19 @@ func (tc *tdengineClient) QueryLog(query interface{}) (APIResponse, error) {
q.Query = q.Query + " limit 200"
}
data, err := tc.QueryTable(q.Query)
data, err := td.QueryTable(q.Query)
if err != nil {
return data, err
return nil, 0, err
}
return TimeFormat(data, q.Keys.TimeFormat), err
return ConvertToTable(data), int64(len(data.Data)), nil
}
func TimeFormat(src APIResponse, timeFormat string) APIResponse {
if timeFormat == "" {
return src
}
tsIdx := -1
for colIndex, colData := range src.ColumnMeta {
// 类型参考 https://docs.taosdata.com/taos-sql/data-type/
// 处理v2版本数字类型和v3版本字符串类型
switch t := colData[1].(type) {
case float64:
// v2版本数字类型映射
if int(t) == 9 { // TIMESTAMP type in v2
tsIdx = colIndex
break
}
case string:
// v3版本直接使用字符串类型
if t == "TIMESTAMP" {
tsIdx = colIndex
break
}
default:
logger.Warningf("unexpected column type: %v", colData[1])
continue
}
}
if tsIdx == -1 {
return src
}
for i := range src.Data {
var t time.Time
var err error
switch tsVal := src.Data[i][tsIdx].(type) {
case string:
// 尝试解析不同格式的时间字符串
t, err = parseTimeString(tsVal)
if err != nil {
logger.Warningf("parse timestamp string failed: %v, value: %v", err, tsVal)
continue
}
default:
logger.Warningf("unexpected timestamp type: %T, value: %v", tsVal, tsVal)
continue
}
src.Data[i][tsIdx] = t.In(time.Local).Format(timeFormat)
}
return src
func (td *TDengine) QueryMapData(ctx context.Context, query interface{}) ([]map[string]string, error) {
return nil, nil
}
func parseTimeString(ts string) (time.Time, error) {
// 尝试不同的时间格式
formats := []string{
// 标准格式
time.Layout, // "01/02 03:04:05PM '06 -0700"
time.ANSIC, // "Mon Jan _2 15:04:05 2006"
time.UnixDate, // "Mon Jan _2 15:04:05 MST 2006"
time.RubyDate, // "Mon Jan 02 15:04:05 -0700 2006"
time.RFC822, // "02 Jan 06 15:04 MST"
time.RFC822Z, // "02 Jan 06 15:04 -0700"
time.RFC850, // "Monday, 02-Jan-06 15:04:05 MST"
time.RFC1123, // "Mon, 02 Jan 2006 15:04:05 MST"
time.RFC1123Z, // "Mon, 02 Jan 2006 15:04:05 -0700"
time.RFC3339, // "2006-01-02T15:04:05Z07:00"
time.RFC3339Nano, // "2006-01-02T15:04:05.999999999Z07:00"
time.Kitchen, // "3:04PM"
// 实用时间戳格式
time.Stamp, // "Jan _2 15:04:05"
time.StampMilli, // "Jan _2 15:04:05.000"
time.StampMicro, // "Jan _2 15:04:05.000000"
time.StampNano, // "Jan _2 15:04:05.000000000"
time.DateTime, // "2006-01-02 15:04:05"
time.DateOnly, // "2006-01-02"
time.TimeOnly, // "15:04:05"
// 常用自定义格式
"2006-01-02T15:04:05", // 无时区的ISO格式
"2006-01-02T15:04:05.000Z",
"2006-01-02T15:04:05Z",
"2006-01-02 15:04:05.999999999", // 纳秒
"2006-01-02 15:04:05.999999", // 微秒
"2006-01-02 15:04:05.999", // 毫秒
"2006/01/02",
"20060102",
"01/02/2006",
"2006年01月02日",
"2006年01月02日 15:04:05",
}
var lastErr error
for _, format := range formats {
t, err := time.Parse(format, ts)
if err == nil {
return t, nil
}
lastErr = err
}
// 尝试解析 Unix 时间戳
if timestamp, err := strconv.ParseInt(ts, 10, 64); err == nil {
switch len(ts) {
case 10: // 秒
return time.Unix(timestamp, 0), nil
case 13: // 毫秒
return time.Unix(timestamp/1000, (timestamp%1000)*1000000), nil
case 16: // 微秒
return time.Unix(timestamp/1000000, (timestamp%1000000)*1000), nil
case 19: // 纳秒
return time.Unix(timestamp/1000000000, timestamp%1000000000), nil
}
}
return time.Time{}, fmt.Errorf("failed to parse time with any format: %v", lastErr)
}
func (tc *tdengineClient) Query(query interface{}, delay ...int) ([]models.DataResp, error) {
func (td *TDengine) Query(query interface{}, delay ...int) ([]models.DataResp, error) {
b, err := json.Marshal(query)
if err != nil {
return nil, err
@@ -384,7 +208,7 @@ func (tc *tdengineClient) Query(query interface{}, delay ...int) ([]models.DataR
q.Query = strings.ReplaceAll(q.Query, key, val)
}
data, err := tc.QueryTable(q.Query)
data, err := td.QueryTable(q.Query)
if err != nil {
return nil, err
}
@@ -393,154 +217,7 @@ func (tc *tdengineClient) Query(query interface{}, delay ...int) ([]models.DataR
return ConvertToTStData(data, q.Keys, q.Ref)
}
// get tdendgine databases
func (tc *tdengineClient) GetDatabases() ([]string, error) {
var databases []string
data, err := tc.QueryTable("show databases")
if err != nil {
return databases, err
}
for _, row := range data.Data {
databases = append(databases, row[0].(string))
}
return databases, nil
}
// get tdendgine tables by database
func (tc *tdengineClient) GetTables(database string, isStable bool) ([]string, error) {
var tables []string
sql := fmt.Sprintf("show %s.tables", database)
if isStable {
sql = fmt.Sprintf("show %s.stables", database)
}
data, err := tc.QueryTable(sql)
if err != nil {
return tables, err
}
for _, row := range data.Data {
tables = append(tables, row[0].(string))
}
return tables, nil
}
type Column struct {
Name string `json:"name"`
Type string `json:"type"`
Size int `json:"size"`
}
func (tc *tdengineClient) GetColumns(database, table string) ([]Column, error) {
var columns []Column
sql := fmt.Sprintf("select * from %s.%s limit 1", database, table)
data, err := tc.QueryTable(sql)
if err != nil {
return columns, err
}
for _, row := range data.ColumnMeta {
var colType string
switch t := row[1].(type) {
case float64:
// v2版本数字类型映射
switch int(t) {
case 1:
colType = "BOOL"
case 2:
colType = "TINYINT"
case 3:
colType = "SMALLINT"
case 4:
colType = "INT"
case 5:
colType = "BIGINT"
case 6:
colType = "FLOAT"
case 7:
colType = "DOUBLE"
case 8:
colType = "BINARY"
case 9:
colType = "TIMESTAMP"
case 10:
colType = "NCHAR"
default:
colType = "UNKNOWN"
}
case string:
// v3版本直接使用字符串类型
colType = t
default:
logger.Warningf("unexpected column type format: %v", row[1])
colType = "UNKNOWN"
}
column := Column{
Name: row[0].(string),
Type: colType,
Size: int(row[2].(float64)),
}
columns = append(columns, column)
}
return columns, nil
}
// {
// "code": 0,
// "column_meta": [
// ["ts", "TIMESTAMP", 8],
// ["count", "BIGINT", 8],
// ["endpoint", "VARCHAR", 45],
// ["status_code", "INT", 4],
// ["client_ip", "VARCHAR", 40],
// ["request_method", "VARCHAR", 15],
// ["request_uri", "VARCHAR", 128]
// ],
// "data": [
// [
// "2022-06-29T05:50:55.401Z",
// 2,
// "LAPTOP-NNKFTLTG:6041",
// 200,
// "172.23.208.1",
// "POST",
// "/rest/sql"
// ],
// [
// "2022-06-29T05:52:16.603Z",
// 1,
// "LAPTOP-NNKFTLTG:6041",
// 200,
// "172.23.208.1",
// "POST",
// "/rest/sql"
// ]
// ],
// "rows": 2
// }
// {
// "dat": [
// {
// "ref": "",
// "metric": {
// "__name__": "count",
// "host":"host1"
// },
// "values": [
// [
// 1693219500,
// 12
// ]
// ]
// }
// ],
// "err": ""
// }
func ConvertToTStData(src APIResponse, key Keys, ref string) ([]models.DataResp, error) {
func ConvertToTStData(src td.APIResponse, key Keys, ref string) ([]models.DataResp, error) {
metricIdxMap := make(map[string]int)
labelIdxMap := make(map[string]int)
@@ -702,3 +379,82 @@ func interfaceToFloat64(input interface{}) (float64, error) {
return 0, fmt.Errorf("unsupported type: %T", input)
}
}
func parseTimeString(ts string) (time.Time, error) {
// 尝试不同的时间格式
formats := []string{
// 标准格式
time.Layout, // "01/02 03:04:05PM '06 -0700"
time.ANSIC, // "Mon Jan _2 15:04:05 2006"
time.UnixDate, // "Mon Jan _2 15:04:05 MST 2006"
time.RubyDate, // "Mon Jan 02 15:04:05 -0700 2006"
time.RFC822, // "02 Jan 06 15:04 MST"
time.RFC822Z, // "02 Jan 06 15:04 -0700"
time.RFC850, // "Monday, 02-Jan-06 15:04:05 MST"
time.RFC1123, // "Mon, 02 Jan 2006 15:04:05 MST"
time.RFC1123Z, // "Mon, 02 Jan 2006 15:04:05 -0700"
time.RFC3339, // "2006-01-02T15:04:05Z07:00"
time.RFC3339Nano, // "2006-01-02T15:04:05.999999999Z07:00"
time.Kitchen, // "3:04PM"
// 实用时间戳格式
time.Stamp, // "Jan _2 15:04:05"
time.StampMilli, // "Jan _2 15:04:05.000"
time.StampMicro, // "Jan _2 15:04:05.000000"
time.StampNano, // "Jan _2 15:04:05.000000000"
time.DateTime, // "2006-01-02 15:04:05"
time.DateOnly, // "2006-01-02"
time.TimeOnly, // "15:04:05"
// 常用自定义格式
"2006-01-02T15:04:05", // 无时区的ISO格式
"2006-01-02T15:04:05.000Z",
"2006-01-02T15:04:05Z",
"2006-01-02 15:04:05.999999999", // 纳秒
"2006-01-02 15:04:05.999999", // 微秒
"2006-01-02 15:04:05.999", // 毫秒
"2006/01/02",
"20060102",
"01/02/2006",
"2006年01月02日",
"2006年01月02日 15:04:05",
}
var lastErr error
for _, format := range formats {
t, err := time.Parse(format, ts)
if err == nil {
return t, nil
}
lastErr = err
}
// 尝试解析 Unix 时间戳
if timestamp, err := strconv.ParseInt(ts, 10, 64); err == nil {
switch len(ts) {
case 10: // 秒
return time.Unix(timestamp, 0), nil
case 13: // 毫秒
return time.Unix(timestamp/1000, (timestamp%1000)*1000000), nil
case 16: // 微秒
return time.Unix(timestamp/1000000, (timestamp%1000000)*1000), nil
case 19: // 纳秒
return time.Unix(timestamp/1000000000, timestamp%1000000000), nil
}
}
return time.Time{}, fmt.Errorf("failed to parse time with any format: %v", lastErr)
}
func ConvertToTable(src td.APIResponse) []interface{} {
var resp []interface{}
for i := range src.Data {
cur := make(map[string]interface{})
for j := range src.Data[i] {
cur[src.ColumnMeta[j][0].(string)] = src.Data[i][j]
}
resp = append(resp, cur)
}
return resp
}

View File

@@ -887,4 +887,20 @@ CREATE TABLE builtin_payloads (
CREATE INDEX idx_component ON builtin_payloads (component);
CREATE INDEX idx_builtin_payloads_name ON builtin_payloads (name);
CREATE INDEX idx_cate ON builtin_payloads (cate);
CREATE INDEX idx_type ON builtin_payloads (type);
CREATE INDEX idx_type ON builtin_payloads (type);
CREATE TABLE dash_annotation (
id bigserial PRIMARY KEY,
dashboard_id bigint not null,
panel_id varchar(191) not null,
tags text,
description text,
config text,
time_start bigint not null default 0,
time_end bigint not null default 0,
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 ''
);

View File

@@ -533,10 +533,9 @@ CREATE TABLE `builtin_components` (
`updated_by` varchar(191) NOT NULL DEFAULT '' COMMENT '''updater''',
`disabled` int NOT NULL DEFAULT 0 COMMENT '''is disabled or not''',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_ident` (`ident`) USING BTREE
UNIQUE KEY `idx_ident` (`ident`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `builtin_payloads` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '''unique identifier''',
`component_id` bigint NOT NULL DEFAULT 0 COMMENT '''component_id of payload''',
@@ -747,6 +746,24 @@ CREATE TABLE `target_busi_group` (
UNIQUE KEY `idx_target_group` (`target_ident`,`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `dash_annotation` (
`id` bigint unsigned not null auto_increment,
`dashboard_id` bigint not null comment 'dashboard id',
`panel_id` varchar(191) not null comment 'panel id',
`tags` text comment 'tags array json string',
`description` text comment 'annotation description',
`config` text comment 'annotation config',
`time_start` bigint not null default 0 comment 'start timestamp',
`time_end` bigint not null default 0 comment 'end timestamp',
`create_at` bigint not null default 0 comment 'create time',
`create_by` varchar(64) not null default '' comment 'creator',
`update_at` bigint not null default 0 comment 'update time',
`update_by` varchar(64) not null default '' comment 'updater',
PRIMARY KEY (`id`),
KEY `idx_dashboard_id` (`dashboard_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `task_meta`
(
`id` bigint unsigned NOT NULL AUTO_INCREMENT,

View File

@@ -129,4 +129,22 @@ ALTER TABLE `builtin_components` MODIFY COLUMN `logo` mediumtext COMMENT '''logo
/* v8.0.0-beta.2 2024-12-26 */
ALTER TABLE `es_index_pattern` ADD COLUMN `cross_cluster_enabled` int not null default 0;
ALTER TABLE builtin_components ADD COLUMN disabled INT NOT NULL DEFAULT 0 COMMENT 'is disabled or not';
/* v8.0.0-beta.3 2024-01-03 */
ALTER TABLE `builtin_components` ADD COLUMN `disabled` INT NOT NULL DEFAULT 0 COMMENT 'is disabled or not';
CREATE TABLE `dash_annotation` (
`id` bigint unsigned not null auto_increment,
`dashboard_id` bigint not null comment 'dashboard id',
`panel_id` varchar(191) not null comment 'panel id',
`tags` text comment 'tags array json string',
`description` text comment 'annotation description',
`config` text comment 'annotation config',
`time_start` bigint not null default 0 comment 'start timestamp',
`time_end` bigint not null default 0 comment 'end timestamp',
`create_at` bigint not null default 0 comment 'create time',
`create_by` varchar(64) not null default '' comment 'creator',
`update_at` bigint not null default 0 comment 'update time',
`update_by` varchar(64) not null default '' comment 'updater',
PRIMARY KEY (`id`),
KEY `idx_dashboard_id` (`dashboard_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@@ -684,6 +684,22 @@ CREATE TABLE `target_busi_group` (
CREATE UNIQUE INDEX idx_target_busi_group ON target_busi_group (target_ident, group_id);
CREATE TABLE `dash_annotation` (
`id` integer primary key autoincrement,
`dashboard_id` bigint not null,
`panel_id` varchar(191) not null,
`tags` text,
`description` text,
`config` text,
`time_start` bigint not null default 0,
`time_end` bigint not null default 0,
`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 ''
);
CREATE TABLE `task_meta`
(
`id` integer primary key autoincrement,

59
dscache/cache.go Normal file
View File

@@ -0,0 +1,59 @@
package dscache
import (
"sync"
"github.com/ccfos/nightingale/v6/datasource"
"github.com/toolkits/pkg/logger"
)
type Cache struct {
datas map[string]map[int64]datasource.Datasource
mutex *sync.RWMutex
}
var DsCache = Cache{
datas: make(map[string]map[int64]datasource.Datasource),
mutex: new(sync.RWMutex),
}
func (cs *Cache) Put(cate string, dsId int64, ds datasource.Datasource) {
cs.mutex.Lock()
if _, found := cs.datas[cate]; !found {
cs.datas[cate] = make(map[int64]datasource.Datasource)
}
if _, found := cs.datas[cate][dsId]; found {
if cs.datas[cate][dsId].Equal(ds) {
cs.mutex.Unlock()
return
}
}
cs.mutex.Unlock()
// InitClient() 在用户配置错误或远端不可用时, 会非常耗时, mutex被长期持有, 导致Get()会超时
err := ds.InitClient()
if err != nil {
logger.Errorf("init plugin:%s %d %+v client fail: %v", cate, dsId, ds, err)
return
}
logger.Debugf("init plugin:%s %d %+v client success", cate, dsId, ds)
cs.mutex.Lock()
cs.datas[cate][dsId] = ds
cs.mutex.Unlock()
}
func (cs *Cache) Get(cate string, dsId int64) (datasource.Datasource, bool) {
cs.mutex.RLock()
defer cs.mutex.RUnlock()
if _, found := cs.datas[cate]; !found {
return nil, false
}
if _, found := cs.datas[cate][dsId]; !found {
return nil, false
}
return cs.datas[cate][dsId], true
}

190
dscache/sync.go Normal file
View File

@@ -0,0 +1,190 @@
package dscache
import (
"context"
"strings"
"sync/atomic"
"time"
"github.com/ccfos/nightingale/v6/datasource"
_ "github.com/ccfos/nightingale/v6/datasource/ck"
"github.com/ccfos/nightingale/v6/datasource/es"
"github.com/ccfos/nightingale/v6/dskit/tdengine"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/toolkits/pkg/logger"
)
var FromAPIHook func()
func Init(ctx *ctx.Context, fromAPI bool) {
go getDatasourcesFromDBLoop(ctx, fromAPI)
}
type ListInput struct {
Page int `json:"p"`
Limit int `json:"limit"`
Category string `json:"category"`
PluginType string `json:"plugin_type"` // promethues
Status string `json:"status"`
}
type DSReply struct {
RequestID string `json:"request_id"`
Data struct {
Items []datasource.DatasourceInfo `json:"items"`
} `json:"data"`
}
type DSReplyEncrypt struct {
RequestID string `json:"request_id"`
Data string `json:"data"`
}
var PromDefaultDatasourceId int64
func getDatasourcesFromDBLoop(ctx *ctx.Context, fromAPI bool) {
for {
if !fromAPI {
items, err := models.GetDatasources(ctx)
if err != nil {
logger.Errorf("get datasource from database fail: %v", err)
//stat.CounterExternalErrorTotal.WithLabelValues("db", "get_cluster").Inc()
time.Sleep(time.Second * 2)
continue
}
var dss []datasource.DatasourceInfo
for _, item := range items {
if item.PluginType == "prometheus" && item.IsDefault {
atomic.StoreInt64(&PromDefaultDatasourceId, item.Id)
}
logger.Debugf("get datasource: %+v", item)
ds := datasource.DatasourceInfo{
Id: item.Id,
Name: item.Name,
Description: item.Description,
Category: item.Category,
PluginId: item.PluginId,
Type: item.PluginType,
PluginTypeName: item.PluginTypeName,
Settings: item.SettingsJson,
HTTPJson: item.HTTPJson,
AuthJson: item.AuthJson,
Status: item.Status,
IsDefault: item.IsDefault,
}
if item.PluginType == "elasticsearch" {
esN9eToDatasourceInfo(&ds, item)
} else if item.PluginType == "opensearch" {
osN9eToDatasourceInfo(&ds, item)
} else if item.PluginType == "tdengine" {
tdN9eToDatasourceInfo(&ds, item)
} else {
ds.Settings = make(map[string]interface{})
for k, v := range item.SettingsJson {
ds.Settings[k] = v
}
}
dss = append(dss, ds)
}
PutDatasources(dss)
} else {
FromAPIHook()
}
time.Sleep(time.Second * 2)
}
}
func tdN9eToDatasourceInfo(ds *datasource.DatasourceInfo, item models.Datasource) {
ds.Settings = make(map[string]interface{})
ds.Settings["tdengine.cluster_name"] = item.Name
ds.Settings["tdengine.addr"] = item.HTTPJson.Url
ds.Settings["tdengine.timeout"] = item.HTTPJson.Timeout
ds.Settings["tdengine.dial_timeout"] = item.HTTPJson.DialTimeout
ds.Settings["tdengine.max_idle_conns_per_host"] = item.HTTPJson.MaxIdleConnsPerHost
ds.Settings["tdengine.headers"] = item.HTTPJson.Headers
ds.Settings["tdengine.basic"] = tdengine.TDengineBasicAuth{
User: item.AuthJson.BasicAuthUser,
Password: item.AuthJson.BasicAuthPassword,
}
}
func esN9eToDatasourceInfo(ds *datasource.DatasourceInfo, item models.Datasource) {
ds.Settings = make(map[string]interface{})
ds.Settings["es.nodes"] = []string{item.HTTPJson.Url}
if len(item.HTTPJson.Urls) > 0 {
ds.Settings["es.nodes"] = item.HTTPJson.Urls
}
ds.Settings["es.timeout"] = item.HTTPJson.Timeout
ds.Settings["es.basic"] = es.BasicAuth{
Username: item.AuthJson.BasicAuthUser,
Password: item.AuthJson.BasicAuthPassword,
}
ds.Settings["es.tls"] = es.TLS{
SkipTlsVerify: item.HTTPJson.TLS.SkipTlsVerify,
}
ds.Settings["es.version"] = item.SettingsJson["version"]
ds.Settings["es.headers"] = item.HTTPJson.Headers
ds.Settings["es.min_interval"] = item.SettingsJson["min_interval"]
ds.Settings["es.max_shard"] = item.SettingsJson["max_shard"]
ds.Settings["es.enable_write"] = item.SettingsJson["enable_write"]
}
// for opensearch
func osN9eToDatasourceInfo(ds *datasource.DatasourceInfo, item models.Datasource) {
ds.Settings = make(map[string]interface{})
ds.Settings["os.nodes"] = []string{item.HTTPJson.Url}
ds.Settings["os.timeout"] = item.HTTPJson.Timeout
ds.Settings["os.basic"] = es.BasicAuth{
Username: item.AuthJson.BasicAuthUser,
Password: item.AuthJson.BasicAuthPassword,
}
ds.Settings["os.tls"] = es.TLS{
SkipTlsVerify: item.HTTPJson.TLS.SkipTlsVerify,
}
ds.Settings["os.version"] = item.SettingsJson["version"]
ds.Settings["os.headers"] = item.HTTPJson.Headers
ds.Settings["os.min_interval"] = item.SettingsJson["min_interval"]
ds.Settings["os.max_shard"] = item.SettingsJson["max_shard"]
}
func PutDatasources(items []datasource.DatasourceInfo) {
ids := make([]int64, 0)
for _, item := range items {
if item.Type == "prometheus" {
continue
}
if item.Type == "loki" {
continue
}
if item.Name == "" {
logger.Warningf("cluster name is empty, ignore %+v", item)
continue
}
typ := strings.ReplaceAll(item.Type, ".logging", "")
ds, err := datasource.GetDatasourceByType(typ, item.Settings)
if err != nil {
logger.Debugf("get plugin:%+v fail: %v", item, err)
continue
}
err = ds.Validate(context.Background())
if err != nil {
logger.Warningf("get plugin:%+v fail: %v", item, err)
continue
}
ids = append(ids, item.Id)
// 异步初始化 client 不然数据源同步的会很慢
go DsCache.Put(typ, item.Id, ds)
}
logger.Debugf("get plugin by type success Ids:%v", ids)
}

View File

@@ -0,0 +1,303 @@
package clickhouse
import (
"context"
"database/sql"
"errors"
"fmt"
"io"
"strings"
"time"
"github.com/ccfos/nightingale/v6/dskit/sqlbase"
"github.com/ccfos/nightingale/v6/dskit/types"
"github.com/ClickHouse/clickhouse-go/v2"
"github.com/mitchellh/mapstructure"
"github.com/toolkits/pkg/net/httplib"
ckDriver "gorm.io/driver/clickhouse"
"gorm.io/gorm"
)
const (
ckDataSource = "clickhouse://%s:%s@%s?read_timeout=10s"
DefaultLimit = 500
)
type Clickhouse struct {
Nodes []string `json:"ck.nodes" mapstructure:"ck.nodes"`
User string `json:"ck.user" mapstructure:"ck.user"`
Password string `json:"ck.password" mapstructure:"ck.password"`
Timeout int `json:"ck.timeout" mapstructure:"ck.timeout"`
MaxQueryRows int `json:"ck.max_query_rows" mapstructure:"ck.max_query_rows"`
Client *gorm.DB `json:"-"`
ClientByHTTP *sql.DB `json:"-"`
}
func (c *Clickhouse) InitCli() error {
if c.MaxQueryRows == 0 {
c.MaxQueryRows = DefaultLimit
}
if len(c.Nodes) == 0 {
return fmt.Errorf("not found ck shard, please check datasource config")
}
addr := c.Nodes[0]
url := addr
if !strings.HasPrefix(url, "http://") {
url = "http://" + url
}
resp, err := httplib.Get(url).SetTimeout(time.Second * 1).Response()
// 忽略HTTP Code错误, 因为可能不是HTTP协议
if err != nil {
return err
}
defer resp.Body.Close()
// HTTP 协议
if resp.StatusCode == 200 {
jsonBytes, _ := io.ReadAll(resp.Body)
if len(jsonBytes) > 0 && strings.Contains(strings.ToLower(string(jsonBytes)), "ok.") {
ckconn := clickhouse.OpenDB(&clickhouse.Options{
Addr: []string{addr},
Auth: clickhouse.Auth{
Username: c.User,
Password: c.Password,
},
Settings: clickhouse.Settings{
"max_execution_time": 60,
},
DialTimeout: 10 * time.Second,
Protocol: clickhouse.HTTP,
})
if ckconn == nil {
return errors.New("db conn failed")
}
c.ClientByHTTP = ckconn
return nil
}
}
db, err := gorm.Open(
ckDriver.New(
ckDriver.Config{
DSN: fmt.Sprintf(ckDataSource,
c.User, c.Password, addr),
DisableDatetimePrecision: true,
DontSupportRenameColumn: true,
SkipInitializeWithVersion: false,
}),
)
if err != nil {
return err
}
c.Client = db
return nil
}
const (
ShowDatabases = "SHOW DATABASES"
ShowTables = "SELECT name FROM system.tables WHERE database = '%s'"
DescTable = "SELECT name,type FROM system.columns WHERE database='%s' AND table = '%s';"
)
func (c *Clickhouse) QueryRows(ctx context.Context, query string) (*sql.Rows, error) {
var (
rows *sql.Rows
err error
)
if c.ClientByHTTP != nil {
rows, err = c.ClientByHTTP.Query(query)
if err != nil {
return nil, err
}
} else if c.Client != nil {
rows, err = c.Client.Raw(query).Rows()
if err != nil {
return nil, err
}
} else {
return nil, fmt.Errorf("clickhouse client is nil")
}
return rows, nil
}
// ShowDatabases lists all databases in Clickhouse
func (c *Clickhouse) ShowDatabases(ctx context.Context) ([]string, error) {
var (
res []string
)
rows, err := c.QueryRows(ctx, ShowDatabases)
if err != nil {
return nil, err
}
for rows.Next() {
var r string
if err := rows.Scan(&r); err != nil {
return nil, err
}
res = append(res, r)
}
return res, nil
}
// ShowTables lists all tables in a given database
func (c *Clickhouse) ShowTables(ctx context.Context, database string) ([]string, error) {
var (
res []string
)
showTables := fmt.Sprintf(ShowTables, database)
rows, err := c.QueryRows(ctx, showTables)
if err != nil {
return nil, err
}
for rows.Next() {
var r string
if err := rows.Scan(&r); err != nil {
return nil, err
}
res = append(res, r)
}
return res, nil
}
// DescribeTable describes the schema of a specified table in Clickhouse
func (c *Clickhouse) DescribeTable(ctx context.Context, query interface{}) ([]*types.ColumnProperty, error) {
var (
ret []*types.ColumnProperty
)
ckQueryParam := new(QueryParam)
if err := mapstructure.Decode(query, ckQueryParam); err != nil {
return nil, err
}
descTable := fmt.Sprintf(DescTable, ckQueryParam.Database, ckQueryParam.Table)
rows, err := c.QueryRows(ctx, descTable)
if err != nil {
return nil, err
}
for rows.Next() {
var column types.ColumnProperty
if err := rows.Scan(&column.Field, &column.Type); err != nil {
return nil, err
}
ret = append(ret, &column)
}
return ret, nil
}
func (c *Clickhouse) ExecQueryBySqlDB(ctx context.Context, sql string) ([]map[string]interface{}, error) {
rows, err := c.QueryRows(ctx, sql)
if err != nil {
return nil, err
}
defer rows.Close()
columns, err := rows.Columns()
if err != nil {
return nil, err
}
var results []map[string]interface{}
for rows.Next() {
columnValues := make([]interface{}, len(columns))
columnPointers := make([]interface{}, len(columns))
for i := range columnValues {
columnPointers[i] = &columnValues[i]
}
if err := rows.Scan(columnPointers...); err != nil {
continue
}
rowMap := make(map[string]interface{})
for i, colName := range columns {
val := columnValues[i]
bytes, ok := val.([]byte)
if ok {
rowMap[colName] = string(bytes)
} else {
rowMap[colName] = val
}
}
results = append(results, rowMap)
}
return results, nil
}
func (c *Clickhouse) Query(ctx context.Context, query interface{}) ([]map[string]interface{}, error) {
ckQuery := new(QueryParam)
if err := mapstructure.Decode(query, ckQuery); err != nil {
return nil, err
}
// 校验SQL的合法性, 过滤掉 write请求
sqlItem := strings.Split(strings.ToUpper(ckQuery.Sql), " ")
for _, item := range sqlItem {
if _, ok := ckBannedOp[item]; ok {
return nil, fmt.Errorf("operation %s is forbid, only read db, please check your sql", item)
}
}
// 检查匹配数据长度,防止数据量过大
err := c.CheckMaxQueryRows(ctx, ckQuery.Sql)
if err != nil {
return nil, err
}
dbRows := make([]map[string]interface{}, 0)
if c.ClientByHTTP != nil {
dbRows, err = c.ExecQueryBySqlDB(ctx, ckQuery.Sql)
} else {
err = c.Client.Raw(ckQuery.Sql).Find(&dbRows).Error
}
if err != nil {
return nil, fmt.Errorf("fetch data failed, sql is %s, err is %s", ckQuery.Sql, err.Error())
}
return dbRows, nil
}
func (c *Clickhouse) CheckMaxQueryRows(ctx context.Context, sql string) error {
subSql := strings.ReplaceAll(sql, ";", "")
subSql = fmt.Sprintf("SELECT COUNT(*) as count FROM (%s) AS subquery;", subSql)
dbRows, err := c.ExecQueryBySqlDB(ctx, subSql)
if err != nil {
return fmt.Errorf("fetch data failed, sql is %s, err is %s", subSql, err.Error())
}
if len(dbRows) > 0 {
if count, exists := dbRows[0]["count"]; exists {
v, err := sqlbase.ParseFloat64Value(count)
if err != nil {
return err
}
if v > float64(c.MaxQueryRows) {
return fmt.Errorf("query result rows count %d exceeds the maximum limit %d", int(v), c.MaxQueryRows)
}
}
}
return nil
}

View File

@@ -0,0 +1,43 @@
package clickhouse
import (
"context"
"encoding/json"
"fmt"
"testing"
"time"
"github.com/ccfos/nightingale/v6/dskit/types"
)
func Test_Timeseries(t *testing.T) {
ck := &Clickhouse{
Nodes: []string{"127.0.0.1:8123"},
User: "default",
Password: "123456",
}
err := ck.InitCli()
if err != nil {
t.Fatal(err)
}
data, err := ck.QueryTimeseries(context.TODO(), &QueryParam{
Sql: `select * from default.student limit 20`,
From: time.Now().Unix() - 300,
To: time.Now().Unix(),
TimeField: "created_at",
TimeFormat: "datetime",
Keys: types.Keys{
LabelKey: "age",
},
})
if err != nil {
t.Fatal(err)
}
bs, err := json.Marshal(data)
if err != nil {
t.Fatal(err)
}
fmt.Println(string(bs))
}

View File

@@ -0,0 +1,58 @@
package clickhouse
import (
"context"
"fmt"
"github.com/ccfos/nightingale/v6/dskit/sqlbase"
"github.com/ccfos/nightingale/v6/dskit/types"
)
const (
TimeFieldFormatEpochMilli = "epoch_millis"
TimeFieldFormatEpochSecond = "epoch_second"
)
// 时序数据相关的API
type QueryParam struct {
Limit int `json:"limit" mapstructure:"limit"`
Sql string `json:"sql" mapstructure:"sql"`
Ref string `json:"ref" mapstructure:"ref"`
From int64 `json:"from" mapstructure:"from"`
To int64 `json:"to" mapstructure:"to"`
TimeField string `json:"time_field" mapstructure:"time_field"`
TimeFormat string `json:"time_format" mapstructure:"time_format"`
Keys types.Keys `json:"keys" mapstructure:"keys"`
Database string `json:"database" mapstructure:"database"`
Table string `json:"table" mapstructure:"table"`
}
var (
ckBannedOp = map[string]struct{}{
"CREATE": {},
"INSERT": {},
"ALTER": {},
"REVOKE": {},
"DROP": {},
"RENAME": {},
"ATTACH": {},
"DETACH": {},
"OPTIMIZE": {},
"TRUNCATE": {},
"SET": {},
}
)
func (c *Clickhouse) QueryTimeseries(ctx context.Context, query *QueryParam) ([]types.MetricValues, error) {
if query.Keys.ValueKey == "" {
return nil, fmt.Errorf("valueKey is required")
}
rows, err := c.Query(ctx, query)
if err != nil {
return nil, err
}
// 构造成时续数据
return sqlbase.FormatMetricValues(query.Keys, rows, true), nil
}

236
dskit/sqlbase/base.go Normal file
View File

@@ -0,0 +1,236 @@
// @Author: Ciusyan 5/19/24
package sqlbase
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
"github.com/ccfos/nightingale/v6/dskit/types"
"gorm.io/gorm"
)
// NewDB creates a new Gorm DB instance based on the provided gorm.Dialector and configures the connection pool
func NewDB(ctx context.Context, dialector gorm.Dialector, maxIdleConns, maxOpenConns int, connMaxLifetime time.Duration) (*gorm.DB, error) {
// Create a new Gorm DB instance
db, err := gorm.Open(dialector, &gorm.Config{})
if err != nil {
return nil, err
}
// Configure the connection pool
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxIdleConns(maxIdleConns)
sqlDB.SetMaxOpenConns(maxOpenConns)
sqlDB.SetConnMaxLifetime(connMaxLifetime)
return db.WithContext(ctx), sqlDB.Ping()
}
// ShowTables retrieves a list of all tables in the specified database
func ShowTables(ctx context.Context, db *gorm.DB, query string) ([]string, error) {
var tables []string
rows, err := db.WithContext(ctx).Raw(query).Rows()
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var table string
if err := rows.Scan(&table); err != nil {
return nil, err
}
tables = append(tables, table)
}
return tables, nil
}
// ShowDatabases retrieves a list of all databases in the connected database server
func ShowDatabases(ctx context.Context, db *gorm.DB, query string) ([]string, error) {
var databases []string
rows, err := db.WithContext(ctx).Raw(query).Rows()
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var database string
if err := rows.Scan(&database); err != nil {
return nil, err
}
databases = append(databases, database)
}
return databases, nil
}
// DescTable describes the schema of a specified table in MySQL or PostgreSQL
func DescTable(ctx context.Context, db *gorm.DB, query string) ([]*types.ColumnProperty, error) {
rows, err := db.WithContext(ctx).Raw(query).Rows()
if err != nil {
return nil, err
}
defer rows.Close()
var columns []*types.ColumnProperty
for rows.Next() {
var (
field string
typ string
null string
key sql.NullString
defaultValue sql.NullString
extra sql.NullString
)
switch db.Dialector.Name() {
case "mysql":
if err := rows.Scan(&field, &typ, &null, &key, &defaultValue, &extra); err != nil {
continue
}
case "postgres", "sqlserver":
if err := rows.Scan(&field, &typ, &null, &defaultValue); err != nil {
continue
}
case "oracle":
if err := rows.Scan(&field, &typ, &null); err != nil {
continue
}
}
// Convert the database-specific type to internal type
type2, indexable := convertDBType(db.Dialector.Name(), typ)
columns = append(columns, &types.ColumnProperty{
Field: field,
Type: typ,
Type2: type2,
Indexable: indexable,
})
}
return columns, nil
}
// ExecQuery executes the specified query and returns the result rows
func ExecQuery(ctx context.Context, db *gorm.DB, sql string) ([]map[string]interface{}, error) {
rows, err := db.WithContext(ctx).Raw(sql).Rows()
if err != nil {
return nil, err
}
defer rows.Close()
columns, err := rows.Columns()
if err != nil {
return nil, err
}
var results []map[string]interface{}
for rows.Next() {
columnValues := make([]interface{}, len(columns))
columnPointers := make([]interface{}, len(columns))
for i := range columnValues {
columnPointers[i] = &columnValues[i]
}
if err := rows.Scan(columnPointers...); err != nil {
continue
}
rowMap := make(map[string]interface{})
for i, colName := range columns {
val := columnValues[i]
bytes, ok := val.([]byte)
if ok {
rowMap[colName] = string(bytes)
} else {
rowMap[colName] = val
}
}
results = append(results, rowMap)
}
return results, nil
}
// SelectRows selects rows from a specified table based on a given query
func SelectRows(ctx context.Context, db *gorm.DB, table, query string) ([]map[string]interface{}, error) {
sql := fmt.Sprintf("SELECT * FROM %s", table)
if query != "" {
sql += " WHERE " + query
}
return ExecQuery(ctx, db, sql)
}
// convertDBType converts MySQL or PostgreSQL data types to custom internal types and determines if they are indexable
func convertDBType(dialect, dbType string) (string, bool) {
typ := strings.ToLower(dbType)
// Common type conversions
switch {
case strings.HasPrefix(typ, "int"), strings.HasPrefix(typ, "tinyint"),
strings.HasPrefix(typ, "smallint"), strings.HasPrefix(typ, "mediumint"),
strings.HasPrefix(typ, "bigint"), strings.HasPrefix(typ, "serial"),
strings.HasPrefix(typ, "bigserial"):
return types.LogExtractValueTypeLong, true
case strings.HasPrefix(typ, "varchar"), strings.HasPrefix(typ, "text"),
strings.HasPrefix(typ, "char"), strings.HasPrefix(typ, "tinytext"),
strings.HasPrefix(typ, "mediumtext"), strings.HasPrefix(typ, "longtext"),
strings.HasPrefix(typ, "character varying"), strings.HasPrefix(typ, "nvarchar"),
strings.HasPrefix(typ, "nchar"):
return types.LogExtractValueTypeText, true
case strings.HasPrefix(typ, "float"), strings.HasPrefix(typ, "double"),
strings.HasPrefix(typ, "decimal"), strings.HasPrefix(typ, "numeric"),
strings.HasPrefix(typ, "real"), strings.HasPrefix(typ, "double precision"):
return types.LogExtractValueTypeFloat, true
case strings.HasPrefix(typ, "date"), strings.HasPrefix(typ, "datetime"),
strings.HasPrefix(typ, "timestamp"), strings.HasPrefix(typ, "timestamptz"),
strings.HasPrefix(typ, "time"), strings.HasPrefix(typ, "smalldatetime"):
return types.LogExtractValueTypeDate, false
case strings.HasPrefix(typ, "boolean"), strings.HasPrefix(typ, "bit"):
return types.LogExtractValueTypeBool, false
}
// Specific type conversions for MySQL
if dialect == "mysql" {
switch {
default:
return typ, false
}
}
// Specific type conversions for PostgreSQL
if dialect == "postgres" {
switch {
default:
return typ, false
}
}
if dialect == "oracle" {
switch {
default:
return typ, false
}
}
// Can continue to add specific 'dialect' type ...
return typ, false
}

250
dskit/sqlbase/timeseries.go Normal file
View File

@@ -0,0 +1,250 @@
// @Author: Ciusyan 5/20/24
package sqlbase
import (
"context"
"crypto/md5"
"encoding/json"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"time"
"github.com/ccfos/nightingale/v6/dskit/types"
"github.com/prometheus/common/model"
"gorm.io/gorm"
)
type QueryParam struct {
Sql string `json:"sql"`
Keys types.Keys `json:"keys" mapstructure:"keys"`
}
var (
BannedOp = map[string]struct{}{
"CREATE": {},
"INSERT": {},
"UPDATE": {},
"DELETE": {},
"ALTER": {},
"REVOKE": {},
"DROP": {},
"RENAME": {},
"TRUNCATE": {},
"SET": {},
}
)
// Query executes a given SQL query and returns the results
func Query(ctx context.Context, db *gorm.DB, query *QueryParam) ([]map[string]interface{}, error) {
// Validate SQL to prevent write operations if needed
sqlItem := strings.Split(strings.ToUpper(query.Sql), " ")
for _, item := range sqlItem {
if _, ok := BannedOp[item]; ok {
return nil, fmt.Errorf("operation %s is forbidden, only read operations are allowed, please check your SQL", item)
}
}
return ExecQuery(ctx, db, query.Sql)
}
// QueryTimeseries executes a time series data query using the given parameters
func QueryTimeseries(ctx context.Context, db *gorm.DB, query *QueryParam, ignoreDefault ...bool) ([]types.MetricValues, error) {
rows, err := Query(ctx, db, query)
if err != nil {
return nil, err
}
return FormatMetricValues(query.Keys, rows, ignoreDefault...), nil
}
func FormatMetricValues(keys types.Keys, rows []map[string]interface{}, ignoreDefault ...bool) []types.MetricValues {
ignore := false
if len(ignoreDefault) > 0 {
ignore = ignoreDefault[0]
}
keyMap := make(map[string]string)
for _, valueMetric := range strings.Split(keys.ValueKey, " ") {
keyMap[valueMetric] = "value"
}
for _, labelMetric := range strings.Split(keys.LabelKey, " ") {
keyMap[labelMetric] = "label"
}
if keys.TimeKey == "" {
keys.TimeKey = "time"
}
if len(keys.TimeKey) > 0 {
keyMap[keys.TimeKey] = "time"
}
var dataResps []types.MetricValues
dataMap := make(map[string]*types.MetricValues)
for _, row := range rows {
labels := make(map[string]string)
metricValue := make(map[string]float64)
metricTs := make(map[string]float64)
// Process each column based on its designated role (value, label, time)
for k, v := range row {
switch keyMap[k] {
case "value":
val, err := ParseFloat64Value(v)
if err != nil {
continue
}
metricValue[k] = val
case "label":
labels[k] = fmt.Sprintf("%v", v)
case "time":
ts, err := ParseTime(v, keys.TimeFormat)
if err != nil {
continue
}
metricTs[k] = float64(ts.Unix())
default:
// Default to labels for any unrecognized columns
if !ignore {
labels[k] = fmt.Sprintf("%v", v)
}
}
}
// Compile and store the metric values
for metricName, value := range metricValue {
metrics := make(model.Metric)
var labelsStr []string
for k1, v1 := range labels {
metrics[model.LabelName(k1)] = model.LabelValue(v1)
labelsStr = append(labelsStr, fmt.Sprintf("%s=%s", k1, v1))
}
metrics["__name__"] = model.LabelValue(metricName)
labelsStr = append(labelsStr, fmt.Sprintf("__name__=%s", metricName))
// Hash the labels to use as a key
sort.Strings(labelsStr)
labelsStrHash := fmt.Sprintf("%x", md5.Sum([]byte(strings.Join(labelsStr, ","))))
// Append new values to the existing metric, if present
ts, exists := metricTs[keys.TimeKey]
if !exists {
ts = float64(time.Now().Unix()) // Default to current time if not specified
}
valuePair := []float64{ts, value}
if existing, ok := dataMap[labelsStrHash]; ok {
existing.Values = append(existing.Values, valuePair)
} else {
dataResp := types.MetricValues{
Metric: metrics,
Values: [][]float64{valuePair},
}
dataMap[labelsStrHash] = &dataResp
}
}
}
// Convert the map to a slice for the response
for _, v := range dataMap {
sort.Slice(v.Values, func(i, j int) bool { return v.Values[i][0] < v.Values[j][0] }) // Sort by timestamp
dataResps = append(dataResps, *v)
}
return dataResps
}
// parseFloat64Value attempts to convert an interface{} to float64 using reflection
func ParseFloat64Value(val interface{}) (float64, error) {
v := reflect.ValueOf(val)
switch v.Kind() {
case reflect.Float64, reflect.Float32:
return v.Float(), nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return float64(v.Int()), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return float64(v.Uint()), nil
case reflect.String:
return strconv.ParseFloat(v.String(), 64)
case reflect.Slice:
if v.Type().Elem().Kind() == reflect.Uint8 {
return strconv.ParseFloat(string(v.Bytes()), 64)
}
case reflect.Interface:
return ParseFloat64Value(v.Interface())
case reflect.Ptr:
if !v.IsNil() {
return ParseFloat64Value(v.Elem().Interface())
}
case reflect.Struct:
if num, ok := val.(json.Number); ok {
return num.Float64()
}
}
return 0, fmt.Errorf("cannot convert type %T to float64", val)
}
// ParseTime attempts to parse a time value from an interface{} using a specified format
func ParseTime(val interface{}, format string) (time.Time, error) {
v := reflect.ValueOf(val)
switch v.Kind() {
case reflect.String:
str := v.String()
return parseTimeFromString(str, format)
case reflect.Slice:
if v.Type().Elem().Kind() == reflect.Uint8 {
str := string(v.Bytes())
return parseTimeFromString(str, format)
}
case reflect.Int, reflect.Int64:
return time.Unix(v.Int(), 0), nil
case reflect.Float64:
return time.Unix(int64(v.Float()), 0), nil
case reflect.Interface:
return ParseTime(v.Interface(), format)
case reflect.Ptr:
if !v.IsNil() {
return ParseTime(v.Elem().Interface(), format)
}
case reflect.Struct:
if t, ok := val.(time.Time); ok {
return t, nil
}
}
return time.Time{}, fmt.Errorf("invalid time value type: %v", val)
}
func parseTimeFromString(str, format string) (time.Time, error) {
// If a custom time format is provided, use it to parse the string
if format != "" {
parsedTime, err := time.Parse(format, str)
if err == nil {
return parsedTime, nil
}
return time.Time{}, fmt.Errorf("failed to parse time '%s' with format '%s': %v", str, format, err)
}
// Try to parse the string as RFC3339, RFC3339Nano, or Unix timestamp
if parsedTime, err := time.Parse(time.RFC3339, str); err == nil {
return parsedTime, nil
}
if parsedTime, err := time.Parse(time.RFC3339Nano, str); err == nil {
return parsedTime, nil
}
if timestamp, err := strconv.ParseInt(str, 10, 64); err == nil {
return time.Unix(timestamp, 0), nil
}
if timestamp, err := strconv.ParseFloat(str, 64); err == nil {
return time.Unix(int64(timestamp), 0), nil
}
return time.Time{}, fmt.Errorf("failed to parse time '%s'", str)
}

View File

@@ -0,0 +1,149 @@
// @Author: Ciusyan 5/17/24
package sqlbase
import (
"encoding/json"
"testing"
"time"
"github.com/ccfos/nightingale/v6/dskit/types"
)
func TestFormatMetricValues(t *testing.T) {
tests := []struct {
name string
keys types.Keys
rows []map[string]interface{}
want []types.MetricValues
}{
{
name: "cases1",
keys: types.Keys{
ValueKey: "grade a_grade",
LabelKey: "id student_name",
TimeKey: "update_time",
TimeFormat: "2006-01-02 15:04:05",
},
rows: []map[string]interface{}{
{
"id": "10007",
"grade": 20003,
"student_name": "邵子韬",
"a_grade": 69,
"update_time": "2024-05-14 10:00:00",
},
{
"id": "10007",
"grade": 20003,
"student_name": "邵子韬",
"a_grade": 69,
"update_time": "2024-05-14 10:05:00",
},
{
"id": "10007",
"grade": 20003,
"student_name": "邵子韬",
"a_grade": 69,
"update_time": "2024-05-14 10:10:00",
},
{
"id": "10008",
"grade": 20004,
"student_name": "Ciusyan",
"a_grade": 100,
"update_time": "2024-05-14 12:00:00",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := FormatMetricValues(tt.keys, tt.rows)
for _, g := range got {
t.Log(g)
}
})
}
}
func TestParseFloat64Value(t *testing.T) {
ptr := func(val float64) *float64 {
return &val
}
tests := []struct {
name string
input interface{}
want float64
wantErr bool
}{
{"float64", 1.23, 1.23, false},
{"float32", float32(1.23), float64(float32(1.23)), false},
{"int", 123, 123, false},
{"int64", int64(123), 123, false},
{"uint", uint(123), 123, false},
{"uint64", uint64(123), 123, false},
{"string", "1.23", 1.23, false},
{"[]byte", []byte("1.23"), 1.23, false},
{"json.Number", json.Number("1.23"), 1.23, false},
{"interface", interface{}(1.23), 1.23, false},
{"pointer", ptr(1.23), 1.23, false},
{"invalid string", "abc", 0, true},
{"invalid type", struct{}{}, 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseFloat64Value(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("parseFloat64Value() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("parseFloat64Value() = %v, want %v", got, tt.want)
}
})
}
}
func TestParseTime(t *testing.T) {
ptrTime := func(t time.Time) *time.Time {
return &t
}
tests := []struct {
name string
input interface{}
format string
want time.Time
wantErr bool
}{
{"RFC3339", "2024-05-14T12:34:56Z", "", time.Date(2024, 5, 14, 12, 34, 56, 0, time.UTC), false},
{"RFC3339Nano", "2024-05-14T12:34:56.789Z", "", time.Date(2024, 5, 14, 12, 34, 56, 789000000, time.UTC), false},
{"Unix timestamp int", int64(1715642135), "", time.Unix(1715642135, 0), false},
{"Unix timestamp float64", 1715642135.0, "", time.Unix(int64(1715642135), 0), false},
{"custom format", "14/05/2024", "02/01/2006", time.Date(2024, 5, 14, 0, 0, 0, 0, time.UTC), false},
{"slice", []byte("2024-05-14T12:34:56Z"), "", time.Date(2024, 5, 14, 12, 34, 56, 0, time.UTC), false},
{"interface", interface{}("2024-05-14T12:34:56Z"), "", time.Date(2024, 5, 14, 12, 34, 56, 0, time.UTC), false},
{"pointer", ptrTime(time.Date(2024, 5, 14, 12, 34, 56, 0, time.UTC)), "", time.Date(2024, 5, 14, 12, 34, 56, 0, time.UTC), false},
{"invalid format", "14-05-2024", "02/01/2006", time.Time{}, true},
{"invalid type", struct{}{}, "", time.Time{}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseTime(tt.input, tt.format)
if (err != nil) != tt.wantErr {
t.Errorf("ParseTime() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !got.Equal(tt.want) {
t.Errorf("ParseTime() = %v, want %v", got, tt.want)
}
})
}
}

207
dskit/tdengine/tdengine.go Normal file
View File

@@ -0,0 +1,207 @@
package tdengine
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net"
"net/http"
"strings"
"time"
"github.com/ccfos/nightingale/v6/dskit/types"
"github.com/ccfos/nightingale/v6/pkg/tlsx"
"github.com/toolkits/pkg/logger"
)
type Tdengine struct {
Addr string `json:"tdengine.addr" mapstructure:"tdengine.addr"`
Basic *TDengineBasicAuth `json:"tdengine.basic" mapstructure:"tdengine.basic"`
Token string `json:"tdengine.token" mapstructure:"tdengine.token"`
Timeout int64 `json:"tdengine.timeout" mapstructure:"tdengine.timeout"`
DialTimeout int64 `json:"tdengine.dial_timeout" mapstructure:"tdengine.dial_timeout"`
MaxIdleConnsPerHost int `json:"tdengine.max_idle_conns_per_host" mapstructure:"tdengine.max_idle_conns_per_host"`
Headers map[string]string `json:"tdengine.headers" mapstructure:"tdengine.headers"`
SkipTlsVerify bool `json:"tdengine.skip_tls_verify" mapstructure:"tdengine.skip_tls_verify"`
tlsx.ClientConfig
header map[string][]string `json:"-"`
client *http.Client `json:"-"`
}
type TDengineBasicAuth struct {
User string `json:"tdengine.user" mapstructure:"tdengine.user"`
Password string `json:"tdengine.password" mapstructure:"tdengine.password"`
IsEncrypt bool `json:"tdengine.is_encrypt" mapstructure:"tdengine.is_encrypt"`
}
type APIResponse struct {
Code int `json:"code"`
ColumnMeta [][]interface{} `json:"column_meta"`
Data [][]interface{} `json:"data"`
Rows int `json:"rows"`
}
type QueryParam struct {
Database string `json:"database"`
Table string `json:"table"`
}
func (tc *Tdengine) InitCli() {
tc.client = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
DisableCompression: true,
},
}
tc.header = map[string][]string{
"Connection": {"keep-alive"},
}
for k, v := range tc.Headers {
kv := strings.Split(v, ":")
if len(kv) != 2 {
continue
}
tc.header[k] = []string{v}
}
if tc.Basic != nil {
basic := base64.StdEncoding.EncodeToString([]byte(tc.Basic.User + ":" + tc.Basic.Password))
tc.header["Authorization"] = []string{fmt.Sprintf("Basic %s", basic)}
}
}
func (tc *Tdengine) QueryTable(query string) (APIResponse, error) {
var apiResp APIResponse
req, err := http.NewRequest("POST", tc.Addr+"/rest/sql", strings.NewReader(query))
if err != nil {
return apiResp, err
}
for k, v := range tc.header {
req.Header[k] = v
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := tc.client.Do(req)
if err != nil {
return apiResp, err
}
defer resp.Body.Close()
// 限制响应体大小为10MB
maxSize := int64(10 * 1024 * 1024) // 10MB
limitedReader := http.MaxBytesReader(nil, resp.Body, maxSize)
if resp.StatusCode != http.StatusOK {
return apiResp, fmt.Errorf("HTTP error, status: %s", resp.Status)
}
err = json.NewDecoder(limitedReader).Decode(&apiResp)
if err != nil {
if strings.Contains(err.Error(), "http: request body too large") {
return apiResp, fmt.Errorf("response body exceeds 10MB limit")
}
return apiResp, err
}
return apiResp, nil
}
func (tc *Tdengine) ShowDatabases(context.Context) ([]string, error) {
var databases []string
data, err := tc.QueryTable("show databases")
if err != nil {
return databases, err
}
for _, row := range data.Data {
databases = append(databases, row[0].(string))
}
return databases, nil
}
func (tc *Tdengine) ShowTables(ctx context.Context, database string) ([]string, error) {
var tables []string
sql := fmt.Sprintf("show %s", database)
data, err := tc.QueryTable(sql)
if err != nil {
return tables, err
}
for _, row := range data.Data {
tables = append(tables, row[0].(string))
}
return tables, nil
}
func (tc *Tdengine) DescribeTable(ctx context.Context, query interface{}) ([]*types.ColumnProperty, error) {
var columns []*types.ColumnProperty
queryMap, ok := query.(map[string]string)
if !ok {
return nil, fmt.Errorf("invalid query")
}
sql := fmt.Sprintf("select * from %s.%s limit 1", queryMap["database"], queryMap["table"])
data, err := tc.QueryTable(sql)
if err != nil {
return columns, err
}
for _, row := range data.ColumnMeta {
var colType string
switch t := row[1].(type) {
case float64:
// v2版本数字类型映射
switch int(t) {
case 1:
colType = "BOOL"
case 2:
colType = "TINYINT"
case 3:
colType = "SMALLINT"
case 4:
colType = "INT"
case 5:
colType = "BIGINT"
case 6:
colType = "FLOAT"
case 7:
colType = "DOUBLE"
case 8:
colType = "BINARY"
case 9:
colType = "TIMESTAMP"
case 10:
colType = "NCHAR"
default:
colType = "UNKNOWN"
}
case string:
// v3版本直接使用字符串类型
colType = t
default:
logger.Warningf("unexpected column type format: %v", row[1])
colType = "UNKNOWN"
}
column := &types.ColumnProperty{
Field: row[0].(string),
Type: colType,
}
columns = append(columns, column)
}
return columns, nil
}

51
dskit/types/timeseries.go Normal file
View File

@@ -0,0 +1,51 @@
package types
import (
"bytes"
"fmt"
"strconv"
"github.com/prometheus/common/model"
)
// 时序数据
type MetricValues struct {
Metric model.Metric `json:"metric"`
Values [][]float64 `json:"values"`
}
type HistogramValues struct {
Total int64 `json:"total"`
Values [][]float64 `json:"values"`
}
// 瞬时值
type AggregateValues struct {
Labels map[string]string `json:"labels"`
Values map[string]float64 `json:"values"`
}
// string
func (m *MetricValues) String() string {
var buf bytes.Buffer
buf.WriteString(fmt.Sprintf("Metric: %+v ", m.Metric))
buf.WriteString("Values: ")
for _, v := range m.Values {
buf.WriteString(" [")
for i, ts := range v {
if i > 0 {
buf.WriteString(", ")
}
buf.WriteString(strconv.FormatFloat(ts, 'f', -1, 64))
}
buf.WriteString("] ")
}
return buf.String()
}
type Keys struct {
ValueKey string `json:"valueKey" mapstructure:"valueKey"` // 多个用空格分隔
LabelKey string `json:"labelKey" mapstructure:"labelKey"` // 多个用空格分隔
TimeKey string `json:"timeKey" mapstructure:"timeKey"`
TimeFormat string `json:"timeFormat" mapstructure:"timeFormat"` // not used anymore
}

19
dskit/types/types.go Normal file
View File

@@ -0,0 +1,19 @@
package types
const (
LogExtractValueTypeLong = "long"
LogExtractValueTypeFloat = "float"
LogExtractValueTypeText = "text"
LogExtractValueTypeDate = "date"
LogExtractValueTypeBool = "bool"
LogExtractValueTypeObject = "object"
LogExtractValueTypeArray = "array"
LogExtractValueTypeJSON = "json"
)
type ColumnProperty struct {
Field string `json:"field"`
Type string `json:"type"`
Type2 string `json:"type2,omitempty"` // field_property.Type
Indexable bool `json:"indexable"` // 是否可以索引
}

66
go.mod
View File

@@ -3,8 +3,11 @@ module github.com/ccfos/nightingale/v6
go 1.22
require (
github.com/BurntSushi/toml v0.3.1
github.com/BurntSushi/toml v1.4.0
github.com/ClickHouse/clickhouse-go/v2 v2.23.2
github.com/VictoriaMetrics/metricsql v0.81.1
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
github.com/bitly/go-simplejson v0.5.1
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/dgrijalva/jwt-go v3.2.0+incompatible
@@ -16,35 +19,54 @@ require (
github.com/go-ldap/ldap/v3 v3.4.4
github.com/gogo/protobuf v1.3.2
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang/protobuf v1.5.3
github.com/golang/protobuf v1.5.4
github.com/golang/snappy v0.0.4
github.com/google/uuid v1.3.0
github.com/google/uuid v1.6.0
github.com/hashicorp/go-version v1.6.0
github.com/jinzhu/copier v0.4.0
github.com/json-iterator/go v1.1.12
github.com/koding/multiconfig v0.0.0-20171124222453-69c27309b2d7
github.com/mailru/easyjson v0.7.7
github.com/mattn/go-isatty v0.0.19
github.com/mitchellh/mapstructure v1.5.0
github.com/mojocn/base64Captcha v1.3.6
github.com/olivere/elastic/v7 v7.0.32
github.com/pelletier/go-toml/v2 v2.0.8
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.16.0
github.com/prometheus/common v0.44.0
github.com/prometheus/client_golang v1.20.5
github.com/prometheus/common v0.60.1
github.com/prometheus/prometheus v0.47.1
github.com/rakyll/statik v0.1.7
github.com/redis/go-redis/v9 v9.0.2
github.com/spaolacci/murmur3 v1.1.0
github.com/stretchr/testify v1.8.4
github.com/tidwall/gjson v1.14.0
github.com/stretchr/testify v1.9.0
github.com/tidwall/gjson v1.14.2
github.com/toolkits/pkg v1.3.8
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1
golang.org/x/oauth2 v0.10.0
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
golang.org/x/oauth2 v0.23.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/yaml.v2 v2.4.0
gorm.io/driver/clickhouse v0.6.1
gorm.io/driver/mysql v1.4.4
gorm.io/driver/postgres v1.5.11
gorm.io/driver/sqlite v1.5.5
gorm.io/gorm v1.25.12
gorm.io/gorm v1.25.10
)
require (
github.com/ClickHouse/ch-go v0.61.5 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/paulmach/orb v0.11.1 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
go.opentelemetry.io/otel v1.32.0 // indirect
go.opentelemetry.io/otel/trace v1.32.0 // indirect
)
require (
@@ -54,8 +76,8 @@ require (
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/jackc/pgx/v5 v5.7.1 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/valyala/fastrand v1.1.0 // indirect
github.com/valyala/histogram v1.2.0 // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect
@@ -71,7 +93,7 @@ require (
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
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dennwc/varint v1.0.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
@@ -89,26 +111,19 @@ require (
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.13.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.1 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgtype v1.12.0 // indirect
github.com/jackc/pgx/v4 v4.17.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pquerna/cachecontrol v0.1.0 // indirect
github.com/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/procfs v0.11.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/robfig/cron/v3 v3.0.1
github.com/tidwall/match v1.1.1
github.com/tidwall/pretty v1.2.0 // indirect
@@ -119,12 +134,13 @@ require (
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/image v0.18.0 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.33.0 // indirect
google.golang.org/protobuf v1.35.1 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace golang.org/x/exp v0.0.0-20231006140011-7918f672742d => golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1

273
go.sum
View File

@@ -9,10 +9,12 @@ github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft
github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 h1:OBhqkivkhkMqLPymWEppkm7vgPQY2XsHoEkaMQ0AdZY=
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/ClickHouse/ch-go v0.61.5 h1:zwR8QbYI0tsMiEcze/uIMK+Tz1D3XZXLdNrlaOpeEI4=
github.com/ClickHouse/ch-go v0.61.5/go.mod h1:s1LJW/F/LcFs5HJnuogFMta50kKDO0lf9zzfrbl0RQg=
github.com/ClickHouse/clickhouse-go/v2 v2.23.2 h1:+DAKPMnxLS7pduQZsrJc8OhdLS2L9MfDEJ2TS+hpYDM=
github.com/ClickHouse/clickhouse-go/v2 v2.23.2/go.mod h1:aNap51J1OM3yxQJRgM+AlP/MPkGBCL8A74uQThoQhR0=
github.com/VictoriaMetrics/metrics v1.34.0 h1:0i8k/gdOJdSoZB4Z9pikVnVQXfhcIvnG7M7h2WaQW2w=
github.com/VictoriaMetrics/metrics v1.34.0/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8=
github.com/VictoriaMetrics/metricsql v0.81.1 h1:1gpqI3Mwru1tCM8nZiKxBG0P+DNkjlRwLhRPII3cuho=
@@ -23,10 +25,16 @@ github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZp
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/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
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=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow=
github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q=
github.com/bsm/ginkgo/v2 v2.5.0 h1:aOAnND1T40wEdAtkGSkvSICWeQ8L3UASX7YVCqQx+eQ=
github.com/bsm/ginkgo/v2 v2.5.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
github.com/bsm/gomega v1.20.0 h1:JhAwLmtRzXFTx2AkALSLa8ijZafntmhSoU63Ok18Uq8=
@@ -34,18 +42,13 @@ github.com/bsm/gomega v1.20.0/go.mod h1:JifAceMQ4crZIWYUKrlGcmbN3bqHogVTADMD2ATs
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -67,6 +70,8 @@ github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/flashcatcloud/ibex v1.3.5 h1:8GOOf5+aJT0TP/MC6izz7CO5JKJSdKVFBwL0vQp93Nc=
github.com/flashcatcloud/ibex v1.3.5/go.mod h1:T8hbMUySK2q6cXUaYp0AUVeKkU9Od2LjzwmB5lmTRBM=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/garyburd/redigo v1.6.2/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
@@ -83,12 +88,14 @@ github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GM
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A=
github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs=
github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
@@ -105,12 +112,9 @@ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
@@ -119,78 +123,31 @@ github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOW
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 h1:n6vlPhxsA+BW/XsS5+uqi7GyzaLa5MH7qlSLBZtRdiA=
github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd h1:PpuIBO5P3e9hpqBD0O/HjhShYuM6XE0i/lbE6J94kww=
github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys=
github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y=
github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w=
github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E=
github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw=
github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0=
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
@@ -210,22 +167,21 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/koding/multiconfig v0.0.0-20171124222453-69c27309b2d7 h1:SWlt7BoQNASbhTUD0Oy5yysI2seJ7vWuGUp///OM4TM=
github.com/koding/multiconfig v0.0.0-20171124222453-69c27309b2d7/go.mod h1:Y2SaZf2Rzd0pXkLVhLlCiAXFCLSXAIbTKDivVgff/AM=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
@@ -234,25 +190,16 @@ github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -260,17 +207,26 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mojocn/base64Captcha v1.3.6 h1:gZEKu1nsKpttuIAQgWHO+4Mhhls8cAKyiV2Ew03H+Tw=
github.com/mojocn/base64Captcha v1.3.6/go.mod h1:i5CtHvm+oMbj1UzEPXaA8IH/xHFZ3DGY3Wh3dBpZ28E=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olivere/elastic/v7 v7.0.32 h1:R7CXvbu8Eq+WlsLgxmKVKPox0oOwAE/2T9Si5BnvK6E=
github.com/olivere/elastic/v7 v7.0.32/go.mod h1:c7PVmLe3Fxq77PIfY/bZmxY/TAamBhCzZ8xDOE09a9k=
github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -280,16 +236,16 @@ github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8
github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc=
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4=
github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI=
github.com/prometheus/procfs v0.11.0 h1:5EAgkfkMl659uZPbe9AS2N68a7Cc1TJbPEuGzFuRbyk=
github.com/prometheus/procfs v0.11.0/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/prometheus/prometheus v0.47.1 h1:bd2LiZyxzHn9Oo2Ei4eK2D86vz/L/OiqR1qYo0XmMBo=
github.com/prometheus/prometheus v0.47.1/go.mod h1:J/bmOSjgH7lFxz2gZhrWEZs2i64vMS+HIuZfmYNhJ/M=
github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ=
@@ -299,34 +255,26 @@ github.com/redis/go-redis/v9 v9.0.2/go.mod h1:/xDTe9EF1LM61hek62Poq2nzQSGj0xSrEt
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62/go.mod h1:65XQgovT59RWatovFwnwocoUxiI/eENTnOY5GK3STuY=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -335,12 +283,13 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w=
github.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/toolkits/pkg v1.3.8 h1:2yamC20c5mHRtbcGiLY99Lm/2mVitFn6onE8KKvMT1o=
@@ -355,45 +304,36 @@ github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G
github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ=
github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
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=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/automaxprocs v1.4.0/go.mod h1:/mTEdr7LvHhs0v7mjdxDreTz1OG5zdZGqgOnhWiR/+Q=
go.uber.org/automaxprocs v1.5.2 h1:2LxUOGiR3O6tw8ui5sZa2LAaHnsviZdVOUZw4fvbnME=
go.uber.org/automaxprocs v1.5.2/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
@@ -403,18 +343,12 @@ golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N0
golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -423,28 +357,20 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -461,16 +387,13 @@ golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
@@ -481,32 +404,21 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -516,7 +428,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -526,21 +437,17 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/clickhouse v0.6.1 h1:t7JMB6sLBXxN8hEO6RdzCbJCwq/jAEVZdwXlmQs1Sd4=
gorm.io/driver/clickhouse v0.6.1/go.mod h1:riMYpJcGZ3sJ/OAZZ1rEP1j/Y0H6cByOAnwz7fo2AyM=
gorm.io/driver/mysql v1.4.4 h1:MX0K9Qvy0Na4o7qSC/YI7XxqUw5KDw01umqgID+svdQ=
gorm.io/driver/mysql v1.4.4/go.mod h1:BCg8cKI+R0j/rZRQxeKis/forqRwRSYOR8OM3Wo6hOM=
gorm.io/driver/postgres v1.4.5 h1:mTeXTTtHAgnS9PgmhN2YeUbazYpLhUI1doLnw42XUZc=
gorm.io/driver/postgres v1.4.5/go.mod h1:GKNQYSJ14qvWkvPwXljMGehpKrhlDNsqYRr5HnYGncg=
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.24.1-0.20221019064659-5dd2bb482755/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=

View File

@@ -1,3 +1,4 @@
interval="5m"
[[instances]]
## Amazon Region
# list of region and endpoint, see https://docs.aws.amazon.com/general/latest/gr/cw_region.html
@@ -40,17 +41,13 @@
## particular metric, that metric will not be returned by the CloudWatch API
## and will not be collected by Telegraf.
#
## Requested CloudWatch aggregation Period (required)
## Must be a multiple of 60s.
period = "5m"
## Collection Delay (required)
## Must account for metrics availability via CloudWatch API
delay = "5m"
## Recommended: use metric 'interval' that is a multiple of 'period' to avoid
## gaps or overlap in pulled data
interval = "5m"
## Requested CloudWatch aggregation Period (required)
## Must be a multiple of 60s.
period = "5m"
## Recommended if "delay" and "period" are both within 3 hours of request
## time. Invalid values will be ignored. Recently Active feature will only

View File

@@ -71,18 +71,14 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
## Note that if a period is configured that is smaller than the minimum for a
## particular metric, that metric will not be returned by the CloudWatch API
## and will not be collected by Categraf.
#
## Requested CloudWatch aggregation Period (required)
## Must be a multiple of 60s.
period = "5m"
## Collection Delay (required)
## Must account for metrics availability via CloudWatch API
delay = "5m"
## Recommended: use metric 'interval' that is a multiple of 'period' to avoid
## gaps or overlap in pulled data
interval = "5m"
## Requested CloudWatch aggregation Period (required)
## Must be a multiple of 60s.
period = "5m"
## Recommended if "delay" and "period" are both within 3 hours of request
## time. Invalid values will be ignored. Recently Active feature will only

View File

@@ -10,6 +10,7 @@ import (
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/pkg/poster"
"github.com/ccfos/nightingale/v6/pushgw/pconf"
"github.com/robfig/cron/v3"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
@@ -27,6 +28,8 @@ const (
PROMETHEUS = "prometheus"
TDENGINE = "tdengine"
ELASTICSEARCH = "elasticsearch"
CLICKHOUSE = "ck"
)
const (
@@ -158,9 +161,9 @@ type PromRuleConfig struct {
type RecoverJudge int
const (
Origin RecoverJudge = 0
RecoverWithoutData RecoverJudge = 1
RecoverOnCondition RecoverJudge = 2
Origin RecoverJudge = 0
NotRecoverWhenNoData RecoverJudge = 1
RecoverOnCondition RecoverJudge = 2
)
type RecoverConfig struct {
@@ -191,10 +194,20 @@ type HostTrigger struct {
}
type RuleQuery struct {
Version string `json:"version"`
Inhibit bool `json:"inhibit"`
Queries []interface{} `json:"queries"`
Triggers []Trigger `json:"triggers"`
Version string `json:"version"`
Inhibit bool `json:"inhibit"`
Queries []interface{} `json:"queries"`
ExpTriggerDisable bool `json:"exp_trigger_disable"`
Triggers []Trigger `json:"triggers"`
NodataTrigger NodataTrigger `json:"nodata_trigger"`
TriggerType TriggerType `json:"trigger_type,omitempty"`
}
type NodataTrigger struct {
Enable bool `json:"enable"`
Severity int `json:"severity"`
ResolveAfterEnable bool `json:"resolve_after_enable"`
ResolveAfter int `json:"resolve_after"` // 单位秒
}
type Trigger struct {
@@ -493,6 +506,27 @@ func (ar *AlertRule) Verify() error {
}
}
if err := ar.validateCronPattern(); err != nil {
return err
}
return nil
}
func (ar *AlertRule) validateCronPattern() error {
if ar.CronPattern == "" {
return nil
}
// 创建一个临时的 cron scheduler 来验证表达式
scheduler := cron.New(cron.WithSeconds())
// 尝试添加一个空函数来验证 cron 表达式
_, err := scheduler.AddFunc(ar.CronPattern, func() {})
if err != nil {
return fmt.Errorf("invalid cron pattern: %s, error: %v", ar.CronPattern, err)
}
return nil
}
@@ -1124,6 +1158,14 @@ func (ar *AlertRule) GetRuleType() string {
return ar.Prod
}
func (ar *AlertRule) IsClickHouseRule() bool {
return ar.Cate == CLICKHOUSE
}
func (ar *AlertRule) IsElasticSearch() bool {
return ar.Cate == ELASTICSEARCH
}
func (ar *AlertRule) GenerateNewEvent(ctx *ctx.Context) *AlertCurEvent {
event := &AlertCurEvent{}
ar.UpdateEvent(event)
@@ -1143,8 +1185,6 @@ func (ar *AlertRule) UpdateEvent(event *AlertCurEvent) {
event.RuleProd = ar.Prod
event.RuleAlgo = ar.Algorithm
event.PromForDuration = ar.PromForDuration
event.RuleConfig = ar.RuleConfig
event.RuleConfigJson = ar.RuleConfigJson
event.Callbacks = ar.Callbacks
event.CallbacksJSON = ar.CallbacksJSON
event.RunbookUrl = ar.RunbookUrl

View File

@@ -20,8 +20,16 @@ type AnomalyPoint struct {
Values string `json:"values"`
ValuesUnit map[string]unit.FormattedValue `json:"values_unit"`
RecoverConfig RecoverConfig `json:"recover_config"`
TriggerType TriggerType `json:"trigger_type"`
}
type TriggerType string
const (
TriggerTypeNormal TriggerType = "normal"
TriggerTypeNodata TriggerType = "nodata"
)
func NewAnomalyPoint(key string, labels map[string]string, ts int64, value float64, severity int) AnomalyPoint {
anomalyPointLabels := make(model.Metric)
for k, v := range labels {

View File

@@ -11,7 +11,7 @@ import (
// BuiltinComponent represents a builtin component along with its metadata.
type BuiltinComponent 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"`
Ident string `json:"ident" gorm:"type:varchar(191);not null;index:idx_ident"`
Logo string `json:"logo" gorm:"type:mediumtext;comment:'logo of component'"`
Readme string `json:"readme" gorm:"type:text;not null;comment:'readme of component'"`
Disabled int `json:"disabled" gorm:"type:int;not null;default:0;comment:'is disabled or not'"`
@@ -23,7 +23,7 @@ type BuiltinComponent struct {
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'"`
Ident string `json:"ident" gorm:"type:varchar(191);not null;index:idx_ident;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'"`
Disabled int `json:"disabled" gorm:"type:int;not null;default:0;comment:'is disabled or not'"`

View File

@@ -162,11 +162,6 @@ var entries = []CheckEntry{
ErrorMessage: "Some recovery scripts still in the BusiGroup",
FieldName: "group_id",
},
{
Entry: &TaskRecord{},
ErrorMessage: "Some Task Record records still in the BusiGroup",
FieldName: "group_id",
},
{
Entry: &TargetBusiGroup{},
ErrorMessage: "Some target busigroups still in the BusiGroup",

89
models/dash_annotation.go Normal file
View File

@@ -0,0 +1,89 @@
package models
import (
"encoding/json"
"github.com/ccfos/nightingale/v6/pkg/ctx"
)
type DashAnnotation struct {
Id int64 `json:"id" gorm:"primaryKey"`
DashboardId int64 `json:"dashboard_id"`
PanelId string `json:"panel_id"`
Tags string `json:"-"`
TagsJSON []string `json:"tags" gorm:"-"`
Description string `json:"description"`
Config string `json:"config"`
TimeStart int64 `json:"time_start"`
TimeEnd int64 `json:"time_end"`
CreateAt int64 `json:"create_at"`
CreateBy string `json:"create_by"`
UpdateAt int64 `json:"update_at"`
UpdateBy string `json:"update_by"`
}
func (da *DashAnnotation) TableName() string {
return "dash_annotation"
}
func (da *DashAnnotation) DB2FE() error {
return json.Unmarshal([]byte(da.Tags), &da.TagsJSON)
}
func (da *DashAnnotation) FE2DB() error {
b, err := json.Marshal(da.TagsJSON)
if err != nil {
return err
}
da.Tags = string(b)
return nil
}
func (da *DashAnnotation) Add(ctx *ctx.Context) error {
if err := da.FE2DB(); err != nil {
return err
}
return Insert(ctx, da)
}
func (da *DashAnnotation) Update(ctx *ctx.Context) error {
if err := da.FE2DB(); err != nil {
return err
}
return DB(ctx).Model(da).Select("dashboard_id", "panel_id", "tags", "description", "config", "time_start", "time_end", "update_at", "update_by").Updates(da).Error
}
func DashAnnotationDel(ctx *ctx.Context, id int64) error {
return DB(ctx).Where("id = ?", id).Delete(&DashAnnotation{}).Error
}
func DashAnnotationGet(ctx *ctx.Context, where string, args ...interface{}) (*DashAnnotation, error) {
var lst []*DashAnnotation
err := DB(ctx).Where(where, args...).Find(&lst).Error
if err != nil {
return nil, err
}
if len(lst) == 0 {
return nil, nil
}
err = lst[0].DB2FE()
return lst[0], err
}
func DashAnnotationGets(ctx *ctx.Context, dashboardId int64, from, to int64, limit int) ([]DashAnnotation, error) {
session := DB(ctx).Where("dashboard_id = ? AND time_start <= ? AND time_end >= ?", dashboardId, to, from)
var lst []DashAnnotation
err := session.Order("id").Limit(limit).Find(&lst).Error
if err != nil {
return nil, err
}
for i := 0; i < len(lst); i++ {
lst[i].DB2FE()
}
return lst, nil
}

View File

@@ -67,7 +67,7 @@ func MigrateTables(db *gorm.DB) error {
&TaskRecord{}, &ChartShare{}, &Target{}, &Configs{}, &Datasource{}, &NotifyTpl{},
&Board{}, &BoardBusigroup{}, &Users{}, &SsoConfig{}, &models.BuiltinMetric{},
&models.MetricFilter{}, &models.NotificaitonRecord{},
&models.TargetBusiGroup{}, &EsIndexPatternMigrate{}}
&models.TargetBusiGroup{}, &EsIndexPatternMigrate{}, &DashAnnotation{}}
if isPostgres(db) {
dts = append(dts, &models.PostgresBuiltinComponent{})
@@ -327,3 +327,22 @@ type EsIndexPatternMigrate struct {
func (EsIndexPatternMigrate) TableName() string {
return "es_index_pattern"
}
type DashAnnotation struct {
Id int64 `gorm:"column:id;primaryKey;autoIncrement"`
DashboardId int64 `gorm:"column:dashboard_id;not null"`
PanelId string `gorm:"column:panel_id;type:varchar(191);not null"`
Tags string `gorm:"column:tags;type:text"`
Description string `gorm:"column:description;type:text"`
Config string `gorm:"column:config;type:text"`
TimeStart int64 `gorm:"column:time_start;not null;default:0"`
TimeEnd int64 `gorm:"column:time_end;not null;default:0"`
CreateAt int64 `gorm:"column:create_at;not null;default:0"`
CreateBy string `gorm:"column:create_by;type:varchar(64);not null;default:''"`
UpdateAt int64 `gorm:"column:update_at;not null;default:0"`
UpdateBy string `gorm:"column:update_by;type:varchar(64);not null;default:''"`
}
func (DashAnnotation) TableName() string {
return "dash_annotation"
}

View File

@@ -53,6 +53,12 @@ func (d *DataResp) MetricName() string {
return string(metric)
}
// labels 转换为 string
func (d *DataResp) LabelsString() string {
labels := d.Metric
return labels.String()
}
type RelationKey struct {
LeftKey string `json:"left_key"`
RightKey string `json:"right_key"`

11
pkg/macros/macros.go Normal file
View File

@@ -0,0 +1,11 @@
package macros
var Macro func(sql string, start, end int64) (string, error)
func RegisterMacro(f func(sql string, start, end int64) (string, error)) {
Macro = f
}
func MacroInVain(sql string, start, end int64) (string, error) {
return sql, nil
}

View File

@@ -97,3 +97,13 @@ func between(target float64, arr []interface{}) bool {
return target >= min && target <= max
}
func CalcWithRid(s string, data map[string]interface{}, rid int64) bool {
v, err := MathCalc(s, data)
if err != nil {
logger.Errorf("rid:%d exp:%s data:%v error: %v", rid, s, data, err)
return false
}
return v > 0
}

View File

@@ -23,24 +23,31 @@ type DataResponse[T any] struct {
}
func GetByUrls[T any](ctx *ctx.Context, path string) (T, error) {
var err error
addrs := ctx.CenterApi.Addrs
if len(addrs) == 0 {
var dat T
return dat, fmt.Errorf("no center api addresses configured")
}
rand.Shuffle(len(addrs), func(i, j int) { addrs[i], addrs[j] = addrs[j], addrs[i] })
for _, addr := range addrs {
url := fmt.Sprintf("%s%s", addr, path)
dat, e := GetByUrl[T](url, ctx.CenterApi)
if e != nil {
err = e
// 随机选择起始位置
startIdx := rand.Intn(len(addrs))
// 从随机位置开始遍历所有地址
var dat T
var err error
for i := 0; i < len(addrs); i++ {
idx := (startIdx + i) % len(addrs)
url := fmt.Sprintf("%s%s", addrs[idx], path)
dat, err = GetByUrl[T](url, ctx.CenterApi)
if err != nil {
logger.Warningf("failed to get data from center, url: %s, err: %v", url, err)
continue
}
return dat, nil
}
var dat T
err = fmt.Errorf("failed to get data from center, path= %s, ctx.CenterApi.Addrs= %v", path, addrs)
return dat, err
return dat, fmt.Errorf("failed to get data from center, path= %s, addrs= %v err: %v", path, addrs, err)
}
func GetByUrl[T any](url string, cfg conf.CenterApi) (T, error) {
@@ -97,25 +104,32 @@ func GetByUrl[T any](url string, cfg conf.CenterApi) (T, error) {
return dataResp.Dat, nil
}
func PostByUrls(ctx *ctx.Context, path string, v interface{}) (err error) {
func PostByUrls(ctx *ctx.Context, path string, v interface{}) error {
addrs := ctx.CenterApi.Addrs
rand.Shuffle(len(addrs), func(i, j int) { addrs[i], addrs[j] = addrs[j], addrs[i] })
for _, addr := range addrs {
url := fmt.Sprintf("%s%s", addr, path)
_, err = PostByUrl[interface{}](url, ctx.CenterApi, v)
if err == nil {
return
}
}
if len(addrs) < 1 {
err = fmt.Errorf("submission of the POST request from the center has failed, "+
if len(addrs) == 0 {
return fmt.Errorf("submission of the POST request from the center has failed, "+
"path= %s, v= %v, ctx.CenterApi.Addrs= %v", path, v, addrs)
}
return
// 随机选择起始位置
startIdx := rand.Intn(len(addrs))
// 从随机位置开始遍历所有地址
for i := 0; i < len(addrs); i++ {
idx := (startIdx + i) % len(addrs)
url := fmt.Sprintf("%s%s", addrs[idx], path)
_, err := PostByUrl[interface{}](url, ctx.CenterApi, v)
if err != nil {
logger.Warningf("failed to post data to center, url: %s, err: %v", url, err)
continue
}
return nil
}
return fmt.Errorf("failed to post data to center, path= %s, addrs= %v", path, addrs)
}
func PostByUrlsWithResp[T any](ctx *ctx.Context, path string, v interface{}) (t T, err error) {
addrs := ctx.CenterApi.Addrs
if len(addrs) < 1 {
@@ -123,14 +137,24 @@ func PostByUrlsWithResp[T any](ctx *ctx.Context, path string, v interface{}) (t
"path= %s, v= %v, ctx.CenterApi.Addrs= %v", path, v, addrs)
return
}
rand.Shuffle(len(addrs), func(i, j int) { addrs[i], addrs[j] = addrs[j], addrs[i] })
for _, addr := range addrs {
t, err = PostByUrl[T](fmt.Sprintf("%s%s", addr, path), ctx.CenterApi, v)
if err == nil {
break
// 随机选择起始位置
startIdx := rand.Intn(len(addrs))
// 从随机位置开始遍历所有地址
for i := 0; i < len(addrs); i++ {
idx := (startIdx + i) % len(addrs)
url := fmt.Sprintf("%s%s", addrs[idx], path)
t, err = PostByUrl[T](url, ctx.CenterApi, v)
if err != nil {
logger.Warningf("failed to post data to center, url: %s, err: %v", url, err)
continue
}
return t, nil
}
return
return t, fmt.Errorf("failed to post data to center, path= %s, addrs= %v err: %v", path, addrs, err)
}
func PostByUrl[T any](url string, cfg conf.CenterApi, v interface{}) (t T, err error) {

View File

@@ -96,7 +96,7 @@ type TargetUpdate struct {
func (s *Set) UpdateTargets(lst []string, now int64) error {
err := updateTargetsUpdateTs(lst, now, s.redis)
if err != nil {
if err != nil && s.ctx.IsCenter {
logger.Errorf("failed to update targets:%v update_ts: %v", lst, err)
}

View File

@@ -28,6 +28,9 @@ type WriterGlobalOpt struct {
QueuePopSize int
AllQueueMaxSize int
AllQueueMaxSizeInterval int
RetryCount int
RetryInterval int64
OverLimitStatusCode int
}
type WriterOptions struct {
@@ -80,13 +83,25 @@ func (p *Pushgw) PreCheck() {
}
if p.WriterOpt.AllQueueMaxSize <= 0 {
p.WriterOpt.AllQueueMaxSize = 10000000
p.WriterOpt.AllQueueMaxSize = 5000000
}
if p.WriterOpt.AllQueueMaxSizeInterval <= 0 {
p.WriterOpt.AllQueueMaxSizeInterval = 200
}
if p.WriterOpt.RetryCount <= 0 {
p.WriterOpt.RetryCount = 1000
}
if p.WriterOpt.RetryInterval <= 0 {
p.WriterOpt.RetryInterval = 1
}
if p.WriterOpt.OverLimitStatusCode <= 0 {
p.WriterOpt.OverLimitStatusCode = 499
}
if p.WriteConcurrency <= 0 {
p.WriteConcurrency = 5000
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/ccfos/nightingale/v6/center/metas"
"github.com/ccfos/nightingale/v6/conf"
"github.com/ccfos/nightingale/v6/dscache"
"github.com/ccfos/nightingale/v6/memsto"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/pkg/httpx"
@@ -55,7 +56,7 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
r := httpx.GinEngine(config.Global.RunMode, config.HTTP, configCvalCache.PrintBodyPaths, configCvalCache.PrintAccessLog)
rt := router.New(config.HTTP, config.Pushgw, config.Alert, targetCache, busiGroupCache, idents, metas, writers, ctx)
rt.Config(r)
dscache.Init(ctx, false)
httpClean := httpx.Init(config.HTTP, r)
return func() {

View File

@@ -145,38 +145,39 @@ func matchSample(filterMap, sampleMap map[string]string) bool {
return true
}
func (rt *Router) ForwardByIdent(clientIP string, ident string, v *prompb.TimeSeries) {
func (rt *Router) ForwardByIdent(clientIP string, ident string, v *prompb.TimeSeries) error {
v = rt.BeforePush(clientIP, v)
if v == nil {
return
return nil
}
IdentStats.Increment(ident, 1)
if rt.DropSample(v) {
CounterDropSampleTotal.WithLabelValues(ident).Inc()
return
return nil
}
count := IdentStats.Get(ident)
if count > rt.Pushgw.IdentDropThreshold {
CounterDropSampleTotal.WithLabelValues(ident).Inc()
return
// 单个 ident 的样本数超过阈值,不算异常,不影响其他机器的上报,不返回 err
return nil
}
rt.Writers.PushSample(ident, *v)
return rt.Writers.PushSample(ident, *v)
}
func (rt *Router) ForwardByMetric(clientIP string, metric string, v *prompb.TimeSeries) {
func (rt *Router) ForwardByMetric(clientIP string, metric string, v *prompb.TimeSeries) error {
v = rt.BeforePush(clientIP, v)
rt.debugSample(clientIP, v)
if v == nil {
return
return nil
}
IdentStats.Increment(metric, 1)
if rt.DropSample(v) {
CounterDropSampleTotal.WithLabelValues(metric).Inc()
return
return nil
}
var hashkey string
@@ -185,7 +186,8 @@ func (rt *Router) ForwardByMetric(clientIP string, metric string, v *prompb.Time
} else {
hashkey = metric[0:1]
}
rt.Writers.PushSample(hashkey, *v)
return rt.Writers.PushSample(hashkey, *v)
}
func (rt *Router) BeforePush(clientIP string, v *prompb.TimeSeries) *prompb.TimeSeries {

View File

@@ -1,6 +1,9 @@
package router
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
"github.com/prometheus/prometheus/prompb"
@@ -30,6 +33,20 @@ type Router struct {
HeartbeartApi string
}
func stat() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
code := fmt.Sprintf("%d", c.Writer.Status())
method := c.Request.Method
labels := []string{"pushgw", code, c.FullPath(), method}
RequestCounter.WithLabelValues(labels...).Inc()
RequestDuration.WithLabelValues(labels...).Observe(float64(time.Since(start).Seconds()))
}
}
func New(httpConfig httpx.Config, pushgw pconf.Pushgw, aconf aconf.Alert, tc *memsto.TargetCacheType, bg *memsto.BusiGroupCacheType,
idents *idents.Set, metas *metas.Set,
writers *writer.WritersType, ctx *ctx.Context) *Router {
@@ -61,6 +78,7 @@ func (rt *Router) Config(r *gin.Engine) {
registerMetrics()
go rt.ReportIdentStats()
r.Use(stat())
// datadog url: http://n9e-pushgw.foo.com/datadog
// use apiKey not basic auth
r.POST("/datadog/api/v1/series", rt.datadogSeries)

View File

@@ -257,9 +257,14 @@ func (r *Router) datadogSeries(c *gin.Context) {
}
if ident != "" {
r.ForwardByIdent(c.ClientIP(), ident, pt)
err = r.ForwardByIdent(c.ClientIP(), ident, pt)
} else {
r.ForwardByMetric(c.ClientIP(), item.Metric, pt)
err = r.ForwardByMetric(c.ClientIP(), item.Metric, pt)
}
if err != nil {
c.String(r.Pushgw.WriterOpt.OverLimitStatusCode, err.Error())
return
}
succ++

View File

@@ -207,9 +207,14 @@ func (rt *Router) falconPush(c *gin.Context) {
}
if ident != "" {
rt.ForwardByIdent(c.ClientIP(), ident, pt)
err = rt.ForwardByIdent(c.ClientIP(), ident, pt)
} else {
rt.ForwardByMetric(c.ClientIP(), arr[i].Metric, pt)
err = rt.ForwardByMetric(c.ClientIP(), arr[i].Metric, pt)
}
if err != nil {
c.String(rt.Pushgw.WriterOpt.OverLimitStatusCode, err.Error())
return
}
succ++

View File

@@ -202,9 +202,14 @@ func (rt *Router) openTSDBPut(c *gin.Context) {
}
if host != "" {
rt.ForwardByIdent(c.ClientIP(), host, pt)
err = rt.ForwardByIdent(c.ClientIP(), host, pt)
} else {
rt.ForwardByMetric(c.ClientIP(), arr[i].Metric, pt)
err = rt.ForwardByMetric(c.ClientIP(), arr[i].Metric, pt)
}
if err != nil {
c.String(rt.Pushgw.WriterOpt.OverLimitStatusCode, err.Error())
return
}
succ++

View File

@@ -1,15 +1,18 @@
package router
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"github.com/ccfos/nightingale/v6/pushgw/writer"
"github.com/gin-gonic/gin"
"github.com/gogo/protobuf/proto"
"github.com/golang/snappy"
"github.com/prometheus/prometheus/prompb"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/logger"
)
func extractMetricFromTimeSeries(s *prompb.TimeSeries) string {
@@ -99,6 +102,15 @@ func duplicateLabelKey(series *prompb.TimeSeries) bool {
}
func (rt *Router) remoteWrite(c *gin.Context) {
curLen := rt.Writers.AllQueueLen.Load().(int)
if curLen > rt.Pushgw.WriterOpt.AllQueueMaxSize {
err := fmt.Errorf("write queue full, metric count over limit: %d", curLen)
logger.Warning(err)
writer.CounterPushQueueOverLimitTotal.Inc()
c.String(rt.Pushgw.WriterOpt.OverLimitStatusCode, err.Error())
return
}
req, err := DecodeWriteRequest(c.Request.Body)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
@@ -138,15 +150,23 @@ func (rt *Router) remoteWrite(c *gin.Context) {
ids[ident] = struct{}{}
}
var err error
if len(ident) > 0 {
rt.ForwardByIdent(c.ClientIP(), ident, &req.Timeseries[i])
err = rt.ForwardByIdent(c.ClientIP(), ident, &req.Timeseries[i])
} else {
rt.ForwardByMetric(c.ClientIP(), extractMetricFromTimeSeries(&req.Timeseries[i]), &req.Timeseries[i])
err = rt.ForwardByMetric(c.ClientIP(), extractMetricFromTimeSeries(&req.Timeseries[i]), &req.Timeseries[i])
}
if err != nil {
c.String(rt.Pushgw.WriterOpt.OverLimitStatusCode, err.Error())
return
}
}
CounterSampleTotal.WithLabelValues("prometheus").Add(float64(count))
rt.IdentSet.MSet(ids)
c.String(200, "")
}
// DecodeWriteRequest from an io.Reader into a prompb.WriteRequest, handling

View File

@@ -8,6 +8,8 @@ const (
)
var (
labels = []string{"service", "code", "path", "method"}
CounterSampleTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
@@ -28,6 +30,24 @@ var (
Name: "sample_received_by_ident",
Help: "Number of sample push by ident.",
}, []string{"host_ident"})
RequestCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "http_request_count_total",
Help: "Total number of HTTP requests made.",
}, labels,
)
RequestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "http_request_duration_seconds",
Help: "HTTP request latencies in seconds.",
}, labels,
)
)
func registerMetrics() {
@@ -35,5 +55,7 @@ func registerMetrics() {
CounterSampleTotal,
CounterDropSampleTotal,
CounterSampleReceivedByIdent,
RequestCounter,
RequestDuration,
)
}

View File

@@ -28,6 +28,13 @@ var (
}, []string{"host_ident"},
)
GaugeAllQueueSize = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "all_queue_size",
Help: "The size of all queue.",
})
CounterWirteTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
@@ -48,6 +55,13 @@ var (
Name: "push_queue_error_total",
Help: "Number of push queue error.",
}, []string{"host_ident"})
CounterPushQueueOverLimitTotal = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "push_queue_over_limit_error_total",
Help: "Number of push queue over limit.",
})
)
func init() {
@@ -57,5 +71,7 @@ func init() {
CounterWirteErrorTotal,
CounterPushQueueErrorTotal,
GaugeSampleQueueSize,
CounterPushQueueOverLimitTotal,
GaugeAllQueueSize,
)
}

View File

@@ -25,6 +25,8 @@ type WriterType struct {
Opts pconf.WriterOptions
ForceUseServerTS bool
Client api.Client
RetryCount int
RetryInterval int64 // 单位秒
}
func (w WriterType) writeRelabel(items []prompb.TimeSeries) []prompb.TimeSeries {
@@ -76,21 +78,32 @@ func (w WriterType) Write(key string, items []prompb.TimeSeries, headers ...map[
return
}
if err := w.Post(snappy.Encode(nil, data), headers...); err != nil {
for i := 0; i < w.RetryCount; i++ {
err := w.Post(snappy.Encode(nil, data), headers...)
if err == nil {
break
}
CounterWirteErrorTotal.WithLabelValues(key).Add(float64(len(items)))
logger.Warningf("post to %s got error: %v", w.Opts.Url, err)
logger.Warning("example timeseries:", items[0].String())
logger.Warningf("post to %s got error: %v in %d times", w.Opts.Url, err, i)
if i == 0 {
logger.Warning("example timeseries:", items[0].String())
}
time.Sleep(time.Duration(w.RetryInterval) * time.Second)
}
}
func (w WriterType) Post(req []byte, headers ...map[string]string) error {
urls := strings.Split(w.Opts.Url, ",")
var err error
var newRequestErr error
var httpReq *http.Request
for _, url := range urls {
httpReq, err = http.NewRequest("POST", url, bytes.NewReader(req))
if err != nil {
logger.Warningf("create remote write:%s request got error: %s", url, err.Error())
httpReq, newRequestErr = http.NewRequest("POST", url, bytes.NewReader(req))
if newRequestErr != nil {
logger.Warningf("create remote write:%s request got error: %s", url, newRequestErr.Error())
continue
}
@@ -126,12 +139,18 @@ func (w WriterType) Post(req []byte, headers ...map[string]string) error {
continue
}
if resp.StatusCode >= 400 {
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
logger.Warningf("push data with remote write:%s request got status code: %v, response body: %s", url, resp.StatusCode, string(body))
continue
}
if resp.StatusCode >= 500 {
err = fmt.Errorf("push data with remote write:%s request got status code: %v, response body: %s", url, resp.StatusCode, string(body))
logger.Warning(err)
continue
}
err = nil
break
}
@@ -142,7 +161,7 @@ type WritersType struct {
pushgw pconf.Pushgw
backends map[string]WriterType
queues map[string]*IdentQueue
allQueueLen atomic.Value
AllQueueLen atomic.Value
sync.RWMutex
}
@@ -159,6 +178,8 @@ func (ws *WritersType) ReportQueueStats(ident string, identQueue *IdentQueue) (i
if count > ws.pushgw.IdentStatsThreshold {
GaugeSampleQueueSize.WithLabelValues(ident).Set(float64(count))
}
GaugeAllQueueSize.Set(float64(ws.AllQueueLen.Load().(int)))
}
}
@@ -170,7 +191,7 @@ func (ws *WritersType) SetAllQueueLen() {
curMetricLen += q.list.Len()
}
ws.RUnlock()
ws.allQueueLen.Store(curMetricLen)
ws.AllQueueLen.Store(curMetricLen)
time.Sleep(time.Duration(ws.pushgw.WriterOpt.AllQueueMaxSizeInterval) * time.Millisecond)
}
}
@@ -180,7 +201,7 @@ func NewWriters(pushgwConfig pconf.Pushgw) *WritersType {
backends: make(map[string]WriterType),
queues: make(map[string]*IdentQueue),
pushgw: pushgwConfig,
allQueueLen: atomic.Value{},
AllQueueLen: atomic.Value{},
}
writers.Init()
@@ -215,7 +236,7 @@ func (ws *WritersType) CleanExpQueue() {
}
}
func (ws *WritersType) PushSample(ident string, v interface{}) {
func (ws *WritersType) PushSample(ident string, v interface{}) error {
ws.RLock()
identQueue := ws.queues[ident]
ws.RUnlock()
@@ -235,11 +256,12 @@ func (ws *WritersType) PushSample(ident string, v interface{}) {
}
identQueue.ts = time.Now().Unix()
curLen := ws.allQueueLen.Load().(int)
curLen := ws.AllQueueLen.Load().(int)
if curLen > ws.pushgw.WriterOpt.AllQueueMaxSize {
logger.Warningf("Write %+v full, metric count over limit: %d", v, curLen)
CounterPushQueueErrorTotal.WithLabelValues(ident).Inc()
return
err := fmt.Errorf("write queue full, metric count over limit: %d", curLen)
logger.Warning(err)
CounterPushQueueOverLimitTotal.Inc()
return err
}
succ := identQueue.list.PushFront(v)
@@ -247,6 +269,7 @@ func (ws *WritersType) PushSample(ident string, v interface{}) {
logger.Warningf("Write channel(%s) full, current channel size: %d", ident, identQueue.list.Len())
CounterPushQueueErrorTotal.WithLabelValues(ident).Inc()
}
return nil
}
func (ws *WritersType) StartConsumer(identQueue *IdentQueue) {
@@ -270,7 +293,7 @@ func (ws *WritersType) StartConsumer(identQueue *IdentQueue) {
func (ws *WritersType) Init() error {
opts := ws.pushgw.Writers
ws.allQueueLen.Store(0)
ws.AllQueueLen.Store(0)
for i := 0; i < len(opts); i++ {
tlsConf, err := opts[i].ClientConfig.TLSConfig()
@@ -310,6 +333,8 @@ func (ws *WritersType) Init() error {
Opts: opts[i],
Client: cli,
ForceUseServerTS: ws.pushgw.ForceUseServerTS,
RetryCount: ws.pushgw.WriterOpt.RetryCount,
RetryInterval: ws.pushgw.WriterOpt.RetryInterval,
}
ws.Put(opts[i].Url, writer)

View File

@@ -1,93 +0,0 @@
package tdengine
import (
"sync"
"github.com/ccfos/nightingale/v6/pkg/tlsx"
)
type TdengineOption struct {
DatasourceName string
Url string
BasicAuthUser string
BasicAuthPass string
Token string
Timeout int64
DialTimeout int64
MaxIdleConnsPerHost int
Headers []string
tlsx.ClientConfig
}
func (po *TdengineOption) Equal(target TdengineOption) bool {
if po.Url != target.Url {
return false
}
if po.BasicAuthUser != target.BasicAuthUser {
return false
}
if po.BasicAuthPass != target.BasicAuthPass {
return false
}
if po.Token != target.Token {
return false
}
if po.Timeout != target.Timeout {
return false
}
if po.DialTimeout != target.DialTimeout {
return false
}
if po.MaxIdleConnsPerHost != target.MaxIdleConnsPerHost {
return false
}
if len(po.Headers) != len(target.Headers) {
return false
}
for i := 0; i < len(po.Headers); i++ {
if po.Headers[i] != target.Headers[i] {
return false
}
}
return true
}
type TdengineOptionsStruct struct {
Data map[int64]TdengineOption
sync.RWMutex
}
func (pos *TdengineOptionsStruct) Set(datasourceId int64, po TdengineOption) {
pos.Lock()
pos.Data[datasourceId] = po
pos.Unlock()
}
func (pos *TdengineOptionsStruct) Del(datasourceId int64) {
pos.Lock()
delete(pos.Data, datasourceId)
pos.Unlock()
}
func (pos *TdengineOptionsStruct) Get(datasourceId int64) (TdengineOption, bool) {
pos.RLock()
defer pos.RUnlock()
ret, has := pos.Data[datasourceId]
return ret, has
}
// Data key is cluster name
var TdengineOptions = &TdengineOptionsStruct{Data: make(map[int64]TdengineOption)}

View File

@@ -1,140 +0,0 @@
package tdengine
import (
"fmt"
"strings"
"time"
"github.com/ccfos/nightingale/v6/alert/aconf"
"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"
)
func NewTdengineClient(ctx *ctx.Context, heartbeat aconf.HeartbeatConfig) *TdengineClientMap {
pc := &TdengineClientMap{
ReaderClients: make(map[int64]*tdengineClient),
heartbeat: heartbeat,
ctx: ctx,
}
pc.InitReader()
return pc
}
func (pc *TdengineClientMap) InitReader() error {
go func() {
for {
pc.loadFromDatabase()
time.Sleep(time.Second)
}
}()
return nil
}
func (pc *TdengineClientMap) loadFromDatabase() {
var datasources []*models.Datasource
var err error
if !pc.ctx.IsCenter {
datasources, err = poster.GetByUrls[[]*models.Datasource](pc.ctx, "/v1/n9e/datasources?typ="+models.TDENGINE)
if err != nil {
logger.Errorf("failed to get datasources, error: %v", err)
return
}
for i := 0; i < len(datasources); i++ {
datasources[i].FE2DB()
}
} else {
datasources, err = models.GetDatasourcesGetsBy(pc.ctx, models.TDENGINE, "", "", "")
if err != nil {
logger.Errorf("failed to get datasources, error: %v", err)
return
}
}
newCluster := make(map[int64]struct{})
for _, ds := range datasources {
dsId := ds.Id
var header []string
for k, v := range ds.HTTPJson.Headers {
header = append(header, k)
header = append(header, v)
}
po := TdengineOption{
DatasourceName: ds.Name,
Url: ds.HTTPJson.Url,
BasicAuthUser: ds.AuthJson.BasicAuthUser,
BasicAuthPass: ds.AuthJson.BasicAuthPassword,
Timeout: ds.HTTPJson.Timeout,
DialTimeout: ds.HTTPJson.DialTimeout,
MaxIdleConnsPerHost: ds.HTTPJson.MaxIdleConnsPerHost,
Headers: header,
}
if strings.HasPrefix(ds.HTTPJson.Url, "https") {
po.UseTLS = true
po.InsecureSkipVerify = ds.HTTPJson.TLS.SkipTlsVerify
}
if len(ds.SettingsJson) > 0 {
for k, v := range ds.SettingsJson {
if strings.Contains(k, "token") {
po.Headers = append(po.Headers, "Authorization")
po.Headers = append(po.Headers, "Taosd "+v.(string))
}
}
}
newCluster[dsId] = struct{}{}
if pc.IsNil(dsId) {
// first time
if err = pc.setClientFromTdengineOption(dsId, po); err != nil {
logger.Errorf("failed to setClientFromTdengineOption po:%+v err:%v", po, err)
continue
}
logger.Info("setClientFromTdengineOption success: ", dsId)
TdengineOptions.Set(dsId, po)
continue
}
localPo, has := TdengineOptions.Get(dsId)
if !has || !localPo.Equal(po) {
if err = pc.setClientFromTdengineOption(dsId, po); err != nil {
logger.Errorf("failed to setClientFromTdengineOption: %v", err)
continue
}
TdengineOptions.Set(dsId, po)
}
}
// delete useless cluster
oldIds := pc.GetDatasourceIds()
for _, oldId := range oldIds {
if _, has := newCluster[oldId]; !has {
pc.Del(oldId)
TdengineOptions.Del(oldId)
logger.Info("delete cluster: ", oldId)
}
}
}
func (pc *TdengineClientMap) setClientFromTdengineOption(datasourceId int64, po TdengineOption) error {
if datasourceId < 0 {
return fmt.Errorf("argument clusterName is blank")
}
if po.Url == "" {
return fmt.Errorf("prometheus url is blank")
}
reader := newTdengine(po)
logger.Debugf("setClientFromTdengineOption: %d, %+v", datasourceId, po)
pc.Set(datasourceId, reader)
return nil
}