Compare commits
1 Commits
refactor/q
...
fix-id-das
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28d5b7210f |
@@ -6,8 +6,8 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/dashboards"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
@@ -41,10 +41,15 @@ func (ah *APIHandler) lockUnlockDashboard(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
dashboard, err := dashboards.GetDashboard(r.Context(), claims.OrgID, uuid)
|
||||
// dashboard, err := dashboards.GetDashboard(r.Context(), claims.OrgID, uuid)
|
||||
// if err != nil {
|
||||
// render.Error(w, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to get dashboard"))
|
||||
// return
|
||||
// }
|
||||
|
||||
dashboard, err := ah.Signoz.Modules.Dashboard.Get(r.Context(), valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(uuid))
|
||||
if err != nil {
|
||||
render.Error(w, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to get dashboard"))
|
||||
return
|
||||
render.Error(w, err)
|
||||
}
|
||||
|
||||
if err := claims.IsAdmin(); err != nil && (dashboard.CreatedBy != claims.Email) {
|
||||
@@ -52,12 +57,17 @@ func (ah *APIHandler) lockUnlockDashboard(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
// Lock/Unlock the dashboard
|
||||
err = dashboards.LockUnlockDashboard(r.Context(), claims.OrgID, uuid, lock)
|
||||
if err != nil {
|
||||
render.Error(w, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to lock/unlock dashboard"))
|
||||
if err := ah.Signoz.Modules.Dashboard.Update(r.Context(), nil); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Lock/Unlock the dashboard
|
||||
// err = dashboards.LockUnlockDashboard(r.Context(), claims.OrgID, uuid, lock)
|
||||
// if err != nil {
|
||||
// render.Error(w, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to lock/unlock dashboard"))
|
||||
// return
|
||||
// }
|
||||
|
||||
ah.Respond(w, "Dashboard updated successfully")
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
|
||||
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/dashboards"
|
||||
baseexplorer "github.com/SigNoz/signoz/pkg/query-service/app/explorer"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
|
||||
@@ -102,10 +101,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := dashboards.InitDB(serverOptions.SigNoz.SQLStore); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gatewayProxy, err := gateway.NewProxy(serverOptions.GatewayUrl, gateway.RoutePrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
22
pkg/modules/dashboard/dashboard.go
Normal file
22
pkg/modules/dashboard/dashboard.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Module interface {
|
||||
Create(context.Context, valuer.UUID, map[string]any, string) error
|
||||
|
||||
Get(context.Context, valuer.UUID, valuer.UUID) (*dashboardtypes.Dashboard, error)
|
||||
|
||||
GetByMetricNames(context.Context, valuer.UUID, []string) ([]*dashboardtypes.Dashboard, error)
|
||||
|
||||
List(context.Context, valuer.UUID) ([]*dashboardtypes.Dashboard, error)
|
||||
|
||||
Update(context.Context, *dashboardtypes.Dashboard) error
|
||||
|
||||
Delete(context.Context, valuer.UUID, valuer.UUID) error
|
||||
}
|
||||
236
pkg/modules/dashboard/impldashboard/module.go
Normal file
236
pkg/modules/dashboard/impldashboard/module.go
Normal file
@@ -0,0 +1,236 @@
|
||||
package impldashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type module struct {
|
||||
store dashboardtypes.Store
|
||||
}
|
||||
|
||||
func NewModule(store dashboardtypes.Store) dashboard.Module {
|
||||
return &module{store: store}
|
||||
}
|
||||
|
||||
func (module *module) Create(ctx context.Context, orgID valuer.UUID, data map[string]any, email string) error {
|
||||
storableDashboard, err := dashboardtypes.NewStorableDashboard(data, email, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return module.store.Create(ctx, storableDashboard)
|
||||
}
|
||||
|
||||
func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error) {
|
||||
storableDashboard, err := module.store.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dashboardtypes.NewDashboardFromStorableDashboard(storableDashboard), nil
|
||||
}
|
||||
|
||||
func (module *module) GetByMetricNames(ctx context.Context, orgID valuer.UUID, metricNames []string) ([]*dashboardtypes.Dashboard, error) {
|
||||
// dashboards := []types.Dashboard{}
|
||||
// err := store.BunDB().NewSelect().Model(&dashboards).Where("org_id = ?", orgID).Scan(ctx)
|
||||
// if err != nil {
|
||||
// zap.L().Error("Error in getting dashboards", zap.Error(err))
|
||||
// return nil, &model.ApiError{Typ: model.ErrorExec, Err: err}
|
||||
// }
|
||||
// if err != nil {
|
||||
// zap.L().Error("Error in getting dashboards", zap.Error(err))
|
||||
// return nil, &model.ApiError{Typ: model.ErrorExec, Err: err}
|
||||
// }
|
||||
|
||||
// // Initialize result map for each metric
|
||||
// result := make(map[string][]map[string]string)
|
||||
|
||||
// // Process the JSON data in Go
|
||||
// for _, dashboard := range dashboards {
|
||||
// var dashData = dashboard.Data
|
||||
|
||||
// dashTitle, _ := dashData["title"].(string)
|
||||
// widgets, ok := dashData["widgets"].([]interface{})
|
||||
// if !ok {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// for _, w := range widgets {
|
||||
// widget, ok := w.(map[string]interface{})
|
||||
// if !ok {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// widgetTitle, _ := widget["title"].(string)
|
||||
// widgetID, _ := widget["id"].(string)
|
||||
|
||||
// query, ok := widget["query"].(map[string]interface{})
|
||||
// if !ok {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// builder, ok := query["builder"].(map[string]interface{})
|
||||
// if !ok {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// queryData, ok := builder["queryData"].([]interface{})
|
||||
// if !ok {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// for _, qd := range queryData {
|
||||
// data, ok := qd.(map[string]interface{})
|
||||
// if !ok {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// if dataSource, ok := data["dataSource"].(string); !ok || dataSource != "metrics" {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// aggregateAttr, ok := data["aggregateAttribute"].(map[string]interface{})
|
||||
// if !ok {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// if key, ok := aggregateAttr["key"].(string); ok {
|
||||
// // Check if this metric is in our list of interest
|
||||
// for _, metricName := range metricNames {
|
||||
// if strings.TrimSpace(key) == metricName {
|
||||
// result[metricName] = append(result[metricName], map[string]string{
|
||||
// "dashboard_id": dashboard.UUID,
|
||||
// "widget_name": widgetTitle,
|
||||
// "widget_id": widgetID,
|
||||
// "dashboard_name": dashTitle,
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// return result, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error) {
|
||||
storableDashboards, err := module.store.List(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dashboards := make([]*dashboardtypes.Dashboard, len(storableDashboards))
|
||||
for idx, storableDashboard := range storableDashboards {
|
||||
dashboards[idx] = dashboardtypes.NewDashboardFromStorableDashboard(storableDashboard)
|
||||
}
|
||||
|
||||
return dashboards, nil
|
||||
}
|
||||
|
||||
func (module *module) Update(ctx context.Context, updatedOrganization *dashboardtypes.Dashboard) error {
|
||||
return nil
|
||||
// mapData, err := json.Marshal(data)
|
||||
// if err != nil {
|
||||
// zap.L().Error("Error in marshalling data field in dashboard: ", zap.Any("data", data), zap.Error(err))
|
||||
// return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
||||
// }
|
||||
|
||||
// dashboard, apiErr := GetDashboard(ctx, orgID, uuid)
|
||||
// if apiErr != nil {
|
||||
// return nil, apiErr
|
||||
// }
|
||||
|
||||
// if dashboard.Locked != nil && *dashboard.Locked == 1 {
|
||||
// return nil, model.BadRequest(fmt.Errorf("dashboard is locked, please unlock the dashboard to be able to edit it"))
|
||||
// }
|
||||
|
||||
// // if the total count of panels has reduced by more than 1,
|
||||
// // return error
|
||||
// existingIds := getWidgetIds(dashboard.Data)
|
||||
// newIds := getWidgetIds(data)
|
||||
|
||||
// differenceIds := getIdDifference(existingIds, newIds)
|
||||
|
||||
// if len(differenceIds) > 1 {
|
||||
// return nil, model.BadRequest(fmt.Errorf("deleting more than one panel is not supported"))
|
||||
// }
|
||||
|
||||
// dashboard.UpdatedAt = time.Now()
|
||||
// dashboard.UpdatedBy = userEmail
|
||||
// dashboard.Data = data
|
||||
|
||||
// _, err = store.BunDB().NewUpdate().Model(dashboard).Set("updated_at = ?", dashboard.UpdatedAt).Set("updated_by = ?", userEmail).Set("data = ?", mapData).Where("uuid = ?", dashboard.UUID).Exec(ctx)
|
||||
|
||||
// if err != nil {
|
||||
// zap.L().Error("Error in inserting dashboard data", zap.Any("data", data), zap.Error(err))
|
||||
// return nil, &model.ApiError{Typ: model.ErrorExec, Err: err}
|
||||
// }
|
||||
// return dashboard, nil
|
||||
}
|
||||
|
||||
func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
|
||||
dashboard, err := module.Get(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dashboard.Locked {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is locked")
|
||||
}
|
||||
|
||||
return module.store.Delete(ctx, id)
|
||||
}
|
||||
|
||||
// func getIdDifference(existingIds []string, newIds []string) []string {
|
||||
// // Convert newIds array to a map for faster lookups
|
||||
// newIdsMap := make(map[string]bool)
|
||||
// for _, id := range newIds {
|
||||
// newIdsMap[id] = true
|
||||
// }
|
||||
|
||||
// // Initialize a map to keep track of elements in the difference array
|
||||
// differenceMap := make(map[string]bool)
|
||||
|
||||
// // Initialize the difference array
|
||||
// difference := []string{}
|
||||
|
||||
// // Iterate through existingIds
|
||||
// for _, id := range existingIds {
|
||||
// // If the id is not found in newIds, and it's not already in the difference array
|
||||
// if _, found := newIdsMap[id]; !found && !differenceMap[id] {
|
||||
// difference = append(difference, id)
|
||||
// differenceMap[id] = true // Mark the id as seen in the difference array
|
||||
// }
|
||||
// }
|
||||
|
||||
// return difference
|
||||
// }
|
||||
|
||||
// func LockUnlockDashboard(ctx context.Context, orgID, uuid string, lock bool) *model.ApiError {
|
||||
// dashboard, apiErr := GetDashboard(ctx, orgID, uuid)
|
||||
// if apiErr != nil {
|
||||
// return apiErr
|
||||
// }
|
||||
|
||||
// var lockValue int
|
||||
// if lock {
|
||||
// lockValue = 1
|
||||
// } else {
|
||||
// lockValue = 0
|
||||
// }
|
||||
|
||||
// _, err := store.BunDB().NewUpdate().Model(dashboard).Set("locked = ?", lockValue).Where("org_id = ?", orgID).Where("uuid = ?", uuid).Exec(ctx)
|
||||
// if err != nil {
|
||||
// zap.L().Error("Error in updating dashboard", zap.String("uuid", uuid), zap.Error(err))
|
||||
// return &model.ApiError{Typ: model.ErrorExec, Err: err}
|
||||
// }
|
||||
|
||||
// return nil
|
||||
// }
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
@@ -510,10 +511,8 @@ func (c *Controller) UpdateServiceConfig(
|
||||
|
||||
// All dashboards that are available based on cloud integrations configuration
|
||||
// across all cloud providers
|
||||
func (c *Controller) AvailableDashboards(ctx context.Context, orgId string) (
|
||||
[]types.Dashboard, *model.ApiError,
|
||||
) {
|
||||
allDashboards := []types.Dashboard{}
|
||||
func (c *Controller) AvailableDashboards(ctx context.Context, orgId string) ([]*dashboardtypes.Dashboard, *model.ApiError) {
|
||||
allDashboards := []*dashboardtypes.Dashboard{}
|
||||
|
||||
for _, provider := range []string{"aws"} {
|
||||
providerDashboards, apiErr := c.AvailableDashboardsForCloudProvider(ctx, orgId, provider)
|
||||
@@ -529,10 +528,7 @@ func (c *Controller) AvailableDashboards(ctx context.Context, orgId string) (
|
||||
return allDashboards, nil
|
||||
}
|
||||
|
||||
func (c *Controller) AvailableDashboardsForCloudProvider(
|
||||
ctx context.Context, orgID string, cloudProvider string,
|
||||
) ([]types.Dashboard, *model.ApiError) {
|
||||
|
||||
func (c *Controller) AvailableDashboardsForCloudProvider(ctx context.Context, orgID string, cloudProvider string) ([]*dashboardtypes.Dashboard, *model.ApiError) {
|
||||
accountRecords, apiErr := c.accountsRepo.listConnected(ctx, orgID, cloudProvider)
|
||||
if apiErr != nil {
|
||||
return nil, model.WrapApiError(apiErr, "couldn't list connected cloud accounts")
|
||||
@@ -563,16 +559,15 @@ func (c *Controller) AvailableDashboardsForCloudProvider(
|
||||
return nil, apiErr
|
||||
}
|
||||
|
||||
svcDashboards := []types.Dashboard{}
|
||||
svcDashboards := []*dashboardtypes.Dashboard{}
|
||||
for _, svc := range allServices {
|
||||
serviceDashboardsCreatedAt := servicesWithAvailableMetrics[svc.Id]
|
||||
if serviceDashboardsCreatedAt != nil {
|
||||
for _, d := range svc.Assets.Dashboards {
|
||||
isLocked := 1
|
||||
author := fmt.Sprintf("%s-integration", cloudProvider)
|
||||
svcDashboards = append(svcDashboards, types.Dashboard{
|
||||
UUID: c.dashboardUuid(cloudProvider, svc.Id, d.Id),
|
||||
Locked: &isLocked,
|
||||
svcDashboards = append(svcDashboards, &dashboardtypes.Dashboard{
|
||||
ID: c.dashboardUuid(cloudProvider, svc.Id, d.Id),
|
||||
Locked: true,
|
||||
Data: *d.Definition,
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: *serviceDashboardsCreatedAt,
|
||||
@@ -590,11 +585,7 @@ func (c *Controller) AvailableDashboardsForCloudProvider(
|
||||
|
||||
return svcDashboards, nil
|
||||
}
|
||||
func (c *Controller) GetDashboardById(
|
||||
ctx context.Context,
|
||||
orgId string,
|
||||
dashboardUuid string,
|
||||
) (*types.Dashboard, *model.ApiError) {
|
||||
func (c *Controller) GetDashboardById(ctx context.Context, orgId string, dashboardUuid string) (*dashboardtypes.Dashboard, *model.ApiError) {
|
||||
cloudProvider, _, _, apiErr := c.parseDashboardUuid(dashboardUuid)
|
||||
if apiErr != nil {
|
||||
return nil, apiErr
|
||||
@@ -608,8 +599,8 @@ func (c *Controller) GetDashboardById(
|
||||
}
|
||||
|
||||
for _, d := range allDashboards {
|
||||
if d.UUID == dashboardUuid {
|
||||
return &d, nil
|
||||
if d.ID == dashboardUuid {
|
||||
return d, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
)
|
||||
|
||||
type CloudServiceSummary struct {
|
||||
@@ -42,7 +43,7 @@ type CloudServiceDashboard struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Image string `json:"image"`
|
||||
Definition *types.DashboardData `json:"definition,omitempty"`
|
||||
Definition *dashboardtypes.Data `json:"definition,omitempty"`
|
||||
}
|
||||
|
||||
type SupportedSignals struct {
|
||||
|
||||
@@ -1,533 +0,0 @@
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/telemetry"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// This time the global variable is unexported.
|
||||
var store sqlstore.SQLStore
|
||||
|
||||
// User for mapping job,instance from grafana
|
||||
var (
|
||||
instanceEQRE = regexp.MustCompile("instance(?s)=(?s)\\\"{{.instance}}\\\"")
|
||||
nodeEQRE = regexp.MustCompile("instance(?s)=(?s)\\\"{{.node}}\\\"")
|
||||
jobEQRE = regexp.MustCompile("job(?s)=(?s)\\\"{{.job}}\\\"")
|
||||
instanceRERE = regexp.MustCompile("instance(?s)=~(?s)\\\"{{.instance}}\\\"")
|
||||
nodeRERE = regexp.MustCompile("instance(?s)=~(?s)\\\"{{.node}}\\\"")
|
||||
jobRERE = regexp.MustCompile("job(?s)=~(?s)\\\"{{.job}}\\\"")
|
||||
)
|
||||
|
||||
// InitDB sets up setting up the connection pool global variable.
|
||||
func InitDB(sqlStore sqlstore.SQLStore) error {
|
||||
store = sqlStore
|
||||
telemetry.GetInstance().SetDashboardsInfoCallback(GetDashboardsInfo)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateDashboard creates a new dashboard
|
||||
func CreateDashboard(ctx context.Context, orgID string, email string, data map[string]interface{}) (*types.Dashboard, *model.ApiError) {
|
||||
dash := &types.Dashboard{
|
||||
Data: data,
|
||||
}
|
||||
|
||||
dash.OrgID = orgID
|
||||
dash.CreatedAt = time.Now()
|
||||
dash.CreatedBy = email
|
||||
dash.UpdatedAt = time.Now()
|
||||
dash.UpdatedBy = email
|
||||
dash.UpdateSlug()
|
||||
dash.UUID = uuid.New().String()
|
||||
if data["uuid"] != nil {
|
||||
dash.UUID = data["uuid"].(string)
|
||||
}
|
||||
|
||||
err := store.BunDB().NewInsert().Model(dash).Returning("id").Scan(ctx, &dash.ID)
|
||||
if err != nil {
|
||||
zap.L().Error("Error in inserting dashboard data: ", zap.Any("dashboard", dash), zap.Error(err))
|
||||
return nil, &model.ApiError{Typ: model.ErrorExec, Err: err}
|
||||
}
|
||||
|
||||
return dash, nil
|
||||
}
|
||||
|
||||
func GetDashboards(ctx context.Context, orgID string) ([]types.Dashboard, *model.ApiError) {
|
||||
dashboards := []types.Dashboard{}
|
||||
|
||||
err := store.BunDB().NewSelect().Model(&dashboards).Where("org_id = ?", orgID).Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, &model.ApiError{Typ: model.ErrorExec, Err: err}
|
||||
}
|
||||
|
||||
return dashboards, nil
|
||||
}
|
||||
|
||||
func DeleteDashboard(ctx context.Context, orgID, uuid string) *model.ApiError {
|
||||
|
||||
dashboard, dErr := GetDashboard(ctx, orgID, uuid)
|
||||
if dErr != nil {
|
||||
zap.L().Error("Error in getting dashboard: ", zap.String("uuid", uuid), zap.Any("error", dErr))
|
||||
return dErr
|
||||
}
|
||||
|
||||
if dashboard.Locked != nil && *dashboard.Locked == 1 {
|
||||
return model.BadRequest(fmt.Errorf("dashboard is locked, please unlock the dashboard to be able to delete it"))
|
||||
}
|
||||
|
||||
result, err := store.BunDB().NewDelete().Model(&types.Dashboard{}).Where("org_id = ?", orgID).Where("uuid = ?", uuid).Exec(ctx)
|
||||
if err != nil {
|
||||
return &model.ApiError{Typ: model.ErrorExec, Err: err}
|
||||
}
|
||||
|
||||
affectedRows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return &model.ApiError{Typ: model.ErrorExec, Err: err}
|
||||
}
|
||||
if affectedRows == 0 {
|
||||
return &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("no dashboard found with uuid: %s", uuid)}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetDashboard(ctx context.Context, orgID, uuid string) (*types.Dashboard, *model.ApiError) {
|
||||
|
||||
dashboard := types.Dashboard{}
|
||||
err := store.BunDB().NewSelect().Model(&dashboard).Where("org_id = ?", orgID).Where("uuid = ?", uuid).Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("no dashboard found with uuid: %s", uuid)}
|
||||
}
|
||||
|
||||
return &dashboard, nil
|
||||
}
|
||||
|
||||
func UpdateDashboard(ctx context.Context, orgID, userEmail, uuid string, data map[string]interface{}) (*types.Dashboard, *model.ApiError) {
|
||||
|
||||
mapData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
zap.L().Error("Error in marshalling data field in dashboard: ", zap.Any("data", data), zap.Error(err))
|
||||
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
||||
}
|
||||
|
||||
dashboard, apiErr := GetDashboard(ctx, orgID, uuid)
|
||||
if apiErr != nil {
|
||||
return nil, apiErr
|
||||
}
|
||||
|
||||
if dashboard.Locked != nil && *dashboard.Locked == 1 {
|
||||
return nil, model.BadRequest(fmt.Errorf("dashboard is locked, please unlock the dashboard to be able to edit it"))
|
||||
}
|
||||
|
||||
// if the total count of panels has reduced by more than 1,
|
||||
// return error
|
||||
existingIds := getWidgetIds(dashboard.Data)
|
||||
newIds := getWidgetIds(data)
|
||||
|
||||
differenceIds := getIdDifference(existingIds, newIds)
|
||||
|
||||
if len(differenceIds) > 1 {
|
||||
return nil, model.BadRequest(fmt.Errorf("deleting more than one panel is not supported"))
|
||||
}
|
||||
|
||||
dashboard.UpdatedAt = time.Now()
|
||||
dashboard.UpdatedBy = userEmail
|
||||
dashboard.Data = data
|
||||
|
||||
_, err = store.BunDB().NewUpdate().Model(dashboard).Set("updated_at = ?", dashboard.UpdatedAt).Set("updated_by = ?", userEmail).Set("data = ?", mapData).Where("uuid = ?", dashboard.UUID).Exec(ctx)
|
||||
|
||||
if err != nil {
|
||||
zap.L().Error("Error in inserting dashboard data", zap.Any("data", data), zap.Error(err))
|
||||
return nil, &model.ApiError{Typ: model.ErrorExec, Err: err}
|
||||
}
|
||||
return dashboard, nil
|
||||
}
|
||||
|
||||
func LockUnlockDashboard(ctx context.Context, orgID, uuid string, lock bool) *model.ApiError {
|
||||
dashboard, apiErr := GetDashboard(ctx, orgID, uuid)
|
||||
if apiErr != nil {
|
||||
return apiErr
|
||||
}
|
||||
|
||||
var lockValue int
|
||||
if lock {
|
||||
lockValue = 1
|
||||
} else {
|
||||
lockValue = 0
|
||||
}
|
||||
|
||||
_, err := store.BunDB().NewUpdate().Model(dashboard).Set("locked = ?", lockValue).Where("org_id = ?", orgID).Where("uuid = ?", uuid).Exec(ctx)
|
||||
if err != nil {
|
||||
zap.L().Error("Error in updating dashboard", zap.String("uuid", uuid), zap.Error(err))
|
||||
return &model.ApiError{Typ: model.ErrorExec, Err: err}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsPostDataSane(data *map[string]interface{}) error {
|
||||
val, ok := (*data)["title"]
|
||||
if !ok || val == nil {
|
||||
return fmt.Errorf("title not found in post data")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getWidgetIds(data map[string]interface{}) []string {
|
||||
widgetIds := []string{}
|
||||
if data != nil && data["widgets"] != nil {
|
||||
widgets, ok := data["widgets"]
|
||||
if ok {
|
||||
data, ok := widgets.([]interface{})
|
||||
if ok {
|
||||
for _, widget := range data {
|
||||
sData, ok := widget.(map[string]interface{})
|
||||
if ok && sData["query"] != nil && sData["id"] != nil {
|
||||
id, ok := sData["id"].(string)
|
||||
|
||||
if ok {
|
||||
widgetIds = append(widgetIds, id)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return widgetIds
|
||||
}
|
||||
|
||||
func getIdDifference(existingIds []string, newIds []string) []string {
|
||||
// Convert newIds array to a map for faster lookups
|
||||
newIdsMap := make(map[string]bool)
|
||||
for _, id := range newIds {
|
||||
newIdsMap[id] = true
|
||||
}
|
||||
|
||||
// Initialize a map to keep track of elements in the difference array
|
||||
differenceMap := make(map[string]bool)
|
||||
|
||||
// Initialize the difference array
|
||||
difference := []string{}
|
||||
|
||||
// Iterate through existingIds
|
||||
for _, id := range existingIds {
|
||||
// If the id is not found in newIds, and it's not already in the difference array
|
||||
if _, found := newIdsMap[id]; !found && !differenceMap[id] {
|
||||
difference = append(difference, id)
|
||||
differenceMap[id] = true // Mark the id as seen in the difference array
|
||||
}
|
||||
}
|
||||
|
||||
return difference
|
||||
}
|
||||
|
||||
// GetDashboardsInfo returns analytics data for dashboards
|
||||
func GetDashboardsInfo(ctx context.Context) (*model.DashboardsInfo, error) {
|
||||
dashboardsInfo := model.DashboardsInfo{}
|
||||
// fetch dashboards from dashboard db
|
||||
dashboards := []types.Dashboard{}
|
||||
err := store.BunDB().NewSelect().Model(&dashboards).Scan(ctx)
|
||||
if err != nil {
|
||||
zap.L().Error("Error in processing sql query", zap.Error(err))
|
||||
return &dashboardsInfo, err
|
||||
}
|
||||
totalDashboardsWithPanelAndName := 0
|
||||
var dashboardNames []string
|
||||
count := 0
|
||||
queriesWithTagAttrs := 0
|
||||
for _, dashboard := range dashboards {
|
||||
if isDashboardWithPanelAndName(dashboard.Data) {
|
||||
totalDashboardsWithPanelAndName = totalDashboardsWithPanelAndName + 1
|
||||
}
|
||||
dashboardName := extractDashboardName(dashboard.Data)
|
||||
if dashboardName != "" {
|
||||
dashboardNames = append(dashboardNames, dashboardName)
|
||||
}
|
||||
dashboardInfo := countPanelsInDashboard(dashboard.Data)
|
||||
dashboardsInfo.LogsBasedPanels += dashboardInfo.LogsBasedPanels
|
||||
dashboardsInfo.TracesBasedPanels += dashboardInfo.TracesBasedPanels
|
||||
dashboardsInfo.MetricBasedPanels += dashboardInfo.MetricBasedPanels
|
||||
dashboardsInfo.LogsPanelsWithAttrContainsOp += dashboardInfo.LogsPanelsWithAttrContainsOp
|
||||
dashboardsInfo.DashboardsWithLogsChQuery += dashboardInfo.DashboardsWithLogsChQuery
|
||||
dashboardsInfo.DashboardsWithTraceChQuery += dashboardInfo.DashboardsWithTraceChQuery
|
||||
if isDashboardWithTSV2(dashboard.Data) {
|
||||
count = count + 1
|
||||
}
|
||||
|
||||
if isDashboardWithTagAttrs(dashboard.Data) {
|
||||
queriesWithTagAttrs += 1
|
||||
}
|
||||
|
||||
if dashboardInfo.DashboardsWithTraceChQuery > 0 {
|
||||
dashboardsInfo.DashboardNamesWithTraceChQuery = append(dashboardsInfo.DashboardNamesWithTraceChQuery, dashboardName)
|
||||
}
|
||||
|
||||
// check if dashboard is a has a log operator with contains
|
||||
}
|
||||
|
||||
dashboardsInfo.DashboardNames = dashboardNames
|
||||
dashboardsInfo.TotalDashboards = len(dashboards)
|
||||
dashboardsInfo.TotalDashboardsWithPanelAndName = totalDashboardsWithPanelAndName
|
||||
dashboardsInfo.QueriesWithTSV2 = count
|
||||
dashboardsInfo.QueriesWithTagAttrs = queriesWithTagAttrs
|
||||
return &dashboardsInfo, nil
|
||||
}
|
||||
|
||||
func isDashboardWithTSV2(data map[string]interface{}) bool {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(string(jsonData), "time_series_v2")
|
||||
}
|
||||
|
||||
func isDashboardWithTagAttrs(data map[string]interface{}) bool {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(string(jsonData), "span_attributes") ||
|
||||
strings.Contains(string(jsonData), "tag_attributes")
|
||||
}
|
||||
|
||||
func isDashboardWithLogsClickhouseQuery(data map[string]interface{}) bool {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
result := strings.Contains(string(jsonData), "signoz_logs.distributed_logs") ||
|
||||
strings.Contains(string(jsonData), "signoz_logs.logs")
|
||||
return result
|
||||
}
|
||||
|
||||
func isDashboardWithTracesClickhouseQuery(data map[string]interface{}) bool {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// also check if the query is actually active
|
||||
str := string(jsonData)
|
||||
result := strings.Contains(str, "signoz_traces.distributed_signoz_index_v2") ||
|
||||
strings.Contains(str, "signoz_traces.distributed_signoz_spans") ||
|
||||
strings.Contains(str, "signoz_traces.distributed_signoz_error_index_v2")
|
||||
return result
|
||||
}
|
||||
|
||||
func isDashboardWithPanelAndName(data map[string]interface{}) bool {
|
||||
isDashboardName := false
|
||||
isDashboardWithPanelAndName := false
|
||||
if data != nil && data["title"] != nil && data["widgets"] != nil {
|
||||
title, ok := data["title"].(string)
|
||||
if ok && title != "Sample Title" {
|
||||
isDashboardName = true
|
||||
}
|
||||
widgets, ok := data["widgets"]
|
||||
if ok && isDashboardName {
|
||||
data, ok := widgets.([]interface{})
|
||||
if ok && len(data) > 0 {
|
||||
isDashboardWithPanelAndName = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return isDashboardWithPanelAndName
|
||||
}
|
||||
|
||||
func extractDashboardName(data map[string]interface{}) string {
|
||||
|
||||
if data != nil && data["title"] != nil {
|
||||
title, ok := data["title"].(string)
|
||||
if ok {
|
||||
return title
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func checkLogPanelAttrContains(data map[string]interface{}) int {
|
||||
var logsPanelsWithAttrContains int
|
||||
filters, ok := data["filters"].(map[string]interface{})
|
||||
if ok && filters["items"] != nil {
|
||||
items, ok := filters["items"].([]interface{})
|
||||
if ok {
|
||||
for _, item := range items {
|
||||
itemMap, ok := item.(map[string]interface{})
|
||||
if ok {
|
||||
opStr, ok := itemMap["op"].(string)
|
||||
if ok {
|
||||
if slices.Contains([]string{"contains", "ncontains", "like", "nlike"}, opStr) {
|
||||
// check if it's not body
|
||||
key, ok := itemMap["key"].(map[string]string)
|
||||
if ok && key["key"] != "body" {
|
||||
logsPanelsWithAttrContains++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return logsPanelsWithAttrContains
|
||||
}
|
||||
|
||||
func countPanelsInDashboard(inputData map[string]interface{}) model.DashboardsInfo {
|
||||
var logsPanelCount, tracesPanelCount, metricsPanelCount, logsPanelsWithAttrContains int
|
||||
traceChQueryCount := 0
|
||||
logChQueryCount := 0
|
||||
|
||||
// totalPanels := 0
|
||||
if inputData != nil && inputData["widgets"] != nil {
|
||||
widgets, ok := inputData["widgets"]
|
||||
if ok {
|
||||
data, ok := widgets.([]interface{})
|
||||
if ok {
|
||||
for _, widget := range data {
|
||||
sData, ok := widget.(map[string]interface{})
|
||||
if ok && sData["query"] != nil {
|
||||
// totalPanels++
|
||||
query, ok := sData["query"].(map[string]interface{})
|
||||
if ok && query["queryType"] == "builder" && query["builder"] != nil {
|
||||
builderData, ok := query["builder"].(map[string]interface{})
|
||||
if ok && builderData["queryData"] != nil {
|
||||
builderQueryData, ok := builderData["queryData"].([]interface{})
|
||||
if ok {
|
||||
for _, queryData := range builderQueryData {
|
||||
data, ok := queryData.(map[string]interface{})
|
||||
if ok {
|
||||
if data["dataSource"] == "traces" {
|
||||
tracesPanelCount++
|
||||
} else if data["dataSource"] == "metrics" {
|
||||
metricsPanelCount++
|
||||
} else if data["dataSource"] == "logs" {
|
||||
logsPanelCount++
|
||||
logsPanelsWithAttrContains += checkLogPanelAttrContains(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ok && query["queryType"] == "clickhouse_sql" && query["clickhouse_sql"] != nil {
|
||||
if isDashboardWithLogsClickhouseQuery(inputData) {
|
||||
logChQueryCount = 1
|
||||
}
|
||||
if isDashboardWithTracesClickhouseQuery(inputData) {
|
||||
traceChQueryCount = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return model.DashboardsInfo{
|
||||
LogsBasedPanels: logsPanelCount,
|
||||
TracesBasedPanels: tracesPanelCount,
|
||||
MetricBasedPanels: metricsPanelCount,
|
||||
|
||||
DashboardsWithLogsChQuery: logChQueryCount,
|
||||
DashboardsWithTraceChQuery: traceChQueryCount,
|
||||
LogsPanelsWithAttrContainsOp: logsPanelsWithAttrContains,
|
||||
}
|
||||
}
|
||||
|
||||
func GetDashboardsWithMetricNames(ctx context.Context, orgID string, metricNames []string) (map[string][]map[string]string, *model.ApiError) {
|
||||
dashboards := []types.Dashboard{}
|
||||
err := store.BunDB().NewSelect().Model(&dashboards).Where("org_id = ?", orgID).Scan(ctx)
|
||||
if err != nil {
|
||||
zap.L().Error("Error in getting dashboards", zap.Error(err))
|
||||
return nil, &model.ApiError{Typ: model.ErrorExec, Err: err}
|
||||
}
|
||||
if err != nil {
|
||||
zap.L().Error("Error in getting dashboards", zap.Error(err))
|
||||
return nil, &model.ApiError{Typ: model.ErrorExec, Err: err}
|
||||
}
|
||||
|
||||
// Initialize result map for each metric
|
||||
result := make(map[string][]map[string]string)
|
||||
|
||||
// Process the JSON data in Go
|
||||
for _, dashboard := range dashboards {
|
||||
var dashData = dashboard.Data
|
||||
|
||||
dashTitle, _ := dashData["title"].(string)
|
||||
widgets, ok := dashData["widgets"].([]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, w := range widgets {
|
||||
widget, ok := w.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
widgetTitle, _ := widget["title"].(string)
|
||||
widgetID, _ := widget["id"].(string)
|
||||
|
||||
query, ok := widget["query"].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
builder, ok := query["builder"].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
queryData, ok := builder["queryData"].([]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, qd := range queryData {
|
||||
data, ok := qd.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if dataSource, ok := data["dataSource"].(string); !ok || dataSource != "metrics" {
|
||||
continue
|
||||
}
|
||||
|
||||
aggregateAttr, ok := data["aggregateAttribute"].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if key, ok := aggregateAttr["key"].(string); ok {
|
||||
// Check if this metric is in our list of interest
|
||||
for _, metricName := range metricNames {
|
||||
if strings.TrimSpace(key) == metricName {
|
||||
result[metricName] = append(result[metricName], map[string]string{
|
||||
"dashboard_id": dashboard.UUID,
|
||||
"widget_name": widgetTitle,
|
||||
"widget_id": widgetID,
|
||||
"dashboard_name": dashTitle,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -58,6 +58,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/postprocess"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
|
||||
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
|
||||
@@ -511,11 +512,11 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
router.HandleFunc("/api/v1/downtime_schedules/{id}", am.EditAccess(aH.editDowntimeSchedule)).Methods(http.MethodPut)
|
||||
router.HandleFunc("/api/v1/downtime_schedules/{id}", am.EditAccess(aH.deleteDowntimeSchedule)).Methods(http.MethodDelete)
|
||||
|
||||
router.HandleFunc("/api/v1/dashboards", am.ViewAccess(aH.getDashboards)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/dashboards", am.EditAccess(aH.createDashboards)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/dashboards/{uuid}", am.ViewAccess(aH.getDashboard)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/dashboards/{uuid}", am.EditAccess(aH.updateDashboard)).Methods(http.MethodPut)
|
||||
router.HandleFunc("/api/v1/dashboards/{uuid}", am.EditAccess(aH.deleteDashboard)).Methods(http.MethodDelete)
|
||||
router.HandleFunc("/api/v2/orgs/me/dashboards", am.ViewAccess(aH.getDashboards)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v2/orgs/me/dashboards", am.EditAccess(aH.createDashboards)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v2/orgs/me/dashboards/{id}", am.ViewAccess(aH.getDashboard)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v2/orgs/me/dashboards/{id}", am.EditAccess(aH.updateDashboard)).Methods(http.MethodPut)
|
||||
router.HandleFunc("/api/v2/orgs/me/dashboards/{uuid}", am.EditAccess(aH.deleteDashboard)).Methods(http.MethodDelete)
|
||||
router.HandleFunc("/api/v2/variables/query", am.ViewAccess(aH.queryDashboardVarsV2)).Methods(http.MethodPost)
|
||||
|
||||
router.HandleFunc("/api/v1/explorer/views", am.ViewAccess(aH.getSavedViews)).Methods(http.MethodGet)
|
||||
@@ -1074,9 +1075,10 @@ func (aH *APIHandler) getDashboards(w http.ResponseWriter, r *http.Request) {
|
||||
render.Error(w, errv2)
|
||||
return
|
||||
}
|
||||
allDashboards, err := dashboards.GetDashboards(r.Context(), claims.OrgID)
|
||||
if err != nil {
|
||||
RespondError(w, err, nil)
|
||||
|
||||
allDashboards, err := aH.Signoz.Modules.Dashboard.List(r.Context(), valuer.MustNewUUID(claims.OrgID))
|
||||
if errv2 != nil {
|
||||
render.Error(w, errv2)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1128,7 +1130,7 @@ func (aH *APIHandler) getDashboards(w http.ResponseWriter, r *http.Request) {
|
||||
inter = Intersection(inter, tags2Dash[tag])
|
||||
}
|
||||
|
||||
filteredDashboards := []types.Dashboard{}
|
||||
filteredDashboards := []*dashboardtypes.Dashboard{}
|
||||
for _, val := range inter {
|
||||
dash := (allDashboards)[val]
|
||||
filteredDashboards = append(filteredDashboards, dash)
|
||||
@@ -1138,21 +1140,18 @@ func (aH *APIHandler) getDashboards(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
||||
func (aH *APIHandler) deleteDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
uuid := mux.Vars(r)["uuid"]
|
||||
claims, errv2 := authtypes.ClaimsFromContext(r.Context())
|
||||
if errv2 != nil {
|
||||
render.Error(w, errv2)
|
||||
return
|
||||
}
|
||||
err := dashboards.DeleteDashboard(r.Context(), claims.OrgID, uuid)
|
||||
|
||||
if err != nil {
|
||||
RespondError(w, err, nil)
|
||||
uuid := mux.Vars(r)["uuid"]
|
||||
if err := aH.Signoz.Modules.Dashboard.Delete(r.Context(), valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(uuid)); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
aH.Respond(w, nil)
|
||||
|
||||
}
|
||||
|
||||
func prepareQuery(r *http.Request) (string, error) {
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/dashboards"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/opamp"
|
||||
@@ -87,10 +86,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := dashboards.InitDB(serverOptions.SigNoz.SQLStore); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := explorer.InitWithDSN(serverOptions.SigNoz.SQLStore); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
225
pkg/query-service/telemetry/dashboard.go
Normal file
225
pkg/query-service/telemetry/dashboard.go
Normal file
@@ -0,0 +1,225 @@
|
||||
package telemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// GetDashboardsInfo returns analytics data for dashboards
|
||||
func GetDashboardsInfo(ctx context.Context) (*model.DashboardsInfo, error) {
|
||||
dashboardsInfo := model.DashboardsInfo{}
|
||||
// fetch dashboards from dashboard db
|
||||
dashboards := []dashboardtypes.Dashboard{}
|
||||
err := store.BunDB().NewSelect().Model(&dashboards).Scan(ctx)
|
||||
if err != nil {
|
||||
zap.L().Error("Error in processing sql query", zap.Error(err))
|
||||
return &dashboardsInfo, err
|
||||
}
|
||||
totalDashboardsWithPanelAndName := 0
|
||||
var dashboardNames []string
|
||||
count := 0
|
||||
queriesWithTagAttrs := 0
|
||||
for _, dashboard := range dashboards {
|
||||
if isDashboardWithPanelAndName(dashboard.Data) {
|
||||
totalDashboardsWithPanelAndName = totalDashboardsWithPanelAndName + 1
|
||||
}
|
||||
dashboardName := extractDashboardName(dashboard.Data)
|
||||
if dashboardName != "" {
|
||||
dashboardNames = append(dashboardNames, dashboardName)
|
||||
}
|
||||
dashboardInfo := countPanelsInDashboard(dashboard.Data)
|
||||
dashboardsInfo.LogsBasedPanels += dashboardInfo.LogsBasedPanels
|
||||
dashboardsInfo.TracesBasedPanels += dashboardInfo.TracesBasedPanels
|
||||
dashboardsInfo.MetricBasedPanels += dashboardInfo.MetricBasedPanels
|
||||
dashboardsInfo.LogsPanelsWithAttrContainsOp += dashboardInfo.LogsPanelsWithAttrContainsOp
|
||||
dashboardsInfo.DashboardsWithLogsChQuery += dashboardInfo.DashboardsWithLogsChQuery
|
||||
dashboardsInfo.DashboardsWithTraceChQuery += dashboardInfo.DashboardsWithTraceChQuery
|
||||
if isDashboardWithTSV2(dashboard.Data) {
|
||||
count = count + 1
|
||||
}
|
||||
|
||||
if isDashboardWithTagAttrs(dashboard.Data) {
|
||||
queriesWithTagAttrs += 1
|
||||
}
|
||||
|
||||
if dashboardInfo.DashboardsWithTraceChQuery > 0 {
|
||||
dashboardsInfo.DashboardNamesWithTraceChQuery = append(dashboardsInfo.DashboardNamesWithTraceChQuery, dashboardName)
|
||||
}
|
||||
|
||||
// check if dashboard is a has a log operator with contains
|
||||
}
|
||||
|
||||
dashboardsInfo.DashboardNames = dashboardNames
|
||||
dashboardsInfo.TotalDashboards = len(dashboards)
|
||||
dashboardsInfo.TotalDashboardsWithPanelAndName = totalDashboardsWithPanelAndName
|
||||
dashboardsInfo.QueriesWithTSV2 = count
|
||||
dashboardsInfo.QueriesWithTagAttrs = queriesWithTagAttrs
|
||||
return &dashboardsInfo, nil
|
||||
}
|
||||
|
||||
func isDashboardWithTSV2(data map[string]interface{}) bool {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(string(jsonData), "time_series_v2")
|
||||
}
|
||||
|
||||
func isDashboardWithTagAttrs(data map[string]interface{}) bool {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(string(jsonData), "span_attributes") ||
|
||||
strings.Contains(string(jsonData), "tag_attributes")
|
||||
}
|
||||
|
||||
func isDashboardWithLogsClickhouseQuery(data map[string]interface{}) bool {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
result := strings.Contains(string(jsonData), "signoz_logs.distributed_logs") ||
|
||||
strings.Contains(string(jsonData), "signoz_logs.logs")
|
||||
return result
|
||||
}
|
||||
|
||||
func isDashboardWithTracesClickhouseQuery(data map[string]interface{}) bool {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// also check if the query is actually active
|
||||
str := string(jsonData)
|
||||
result := strings.Contains(str, "signoz_traces.distributed_signoz_index_v2") ||
|
||||
strings.Contains(str, "signoz_traces.distributed_signoz_spans") ||
|
||||
strings.Contains(str, "signoz_traces.distributed_signoz_error_index_v2")
|
||||
return result
|
||||
}
|
||||
|
||||
func isDashboardWithPanelAndName(data map[string]interface{}) bool {
|
||||
isDashboardName := false
|
||||
isDashboardWithPanelAndName := false
|
||||
if data != nil && data["title"] != nil && data["widgets"] != nil {
|
||||
title, ok := data["title"].(string)
|
||||
if ok && title != "Sample Title" {
|
||||
isDashboardName = true
|
||||
}
|
||||
widgets, ok := data["widgets"]
|
||||
if ok && isDashboardName {
|
||||
data, ok := widgets.([]interface{})
|
||||
if ok && len(data) > 0 {
|
||||
isDashboardWithPanelAndName = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return isDashboardWithPanelAndName
|
||||
}
|
||||
|
||||
func extractDashboardName(data map[string]interface{}) string {
|
||||
|
||||
if data != nil && data["title"] != nil {
|
||||
title, ok := data["title"].(string)
|
||||
if ok {
|
||||
return title
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func checkLogPanelAttrContains(data map[string]interface{}) int {
|
||||
var logsPanelsWithAttrContains int
|
||||
filters, ok := data["filters"].(map[string]interface{})
|
||||
if ok && filters["items"] != nil {
|
||||
items, ok := filters["items"].([]interface{})
|
||||
if ok {
|
||||
for _, item := range items {
|
||||
itemMap, ok := item.(map[string]interface{})
|
||||
if ok {
|
||||
opStr, ok := itemMap["op"].(string)
|
||||
if ok {
|
||||
if slices.Contains([]string{"contains", "ncontains", "like", "nlike"}, opStr) {
|
||||
// check if it's not body
|
||||
key, ok := itemMap["key"].(map[string]string)
|
||||
if ok && key["key"] != "body" {
|
||||
logsPanelsWithAttrContains++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return logsPanelsWithAttrContains
|
||||
}
|
||||
|
||||
func countPanelsInDashboard(inputData map[string]interface{}) model.DashboardsInfo {
|
||||
var logsPanelCount, tracesPanelCount, metricsPanelCount, logsPanelsWithAttrContains int
|
||||
traceChQueryCount := 0
|
||||
logChQueryCount := 0
|
||||
|
||||
// totalPanels := 0
|
||||
if inputData != nil && inputData["widgets"] != nil {
|
||||
widgets, ok := inputData["widgets"]
|
||||
if ok {
|
||||
data, ok := widgets.([]interface{})
|
||||
if ok {
|
||||
for _, widget := range data {
|
||||
sData, ok := widget.(map[string]interface{})
|
||||
if ok && sData["query"] != nil {
|
||||
// totalPanels++
|
||||
query, ok := sData["query"].(map[string]interface{})
|
||||
if ok && query["queryType"] == "builder" && query["builder"] != nil {
|
||||
builderData, ok := query["builder"].(map[string]interface{})
|
||||
if ok && builderData["queryData"] != nil {
|
||||
builderQueryData, ok := builderData["queryData"].([]interface{})
|
||||
if ok {
|
||||
for _, queryData := range builderQueryData {
|
||||
data, ok := queryData.(map[string]interface{})
|
||||
if ok {
|
||||
if data["dataSource"] == "traces" {
|
||||
tracesPanelCount++
|
||||
} else if data["dataSource"] == "metrics" {
|
||||
metricsPanelCount++
|
||||
} else if data["dataSource"] == "logs" {
|
||||
logsPanelCount++
|
||||
logsPanelsWithAttrContains += checkLogPanelAttrContains(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ok && query["queryType"] == "clickhouse_sql" && query["clickhouse_sql"] != nil {
|
||||
if isDashboardWithLogsClickhouseQuery(inputData) {
|
||||
logChQueryCount = 1
|
||||
}
|
||||
if isDashboardWithTracesClickhouseQuery(inputData) {
|
||||
traceChQueryCount = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return model.DashboardsInfo{
|
||||
LogsBasedPanels: logsPanelCount,
|
||||
TracesBasedPanels: tracesPanelCount,
|
||||
MetricBasedPanels: metricsPanelCount,
|
||||
|
||||
DashboardsWithLogsChQuery: logChQueryCount,
|
||||
DashboardsWithTraceChQuery: traceChQueryCount,
|
||||
LogsPanelsWithAttrContainsOp: logsPanelsWithAttrContains,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package signoz
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/preference"
|
||||
@@ -12,11 +13,13 @@ import (
|
||||
type Modules struct {
|
||||
Organization organization.Module
|
||||
Preference preference.Module
|
||||
Dashboard dashboard.Module
|
||||
}
|
||||
|
||||
func NewModules(sqlstore sqlstore.SQLStore) Modules {
|
||||
return Modules{
|
||||
Organization: implorganization.NewModule(implorganization.NewStore(sqlstore)),
|
||||
Preference: implpreference.NewModule(implpreference.NewStore(sqlstore), preferencetypes.NewDefaultPreferenceMap()),
|
||||
Dashboard: nil,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/gosimple/slug"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type Dashboard struct {
|
||||
bun.BaseModel `bun:"table:dashboards"`
|
||||
|
||||
TimeAuditable
|
||||
UserAuditable
|
||||
OrgID string `json:"-" bun:"org_id,notnull"`
|
||||
ID int `json:"id" bun:"id,pk,autoincrement"`
|
||||
UUID string `json:"uuid" bun:"uuid,type:text,notnull,unique"`
|
||||
Data DashboardData `json:"data" bun:"data,type:text,notnull"`
|
||||
Locked *int `json:"isLocked" bun:"locked,notnull,default:0"`
|
||||
|
||||
Slug string `json:"-" bun:"-"`
|
||||
Title string `json:"-" bun:"-"`
|
||||
}
|
||||
|
||||
// UpdateSlug updates the slug
|
||||
func (d *Dashboard) UpdateSlug() {
|
||||
var title string
|
||||
|
||||
if val, ok := d.Data["title"]; ok {
|
||||
title = val.(string)
|
||||
}
|
||||
|
||||
d.Slug = SlugifyTitle(title)
|
||||
}
|
||||
|
||||
func SlugifyTitle(title string) string {
|
||||
s := slug.Make(strings.ToLower(title))
|
||||
if s == "" {
|
||||
// If the dashboard name is only characters outside of the
|
||||
// sluggable characters, the slug creation will return an
|
||||
// empty string which will mess up URLs. This failsafe picks
|
||||
// that up and creates the slug as a base64 identifier instead.
|
||||
s = base64.RawURLEncoding.EncodeToString([]byte(title))
|
||||
if slug.MaxLength != 0 && len(s) > slug.MaxLength {
|
||||
s = s[:slug.MaxLength]
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type DashboardData map[string]interface{}
|
||||
|
||||
func (c DashboardData) Value() (driver.Value, error) {
|
||||
return json.Marshal(c)
|
||||
}
|
||||
|
||||
func (c *DashboardData) Scan(src interface{}) error {
|
||||
var data []byte
|
||||
if b, ok := src.([]byte); ok {
|
||||
data = b
|
||||
} else if s, ok := src.(string); ok {
|
||||
data = []byte(s)
|
||||
}
|
||||
return json.Unmarshal(data, c)
|
||||
}
|
||||
|
||||
type TTLSetting struct {
|
||||
bun.BaseModel `bun:"table:ttl_setting"`
|
||||
Identifiable
|
||||
TimeAuditable
|
||||
TransactionID string `bun:"transaction_id,type:text,notnull"`
|
||||
TableName string `bun:"table_name,type:text,notnull"`
|
||||
TTL int `bun:"ttl,notnull,default:0"`
|
||||
ColdStorageTTL int `bun:"cold_storage_ttl,notnull,default:0"`
|
||||
Status string `bun:"status,type:text,notnull"`
|
||||
OrgID string `json:"-" bun:"org_id,notnull"`
|
||||
}
|
||||
71
pkg/types/dashboardtypes/dashboard.go
Normal file
71
pkg/types/dashboardtypes/dashboard.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type Dashboard struct {
|
||||
ID string `json:"id"`
|
||||
Data Data `json:"data"`
|
||||
Locked bool `json:"isLocked"`
|
||||
OrgID valuer.UUID `json:"orgId"`
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
}
|
||||
|
||||
type StorableDashboard struct {
|
||||
bun.BaseModel `bun:"table:dashboards"`
|
||||
|
||||
types.Identifiable `bun:"id,pk,type:text"`
|
||||
Data Data `bun:"data,type:text,notnull"`
|
||||
IsLocked bool `bun:"is_locked,notnull"`
|
||||
OrgID valuer.UUID `bun:"org_id,notnull"`
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
}
|
||||
|
||||
func NewStorableDashboard(raw map[string]any, email string, orgID valuer.UUID) (*StorableDashboard, error) {
|
||||
data, err := NewData(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &StorableDashboard{
|
||||
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
|
||||
Data: data,
|
||||
IsLocked: false,
|
||||
OrgID: orgID,
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
UserAuditable: types.UserAuditable{
|
||||
CreatedBy: email,
|
||||
UpdatedBy: email,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewDashboardFromStorableDashboard(storableDashboard *StorableDashboard) *Dashboard {
|
||||
return &Dashboard{
|
||||
ID: storableDashboard.ID.String(),
|
||||
Data: storableDashboard.Data,
|
||||
Locked: storableDashboard.IsLocked,
|
||||
OrgID: storableDashboard.OrgID,
|
||||
TimeAuditable: storableDashboard.TimeAuditable,
|
||||
UserAuditable: storableDashboard.UserAuditable,
|
||||
}
|
||||
}
|
||||
|
||||
type Store interface {
|
||||
Create(context.Context, *StorableDashboard) error
|
||||
Get(context.Context, valuer.UUID) (*StorableDashboard, error)
|
||||
List(context.Context, valuer.UUID) ([]*StorableDashboard, error)
|
||||
Update(context.Context, *StorableDashboard) error
|
||||
Delete(context.Context, valuer.UUID) error
|
||||
}
|
||||
68
pkg/types/dashboardtypes/data.go
Normal file
68
pkg/types/dashboardtypes/data.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
)
|
||||
|
||||
type Data map[string]any
|
||||
|
||||
func NewData(input map[string]any) (Data, error) {
|
||||
title, ok := input["title"]
|
||||
if ok && title == "" || !ok {
|
||||
return Data{}, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "title is required")
|
||||
}
|
||||
|
||||
return Data(input), nil
|
||||
}
|
||||
|
||||
func (data Data) Value() (driver.Value, error) {
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
func (data *Data) Scan(val interface{}) error {
|
||||
var b []byte
|
||||
|
||||
switch val := val.(type) {
|
||||
case []byte:
|
||||
b = val
|
||||
case string:
|
||||
b = []byte(val)
|
||||
default:
|
||||
return fmt.Errorf("data: (non-string \"%s\")", reflect.TypeOf(val).String())
|
||||
}
|
||||
|
||||
return json.Unmarshal(b, data)
|
||||
}
|
||||
|
||||
func (data Data) WidgetIDs() []string {
|
||||
if data["widgets"] == nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
widgets, ok := data["widgets"]
|
||||
if !ok {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
widgetsData, ok := widgets.([]any)
|
||||
if !ok {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
var widgetIDs []string
|
||||
for _, widget := range widgetsData {
|
||||
widgetData, ok := widget.(map[string]any)
|
||||
if ok && widgetData["query"] != nil && widgetData["id"] != nil {
|
||||
if id, ok := widgetData["id"].(string); ok {
|
||||
widgetIDs = append(widgetIDs, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return widgetIDs
|
||||
}
|
||||
17
pkg/types/ttl.go
Normal file
17
pkg/types/ttl.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type TTLSetting struct {
|
||||
bun.BaseModel `bun:"table:ttl_setting"`
|
||||
Identifiable
|
||||
TimeAuditable
|
||||
TransactionID string `bun:"transaction_id,type:text,notnull"`
|
||||
TableName string `bun:"table_name,type:text,notnull"`
|
||||
TTL int `bun:"ttl,notnull,default:0"`
|
||||
ColdStorageTTL int `bun:"cold_storage_ttl,notnull,default:0"`
|
||||
Status string `bun:"status,type:text,notnull"`
|
||||
OrgID string `json:"-" bun:"org_id,notnull"`
|
||||
}
|
||||
Reference in New Issue
Block a user