diff --git a/center/cconf/ops.go b/center/cconf/ops.go index 7bd508053..6df158b3d 100644 --- a/center/cconf/ops.go +++ b/center/cconf/ops.go @@ -184,5 +184,13 @@ ops: - "/help/notification-settings" - "/help/migrate" - "/site-settings" + +- name: builtin-metrics + cname: 内置指标 + ops: + - "/builtin-metrics" + - "/builtin-metrics/add" + - "/builtin-metrics/put" + - "/builtin-metrics/del" ` ) diff --git a/center/router/router.go b/center/router/router.go index 5693f7524..172a3a55a 100644 --- a/center/router/router.go +++ b/center/router/router.go @@ -229,6 +229,13 @@ func (rt *Router) Config(r *gin.Engine) { pages.POST("/metric-views", rt.auth(), rt.user(), rt.metricViewAdd) pages.PUT("/metric-views", rt.auth(), rt.user(), rt.metricViewPut) + pages.POST("/builtin-metrics", rt.auth(), rt.user(), rt.perm("/builtin-metrics/add"), rt.builtinMetricsAdd) + pages.GET("/builtin-metrics", rt.auth(), rt.user(), rt.perm("/builtin-metrics"), rt.builtinMetricsGets) + pages.PUT("/builtin-metrics", rt.auth(), rt.user(), rt.perm("/builtin-metrics/put"), rt.builtinMetricsPut) + pages.DELETE("/builtin-metrics", rt.auth(), rt.user(), rt.perm("/builtin-metrics/del"), rt.builtinMetricsDel) + pages.GET("/builtin-metrics/types", rt.auth(), rt.user(), rt.perm("/builtin-metrics"), rt.builtinMetricsTypes) + pages.GET("/builtin-metrics/collectors", rt.auth(), rt.user(), rt.perm("/builtin-metrics"), rt.builtinMetricsCollectors) + pages.GET("/user-groups", rt.auth(), rt.user(), rt.userGroupGets) pages.POST("/user-groups", rt.auth(), rt.user(), rt.perm("/user-groups/add"), rt.userGroupAdd) pages.GET("/user-group/:id", rt.auth(), rt.user(), rt.userGroupGet) diff --git a/center/router/router_buildin_metrics.go b/center/router/router_buildin_metrics.go new file mode 100644 index 000000000..ce717f22c --- /dev/null +++ b/center/router/router_buildin_metrics.go @@ -0,0 +1,83 @@ +package router + +import ( + "net/http" + + "github.com/ccfos/nightingale/v6/models" + + "github.com/gin-gonic/gin" + "github.com/toolkits/pkg/ginx" +) + +// single or import +func (rt *Router) builtinMetricsAdd(c *gin.Context) { + var lst []models.BuiltinMetric + ginx.BindJSON(c, &lst) + username := Username(c) + count := len(lst) + if count == 0 { + ginx.Bomb(http.StatusBadRequest, "input json is empty") + } + reterr := make(map[string]string) + for i := 0; i < count; i++ { + if err := lst[i].Add(rt.Ctx, username); err != nil { + reterr[lst[i].Name] = err.Error() + } + } + ginx.NewRender(c).Data(reterr, nil) +} + +func (rt *Router) builtinMetricsGets(c *gin.Context) { + collector := ginx.QueryStr(c, "collector", "") + typ := ginx.QueryStr(c, "typ", "") + query := ginx.QueryStr(c, "query", "") + limit := ginx.QueryInt(c, "limit", 20) + + bm, err := models.BuiltinMetricGets(rt.Ctx, collector, typ, query, limit, ginx.Offset(c, limit)) + ginx.Dangerous(err) + + total, err := models.BuiltinMetricCount(rt.Ctx, collector, typ, query) + ginx.Dangerous(err) + ginx.NewRender(c).Data(gin.H{ + "list": bm, + "total": total, + }, nil) +} + +func (rt *Router) builtinMetricsPut(c *gin.Context) { + var req models.BuiltinMetric + ginx.BindJSON(c, &req) + + bm, err := models.BuiltinMetricGet(rt.Ctx, "id = ?", req.ID) + ginx.Dangerous(err) + if bm == nil { + ginx.NewRender(c, http.StatusNotFound).Message("No such builtin metric") + return + } + username := Username(c) + + req.UpdatedBy = username + ginx.NewRender(c).Message(bm.Update(rt.Ctx, req)) +} + +func (rt *Router) builtinMetricsDel(c *gin.Context) { + var req idsForm + ginx.BindJSON(c, &req) + req.Verify() + + ginx.NewRender(c).Message(models.BuiltinMetricDels(rt.Ctx, req.Ids)) +} + +func (rt *Router) builtinMetricsTypes(c *gin.Context) { + collector := ginx.QueryStr(c, "collector", "") + query := ginx.QueryStr(c, "query", "") + + ginx.NewRender(c).Data(models.BuiltinMetricTypes(rt.Ctx, collector, query)) +} + +func (rt *Router) builtinMetricsCollectors(c *gin.Context) { + typ := ginx.QueryStr(c, "typ", "") + query := ginx.QueryStr(c, "query", "") + + ginx.NewRender(c).Data(models.BuiltinMetricCollectors(rt.Ctx, typ, query)) +} \ No newline at end of file diff --git a/docker/initsql/a-n9e.sql b/docker/initsql/a-n9e.sql index 6faa11986..98914e38b 100644 --- a/docker/initsql/a-n9e.sql +++ b/docker/initsql/a-n9e.sql @@ -634,3 +634,23 @@ CREATE TABLE `es_index_pattern` ( PRIMARY KEY (`id`), UNIQUE KEY (`datasource_id`, `name`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; + +CREATE TABLE `builtin_metrics` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'unique identifier', + `collector` varchar(191) NOT NULL COMMENT 'type of collector', + `typ` varchar(191) NOT NULL COMMENT 'type of metric', + `name` varchar(191) NOT NULL COMMENT 'name of metric', + `unit` varchar(191) NOT NULL COMMENT 'unit of metric', + `desc_cn` varchar(4096) NOT NULL COMMENT 'description of metric in Chinese', + `desc_en` varchar(4096) NOT NULL COMMENT 'description of metric in English', + `expression` varchar(4096) NOT NULL COMMENT 'expression of metric', + `created_at` bigint NOT NULL DEFAULT 0 COMMENT 'create time', + `created_by` varchar(191) NOT NULL DEFAULT '' COMMENT 'creator', + `updated_at` bigint NOT NULL DEFAULT 0 COMMENT 'update time', + `updated_by` varchar(191) NOT NULL DEFAULT '' COMMENT 'updater', + PRIMARY KEY (`id`), + UNIQUE KEY `idx_collector_typ_name` (`collector`, `typ`, `name`), + INDEX `idx_collector` (`collector`), + INDEX `idx_typ` (`typ`), + INDEX `idx_name` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/models/builtin_metric.go b/models/builtin_metric.go new file mode 100644 index 000000000..7e30fe841 --- /dev/null +++ b/models/builtin_metric.go @@ -0,0 +1,181 @@ +package models + +import ( + "errors" + "strings" + "time" + + "github.com/ccfos/nightingale/v6/pkg/ctx" +) + +// BuiltinMetric represents a metric along with its metadata. +type BuiltinMetric struct { + ID uint64 `json:"id" gorm:"primaryKey;type:bigint;autoIncrement;comment:'unique identifier'"` // Unique identifier + Collector string `json:"collector" gorm:"type:varchar(191);not null;index:idx_collector,sort:asc;comment:'type of collector'"` // Type of collector (e.g., 'categraf', 'telegraf') + Typ string `json:"typ" gorm:"type:varchar(191);not null;index:idx_typ,sort:asc;comment:'type of metric'"` // Type of metric (e.g., 'host', 'mysql', 'redis') + Name string `json:"name" gorm:"type:varchar(191);not null;index:idx_name,sort:asc;comment:'name of metric'"` // Name of the metric + Unit string `json:"unit" gorm:"type:varchar(191);not null;comment:'unit of metric'"` // Unit of the metric + DescCN string `json:"desc_cn" gorm:"type:varchar(4096);not null;comment:'description of metric in Chinese'"` // Description in Chinese + DescEN string `json:"desc_en" gorm:"type:varchar(4096);not null;comment:'description of metric in English'"` // Description in English + Expression string `json:"expression" gorm:"type:varchar(4096);not null;comment:'expression of metric'"` // Expression for calculation + CreatedAt int64 `json:"created_at" gorm:"type:bigint;not null;default:0;comment:'create time'"` // Creation timestamp (unix time) + CreatedBy string `json:"created_by" gorm:"type:varchar(191);not null;default:'';comment:'creator'"` // Creator + UpdatedAt int64 `json:"updated_at" gorm:"type:bigint;not null;default:0;comment:'update time'"` // Update timestamp (unix time) + UpdatedBy string `json:"updated_by" gorm:"type:varchar(191);not null;default:'';comment:'updater'"` // Updater +} + +func (bm *BuiltinMetric) TableName() string { + return "builtin_metrics" +} + +func (bm *BuiltinMetric) Verify() error { + bm.Collector = strings.TrimSpace(bm.Collector) + if bm.Collector == "" { + return errors.New("collector is blank") + } + + bm.Typ = strings.TrimSpace(bm.Typ) + if bm.Typ == "" { + return errors.New("type is blank") + } + + bm.Name = strings.TrimSpace(bm.Name) + if bm.Name == "" { + return errors.New("name is blank") + } + + return nil +} + +func BuiltinMetricExists(ctx *ctx.Context, bm *BuiltinMetric) (bool, error) { + var count int64 + err := DB(ctx).Model(bm).Where("collector = ? and typ = ? and name = ?", bm.Collector, bm.Typ, bm.Name).Count(&count).Error + if err != nil { + return false, err + } + return count > 0, nil +} + +func (bm *BuiltinMetric) Add(ctx *ctx.Context, username string) error { + if err := bm.Verify(); err != nil { + return err + } + // check if the builtin metric already exists + exists, err := BuiltinMetricExists(ctx, bm) + if err != nil { + return err + } + if exists { + return errors.New("builtin metric already exists") + } + now := time.Now().Unix() + bm.CreatedAt = now + bm.UpdatedAt = now + bm.CreatedBy = username + return Insert(ctx, bm) +} + +func (bm *BuiltinMetric) Update(ctx *ctx.Context, req BuiltinMetric) error { + if err := req.Verify(); err != nil { + return err + } + + if bm.Collector != req.Collector && bm.Typ != req.Typ && bm.Name != req.Name { + exists, err := BuiltinMetricExists(ctx, &req) + if err != nil { + return err + } + if exists { + return errors.New("builtin metric already exists") + } + } + req.UpdatedAt = time.Now().Unix() + + return DB(ctx).Model(bm).Select("*").Updates(req).Error +} + +func BuiltinMetricDels(ctx *ctx.Context, ids []int64) error { + if len(ids) == 0 { + return nil + } + return DB(ctx).Where("id in ?", ids).Delete(new(BuiltinMetric)).Error +} + +func BuiltinMetricGets(ctx *ctx.Context, collector, typ, query string, limit, offset int) ([]*BuiltinMetric, error) { + session := DB(ctx) + if collector != "" { + session = session.Where("collector = ?", collector) + } + if typ != "" { + session = session.Where("typ = ?", typ) + } + if query != "" { + queryPattern := "%" + query + "%" + session = session.Where("name LIKE ? OR desc_cn LIKE ? OR desc_en LIKE ?", queryPattern, queryPattern, queryPattern) + } + + var lst []*BuiltinMetric + + err := session.Limit(limit).Offset(offset).Find(&lst).Error + + return lst, err +} + +func BuiltinMetricCount(ctx *ctx.Context, collector, typ, query string) (int64, error) { + session := DB(ctx).Model(&BuiltinMetric{}) + if collector != "" { + session = session.Where("collector = ?", collector) + } + if typ != "" { + session = session.Where("typ = ?", typ) + } + if query != "" { + queryPattern := "%" + query + "%" + session = session.Where("name LIKE ? OR desc_cn LIKE ? OR desc_en LIKE ?", queryPattern, queryPattern, queryPattern) + } + + var cnt int64 + err := session.Count(&cnt).Error + + return cnt, err +} + +func BuiltinMetricGet(ctx *ctx.Context, where string, args ...interface{}) (*BuiltinMetric, error) { + var lst []*BuiltinMetric + err := DB(ctx).Where(where, args...).Find(&lst).Error + if err != nil { + return nil, err + } + + if len(lst) == 0 { + return nil, nil + } + + return lst[0], nil +} + +func BuiltinMetricTypes(ctx *ctx.Context, collector, query string) ([]string, error) { + var typs []string + session := DB(ctx).Model(&BuiltinMetric{}) + if collector != "" { + session = session.Where("collector = ?", collector) + } + if query != "" { + session = session.Where("typ like ?", "%"+query+"%") + } + err := session.Select("distinct(typ)").Pluck("typ", &typs).Error + return typs, err +} + +func BuiltinMetricCollectors(ctx *ctx.Context, typ, query string) ([]string, error) { + var collectors []string + session := DB(ctx).Model(&BuiltinMetric{}) + if typ != "" { + session = session.Where("typ = ?", typ) + } + if query != "" { + session = session.Where("collector like ?", "%"+query+"%") + } + err := session.Select("distinct(collector)").Pluck("collector", &collectors).Error + return collectors, err +} \ No newline at end of file diff --git a/models/migrate/migrate.go b/models/migrate/migrate.go index 4a99c0d3e..0040f4cfc 100644 --- a/models/migrate/migrate.go +++ b/models/migrate/migrate.go @@ -15,7 +15,7 @@ func Migrate(db *gorm.DB) { func MigrateTables(db *gorm.DB) error { dts := []interface{}{&RecordingRule{}, &AlertRule{}, &AlertSubscribe{}, &AlertMute{}, &TaskRecord{}, &ChartShare{}, &Target{}, &Configs{}, &Datasource{}, &NotifyTpl{}, - &Board{}, &BoardBusigroup{}, &Users{}, &SsoConfig{}} + &Board{}, &BoardBusigroup{}, &Users{}, &SsoConfig{}, &models.BuiltinMetric{}} if !columnHasIndex(db, &AlertHisEvent{}, "last_eval_time") { dts = append(dts, &AlertHisEvent{})