mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-21 01:10:39 +01:00
Compare commits
26 Commits
chore/migr
...
feat/cloud
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce141576d6 | ||
|
|
fab7fd001f | ||
|
|
02634894d9 | ||
|
|
83f1070b8a | ||
|
|
b753b95a8a | ||
|
|
4757550189 | ||
|
|
96ad37fea9 | ||
|
|
147005077c | ||
|
|
5419e8461c | ||
|
|
e634eb4452 | ||
|
|
a50bc53f4c | ||
|
|
9f60bdf54a | ||
|
|
e41639dea0 | ||
|
|
d720f76819 | ||
|
|
d76e15d444 | ||
|
|
dc1a5cce76 | ||
|
|
37c97528f3 | ||
|
|
6a8e6e94e9 | ||
|
|
5c480272f3 | ||
|
|
ab26fd3d8c | ||
|
|
80c0801b2e | ||
|
|
bd190b8d88 | ||
|
|
7e5f5bfac4 | ||
|
|
7f72ca19d3 | ||
|
|
231229d73e | ||
|
|
bb88cde296 |
@@ -115,7 +115,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
func(ps factory.ProviderSettings, q querier.Querier, a analytics.Analytics) querier.Handler {
|
||||
return querier.NewHandler(ps, q, a)
|
||||
},
|
||||
func(_ sqlstore.SQLStore, _ global.Global, _ zeus.Zeus, _ gateway.Gateway, _ licensing.Licensing, _ serviceaccount.Module, _ cloudintegration.Config) (cloudintegration.Module, error) {
|
||||
func(_ sqlstore.SQLStore, _ dashboard.Module, _ global.Global, _ zeus.Zeus, _ gateway.Gateway, _ licensing.Licensing, _ serviceaccount.Module, _ cloudintegration.Config) (cloudintegration.Module, error) {
|
||||
return implcloudintegration.NewModule(), nil
|
||||
},
|
||||
func(c cache.Cache, am alertmanager.Alertmanager, ss sqlstore.SQLStore, ts telemetrystore.TelemetryStore, ms telemetrytypes.MetadataStore, p prometheus.Prometheus, og organization.Getter, rsh rulestatehistory.Module, q querier.Querier, qp queryparser.QueryParser) factory.NamedMap[factory.ProviderFactory[ruler.Ruler, ruler.Config]] {
|
||||
|
||||
@@ -167,7 +167,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
communityHandler := querier.NewHandler(ps, q, a)
|
||||
return eequerier.NewHandler(ps, q, communityHandler)
|
||||
},
|
||||
func(sqlStore sqlstore.SQLStore, global global.Global, zeus zeus.Zeus, gateway gateway.Gateway, licensing licensing.Licensing, serviceAccount serviceaccount.Module, config cloudintegration.Config) (cloudintegration.Module, error) {
|
||||
func(sqlStore sqlstore.SQLStore, dashboardModule dashboard.Module, global global.Global, zeus zeus.Zeus, gateway gateway.Gateway, licensing licensing.Licensing, serviceAccount serviceaccount.Module, config cloudintegration.Config) (cloudintegration.Module, error) {
|
||||
defStore := pkgcloudintegration.NewServiceDefinitionStore()
|
||||
awsCloudProviderModule, err := implcloudprovider.NewAWSCloudProvider(defStore)
|
||||
if err != nil {
|
||||
@@ -179,7 +179,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
cloudintegrationtypes.CloudProviderTypeAzure: azureCloudProviderModule,
|
||||
}
|
||||
|
||||
return implcloudintegration.NewModule(pkgcloudintegration.NewStore(sqlStore), global, zeus, gateway, licensing, serviceAccount, cloudProvidersMap, config)
|
||||
return implcloudintegration.NewModule(pkgcloudintegration.NewStore(sqlStore), dashboardModule, global, zeus, gateway, licensing, serviceAccount, cloudProvidersMap, config)
|
||||
},
|
||||
func(c cache.Cache, am alertmanager.Alertmanager, ss sqlstore.SQLStore, ts telemetrystore.TelemetryStore, ms telemetrytypes.MetadataStore, p prometheus.Prometheus, og organization.Getter, rsh rulestatehistory.Module, q querier.Querier, qp queryparser.QueryParser) factory.NamedMap[factory.ProviderFactory[ruler.Ruler, ruler.Config]] {
|
||||
return factory.MustNewNamedMap(signozruler.NewFactory(c, am, ss, ts, ms, p, og, rsh, q, qp, eerules.PrepareTaskFunc, eerules.TestNotification))
|
||||
|
||||
@@ -2342,6 +2342,8 @@ components:
|
||||
type: boolean
|
||||
org_id:
|
||||
type: string
|
||||
source:
|
||||
$ref: '#/components/schemas/DashboardtypesSource'
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
@@ -2371,6 +2373,12 @@ components:
|
||||
timeRangeEnabled:
|
||||
type: boolean
|
||||
type: object
|
||||
DashboardtypesSource:
|
||||
enum:
|
||||
- user
|
||||
- system
|
||||
- integration
|
||||
type: object
|
||||
DashboardtypesStorableDashboardData:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
|
||||
@@ -54,11 +54,6 @@ func (provider *awscloudprovider) GetServiceDefinition(ctx context.Context, serv
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// override cloud integration dashboard id
|
||||
for index, dashboard := range serviceDef.Assets.Dashboards {
|
||||
serviceDef.Assets.Dashboards[index].ID = cloudintegrationtypes.GetCloudIntegrationDashboardID(cloudintegrationtypes.CloudProviderTypeAWS, serviceID.StringValue(), dashboard.ID)
|
||||
}
|
||||
|
||||
return serviceDef, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -38,11 +38,6 @@ func (provider *azurecloudprovider) GetServiceDefinition(ctx context.Context, se
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// override cloud integration dashboard id.
|
||||
for index, dashboard := range serviceDef.Assets.Dashboards {
|
||||
serviceDef.Assets.Dashboards[index].ID = cloudintegrationtypes.GetCloudIntegrationDashboardID(cloudintegrationtypes.CloudProviderTypeAzure, serviceID.StringValue(), dashboard.ID)
|
||||
}
|
||||
|
||||
return serviceDef, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package implcloudintegration
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
@@ -11,6 +10,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
|
||||
type module struct {
|
||||
store cloudintegrationtypes.Store
|
||||
dashboardModule dashboard.Module
|
||||
gateway gateway.Gateway
|
||||
zeus zeus.Zeus
|
||||
licensing licensing.Licensing
|
||||
@@ -34,6 +35,7 @@ type module struct {
|
||||
|
||||
func NewModule(
|
||||
store cloudintegrationtypes.Store,
|
||||
dashboardModule dashboard.Module,
|
||||
global global.Global,
|
||||
zeus zeus.Zeus,
|
||||
gateway gateway.Gateway,
|
||||
@@ -44,6 +46,7 @@ func NewModule(
|
||||
) (cloudintegration.Module, error) {
|
||||
return &module{
|
||||
store: store,
|
||||
dashboardModule: dashboardModule,
|
||||
global: global,
|
||||
zeus: zeus,
|
||||
gateway: gateway,
|
||||
@@ -254,7 +257,70 @@ func (module *module) DisconnectAccount(ctx context.Context, orgID valuer.UUID,
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
return module.store.RemoveAccount(ctx, orgID, accountID, provider)
|
||||
return module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
services, err := module.store.ListServices(ctx, accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Fetch all active accounts for this org+provider to detect shared dashboard usage.
|
||||
allAccounts, err := module.store.ListConnectedAccounts(ctx, orgID, provider)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, svc := range services {
|
||||
svcCfg, err := cloudintegrationtypes.NewServiceConfigFromJSON(provider, svc.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !svcCfg.IsMetricsEnabled(provider) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if any other account has this service with metrics enabled.
|
||||
// If so, the shared dashboard must not be deleted.
|
||||
shared := false
|
||||
for _, otherAccount := range allAccounts {
|
||||
if otherAccount.ID == accountID {
|
||||
continue
|
||||
}
|
||||
otherSvc, err := module.store.GetServiceByServiceID(ctx, otherAccount.ID, svc.Type)
|
||||
if err != nil {
|
||||
if errors.Ast(err, errors.TypeNotFound) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
otherCfg, err := cloudintegrationtypes.NewServiceConfigFromJSON(provider, otherSvc.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if otherCfg.IsMetricsEnabled(provider) {
|
||||
shared = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if shared {
|
||||
continue
|
||||
}
|
||||
|
||||
integrationService := &cloudintegrationtypes.CloudIntegrationService{
|
||||
Type: svc.Type,
|
||||
CloudIntegrationID: accountID,
|
||||
}
|
||||
if err := module.deprovisionDashboards(ctx, orgID, provider, integrationService); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := module.store.DeleteServicesByCloudIntegrationID(ctx, accountID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return module.store.RemoveAccount(ctx, orgID, accountID, provider)
|
||||
})
|
||||
}
|
||||
|
||||
func (module *module) ListServicesMetadata(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, integrationID valuer.UUID) ([]*cloudintegrationtypes.ServiceMetadata, error) {
|
||||
@@ -331,12 +397,16 @@ func (module *module) GetService(ctx context.Context, orgID valuer.UUID, service
|
||||
|
||||
integrationService = cloudintegrationtypes.NewCloudIntegrationServiceFromStorable(storedService, serviceConfig)
|
||||
}
|
||||
|
||||
if err := module.enrichDashboardIDs(ctx, orgID, provider, serviceID, serviceDefinition); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return cloudintegrationtypes.NewService(*serviceDefinition, integrationService), nil
|
||||
}
|
||||
|
||||
func (module *module) CreateService(ctx context.Context, orgID valuer.UUID, service *cloudintegrationtypes.CloudIntegrationService, provider cloudintegrationtypes.CloudProviderType) error {
|
||||
func (module *module) CreateService(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, service *cloudintegrationtypes.CloudIntegrationService, provider cloudintegrationtypes.CloudProviderType) error {
|
||||
_, err := module.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
@@ -357,10 +427,21 @@ func (module *module) CreateService(ctx context.Context, orgID valuer.UUID, serv
|
||||
return err
|
||||
}
|
||||
|
||||
return module.store.CreateService(ctx, cloudintegrationtypes.NewStorableCloudIntegrationService(service, string(configJSON)))
|
||||
metricsEnabled := service.Config.IsMetricsEnabled(provider)
|
||||
storableService := cloudintegrationtypes.NewStorableCloudIntegrationService(service, string(configJSON))
|
||||
|
||||
return module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
if err := module.store.CreateService(ctx, storableService); err != nil {
|
||||
return err
|
||||
}
|
||||
if metricsEnabled {
|
||||
return module.provisionDashboards(ctx, orgID, createdBy, creator, provider, service, serviceDefinition)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (module *module) UpdateService(ctx context.Context, orgID valuer.UUID, integrationService *cloudintegrationtypes.CloudIntegrationService, provider cloudintegrationtypes.CloudProviderType) error {
|
||||
func (module *module) UpdateService(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, integrationService *cloudintegrationtypes.CloudIntegrationService, provider cloudintegrationtypes.CloudProviderType) error {
|
||||
_, err := module.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
@@ -381,43 +462,21 @@ func (module *module) UpdateService(ctx context.Context, orgID valuer.UUID, inte
|
||||
return err
|
||||
}
|
||||
|
||||
metricsEnabled := integrationService.Config.IsMetricsEnabled(provider)
|
||||
storableService := cloudintegrationtypes.NewStorableCloudIntegrationService(integrationService, string(configJSON))
|
||||
|
||||
return module.store.UpdateService(ctx, storableService)
|
||||
}
|
||||
|
||||
func (module *module) GetDashboardByID(ctx context.Context, orgID valuer.UUID, id string) (*dashboardtypes.Dashboard, error) {
|
||||
_, err := module.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
_, _, _, err = cloudintegrationtypes.ParseCloudIntegrationDashboardID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allDashboards, err := module.listDashboards(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, d := range allDashboards {
|
||||
if d.ID == id {
|
||||
return d, nil
|
||||
return module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
if err := module.store.UpdateService(ctx, storableService); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New(errors.TypeNotFound, cloudintegrationtypes.ErrCodeCloudIntegrationNotFound, "cloud integration dashboard not found")
|
||||
}
|
||||
|
||||
func (module *module) ListDashboards(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error) {
|
||||
_, err := module.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
return module.listDashboards(ctx, orgID)
|
||||
switch {
|
||||
case metricsEnabled:
|
||||
return module.provisionDashboards(ctx, orgID, createdBy, creator, provider, integrationService, serviceDefinition)
|
||||
case !metricsEnabled:
|
||||
return module.deprovisionDashboards(ctx, orgID, provider, integrationService)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
|
||||
@@ -493,52 +552,72 @@ func (module *module) getOrCreateAPIKey(ctx context.Context, orgID valuer.UUID,
|
||||
return factorAPIKey.Key, nil
|
||||
}
|
||||
|
||||
func (module *module) listDashboards(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error) {
|
||||
var allDashboards []*dashboardtypes.Dashboard
|
||||
// provisionDashboards creates dashboard and integration_dashboard rows for each dashboard in the service definition.
|
||||
// Must be called within a transaction (ctx carries the tx).
|
||||
func (module *module) provisionDashboards(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, provider cloudintegrationtypes.CloudProviderType, service *cloudintegrationtypes.CloudIntegrationService, serviceDefinition *cloudintegrationtypes.ServiceDefinition) error {
|
||||
// TODO: DB calls are in for loop, can be optimized later.
|
||||
for _, dashboard := range serviceDefinition.Assets.Dashboards {
|
||||
slug := cloudintegrationtypes.IntegrationDashboardSlug(provider, service.Type, dashboard.ID)
|
||||
|
||||
for provider := range module.cloudProvidersMap {
|
||||
cloudProvider, err := module.getCloudProvider(provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
existing, err := module.store.GetIntegrationDashboardBySlug(ctx, orgID, cloudintegrationtypes.IntegrationDashboardProviderCloudIntegration, slug)
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return err
|
||||
}
|
||||
if existing != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
connectedAccounts, err := module.store.ListConnectedAccounts(ctx, orgID, provider)
|
||||
createdDashboard, err := module.dashboardModule.Create(ctx, orgID, createdBy, creator, dashboardtypes.SourceIntegration, dashboardtypes.PostableDashboard(dashboard.Definition))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
for _, storableAccount := range connectedAccounts {
|
||||
storedServices, err := module.store.ListServices(ctx, storableAccount.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, storedSvc := range storedServices {
|
||||
serviceConfig, err := cloudintegrationtypes.NewServiceConfigFromJSON(provider, storedSvc.Config)
|
||||
if err != nil || !serviceConfig.IsMetricsEnabled(provider) {
|
||||
continue
|
||||
}
|
||||
|
||||
svcDef, err := cloudProvider.GetServiceDefinition(ctx, storedSvc.Type)
|
||||
if err != nil || svcDef == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
dashboards := cloudintegrationtypes.GetDashboardsFromAssets(
|
||||
storedSvc.Type.StringValue(),
|
||||
orgID,
|
||||
provider,
|
||||
storableAccount.CreatedAt,
|
||||
svcDef.Assets,
|
||||
)
|
||||
allDashboards = append(allDashboards, dashboards...)
|
||||
}
|
||||
integrationDashboard := cloudintegrationtypes.NewStorableIntegrationDashboard(createdDashboard.ID, cloudintegrationtypes.IntegrationDashboardProviderCloudIntegration, slug)
|
||||
if err := module.store.CreateIntegrationDashboard(ctx, integrationDashboard); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(allDashboards, func(i, j int) bool {
|
||||
return allDashboards[i].ID < allDashboards[j].ID
|
||||
})
|
||||
|
||||
return allDashboards, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// deprovisionDashboards deletes all dashboard and integration_dashboard rows for the given service.
|
||||
func (module *module) deprovisionDashboards(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, service *cloudintegrationtypes.CloudIntegrationService) error {
|
||||
slugPrefix := cloudintegrationtypes.IntegrationDashboardSlugPrefix(provider, service.Type)
|
||||
rows, err := module.store.ListIntegrationDashboardsBySlugPrefix(ctx, orgID, cloudintegrationtypes.IntegrationDashboardProviderCloudIntegration, slugPrefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, row := range rows {
|
||||
dashID, err := valuer.NewUUID(row.DashboardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := module.store.DeleteIntegrationDashboardBySlug(ctx, cloudintegrationtypes.IntegrationDashboardProviderCloudIntegration, row.Slug); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := module.dashboardModule.DeleteBySource(ctx, orgID, dashID, dashboardtypes.SourceIntegration); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// enrichDashboardIDs replaces the raw dashboard name in each Dashboard.ID with the provisioned UUID,
|
||||
// or sets it to nil if the dashboard has not been provisioned yet.
|
||||
func (module *module) enrichDashboardIDs(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, serviceID cloudintegrationtypes.ServiceID, serviceDefinition *cloudintegrationtypes.ServiceDefinition) error {
|
||||
for i, d := range serviceDefinition.Assets.Dashboards {
|
||||
slug := cloudintegrationtypes.IntegrationDashboardSlug(provider, serviceID, d.ID)
|
||||
row, err := module.store.GetIntegrationDashboardBySlug(ctx, orgID, cloudintegrationtypes.IntegrationDashboardProviderCloudIntegration, slug)
|
||||
if err != nil {
|
||||
if errors.Ast(err, errors.TypeNotFound) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
serviceDefinition.Assets.Dashboards[i].ID = row.DashboardID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -49,6 +49,14 @@ func (module *module) CreatePublic(ctx context.Context, orgID valuer.UUID, publi
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
dashboard, err := module.Get(ctx, orgID, publicDashboard.DashboardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dashboard.ErrIfNotPublishable(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
storablePublicDashboard, err := module.store.GetPublic(ctx, publicDashboard.DashboardID.StringValue())
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return err
|
||||
@@ -129,6 +137,14 @@ func (module *module) UpdatePublic(ctx context.Context, orgID valuer.UUID, publi
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
dashboard, err := module.Get(ctx, orgID, publicDashboard.DashboardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dashboard.ErrIfNotPublishable(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return module.store.UpdatePublic(ctx, dashboardtypes.NewStorablePublicDashboardFromPublicDashboard(publicDashboard))
|
||||
}
|
||||
|
||||
@@ -138,6 +154,10 @@ func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.U
|
||||
return err
|
||||
}
|
||||
|
||||
if err := dashboard.ErrIfNotDeletable(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dashboard.Locked {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is locked, please unlock the dashboard to be delete it")
|
||||
}
|
||||
@@ -162,12 +182,38 @@ func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.U
|
||||
return nil
|
||||
}
|
||||
|
||||
func (module *module) DeleteBySource(ctx context.Context, orgID valuer.UUID, id valuer.UUID, source dashboardtypes.Source) error {
|
||||
dashboard, err := module.Get(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dashboard.Source != source {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard source does not match")
|
||||
}
|
||||
|
||||
return module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
if err := module.store.DeletePublic(ctx, id.String()); err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return err
|
||||
}
|
||||
return module.store.Delete(ctx, orgID, id)
|
||||
})
|
||||
}
|
||||
|
||||
func (module *module) DeletePublic(ctx context.Context, orgID valuer.UUID, dashboardID valuer.UUID) error {
|
||||
_, err := module.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
dashboard, err := module.Get(ctx, orgID, dashboardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dashboard.ErrIfNotPublishable(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = module.store.DeletePublic(ctx, dashboardID.StringValue())
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -193,8 +239,8 @@ func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[strin
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, data dashboardtypes.PostableDashboard) (*dashboardtypes.Dashboard, error) {
|
||||
return module.pkgDashboardModule.Create(ctx, orgID, createdBy, creator, data)
|
||||
func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, source dashboardtypes.Source, data dashboardtypes.PostableDashboard) (*dashboardtypes.Dashboard, error) {
|
||||
return module.pkgDashboardModule.Create(ctx, orgID, createdBy, creator, source, data)
|
||||
}
|
||||
|
||||
func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error) {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
const BANNED_COMPONENTS = {
|
||||
Typography: 'Use @signozhq/ui Typography instead of antd Typography.',
|
||||
Badge: 'Use @signozhq/ui/badge instead of antd Badge.',
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -47,7 +47,6 @@ export const TracesFunnels = Loadable(
|
||||
import(/* webpackChunkName: "Traces Funnels" */ 'pages/TracesModulePage'),
|
||||
);
|
||||
export const TracesFunnelDetails = Loadable(
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "Traces Funnel Details" */ 'pages/TracesModulePage'
|
||||
@@ -313,13 +312,6 @@ export const PublicDashboardPage = Loadable(
|
||||
),
|
||||
);
|
||||
|
||||
export const AlertTypeSelectionPage = Loadable(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "Alert Type Selection Page" */ 'pages/AlertTypeSelection'
|
||||
),
|
||||
);
|
||||
|
||||
export const MeterExplorerPage = Loadable(
|
||||
() =>
|
||||
import(/* webpackChunkName: "Meter Explorer Page" */ 'pages/MeterExplorer'),
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
AIAssistantPage,
|
||||
AlertHistory,
|
||||
AlertOverview,
|
||||
AlertTypeSelectionPage,
|
||||
AllAlertChannels,
|
||||
AllErrors,
|
||||
ApiMonitoring,
|
||||
@@ -213,13 +212,6 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
key: 'LIST_ALL_ALERT',
|
||||
},
|
||||
{
|
||||
path: ROUTES.ALERT_TYPE_SELECTION,
|
||||
exact: true,
|
||||
component: AlertTypeSelectionPage,
|
||||
isPrivate: true,
|
||||
key: 'ALERT_TYPE_SELECTION',
|
||||
},
|
||||
{
|
||||
path: ROUTES.ALERTS_NEW,
|
||||
exact: true,
|
||||
@@ -533,18 +525,6 @@ export const LIST_LICENSES: AppRoutes = {
|
||||
key: 'LIST_LICENSES',
|
||||
};
|
||||
|
||||
export const oldRoutes = [
|
||||
'/pipelines',
|
||||
'/logs-explorer',
|
||||
'/logs-explorer/live',
|
||||
'/logs-save-views',
|
||||
'/traces-save-views',
|
||||
'/settings/access-tokens',
|
||||
'/settings/api-keys',
|
||||
'/messaging-queues',
|
||||
'/alerts/edit',
|
||||
];
|
||||
|
||||
export const oldNewRoutesMapping: Record<string, string> = {
|
||||
'/pipelines': '/logs/pipelines',
|
||||
'/logs-explorer': '/logs/logs-explorer',
|
||||
@@ -555,7 +535,9 @@ export const oldNewRoutesMapping: Record<string, string> = {
|
||||
'/settings/api-keys': '/settings/service-accounts',
|
||||
'/messaging-queues': '/messaging-queues/overview',
|
||||
'/alerts/edit': '/alerts/overview',
|
||||
'/alerts/type-selection': '/alerts/new',
|
||||
};
|
||||
export const oldRoutes = Object.keys(oldNewRoutesMapping);
|
||||
|
||||
export const ROUTES_NOT_TO_BE_OVERRIDEN: string[] = [
|
||||
ROUTES.WORKSPACE_LOCKED,
|
||||
|
||||
@@ -2999,6 +2999,11 @@ export interface CoretypesPatchableObjectsDTO {
|
||||
deletions: CoretypesObjectGroupDTO[] | null;
|
||||
}
|
||||
|
||||
export enum DashboardtypesSourceDTO {
|
||||
user = 'user',
|
||||
system = 'system',
|
||||
integration = 'integration',
|
||||
}
|
||||
export interface DashboardtypesDashboardDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -3022,6 +3027,7 @@ export interface DashboardtypesDashboardDTO {
|
||||
* @type string
|
||||
*/
|
||||
org_id?: string;
|
||||
source?: DashboardtypesSourceDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
.breadcrumb {
|
||||
padding-left: 16px;
|
||||
|
||||
ol {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:global(.ant-breadcrumb-separator) {
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-color: var(--l1-border);
|
||||
margin: 16px 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
32
frontend/src/components/AlertBreadcrumb/AlertBreadcrumb.tsx
Normal file
32
frontend/src/components/AlertBreadcrumb/AlertBreadcrumb.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Breadcrumb, Divider } from 'antd';
|
||||
|
||||
import styles from './AlertBreadcrumb.module.scss';
|
||||
import BreadcrumbItem, { BreadcrumbItemConfig } from './BreadcrumbItem';
|
||||
|
||||
export interface AlertBreadcrumbProps {
|
||||
items: BreadcrumbItemConfig[];
|
||||
className?: string;
|
||||
showDivider?: boolean;
|
||||
}
|
||||
|
||||
function AlertBreadcrumb({
|
||||
items,
|
||||
className,
|
||||
showDivider = true,
|
||||
}: AlertBreadcrumbProps): JSX.Element {
|
||||
const breadcrumbItems = items.map((item) => ({
|
||||
title: <BreadcrumbItem {...item} />,
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb
|
||||
className={`${styles.breadcrumb} ${className || ''}`}
|
||||
items={breadcrumbItems}
|
||||
/>
|
||||
{showDivider && <Divider className={styles.divider} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default AlertBreadcrumb;
|
||||
@@ -0,0 +1,9 @@
|
||||
.item {
|
||||
--button-padding: 0;
|
||||
--button-font-size: var(--periscope-font-size-base);
|
||||
}
|
||||
|
||||
.itemLast {
|
||||
color: var(--muted-foreground);
|
||||
font-size: var(--periscope-font-size-base);
|
||||
}
|
||||
45
frontend/src/components/AlertBreadcrumb/BreadcrumbItem.tsx
Normal file
45
frontend/src/components/AlertBreadcrumb/BreadcrumbItem.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
|
||||
import styles from './BreadcrumbItem.module.scss';
|
||||
|
||||
export type BreadcrumbItemConfig =
|
||||
| {
|
||||
title: string | null;
|
||||
route?: string;
|
||||
}
|
||||
| {
|
||||
title: string | null;
|
||||
isLast?: true;
|
||||
};
|
||||
|
||||
function BreadcrumbItem({
|
||||
title,
|
||||
...props
|
||||
}: BreadcrumbItemConfig): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
if ('isLast' in props) {
|
||||
return <div className={styles.itemLast}>{title}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
className={styles.item}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
if (!('route' in props) || !props.route) {
|
||||
return;
|
||||
}
|
||||
|
||||
safeNavigate(props.route, { newTab: isModifierKeyPressed(e) });
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default BreadcrumbItem;
|
||||
6
frontend/src/components/AlertBreadcrumb/index.tsx
Normal file
6
frontend/src/components/AlertBreadcrumb/index.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
export { default } from './AlertBreadcrumb';
|
||||
export {
|
||||
default as BreadcrumbItem,
|
||||
type BreadcrumbItemConfig,
|
||||
} from './BreadcrumbItem';
|
||||
export type { AlertBreadcrumbProps } from './AlertBreadcrumb';
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
import { JsonView } from 'periscope/components/JsonView';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ILogBody } from 'types/api/logs/log';
|
||||
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
@@ -217,20 +218,17 @@ function LogDetailInner({
|
||||
|
||||
const logBody = useMemo(() => {
|
||||
if (!isBodyJsonQueryEnabled) {
|
||||
return log?.body || '';
|
||||
return (log?.body as string) ?? '';
|
||||
}
|
||||
|
||||
try {
|
||||
const json = JSON.parse(log?.body || '');
|
||||
|
||||
if (typeof json?.message === 'string' && json.message !== '') {
|
||||
return json.message;
|
||||
}
|
||||
|
||||
return log?.body || '';
|
||||
} catch {
|
||||
return log?.body || '';
|
||||
// Feature enabled: body is always a map; message is always a string
|
||||
const bodyObj = log?.body as ILogBody;
|
||||
if (!bodyObj) {
|
||||
return '';
|
||||
}
|
||||
if (bodyObj.message) {
|
||||
return bodyObj.message;
|
||||
}
|
||||
return JSON.stringify(bodyObj);
|
||||
}, [isBodyJsonQueryEnabled, log?.body]);
|
||||
|
||||
const htmlBody = useMemo(
|
||||
|
||||
@@ -9,7 +9,10 @@ import { Color } from '@signozhq/design-tokens';
|
||||
import { Tooltip } from 'antd';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
|
||||
import {
|
||||
getBodyDisplayString,
|
||||
getSanitizedLogBody,
|
||||
} from 'container/LogDetailedView/utils';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
// hooks
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
@@ -99,7 +102,7 @@ function RawLogView({
|
||||
// Check if body is selected
|
||||
const showBody = selectedFields.some((field) => field.name === 'body');
|
||||
if (showBody) {
|
||||
parts.push(`${attributesText} ${data.body}`);
|
||||
parts.push(`${attributesText} ${getBodyDisplayString(data.body)}`);
|
||||
} else {
|
||||
parts.push(attributesText);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ import type { ReactElement } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
|
||||
import {
|
||||
getBodyDisplayString,
|
||||
getSanitizedLogBody,
|
||||
} from 'container/LogDetailedView/utils';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
@@ -87,7 +90,7 @@ export function useLogsTableColumns({
|
||||
? {
|
||||
id: 'body',
|
||||
header: 'Body',
|
||||
accessorFn: (log): string => log.body,
|
||||
accessorFn: (log): string => getBodyDisplayString(log.body),
|
||||
canBeHidden: false,
|
||||
width: { default: '100%', min: 300 },
|
||||
cell: ({ value, isActive }): ReactElement => (
|
||||
|
||||
@@ -626,6 +626,10 @@ function TanStackTableInner<TData>(
|
||||
onChange={(value): void => {
|
||||
setLimit(+value);
|
||||
pagination.onLimitChange?.(+value);
|
||||
if (page !== 1) {
|
||||
setPage(1);
|
||||
pagination.onPageChange?.(1);
|
||||
}
|
||||
}}
|
||||
items={paginationPageSizeItems}
|
||||
/>
|
||||
|
||||
@@ -401,6 +401,62 @@ describe('TanStackTableView Integration', () => {
|
||||
expect(onLimitChange).toHaveBeenCalledWith(20);
|
||||
});
|
||||
});
|
||||
|
||||
it('resets page to 1 when limit changes', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
|
||||
const onPageChange = jest.fn();
|
||||
|
||||
renderTanStackTable({
|
||||
props: {
|
||||
pagination: { total: 100, defaultPage: 1, defaultLimit: 10, onPageChange },
|
||||
enableQueryParams: true,
|
||||
},
|
||||
onUrlUpdate,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Navigate to page 2
|
||||
const nav = screen.getByRole('navigation');
|
||||
const page2 = Array.from(nav.querySelectorAll('button')).find(
|
||||
(btn) => btn.textContent?.trim() === '2',
|
||||
);
|
||||
if (!page2) {
|
||||
throw new Error('Page 2 button not found in pagination');
|
||||
}
|
||||
await user.click(page2);
|
||||
|
||||
await waitFor(() => {
|
||||
const lastPage = onUrlUpdate.mock.calls
|
||||
.map((call) => call[0].searchParams.get('page'))
|
||||
.filter(Boolean)
|
||||
.pop();
|
||||
expect(lastPage).toBe('2');
|
||||
});
|
||||
|
||||
// Change page size
|
||||
const comboboxTrigger = document.querySelector(
|
||||
'button[aria-haspopup="dialog"]',
|
||||
) as HTMLElement;
|
||||
await user.click(comboboxTrigger);
|
||||
|
||||
const option20 = await screen.findByRole('option', { name: '20' });
|
||||
await user.click(option20);
|
||||
|
||||
// Verify page reset to 1 (nuqs removes default values from URL)
|
||||
await waitFor(() => {
|
||||
const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1];
|
||||
const lastPage = lastCall[0].searchParams.get('page');
|
||||
expect(lastPage === '1' || lastPage === null).toBe(true);
|
||||
expect(lastCall[0].searchParams.get('limit')).toBe('20');
|
||||
});
|
||||
|
||||
// Verify onPageChange callback was called with 1
|
||||
expect(onPageChange).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sorting', () => {
|
||||
|
||||
@@ -29,7 +29,6 @@ const ROUTES = {
|
||||
ALERTS_NEW: '/alerts/new',
|
||||
ALERT_HISTORY: '/alerts/history',
|
||||
ALERT_OVERVIEW: '/alerts/overview',
|
||||
ALERT_TYPE_SELECTION: '/alerts/type-selection',
|
||||
ALL_CHANNELS: '/settings/channels',
|
||||
CHANNELS_NEW: '/settings/channels/new',
|
||||
CHANNELS_EDIT: '/settings/channels/edit/:channelId',
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
import * as usePrefillAlertConditions from 'container/FormAlertRules/usePrefillAlertConditions';
|
||||
import AlertTypeSelectionPage from 'pages/AlertTypeSelection';
|
||||
import CreateAlertPage from 'pages/CreateAlert';
|
||||
import { act, fireEvent, render } from 'tests/test-utils';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import { ALERT_TYPE_TO_TITLE, ALERT_TYPE_URL_MAP } from './constants';
|
||||
|
||||
jest.mock('react-router-dom-v5-compat', () => ({
|
||||
...jest.requireActual('react-router-dom-v5-compat'),
|
||||
useNavigationType: jest.fn(() => 'PUSH'),
|
||||
useLocation: jest.fn(() => ({
|
||||
pathname: '/alerts/new',
|
||||
search: '',
|
||||
hash: '',
|
||||
state: null,
|
||||
})),
|
||||
useSearchParams: jest.fn(() => [new URLSearchParams(), jest.fn()]),
|
||||
}));
|
||||
|
||||
jest
|
||||
.spyOn(usePrefillAlertConditions, 'usePrefillAlertConditions')
|
||||
.mockReturnValue({
|
||||
@@ -54,20 +65,13 @@ describe('Alert rule documentation redirection', () => {
|
||||
window.open = mockWindowOpen;
|
||||
});
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string } => ({
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.ALERT_TYPE_SELECTION}`,
|
||||
}),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
renderResult = render(
|
||||
<AlertTypeSelectionPage />,
|
||||
<CreateAlertPage />,
|
||||
{},
|
||||
{
|
||||
initialRoute: ROUTES.ALERT_TYPE_SELECTION,
|
||||
initialRoute: ROUTES.ALERTS_NEW,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -15,6 +15,18 @@ jest.mock('react-router-dom', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom-v5-compat', () => ({
|
||||
...jest.requireActual('react-router-dom-v5-compat'),
|
||||
useNavigationType: jest.fn(() => 'PUSH'),
|
||||
useLocation: jest.fn(() => ({
|
||||
pathname: '/alerts/new',
|
||||
search: 'ruleType=anomaly_rule',
|
||||
hash: '',
|
||||
state: null,
|
||||
})),
|
||||
useSearchParams: jest.fn(() => [new URLSearchParams(), jest.fn()]),
|
||||
}));
|
||||
|
||||
window.ResizeObserver =
|
||||
window.ResizeObserver ||
|
||||
jest.fn().mockImplementation(() => ({
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
.create-alert-tabs {
|
||||
&__extra {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.create-alert-wrapper {
|
||||
margin-top: 10px;
|
||||
|
||||
.divider {
|
||||
border-color: var(--l1-border);
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.breadcrumb-divider {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.create-alert__breadcrumb {
|
||||
padding-left: 16px;
|
||||
|
||||
ol {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.25px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ant-breadcrumb-separator,
|
||||
.breadcrumb-item--last {
|
||||
color: var(--muted-foreground);
|
||||
font-family: 'Geist Mono';
|
||||
}
|
||||
}
|
||||
|
||||
.alerts-container {
|
||||
.top-level-tab.periscope-tab {
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.ant-tabs {
|
||||
&-nav {
|
||||
padding: 0 8px;
|
||||
margin-bottom: 0 !important;
|
||||
|
||||
&::before {
|
||||
border-bottom: 1px solid var(--l1-border) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&-tab {
|
||||
&[data-node-key='TriggeredAlerts'] {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-left: 24px !important;
|
||||
}
|
||||
|
||||
[aria-selected='false'] {
|
||||
.periscope-tab {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { getAppContextMockState } from 'container/RoutingPolicies/__tests__/testUtils';
|
||||
import * as appHooks from 'providers/App/App';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import SelectAlertType from '..';
|
||||
|
||||
const useAppContextSpy = jest.spyOn(appHooks, 'useAppContext');
|
||||
|
||||
describe('SelectAlertType', () => {
|
||||
const mockOnSelect = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useAppContextSpy.mockReturnValue(getAppContextMockState());
|
||||
});
|
||||
|
||||
it('should render all alert type options when anomaly detection is enabled', () => {
|
||||
useAppContextSpy.mockReturnValue({
|
||||
...getAppContextMockState({}),
|
||||
featureFlags: [
|
||||
{
|
||||
name: FeatureKeys.ANOMALY_DETECTION,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<SelectAlertType onSelect={mockOnSelect} />);
|
||||
|
||||
expect(screen.getByText('metric_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('log_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('traces_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('exceptions_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('anomaly_based_alert')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render all alert type options except anomaly based alert when anomaly detection is disabled', () => {
|
||||
render(<SelectAlertType onSelect={mockOnSelect} />);
|
||||
|
||||
expect(screen.getByText('metric_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('log_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('traces_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('exceptions_based_alert')).toBeInTheDocument();
|
||||
expect(screen.queryByText('anomaly_based_alert')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onSelect with metrics based alert type', () => {
|
||||
render(<SelectAlertType onSelect={mockOnSelect} />);
|
||||
fireEvent.click(screen.getByText('metric_based_alert'));
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(
|
||||
AlertTypes.METRICS_BASED_ALERT,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should call onSelect with anomaly based alert type', () => {
|
||||
useAppContextSpy.mockReturnValue({
|
||||
...getAppContextMockState({}),
|
||||
featureFlags: [
|
||||
{
|
||||
name: FeatureKeys.ANOMALY_DETECTION,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<SelectAlertType onSelect={mockOnSelect} />);
|
||||
fireEvent.click(screen.getByText('anomaly_based_alert'));
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(
|
||||
AlertTypes.ANOMALY_BASED_ALERT,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should call onSelect with log based alert type', () => {
|
||||
render(<SelectAlertType onSelect={mockOnSelect} />);
|
||||
fireEvent.click(screen.getByText('log_based_alert'));
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(AlertTypes.LOGS_BASED_ALERT, false);
|
||||
});
|
||||
|
||||
it('should call onSelect with traces based alert type', () => {
|
||||
render(<SelectAlertType onSelect={mockOnSelect} />);
|
||||
fireEvent.click(screen.getByText('traces_based_alert'));
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(
|
||||
AlertTypes.TRACES_BASED_ALERT,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should call onSelect with exceptions based alert type', () => {
|
||||
render(<SelectAlertType onSelect={mockOnSelect} />);
|
||||
fireEvent.click(screen.getByText('exceptions_based_alert'));
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(
|
||||
AlertTypes.EXCEPTIONS_BASED_ALERT,
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,37 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { getAppContextMockState } from 'container/RoutingPolicies/__tests__/testUtils';
|
||||
import * as useCompositeQueryParamHooks from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import * as navigateHooks from 'hooks/useSafeNavigate';
|
||||
import * as useUrlQueryHooks from 'hooks/useUrlQuery';
|
||||
import * as appHooks from 'providers/App/App';
|
||||
import { render } from 'tests/test-utils';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import CreateAlertRule from '../index';
|
||||
|
||||
jest.mock('react-router-dom-v5-compat', () => ({
|
||||
...jest.requireActual('react-router-dom-v5-compat'),
|
||||
useNavigationType: jest.fn(() => 'PUSH'),
|
||||
useLocation: jest.fn(() => ({
|
||||
pathname: '/alerts/new',
|
||||
search: '',
|
||||
hash: '',
|
||||
state: null,
|
||||
})),
|
||||
useSearchParams: jest.fn(() => [new URLSearchParams(), jest.fn()]),
|
||||
}));
|
||||
|
||||
jest.mock('container/TopNav/DateTimeSelectionV2', () => ({
|
||||
__esModule: true,
|
||||
default: function MockDateTimeSelector(): JSX.Element {
|
||||
return <div data-testid="datetime-selector">Mock DateTime Selector</div>;
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('container/FormAlertRules', () => ({
|
||||
__esModule: true,
|
||||
default: function MockFormAlertRules({
|
||||
@@ -48,10 +72,14 @@ const useCompositeQueryParamSpy = jest.spyOn(
|
||||
'useGetCompositeQueryParam',
|
||||
);
|
||||
const useUrlQuerySpy = jest.spyOn(useUrlQueryHooks, 'default');
|
||||
const useSafeNavigateSpy = jest.spyOn(navigateHooks, 'useSafeNavigate');
|
||||
const useAppContextSpy = jest.spyOn(appHooks, 'useAppContext');
|
||||
|
||||
const mockSetUrlQuery = jest.fn();
|
||||
const mockToString = jest.fn();
|
||||
const mockGetUrlQuery = jest.fn();
|
||||
const mockSafeNavigate = jest.fn();
|
||||
const mockDeleteUrlQuery = jest.fn();
|
||||
|
||||
const FORM_ALERT_RULES_TEXT = 'Form Alert Rules';
|
||||
const CREATE_ALERT_V2_TEXT = 'Create Alert V2';
|
||||
@@ -63,8 +91,13 @@ describe('CreateAlertRule', () => {
|
||||
set: mockSetUrlQuery,
|
||||
toString: mockToString,
|
||||
get: mockGetUrlQuery,
|
||||
delete: mockDeleteUrlQuery,
|
||||
} as Partial<URLSearchParams> as URLSearchParams);
|
||||
useCompositeQueryParamSpy.mockReturnValue(initialQueriesMap.metrics);
|
||||
useSafeNavigateSpy.mockReturnValue({
|
||||
safeNavigate: mockSafeNavigate,
|
||||
});
|
||||
useAppContextSpy.mockReturnValue(getAppContextMockState());
|
||||
});
|
||||
|
||||
it('should render classic flow when showClassicCreateAlertsPage is true', () => {
|
||||
@@ -72,18 +105,53 @@ describe('CreateAlertRule', () => {
|
||||
if (key === QueryParams.showClassicCreateAlertsPage) {
|
||||
return 'true';
|
||||
}
|
||||
if (key === QueryParams.alertType) {
|
||||
return AlertTypes.METRICS_BASED_ALERT;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
render(<CreateAlertRule />);
|
||||
expect(screen.getByText(FORM_ALERT_RULES_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render new flow by default', () => {
|
||||
mockGetUrlQuery.mockReturnValue(null);
|
||||
it('should render new flow when alertType is provided', () => {
|
||||
mockGetUrlQuery.mockImplementation((key: string) => {
|
||||
if (key === QueryParams.alertType) {
|
||||
return AlertTypes.METRICS_BASED_ALERT;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
render(<CreateAlertRule />);
|
||||
expect(screen.getByText(CREATE_ALERT_V2_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render type selection when no alertType in URL and no compositeQuery', () => {
|
||||
mockGetUrlQuery.mockReturnValue(null);
|
||||
useCompositeQueryParamSpy.mockReturnValue(null);
|
||||
render(<CreateAlertRule />);
|
||||
expect(screen.queryByText(FORM_ALERT_RULES_TEXT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(CREATE_ALERT_V2_TEXT)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should skip type selection and render alert form when compositeQuery is present', () => {
|
||||
mockGetUrlQuery.mockReturnValue(null);
|
||||
useCompositeQueryParamSpy.mockReturnValue({
|
||||
...initialQueriesMap.metrics,
|
||||
builder: {
|
||||
...initialQueriesMap.metrics.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueriesMap.metrics.builder.queryData[0],
|
||||
dataSource: DataSource.METRICS,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
render(<CreateAlertRule />);
|
||||
expect(screen.getByText(CREATE_ALERT_V2_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(AlertTypes.METRICS_BASED_ALERT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render classic flow when ruleType is anomaly_rule even if showClassicCreateAlertsPage is not true', () => {
|
||||
mockGetUrlQuery.mockImplementation((key: string) => {
|
||||
if (key === QueryParams.showClassicCreateAlertsPage) {
|
||||
@@ -111,8 +179,13 @@ describe('CreateAlertRule', () => {
|
||||
expect(screen.getByText(AlertTypes.LOGS_BASED_ALERT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should use alertType from compositeQuery dataSource when alertType is not in URL', () => {
|
||||
mockGetUrlQuery.mockReturnValue(null);
|
||||
it('should use alertType from URL over compositeQuery dataSource', () => {
|
||||
mockGetUrlQuery.mockImplementation((key: string) => {
|
||||
if (key === QueryParams.alertType) {
|
||||
return AlertTypes.LOGS_BASED_ALERT;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
useCompositeQueryParamSpy.mockReturnValue({
|
||||
...initialQueriesMap.metrics,
|
||||
builder: {
|
||||
@@ -127,14 +200,123 @@ describe('CreateAlertRule', () => {
|
||||
});
|
||||
render(<CreateAlertRule />);
|
||||
expect(screen.getByText(CREATE_ALERT_V2_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(AlertTypes.TRACES_BASED_ALERT)).toBeInTheDocument();
|
||||
expect(screen.getByText(AlertTypes.LOGS_BASED_ALERT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should default to METRICS_BASED_ALERT when no alertType and no compositeQuery', () => {
|
||||
mockGetUrlQuery.mockReturnValue(null);
|
||||
useCompositeQueryParamSpy.mockReturnValue(null);
|
||||
render(<CreateAlertRule />);
|
||||
expect(screen.getByText(CREATE_ALERT_V2_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(AlertTypes.METRICS_BASED_ALERT)).toBeInTheDocument();
|
||||
describe('handleSelectType navigation', () => {
|
||||
beforeEach(() => {
|
||||
mockGetUrlQuery.mockReturnValue(null);
|
||||
useCompositeQueryParamSpy.mockReturnValue(null);
|
||||
});
|
||||
|
||||
it('should navigate with threshold alert params for metrics alert', () => {
|
||||
render(<CreateAlertRule />);
|
||||
fireEvent.click(screen.getByText('metric_based_alert'));
|
||||
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.ruleType,
|
||||
'threshold_rule',
|
||||
);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.alertType,
|
||||
AlertTypes.METRICS_BASED_ALERT,
|
||||
);
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should navigate with threshold alert params for logs alert', () => {
|
||||
render(<CreateAlertRule />);
|
||||
fireEvent.click(screen.getByText('log_based_alert'));
|
||||
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.ruleType,
|
||||
'threshold_rule',
|
||||
);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.alertType,
|
||||
AlertTypes.LOGS_BASED_ALERT,
|
||||
);
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should navigate with threshold alert params for traces alert', () => {
|
||||
render(<CreateAlertRule />);
|
||||
fireEvent.click(screen.getByText('traces_based_alert'));
|
||||
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.ruleType,
|
||||
'threshold_rule',
|
||||
);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.alertType,
|
||||
AlertTypes.TRACES_BASED_ALERT,
|
||||
);
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should navigate with threshold alert params for exceptions alert', () => {
|
||||
render(<CreateAlertRule />);
|
||||
fireEvent.click(screen.getByText('exceptions_based_alert'));
|
||||
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.ruleType,
|
||||
'threshold_rule',
|
||||
);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.alertType,
|
||||
AlertTypes.EXCEPTIONS_BASED_ALERT,
|
||||
);
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should navigate with anomaly detection params for anomaly alert', () => {
|
||||
useAppContextSpy.mockReturnValue({
|
||||
...getAppContextMockState({}),
|
||||
featureFlags: [
|
||||
{
|
||||
name: FeatureKeys.ANOMALY_DETECTION,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<CreateAlertRule />);
|
||||
fireEvent.click(screen.getByText('anomaly_based_alert'));
|
||||
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.ruleType,
|
||||
'anomaly_rule',
|
||||
);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.alertType,
|
||||
AlertTypes.METRICS_BASED_ALERT,
|
||||
);
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should navigate even when showClassicCreateAlertsPage flag is present', () => {
|
||||
mockGetUrlQuery.mockImplementation((key: string) => {
|
||||
if (key === QueryParams.showClassicCreateAlertsPage) {
|
||||
return 'true';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
render(<CreateAlertRule />);
|
||||
fireEvent.click(screen.getByText('metric_based_alert'));
|
||||
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.ruleType,
|
||||
'threshold_rule',
|
||||
);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.alertType,
|
||||
AlertTypes.METRICS_BASED_ALERT,
|
||||
);
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -208,3 +208,11 @@ export const ALERTS_VALUES_MAP: Record<AlertTypes, AlertDef> = {
|
||||
[AlertTypes.TRACES_BASED_ALERT]: traceAlertDefaults,
|
||||
[AlertTypes.EXCEPTIONS_BASED_ALERT]: exceptionAlertDefaults,
|
||||
};
|
||||
|
||||
export const ALERT_TYPE_BREADCRUMB_TITLE: Record<AlertTypes, string> = {
|
||||
[AlertTypes.ANOMALY_BASED_ALERT]: 'Anomaly-Based Alert',
|
||||
[AlertTypes.METRICS_BASED_ALERT]: 'Metric-Based Alert',
|
||||
[AlertTypes.LOGS_BASED_ALERT]: 'Log-Based Alert',
|
||||
[AlertTypes.TRACES_BASED_ALERT]: 'Traces-Based Alert',
|
||||
[AlertTypes.EXCEPTIONS_BASED_ALERT]: 'Exceptions-Based Alert',
|
||||
};
|
||||
|
||||
@@ -1,21 +1,34 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Form } from 'antd';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { Form, Tabs, TabsProps } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ConfigureIcon from 'assets/AlertHistory/ConfigureIcon';
|
||||
import AlertBreadcrumb from 'components/AlertBreadcrumb';
|
||||
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import CreateAlertV2 from 'container/CreateAlertV2';
|
||||
import FormAlertRules, { AlertDetectionTypes } from 'container/FormAlertRules';
|
||||
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { AlertListTabs } from 'pages/AlertList/types';
|
||||
import { GalleryVerticalEnd, Pyramid } from '@signozhq/icons';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { AlertDef } from 'types/api/alerts/def';
|
||||
|
||||
import { ALERT_TYPE_VS_SOURCE_MAPPING } from './config';
|
||||
import { ALERTS_VALUES_MAP } from './defaults';
|
||||
import { ALERTS_VALUES_MAP, ALERT_TYPE_BREADCRUMB_TITLE } from './defaults';
|
||||
import SelectAlertType from './SelectAlertType';
|
||||
|
||||
import './CreateAlertRule.styles.scss';
|
||||
|
||||
function CreateRules(): JSX.Element {
|
||||
const [formInstance] = Form.useForm();
|
||||
const compositeQuery = useGetCompositeQueryParam();
|
||||
const queryParams = useUrlQuery();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const ruleTypeFromURL = queryParams.get(QueryParams.ruleType);
|
||||
const alertTypeFromURL = queryParams.get(QueryParams.alertType);
|
||||
@@ -23,6 +36,15 @@ function CreateRules(): JSX.Element {
|
||||
const showClassicCreateAlertsPageFlag =
|
||||
queryParams.get(QueryParams.showClassicCreateAlertsPage) === 'true';
|
||||
|
||||
const isTypeSelectionMode =
|
||||
!alertTypeFromURL && !ruleTypeFromURL && !compositeQuery;
|
||||
|
||||
useEffect(() => {
|
||||
if (isTypeSelectionMode) {
|
||||
logEvent('Alert: New alert data source selection page visited', {});
|
||||
}
|
||||
}, [isTypeSelectionMode]);
|
||||
|
||||
const alertType = useMemo(() => {
|
||||
if (ruleTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT) {
|
||||
return AlertTypes.ANOMALY_BASED_ALERT;
|
||||
@@ -45,22 +67,142 @@ function CreateRules(): JSX.Element {
|
||||
[alertType, version],
|
||||
);
|
||||
|
||||
// Load old alerts flow always for anomaly based alerts and when showClassicCreateAlertsPage is true
|
||||
if (
|
||||
showClassicCreateAlertsPageFlag ||
|
||||
alertType === AlertTypes.ANOMALY_BASED_ALERT
|
||||
) {
|
||||
return (
|
||||
<FormAlertRules
|
||||
alertType={alertType}
|
||||
formInstance={formInstance}
|
||||
initialValue={initialAlertValue}
|
||||
ruleId=""
|
||||
/>
|
||||
);
|
||||
}
|
||||
const handleTabChange = useCallback(
|
||||
(tab: string): void => {
|
||||
queryParams.set('tab', tab);
|
||||
queryParams.delete('subTab');
|
||||
queryParams.delete('search');
|
||||
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${queryParams.toString()}`);
|
||||
},
|
||||
[safeNavigate, queryParams],
|
||||
);
|
||||
|
||||
return <CreateAlertV2 alertType={alertType} />;
|
||||
const handleSelectType = useCallback(
|
||||
(type: AlertTypes, newTab?: boolean): void => {
|
||||
if (type === AlertTypes.ANOMALY_BASED_ALERT) {
|
||||
queryParams.set(
|
||||
QueryParams.ruleType,
|
||||
AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
|
||||
);
|
||||
queryParams.set(QueryParams.alertType, AlertTypes.METRICS_BASED_ALERT);
|
||||
} else {
|
||||
queryParams.set(QueryParams.ruleType, AlertDetectionTypes.THRESHOLD_ALERT);
|
||||
queryParams.set(QueryParams.alertType, type);
|
||||
}
|
||||
|
||||
safeNavigate(`${ROUTES.ALERTS_NEW}?${queryParams.toString()}`, { newTab });
|
||||
},
|
||||
[queryParams, safeNavigate],
|
||||
);
|
||||
|
||||
const alertContent = useMemo(() => {
|
||||
if (isTypeSelectionMode) {
|
||||
return <SelectAlertType onSelect={handleSelectType} />;
|
||||
}
|
||||
|
||||
if (
|
||||
showClassicCreateAlertsPageFlag ||
|
||||
alertType === AlertTypes.ANOMALY_BASED_ALERT
|
||||
) {
|
||||
return (
|
||||
<FormAlertRules
|
||||
alertType={alertType}
|
||||
formInstance={formInstance}
|
||||
initialValue={initialAlertValue}
|
||||
ruleId=""
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <CreateAlertV2 alertType={alertType} />;
|
||||
}, [
|
||||
isTypeSelectionMode,
|
||||
handleSelectType,
|
||||
showClassicCreateAlertsPageFlag,
|
||||
alertType,
|
||||
formInstance,
|
||||
initialAlertValue,
|
||||
]);
|
||||
|
||||
const items: TabsProps['items'] = [
|
||||
{
|
||||
label: (
|
||||
<div className="periscope-tab top-level-tab">
|
||||
<GalleryVerticalEnd size={14} />
|
||||
Triggered Alerts
|
||||
</div>
|
||||
),
|
||||
key: AlertListTabs.TRIGGERED_ALERTS,
|
||||
children: null,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div className="periscope-tab top-level-tab">
|
||||
<Pyramid size={14} />
|
||||
Alert Rules
|
||||
</div>
|
||||
),
|
||||
key: AlertListTabs.ALERT_RULES,
|
||||
children: (
|
||||
<div className="create-alert-wrapper">
|
||||
<AlertBreadcrumb
|
||||
className="create-alert__breadcrumb"
|
||||
items={
|
||||
isTypeSelectionMode
|
||||
? [
|
||||
{
|
||||
title: 'Alert Rules',
|
||||
route: `${ROUTES.LIST_ALL_ALERT}?tab=${AlertListTabs.ALERT_RULES}`,
|
||||
},
|
||||
{ title: 'Select Alert Type', isLast: true },
|
||||
]
|
||||
: [
|
||||
{
|
||||
title: 'Alert Rules',
|
||||
route: `${ROUTES.LIST_ALL_ALERT}?tab=${AlertListTabs.ALERT_RULES}`,
|
||||
},
|
||||
{ title: 'Select Alert Type', route: ROUTES.ALERTS_NEW },
|
||||
{
|
||||
title: ALERT_TYPE_BREADCRUMB_TITLE[alertType],
|
||||
isLast: true,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
{alertContent}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div className="periscope-tab top-level-tab">
|
||||
<ConfigureIcon width={14} height={14} />
|
||||
Configuration
|
||||
</div>
|
||||
),
|
||||
key: AlertListTabs.CONFIGURATION,
|
||||
children: null,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
destroyInactiveTabPane
|
||||
items={items}
|
||||
activeKey={AlertListTabs.ALERT_RULES}
|
||||
onChange={handleTabChange}
|
||||
className="alerts-container create-alert-tabs"
|
||||
tabBarExtraContent={
|
||||
<div className="create-alert-tabs__extra">
|
||||
<DateTimeSelector showAutoRefresh />
|
||||
<HeaderRightSection
|
||||
enableAnnouncements={false}
|
||||
enableShare
|
||||
enableFeedback
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateRules;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { RotateCcw } from '@signozhq/icons';
|
||||
import { useAlertRuleOptional } from 'providers/Alert';
|
||||
import { Labels } from 'types/api/alerts/def';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
@@ -18,6 +19,7 @@ import './styles.scss';
|
||||
|
||||
function CreateAlertHeader(): JSX.Element {
|
||||
const { alertState, setAlertState, isEditMode } = useCreateAlertState();
|
||||
const alertRuleContext = useAlertRuleOptional();
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
@@ -74,9 +76,13 @@ function CreateAlertHeader(): JSX.Element {
|
||||
<Input
|
||||
type="text"
|
||||
value={alertState.name}
|
||||
onChange={(e): void =>
|
||||
setAlertState({ type: 'SET_ALERT_NAME', payload: e.target.value })
|
||||
}
|
||||
onChange={(e): void => {
|
||||
const newName = e.target.value;
|
||||
setAlertState({ type: 'SET_ALERT_NAME', payload: newName });
|
||||
if (isEditMode && alertRuleContext?.setAlertRuleName) {
|
||||
alertRuleContext.setAlertRuleName(newName);
|
||||
}
|
||||
}}
|
||||
className="alert-header__input title"
|
||||
placeholder="Enter alert rule name"
|
||||
data-testid="alert-name-input"
|
||||
|
||||
@@ -20,6 +20,11 @@ import {
|
||||
} from './utils';
|
||||
|
||||
import './styles.scss';
|
||||
import {
|
||||
invalidateGetRuleByID,
|
||||
invalidateListRules,
|
||||
} from 'api/generated/services/rules';
|
||||
import { useQueryClient } from 'react-query';
|
||||
|
||||
function Footer(): JSX.Element {
|
||||
const {
|
||||
@@ -115,6 +120,7 @@ function Footer(): JSX.Element {
|
||||
testAlertRule,
|
||||
]);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const handleSaveAlert = useCallback((): void => {
|
||||
const payload = buildCreateThresholdAlertRulePayload({
|
||||
alertType,
|
||||
@@ -133,6 +139,9 @@ function Footer(): JSX.Element {
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
void invalidateGetRuleByID(queryClient, { id: ruleId });
|
||||
void invalidateListRules(queryClient);
|
||||
|
||||
toast.success('Alert rule updated successfully');
|
||||
safeNavigate('/alerts');
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationS
|
||||
|
||||
import * as createAlertState from '../../context';
|
||||
import Footer from '../Footer';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
|
||||
// Mock the hooks used by Footer component
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
@@ -64,6 +65,12 @@ const mockAlertContextState = createMockAlertContextState({
|
||||
},
|
||||
});
|
||||
|
||||
const WrappedFooter = (): JSX.Element => (
|
||||
<MockQueryClientProvider>
|
||||
<Footer />
|
||||
</MockQueryClientProvider>
|
||||
);
|
||||
|
||||
jest
|
||||
.spyOn(createAlertState, 'useCreateAlertState')
|
||||
.mockReturnValue(mockAlertContextState);
|
||||
@@ -97,20 +104,20 @@ describe('Footer', () => {
|
||||
});
|
||||
|
||||
it('should render the component with 3 buttons', () => {
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
expect(screen.getByText(SAVE_ALERT_RULE_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(TEST_NOTIFICATION_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(DISCARD_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('discard action works correctly', () => {
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
fireEvent.click(screen.getByText(DISCARD_TEXT));
|
||||
expect(mockDiscardAlertRule).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('save alert rule action works correctly', () => {
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
fireEvent.click(screen.getByText(SAVE_ALERT_RULE_TEXT));
|
||||
expect(mockCreateAlertRule).toHaveBeenCalled();
|
||||
});
|
||||
@@ -120,13 +127,13 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isEditMode: true,
|
||||
});
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
fireEvent.click(screen.getByText(SAVE_ALERT_RULE_TEXT));
|
||||
expect(mockUpdateAlertRule).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('test notification action works correctly', () => {
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
fireEvent.click(screen.getByText(TEST_NOTIFICATION_TEXT));
|
||||
expect(mockTestAlertRule).toHaveBeenCalled();
|
||||
});
|
||||
@@ -136,7 +143,7 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isCreatingAlertRule: true,
|
||||
});
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
@@ -152,7 +159,7 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isUpdatingAlertRule: true,
|
||||
});
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
|
||||
// Target the button elements directly instead of the text spans inside them
|
||||
expect(
|
||||
@@ -169,7 +176,7 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isTestingAlertRule: true,
|
||||
});
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
|
||||
// Target the button elements directly instead of the text spans inside them
|
||||
expect(
|
||||
@@ -189,7 +196,7 @@ describe('Footer', () => {
|
||||
name: '',
|
||||
},
|
||||
});
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
@@ -217,7 +224,7 @@ describe('Footer', () => {
|
||||
},
|
||||
});
|
||||
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
@@ -245,7 +252,7 @@ describe('Footer', () => {
|
||||
},
|
||||
});
|
||||
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
@@ -261,7 +268,7 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isTestingAlertRule: true,
|
||||
});
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
|
||||
// When testing alert rule, the play icon is replaced with a loader icon
|
||||
expect(
|
||||
@@ -276,7 +283,7 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isUpdatingAlertRule: true,
|
||||
});
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
|
||||
// When updating alert rule, the check icon is replaced with a loader icon
|
||||
expect(
|
||||
@@ -291,7 +298,7 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isCreatingAlertRule: true,
|
||||
});
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
|
||||
// When creating alert rule, the check icon is replaced with a loader icon
|
||||
expect(
|
||||
|
||||
@@ -38,6 +38,7 @@ import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/map
|
||||
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||
import { isEmpty, isEqual } from 'lodash-es';
|
||||
import Tabs2 from 'periscope/components/Tabs2';
|
||||
import { useAlertRuleOptional } from 'providers/Alert';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -92,7 +93,6 @@ const ALERT_SETUP_GUIDE_URLS: Record<AlertTypes, string> = {
|
||||
'https://signoz.io/docs/alerts-management/anomaly-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
|
||||
};
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function FormAlertRules({
|
||||
alertType,
|
||||
formInstance,
|
||||
@@ -160,6 +160,32 @@ function FormAlertRules({
|
||||
const [alertDef, setAlertDef] = useState<AlertDef>(initialValue);
|
||||
const [yAxisUnit, setYAxisUnit] = useState<string>(currentQuery.unit || '');
|
||||
|
||||
const alertRuleContext = useAlertRuleOptional();
|
||||
const providerAlertName = alertRuleContext?.alertRuleName;
|
||||
useEffect(() => {
|
||||
if (providerAlertName) {
|
||||
setAlertDef((prev) => {
|
||||
if (prev.alert === providerAlertName) {
|
||||
return prev;
|
||||
}
|
||||
return { ...prev, alert: providerAlertName };
|
||||
});
|
||||
formInstance.setFieldsValue({ alert: providerAlertName });
|
||||
}
|
||||
}, [providerAlertName, formInstance]);
|
||||
|
||||
// Wrap setAlertDef to sync alert name to provider when user types
|
||||
const handleSetAlertDef = useCallback(
|
||||
(newDef: AlertDef) => {
|
||||
setAlertDef(newDef);
|
||||
// Sync alert name change to provider for header display
|
||||
if (newDef.alert !== alertDef.alert && alertRuleContext?.setAlertRuleName) {
|
||||
alertRuleContext.setAlertRuleName(newDef.alert);
|
||||
}
|
||||
},
|
||||
[alertDef.alert, alertRuleContext],
|
||||
);
|
||||
|
||||
const alertTypeFromURL = urlQuery.get(QueryParams.ruleType);
|
||||
|
||||
const [detectionMethod, setDetectionMethod] = useState<string | null>(null);
|
||||
@@ -680,7 +706,7 @@ function FormAlertRules({
|
||||
const renderBasicInfo = (): JSX.Element => (
|
||||
<BasicInfo
|
||||
alertDef={alertDef}
|
||||
setAlertDef={setAlertDef}
|
||||
setAlertDef={handleSetAlertDef}
|
||||
isNewRule={isNewRule}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -111,7 +111,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
number: allAlertRules?.length,
|
||||
layout: 'new',
|
||||
});
|
||||
safeNavigate(ROUTES.ALERT_TYPE_SELECTION, {
|
||||
safeNavigate(ROUTES.ALERTS_NEW, {
|
||||
newTab: isModifierKeyPressed(e),
|
||||
});
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@ import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import { ActionItemProps } from './ActionItem';
|
||||
import TableView from './TableView';
|
||||
import { removeEscapeCharacters } from './utils';
|
||||
import { getBodyDisplayString, removeEscapeCharacters } from './utils';
|
||||
|
||||
import './Overview.styles.scss';
|
||||
|
||||
@@ -112,7 +112,7 @@ function Overview({
|
||||
children: (
|
||||
<div className="logs-body-content">
|
||||
<MEditor
|
||||
value={removeEscapeCharacters(logData.body)}
|
||||
value={removeEscapeCharacters(getBodyDisplayString(logData.body))}
|
||||
language="json"
|
||||
options={options}
|
||||
onChange={(): void => {}}
|
||||
|
||||
@@ -106,10 +106,20 @@ function TableView({
|
||||
isListViewPanel,
|
||||
]);
|
||||
|
||||
const flattenLogData: Record<string, string> | null = useMemo(
|
||||
() => (logData ? flattenObject(logData) : null),
|
||||
[logData],
|
||||
);
|
||||
// When USE_JSON_BODY is enabled, body arrives as a pre-parsed object. Serialize it
|
||||
// back to a string so flattenObject keeps `body` as a single table row instead of
|
||||
// recursively expanding it into dotted sub-keys (body.message, body.foo.bar, …),
|
||||
// which would break the tree view in BodyContent that relies on record.field === 'body'.
|
||||
const flattenLogData: Record<string, string> | null = useMemo(() => {
|
||||
if (!logData) {
|
||||
return null;
|
||||
}
|
||||
const normalizedLog =
|
||||
typeof logData.body === 'object' && logData.body !== null
|
||||
? { ...logData, body: JSON.stringify(logData.body) }
|
||||
: logData;
|
||||
return flattenObject(normalizedLog);
|
||||
}, [logData]);
|
||||
|
||||
const handleClick = (
|
||||
operator: string,
|
||||
|
||||
@@ -10,7 +10,7 @@ const MAX_BODY_BYTES = 100 * 1024; // 100 KB
|
||||
|
||||
// Hook for async JSON processing
|
||||
const useAsyncJSONProcessing = (
|
||||
value: string,
|
||||
value: string | Record<string, unknown>,
|
||||
shouldProcess: boolean,
|
||||
handleChangeSelectedView?: ChangeViewFunctionType,
|
||||
): {
|
||||
@@ -40,11 +40,17 @@ const useAsyncJSONProcessing = (
|
||||
return (): void => {};
|
||||
}
|
||||
|
||||
// Avoid processing if the json is too large
|
||||
const byteSize = new Blob([value]).size;
|
||||
if (byteSize > MAX_BODY_BYTES) {
|
||||
return (): void => {};
|
||||
}
|
||||
// When value is already a parsed object skip the size check and JSON parsing
|
||||
const parseBody = (): Record<string, unknown> | null => {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
const byteSize = new Blob([value as string]).size;
|
||||
if (byteSize > MAX_BODY_BYTES) {
|
||||
return null;
|
||||
}
|
||||
return recursiveParseJSON(value as string);
|
||||
};
|
||||
|
||||
processingRef.current = true;
|
||||
setJsonState({ isLoading: true, treeData: null, error: null });
|
||||
@@ -53,8 +59,8 @@ const useAsyncJSONProcessing = (
|
||||
const processAsync = (): void => {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const parsedBody = recursiveParseJSON(value);
|
||||
if (!isEmpty(parsedBody)) {
|
||||
const parsedBody = parseBody();
|
||||
if (parsedBody && !isEmpty(parsedBody)) {
|
||||
const treeData = jsonToDataNodes(parsedBody, {
|
||||
isBodyJsonQueryEnabled,
|
||||
handleChangeSelectedView,
|
||||
@@ -82,8 +88,8 @@ const useAsyncJSONProcessing = (
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
(): void => {
|
||||
try {
|
||||
const parsedBody = recursiveParseJSON(value);
|
||||
if (!isEmpty(parsedBody)) {
|
||||
const parsedBody = parseBody();
|
||||
if (parsedBody && !isEmpty(parsedBody)) {
|
||||
const treeData = jsonToDataNodes(parsedBody, {
|
||||
isBodyJsonQueryEnabled,
|
||||
handleChangeSelectedView,
|
||||
|
||||
@@ -4,7 +4,11 @@ import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||
import dompurify from 'dompurify';
|
||||
import { uniqueId } from 'lodash-es';
|
||||
import { ILog, ILogAggregateAttributesResources } from 'types/api/logs/log';
|
||||
import {
|
||||
ILog,
|
||||
ILogAggregateAttributesResources,
|
||||
ILogBody,
|
||||
} from 'types/api/logs/log';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { FORBID_DOM_PURIFY_ATTR, FORBID_DOM_PURIFY_TAGS } from 'utils/app';
|
||||
|
||||
@@ -433,3 +437,8 @@ export const getSanitizedLogBody = (
|
||||
return '{}';
|
||||
}
|
||||
};
|
||||
|
||||
// Returns a plain string for display contexts (Monaco editor, table cells, raw log row).
|
||||
export function getBodyDisplayString(body: string | ILogBody): string {
|
||||
return typeof body === 'string' ? body : JSON.stringify(body as ILogBody);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
.new-explorer-cta {
|
||||
display: flex;
|
||||
.new-explorer-cta-with-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--muted-foreground);
|
||||
|
||||
/* Bifrost (Ancient)/Content/sm */
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
.ant-btn-icon {
|
||||
margin-inline-end: 0px;
|
||||
}
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
export const RIBBON_STYLES = {
|
||||
top: '-0.75rem',
|
||||
};
|
||||
|
||||
export const buttonText: Record<string, string> = {
|
||||
[ROUTES.LOGS_EXPLORER]: 'Old Explorer',
|
||||
[ROUTES.TRACE]: 'New Explorer',
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Badge, Button } from 'antd';
|
||||
import { Button } from 'antd';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { Undo } from '@signozhq/icons';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
|
||||
import { buttonText, RIBBON_STYLES } from './config';
|
||||
import { buttonText } from './config';
|
||||
|
||||
import './NewExplorerCTA.styles.scss';
|
||||
|
||||
@@ -70,9 +71,12 @@ function NewExplorerCTA(): JSX.Element | null {
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge.Ribbon style={RIBBON_STYLES} text="New">
|
||||
<span className="new-explorer-cta-with-badge">
|
||||
{button}
|
||||
</Badge.Ribbon>
|
||||
<Badge color="robin" variant="default">
|
||||
New
|
||||
</Badge>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Expand } from '@signozhq/icons';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { getBodyDisplayString } from 'container/LogDetailedView/utils';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
@@ -26,7 +27,9 @@ function LogsList({ logs }: LogsListProps): JSX.Element {
|
||||
DATE_TIME_FORMATS.UTC_MONTH_SHORT,
|
||||
)}
|
||||
</div>
|
||||
<div className="logs-preview-list-item-body">{log.body}</div>
|
||||
<div className="logs-preview-list-item-body">
|
||||
{getBodyDisplayString(log.body)}
|
||||
</div>
|
||||
<div
|
||||
className="logs-preview-list-item-expand"
|
||||
onClick={makeLogDetailsHandler(log)}
|
||||
|
||||
@@ -311,7 +311,7 @@ export function PlannedDowntimeForm(
|
||||
default:
|
||||
return `Scheduled for ${formattedStartDate} starting at ${formattedStartTime}.`;
|
||||
}
|
||||
}, [formData, recurrenceType, timezone]);
|
||||
}, [formData, recurrenceType]);
|
||||
|
||||
const endTimeText = useMemo((): string => {
|
||||
const endTime = formData.endTime;
|
||||
@@ -322,7 +322,7 @@ export function PlannedDowntimeForm(
|
||||
const formattedEndTime = endTime.format(TIME_FORMAT);
|
||||
const formattedEndDate = endTime.format(DATE_FORMAT);
|
||||
return `Scheduled to end maintenance on ${formattedEndDate} at ${formattedEndTime}.`;
|
||||
}, [formData, recurrenceType, timezone]);
|
||||
}, [formData, recurrenceType]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
||||
@@ -33,14 +33,12 @@ function TopNav(): JSX.Element | null {
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
const isNewAlertsLandingPage = useMemo(
|
||||
() =>
|
||||
matchPath(location.pathname, { path: ROUTES.ALERTS_NEW, exact: true }) &&
|
||||
!location.search,
|
||||
[location.pathname, location.search],
|
||||
const isAlertCreationPage = useMemo(
|
||||
() => matchPath(location.pathname, { path: ROUTES.ALERTS_NEW, exact: true }),
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
if (isSignUpPage || isDisabled || isRouteToSkip || isNewAlertsLandingPage) {
|
||||
if (isSignUpPage || isDisabled || isRouteToSkip || isAlertCreationPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
}
|
||||
|
||||
.alert-details {
|
||||
margin-top: 10px;
|
||||
.divider {
|
||||
border-color: var(--l1-border);
|
||||
margin: 16px 0;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Breadcrumb, Button, Divider } from 'antd';
|
||||
import { Divider } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import classNames from 'classnames';
|
||||
import AlertBreadcrumb from 'components/AlertBreadcrumb';
|
||||
import { Filters } from 'components/AlertDetailsFilters/Filters';
|
||||
import RouteTab from 'components/RouteTab';
|
||||
import Spinner from 'components/Spinner';
|
||||
@@ -10,13 +11,12 @@ import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { CreateAlertProvider } from 'container/CreateAlertV2/context';
|
||||
import { getCreateAlertLocalStateFromAlertDef } from 'container/CreateAlertV2/utils';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
import { useAlertRule } from 'providers/Alert';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { NEW_ALERT_SCHEMA_VERSION } from 'types/api/alerts/alertTypesV2';
|
||||
import { fromRuleDTOToPostableRuleV2 } from 'types/api/alerts/convert';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
|
||||
import AlertHeader from './AlertHeader/AlertHeader';
|
||||
import AlertNotFound from './AlertNotFound';
|
||||
@@ -24,42 +24,11 @@ import { useGetAlertRuleDetails, useRouteTabUtils } from './hooks';
|
||||
|
||||
import './AlertDetails.styles.scss';
|
||||
|
||||
function BreadCrumbItem({
|
||||
title,
|
||||
isLast,
|
||||
route,
|
||||
}: {
|
||||
title: string | null;
|
||||
isLast?: boolean;
|
||||
route?: string;
|
||||
}): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
if (isLast) {
|
||||
return <div className="breadcrumb-item breadcrumb-item--last">{title}</div>;
|
||||
}
|
||||
const handleNavigate = (e: React.MouseEvent): void => {
|
||||
if (!route) {
|
||||
return;
|
||||
}
|
||||
safeNavigate(ROUTES.LIST_ALL_ALERT, { newTab: isModifierKeyPressed(e) });
|
||||
};
|
||||
|
||||
return (
|
||||
<Button type="text" className="breadcrumb-item" onClick={handleNavigate}>
|
||||
{title}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
BreadCrumbItem.defaultProps = {
|
||||
isLast: false,
|
||||
route: '',
|
||||
};
|
||||
|
||||
function AlertDetails(): JSX.Element {
|
||||
const { pathname } = useLocation();
|
||||
const { routes } = useRouteTabUtils();
|
||||
const params = useUrlQuery();
|
||||
const { alertRuleName } = useAlertRule();
|
||||
|
||||
const { isLoading, isError, ruleId, isValidRuleId, alertDetailsResponse } =
|
||||
useGetAlertRuleDetails();
|
||||
@@ -69,7 +38,7 @@ function AlertDetails(): JSX.Element {
|
||||
}, [params]);
|
||||
|
||||
const getDocumentTitle = useMemo(() => {
|
||||
const alertTitle = alertDetailsResponse?.data?.alert;
|
||||
const alertTitle = alertRuleName ?? alertDetailsResponse?.data?.alert;
|
||||
if (alertTitle) {
|
||||
return alertTitle;
|
||||
}
|
||||
@@ -80,7 +49,7 @@ function AlertDetails(): JSX.Element {
|
||||
return document.title;
|
||||
}
|
||||
return 'Alert Not Found';
|
||||
}, [alertDetailsResponse?.data?.alert, isTestAlert, isLoading]);
|
||||
}, [alertRuleName, alertDetailsResponse?.data?.alert, isTestAlert, isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = getDocumentTitle;
|
||||
@@ -126,20 +95,13 @@ function AlertDetails(): JSX.Element {
|
||||
<div
|
||||
className={classNames('alert-details', { 'alert-details-v2': isV2Alert })}
|
||||
>
|
||||
<Breadcrumb
|
||||
<AlertBreadcrumb
|
||||
className="alert-details__breadcrumb"
|
||||
items={[
|
||||
{
|
||||
title: (
|
||||
<BreadCrumbItem title="Alert Rules" route={ROUTES.LIST_ALL_ALERT} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: <BreadCrumbItem title={ruleId} isLast />,
|
||||
},
|
||||
{ title: 'Alert Rules', route: ROUTES.LIST_ALL_ALERT },
|
||||
{ title: ruleId, isLast: true },
|
||||
]}
|
||||
/>
|
||||
<Divider className="divider breadcrumb-divider" />
|
||||
|
||||
{alertRuleDetails && <AlertHeader alertDetails={alertRuleDetails} />}
|
||||
<Divider className="divider" />
|
||||
|
||||
@@ -33,13 +33,12 @@ const menuItemStyleV2: CSSProperties = {
|
||||
function AlertActionButtons({
|
||||
ruleId,
|
||||
alertDetails,
|
||||
setUpdatedName,
|
||||
}: {
|
||||
ruleId: string;
|
||||
alertDetails: AlertHeaderProps['alertDetails'];
|
||||
setUpdatedName: (name: string) => void;
|
||||
}): JSX.Element {
|
||||
const { alertRuleState, setAlertRuleState } = useAlertRule();
|
||||
const { alertRuleState, setAlertRuleState, alertRuleName, setAlertRuleName } =
|
||||
useAlertRule();
|
||||
const [intermediateName, setIntermediateName] = useState<string>(
|
||||
alertDetails.alert,
|
||||
);
|
||||
@@ -53,7 +52,7 @@ function AlertActionButtons({
|
||||
const { handleAlertDelete } = useAlertRuleDelete({ ruleId });
|
||||
const { handleAlertUpdate, isLoading } = useAlertRuleUpdate({
|
||||
alertDetails: alertDetails as unknown as AlertDef,
|
||||
setUpdatedName,
|
||||
setAlertRuleName,
|
||||
intermediateName,
|
||||
});
|
||||
|
||||
@@ -113,6 +112,12 @@ function AlertActionButtons({
|
||||
}
|
||||
}, [setAlertRuleState, alertRuleState, alertDetails.state]);
|
||||
|
||||
useEffect(() => {
|
||||
if (alertRuleName !== undefined) {
|
||||
setIntermediateName(alertRuleName);
|
||||
}
|
||||
}, [alertRuleName]);
|
||||
|
||||
// on unmount remove the alert state
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(() => (): void => setAlertRuleState(undefined), []);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import type { RuletypesRuleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import CreateAlertV2Header from 'container/CreateAlertV2/CreateAlertHeader';
|
||||
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
|
||||
@@ -20,8 +20,17 @@ export type AlertHeaderProps = {
|
||||
};
|
||||
function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
|
||||
const { state, alert: alertName, labels } = alertDetails;
|
||||
const { alertRuleState } = useAlertRule();
|
||||
const [updatedName, setUpdatedName] = useState(alertName);
|
||||
const { alertRuleState, alertRuleName, setAlertRuleName } = useAlertRule();
|
||||
|
||||
useEffect(() => {
|
||||
if (alertRuleName === undefined && alertName) {
|
||||
setAlertRuleName(alertName);
|
||||
}
|
||||
}, [alertRuleName, alertName, setAlertRuleName]);
|
||||
|
||||
useEffect(() => (): void => setAlertRuleName(undefined), [setAlertRuleName]);
|
||||
|
||||
const displayName = alertRuleName ?? alertName;
|
||||
|
||||
const labelsWithoutSeverity = useMemo(() => {
|
||||
if (labels) {
|
||||
@@ -40,7 +49,7 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
|
||||
<div className="alert-title-wrapper">
|
||||
<AlertState state={alertRuleState ?? state ?? ''} />
|
||||
<div className="alert-title">
|
||||
<LineClampedText text={updatedName || alertName} />
|
||||
<LineClampedText text={displayName || ''} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -64,7 +73,6 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
|
||||
<AlertActionButtons
|
||||
alertDetails={alertDetails}
|
||||
ruleId={alertDetails?.id || ''}
|
||||
setUpdatedName={setUpdatedName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,9 @@ import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import {
|
||||
createRule,
|
||||
deleteRuleByID,
|
||||
getGetRuleByIDQueryKey,
|
||||
invalidateGetRuleByID,
|
||||
invalidateListRules,
|
||||
updateRuleByID,
|
||||
useGetRuleByID,
|
||||
useListRules,
|
||||
@@ -490,11 +492,11 @@ export const useAlertRuleDuplicate = ({
|
||||
};
|
||||
export const useAlertRuleUpdate = ({
|
||||
alertDetails,
|
||||
setUpdatedName,
|
||||
setAlertRuleName,
|
||||
intermediateName,
|
||||
}: {
|
||||
alertDetails: AlertDef;
|
||||
setUpdatedName: (name: string) => void;
|
||||
setAlertRuleName: (name: string | undefined) => void;
|
||||
intermediateName: string;
|
||||
}): {
|
||||
handleAlertUpdate: () => void;
|
||||
@@ -502,17 +504,29 @@ export const useAlertRuleUpdate = ({
|
||||
} => {
|
||||
const { notifications } = useNotifications();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: updateAlertRule, isLoading } = useMutation(
|
||||
[REACT_QUERY_KEY.UPDATE_ALERT_RULE, alertDetails.id],
|
||||
(args: { data: AlertDef; id: string }) =>
|
||||
updateRuleByID({ id: args.id }, toPostableRuleDTOFromAlertDef(args.data)),
|
||||
{
|
||||
onMutate: () => setUpdatedName(intermediateName),
|
||||
onSuccess: () =>
|
||||
notifications.success({ message: 'Alert renamed successfully' }),
|
||||
onMutate: () => setAlertRuleName(intermediateName),
|
||||
onSuccess: () => {
|
||||
const ruleId = alertDetails.id || '';
|
||||
const ruleQueryKey = getGetRuleByIDQueryKey({ id: ruleId });
|
||||
const existingRule = queryClient.getQueryData<GetRuleByID200>(ruleQueryKey);
|
||||
if (existingRule) {
|
||||
queryClient.setQueryData<GetRuleByID200>(ruleQueryKey, {
|
||||
...existingRule,
|
||||
data: { ...existingRule.data, alert: intermediateName },
|
||||
});
|
||||
}
|
||||
void invalidateListRules(queryClient);
|
||||
notifications.success({ message: 'Alert renamed successfully' });
|
||||
},
|
||||
onError: (error) => {
|
||||
setUpdatedName(alertDetails.alert);
|
||||
setAlertRuleName(alertDetails.alert);
|
||||
showErrorModal(
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) as APIError,
|
||||
);
|
||||
@@ -551,7 +565,6 @@ export const useAlertRuleDelete = ({
|
||||
|
||||
history.push(ROUTES.LIST_ALL_ALERT);
|
||||
},
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
onError: (error) =>
|
||||
showErrorModal(
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) as APIError,
|
||||
|
||||
@@ -1,15 +1,44 @@
|
||||
.alerts-container {
|
||||
.ant-tabs-nav {
|
||||
padding: 0 8px;
|
||||
.top-level-tab.periscope-tab {
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.ant-tabs {
|
||||
&-nav {
|
||||
padding: 0 8px;
|
||||
margin-bottom: 0 !important;
|
||||
|
||||
&::before {
|
||||
border-bottom: 1px solid var(--l1-border) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&-tab {
|
||||
&[data-node-key='TriggeredAlerts'] {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-left: 24px !important;
|
||||
}
|
||||
|
||||
[aria-selected='false'] {
|
||||
.periscope-tab {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.configuration-tabs {
|
||||
margin-top: -16px;
|
||||
|
||||
.ant-tabs-nav {
|
||||
.ant-tabs-nav-wrap {
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alert-rules-container {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { Row } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import SelectAlertType from 'container/CreateAlertRule/SelectAlertType';
|
||||
import { AlertDetectionTypes } from 'container/FormAlertRules';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
function AlertTypeSelectionPage(): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const queryParams = useUrlQuery();
|
||||
|
||||
useEffect(() => {
|
||||
logEvent('Alert: New alert data source selection page visited', {});
|
||||
}, []);
|
||||
|
||||
const handleSelectType = useCallback(
|
||||
(type: AlertTypes, newTab?: boolean): void => {
|
||||
// For anamoly based alert, we need to set the ruleType to anomaly_rule
|
||||
// and alertType to metrics_based_alert
|
||||
if (type === AlertTypes.ANOMALY_BASED_ALERT) {
|
||||
queryParams.set(
|
||||
QueryParams.ruleType,
|
||||
AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
|
||||
);
|
||||
queryParams.set(QueryParams.alertType, AlertTypes.METRICS_BASED_ALERT);
|
||||
// For other alerts, we need to set the ruleType to threshold_rule
|
||||
// and alertType to the selected type
|
||||
} else {
|
||||
queryParams.set(QueryParams.ruleType, AlertDetectionTypes.THRESHOLD_ALERT);
|
||||
queryParams.set(QueryParams.alertType, type);
|
||||
}
|
||||
|
||||
const showClassicCreateAlertsPageFlag = queryParams.get(
|
||||
QueryParams.showClassicCreateAlertsPage,
|
||||
);
|
||||
if (showClassicCreateAlertsPageFlag === 'true') {
|
||||
queryParams.set(QueryParams.showClassicCreateAlertsPage, 'true');
|
||||
}
|
||||
|
||||
safeNavigate(`${ROUTES.ALERTS_NEW}?${queryParams.toString()}`, { newTab });
|
||||
},
|
||||
[queryParams, safeNavigate],
|
||||
);
|
||||
|
||||
return (
|
||||
<Row wrap={false}>
|
||||
<SelectAlertType onSelect={handleSelectType} />
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export default AlertTypeSelectionPage;
|
||||
@@ -1,189 +0,0 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { AlertDetectionTypes } from 'container/FormAlertRules';
|
||||
import { getAppContextMockState } from 'container/RoutingPolicies/__tests__/testUtils';
|
||||
import * as navigateHooks from 'hooks/useSafeNavigate';
|
||||
import * as useUrlQueryHooks from 'hooks/useUrlQuery';
|
||||
import * as appHooks from 'providers/App/App';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import AlertTypeSelection from '../AlertTypeSelection';
|
||||
|
||||
const useUrlQuerySpy = jest.spyOn(useUrlQueryHooks, 'default');
|
||||
const useSafeNavigateSpy = jest.spyOn(navigateHooks, 'useSafeNavigate');
|
||||
const useAppContextSpy = jest.spyOn(appHooks, 'useAppContext');
|
||||
|
||||
const mockSetUrlQuery = jest.fn();
|
||||
const mockSafeNavigate = jest.fn();
|
||||
const mockToString = jest.fn();
|
||||
const mockGetUrlQuery = jest.fn();
|
||||
|
||||
describe('AlertTypeSelection', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useAppContextSpy.mockReturnValue(getAppContextMockState());
|
||||
useUrlQuerySpy.mockReturnValue({
|
||||
set: mockSetUrlQuery,
|
||||
toString: mockToString,
|
||||
get: mockGetUrlQuery,
|
||||
} as Partial<URLSearchParams> as URLSearchParams);
|
||||
useSafeNavigateSpy.mockReturnValue({
|
||||
safeNavigate: mockSafeNavigate,
|
||||
});
|
||||
});
|
||||
|
||||
it('should render all alert type options when anomaly detection is enabled', () => {
|
||||
useAppContextSpy.mockReturnValue({
|
||||
...getAppContextMockState({}),
|
||||
featureFlags: [
|
||||
{
|
||||
name: FeatureKeys.ANOMALY_DETECTION,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<AlertTypeSelection />);
|
||||
|
||||
expect(screen.getByText('metric_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('log_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('traces_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('exceptions_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('anomaly_based_alert')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render all alert type options except anomaly based alert when anomaly detection is disabled', () => {
|
||||
render(<AlertTypeSelection />);
|
||||
|
||||
expect(screen.getByText('metric_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('log_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('traces_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('exceptions_based_alert')).toBeInTheDocument();
|
||||
expect(screen.queryByText('anomaly_based_alert')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should navigate to metrics based alert with correct params', () => {
|
||||
render(<AlertTypeSelection />);
|
||||
fireEvent.click(screen.getByText('metric_based_alert'));
|
||||
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledTimes(2);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.ruleType,
|
||||
AlertDetectionTypes.THRESHOLD_ALERT,
|
||||
);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.alertType,
|
||||
AlertTypes.METRICS_BASED_ALERT,
|
||||
);
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should navigate to anomaly based alert with correct params', () => {
|
||||
useAppContextSpy.mockReturnValue({
|
||||
...getAppContextMockState({}),
|
||||
featureFlags: [
|
||||
{
|
||||
name: FeatureKeys.ANOMALY_DETECTION,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<AlertTypeSelection />);
|
||||
fireEvent.click(screen.getByText('anomaly_based_alert'));
|
||||
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledTimes(2);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.ruleType,
|
||||
AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
|
||||
);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.alertType,
|
||||
AlertTypes.METRICS_BASED_ALERT,
|
||||
);
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should navigate to log based alert with correct params', () => {
|
||||
render(<AlertTypeSelection />);
|
||||
fireEvent.click(screen.getByText('log_based_alert'));
|
||||
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledTimes(2);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.ruleType,
|
||||
AlertDetectionTypes.THRESHOLD_ALERT,
|
||||
);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.alertType,
|
||||
AlertTypes.LOGS_BASED_ALERT,
|
||||
);
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should navigate to traces based alert with correct params', () => {
|
||||
render(<AlertTypeSelection />);
|
||||
fireEvent.click(screen.getByText('traces_based_alert'));
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledTimes(2);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.ruleType,
|
||||
AlertDetectionTypes.THRESHOLD_ALERT,
|
||||
);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.alertType,
|
||||
AlertTypes.TRACES_BASED_ALERT,
|
||||
);
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should navigate to exceptions based alert with correct params', () => {
|
||||
render(<AlertTypeSelection />);
|
||||
fireEvent.click(screen.getByText('exceptions_based_alert'));
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledTimes(2);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.ruleType,
|
||||
AlertDetectionTypes.THRESHOLD_ALERT,
|
||||
);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.alertType,
|
||||
AlertTypes.EXCEPTIONS_BASED_ALERT,
|
||||
);
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should navigate to classic create alerts page with correct params if showClassicCreateAlertsPage is true', () => {
|
||||
useUrlQuerySpy.mockReturnValue({
|
||||
set: mockSetUrlQuery,
|
||||
toString: mockToString,
|
||||
get: mockGetUrlQuery.mockImplementation((key: string) => {
|
||||
if (key === QueryParams.showClassicCreateAlertsPage) {
|
||||
return 'true';
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
} as Partial<URLSearchParams> as URLSearchParams);
|
||||
|
||||
render(<AlertTypeSelection />);
|
||||
fireEvent.click(screen.getByText('metric_based_alert'));
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledTimes(3);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.ruleType,
|
||||
AlertDetectionTypes.THRESHOLD_ALERT,
|
||||
);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.alertType,
|
||||
AlertTypes.METRICS_BASED_ALERT,
|
||||
);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.showClassicCreateAlertsPage,
|
||||
'true',
|
||||
);
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
import AlertTypeSelectionPage from './AlertTypeSelection';
|
||||
|
||||
export default AlertTypeSelectionPage;
|
||||
@@ -526,7 +526,7 @@ function SpanDetailsPanel({
|
||||
|
||||
const PANEL_WIDTH = 500;
|
||||
const PANEL_MARGIN_RIGHT = 20;
|
||||
const PANEL_MARGIN_TOP = 25;
|
||||
const PANEL_MARGIN_TOP = 50;
|
||||
const PANEL_MARGIN_BOTTOM = 25;
|
||||
|
||||
const content = (
|
||||
|
||||
@@ -580,10 +580,9 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
}
|
||||
return next;
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Backend mode: trigger API call (current behavior)
|
||||
// keeping this for both mode to support scroll to view to function well.
|
||||
// interestedspan would not make api call in frontend mode so it is safe to use for both mode.
|
||||
// Backend mode: trigger refetch via interestedSpanId
|
||||
setInterestedSpanId({
|
||||
spanId,
|
||||
isUncollapsed: !collapse,
|
||||
@@ -782,19 +781,26 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
[],
|
||||
);
|
||||
|
||||
// Backend mode: scroll + select to the interestedSpanId target. `spans` in
|
||||
// deps so we retry once a refetch lands (chevron / pagination / deep-link).
|
||||
useEffect(() => {
|
||||
if (interestedSpanId.spanId !== '') {
|
||||
const idx = spans.findIndex(
|
||||
(span) => span.span_id === interestedSpanId.spanId,
|
||||
);
|
||||
if (idx !== -1) {
|
||||
scrollSpanIntoView(spans[idx], spans);
|
||||
setSelectedSpan(spans[idx]);
|
||||
}
|
||||
} else {
|
||||
setSelectedSpan((prev) => prev ?? spans[0]);
|
||||
if (isFullDataLoaded || interestedSpanId.spanId === '') {
|
||||
return;
|
||||
}
|
||||
}, [interestedSpanId, setSelectedSpan, spans, scrollSpanIntoView]);
|
||||
const idx = spans.findIndex(
|
||||
(span) => span.span_id === interestedSpanId.spanId,
|
||||
);
|
||||
if (idx !== -1) {
|
||||
scrollSpanIntoView(spans[idx], spans);
|
||||
setSelectedSpan(spans[idx]);
|
||||
}
|
||||
}, [
|
||||
interestedSpanId,
|
||||
setSelectedSpan,
|
||||
spans,
|
||||
scrollSpanIntoView,
|
||||
isFullDataLoaded,
|
||||
]);
|
||||
|
||||
// Covers URL-driven navigation to an already-loaded span (flamegraph /
|
||||
// filter / browser back) that the interestedSpanId-keyed effect doesn't see.
|
||||
|
||||
@@ -199,10 +199,12 @@ const mockSpans = [
|
||||
createMockSpan('span-3', 1),
|
||||
];
|
||||
|
||||
// Shared TestComponent for all tests
|
||||
// Shared TestComponent for all tests. Default selectedSpan to the root mirrors
|
||||
// what TraceDetailsV3's deep-link one-shot effect does when there's no spanId
|
||||
// in the URL — Success no longer owns that default itself.
|
||||
function TestComponent(): JSX.Element {
|
||||
const [selectedSpan, setSelectedSpan] = React.useState<SpanV3 | undefined>(
|
||||
undefined,
|
||||
mockSpans[0],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -75,6 +75,7 @@ function TraceDetailsV3(): JSX.Element {
|
||||
});
|
||||
|
||||
const allSpansRef = useRef<SpanV3[]>([]);
|
||||
const deepLinkResolvedRef = useRef(false);
|
||||
|
||||
// Refetch only when the URL target isn't already loaded. Keeps row clicks
|
||||
// and other in-window URL navigation from triggering a backend window slide.
|
||||
@@ -175,12 +176,36 @@ function TraceDetailsV3(): JSX.Element {
|
||||
}
|
||||
}, [traceData, isFullDataLoaded]);
|
||||
|
||||
// Frontend mode: auto-expand ancestors of the selected span so it becomes visible
|
||||
// Tracks whether we've already done the initial URL→selectedSpan handoff
|
||||
//Lets `interestedSpanId` stay purely as the refetch trigger in frontend mode.
|
||||
useEffect(() => {
|
||||
if (!isFullDataLoaded || !interestedSpanId.spanId || allSpans.length === 0) {
|
||||
if (deepLinkResolvedRef.current) {
|
||||
return;
|
||||
}
|
||||
const ancestors = getAncestorSpanIds(allSpans, interestedSpanId.spanId);
|
||||
if (allSpans.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (selectedSpanId) {
|
||||
const span = allSpans.find((s) => s.span_id === selectedSpanId);
|
||||
if (!span) {
|
||||
// Span not in the current window — wait for more data (backend
|
||||
// pagination) before marking resolved.
|
||||
return;
|
||||
}
|
||||
setSelectedSpan(span);
|
||||
} else {
|
||||
setSelectedSpan((prev) => prev ?? allSpans[0]);
|
||||
}
|
||||
deepLinkResolvedRef.current = true;
|
||||
}, [selectedSpanId, allSpans]);
|
||||
|
||||
// Frontend mode: auto-expand ancestors of the URL-targeted span so it's
|
||||
// visible. Keyed on URL `spanId`(selectedSpanId).
|
||||
useEffect(() => {
|
||||
if (!isFullDataLoaded || !selectedSpanId || allSpans.length === 0) {
|
||||
return;
|
||||
}
|
||||
const ancestors = getAncestorSpanIds(allSpans, selectedSpanId);
|
||||
if (ancestors.size === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -203,7 +228,7 @@ function TraceDetailsV3(): JSX.Element {
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, [isFullDataLoaded, interestedSpanId.spanId, allSpans]);
|
||||
}, [isFullDataLoaded, selectedSpanId, allSpans]);
|
||||
|
||||
const [activeKeys, setActiveKeys] = useState<string[]>(['flame', 'waterfall']);
|
||||
|
||||
@@ -217,7 +242,7 @@ function TraceDetailsV3(): JSX.Element {
|
||||
() =>
|
||||
(getLocalStorageKey(
|
||||
LOCALSTORAGE.TRACE_DETAILS_SPAN_DETAILS_POSITION,
|
||||
) as SpanDetailVariant) || SpanDetailVariant.DOCKED,
|
||||
) as SpanDetailVariant) || SpanDetailVariant.DIALOG,
|
||||
);
|
||||
|
||||
const handleVariantChange = useCallback(
|
||||
|
||||
@@ -9,6 +9,8 @@ import React, {
|
||||
interface AlertRuleContextType {
|
||||
alertRuleState: string | undefined;
|
||||
setAlertRuleState: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
alertRuleName: string | undefined;
|
||||
setAlertRuleName: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
}
|
||||
|
||||
const AlertRuleContext = createContext<AlertRuleContextType | undefined>(
|
||||
@@ -23,13 +25,18 @@ function AlertRuleProvider({
|
||||
const [alertRuleState, setAlertRuleState] = useState<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [alertRuleName, setAlertRuleName] = useState<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ({
|
||||
alertRuleState,
|
||||
setAlertRuleState,
|
||||
alertRuleName,
|
||||
setAlertRuleName,
|
||||
}),
|
||||
[alertRuleState],
|
||||
[alertRuleState, alertRuleName],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -47,4 +54,7 @@ export const useAlertRule = (): AlertRuleContextType => {
|
||||
return context;
|
||||
};
|
||||
|
||||
export const useAlertRuleOptional = (): AlertRuleContextType | undefined =>
|
||||
useContext(AlertRuleContext);
|
||||
|
||||
export default AlertRuleProvider;
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
export interface ILogBody {
|
||||
message?: string | null;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface ILog {
|
||||
date: string;
|
||||
timestamp: number | string;
|
||||
@@ -8,7 +13,7 @@ export interface ILog {
|
||||
traceFlags: number;
|
||||
severityText: string;
|
||||
severityNumber: number;
|
||||
body: string;
|
||||
body: string | ILogBody;
|
||||
resources_string: Record<string, never>;
|
||||
scope_string: Record<string, never>;
|
||||
attributesString: Record<string, never>;
|
||||
|
||||
@@ -132,7 +132,6 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
||||
METER: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
METER_EXPLORER_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
PUBLIC_DASHBOARD: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
ALERT_TYPE_SELECTION: ['ADMIN', 'EDITOR'],
|
||||
AI_ASSISTANT: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
AI_ASSISTANT_ICON_PREVIEW: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
MCP_SERVER: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/statsreporter"
|
||||
citypes "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
@@ -43,23 +42,14 @@ type Module interface {
|
||||
GetService(ctx context.Context, orgID valuer.UUID, serviceID citypes.ServiceID, provider citypes.CloudProviderType, integrationID valuer.UUID) (*citypes.Service, error)
|
||||
|
||||
// CreateService creates a new service for a cloud integration account.
|
||||
CreateService(ctx context.Context, orgID valuer.UUID, service *citypes.CloudIntegrationService, provider citypes.CloudProviderType) error
|
||||
CreateService(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, service *citypes.CloudIntegrationService, provider citypes.CloudProviderType) error
|
||||
|
||||
// UpdateService updates cloud integration service
|
||||
UpdateService(ctx context.Context, orgID valuer.UUID, service *citypes.CloudIntegrationService, provider citypes.CloudProviderType) error
|
||||
UpdateService(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, service *citypes.CloudIntegrationService, provider citypes.CloudProviderType) error
|
||||
|
||||
// AgentCheckIn is called by agent to send heartbeat and get latest config in response.
|
||||
AgentCheckIn(ctx context.Context, orgID valuer.UUID, provider citypes.CloudProviderType, req *citypes.AgentCheckInRequest) (*citypes.AgentCheckInResponse, error)
|
||||
|
||||
// GetDashboardByID returns dashboard JSON for a given dashboard id.
|
||||
// this only returns the dashboard when the service (embedded in dashboard id) is enabled
|
||||
// in the org for any cloud integration account
|
||||
GetDashboardByID(ctx context.Context, orgID valuer.UUID, id string) (*dashboardtypes.Dashboard, error)
|
||||
|
||||
// ListDashboards returns list of dashboards across all connected cloud integration accounts
|
||||
// for enabled services in the org. This list gets added to dashboard list page
|
||||
ListDashboards(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error)
|
||||
|
||||
statsreporter.StatsCollector
|
||||
}
|
||||
|
||||
|
||||
@@ -380,7 +380,7 @@ func (handler *handler) UpdateService(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.module.CreateService(ctx, orgID, cloudIntegrationService, provider)
|
||||
err = handler.module.CreateService(ctx, orgID, claims.Email, valuer.MustNewUUID(claims.IdentityID()), cloudIntegrationService, provider)
|
||||
} else {
|
||||
err = svc.CloudIntegrationService.Update(provider, serviceID, req.Config)
|
||||
if err != nil {
|
||||
@@ -388,7 +388,7 @@ func (handler *handler) UpdateService(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.module.UpdateService(ctx, orgID, svc.CloudIntegrationService, provider)
|
||||
err = handler.module.UpdateService(ctx, orgID, claims.Email, valuer.MustNewUUID(claims.IdentityID()), svc.CloudIntegrationService, provider)
|
||||
}
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
|
||||
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
@@ -45,7 +44,7 @@ func (module *module) DisconnectAccount(ctx context.Context, orgID valuer.UUID,
|
||||
return errors.New(errors.TypeUnsupported, cloudintegrationtypes.ErrCodeUnsupported, "disconnect account is not supported")
|
||||
}
|
||||
|
||||
func (module *module) CreateService(ctx context.Context, orgID valuer.UUID, service *cloudintegrationtypes.CloudIntegrationService, provider cloudintegrationtypes.CloudProviderType) error {
|
||||
func (module *module) CreateService(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, service *cloudintegrationtypes.CloudIntegrationService, provider cloudintegrationtypes.CloudProviderType) error {
|
||||
return errors.New(errors.TypeUnsupported, cloudintegrationtypes.ErrCodeUnsupported, "create service is not supported")
|
||||
}
|
||||
|
||||
@@ -57,7 +56,7 @@ func (module *module) ListServicesMetadata(ctx context.Context, orgID valuer.UUI
|
||||
return nil, errors.New(errors.TypeUnsupported, cloudintegrationtypes.ErrCodeUnsupported, "list services metadata is not supported")
|
||||
}
|
||||
|
||||
func (module *module) UpdateService(ctx context.Context, orgID valuer.UUID, service *cloudintegrationtypes.CloudIntegrationService, provider cloudintegrationtypes.CloudProviderType) error {
|
||||
func (module *module) UpdateService(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, service *cloudintegrationtypes.CloudIntegrationService, provider cloudintegrationtypes.CloudProviderType) error {
|
||||
return errors.New(errors.TypeUnsupported, cloudintegrationtypes.ErrCodeUnsupported, "update service is not supported")
|
||||
}
|
||||
|
||||
@@ -69,14 +68,6 @@ func (module *module) AgentCheckIn(ctx context.Context, orgID valuer.UUID, provi
|
||||
return nil, errors.New(errors.TypeUnsupported, cloudintegrationtypes.ErrCodeUnsupported, "agent check-in is not supported")
|
||||
}
|
||||
|
||||
func (module *module) GetDashboardByID(ctx context.Context, orgID valuer.UUID, id string) (*dashboardtypes.Dashboard, error) {
|
||||
return nil, errors.New(errors.TypeUnsupported, cloudintegrationtypes.ErrCodeUnsupported, "get dashboard by ID is not supported")
|
||||
}
|
||||
|
||||
func (module *module) ListDashboards(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error) {
|
||||
return nil, errors.New(errors.TypeUnsupported, cloudintegrationtypes.ErrCodeUnsupported, "list dashboards is not supported")
|
||||
}
|
||||
|
||||
func (module *module) Collect(context.Context, valuer.UUID) (map[string]any, error) {
|
||||
return nil, errors.New(errors.TypeUnsupported, cloudintegrationtypes.ErrCodeUnsupported, "stats collection is not supported")
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
@@ -200,6 +201,17 @@ func (store *store) CreateService(ctx context.Context, service *cloudintegration
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) DeleteServicesByCloudIntegrationID(ctx context.Context, cloudIntegrationID valuer.UUID) error {
|
||||
_, err := store.
|
||||
store.
|
||||
BunDBCtx(ctx).
|
||||
NewDelete().
|
||||
Model(new(cloudintegrationtypes.StorableCloudIntegrationService)).
|
||||
Where("cloud_integration_id = ?", cloudIntegrationID).
|
||||
Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (store *store) UpdateService(ctx context.Context, service *cloudintegrationtypes.StorableCloudIntegrationService) error {
|
||||
_, err := store.
|
||||
store.
|
||||
@@ -218,3 +230,50 @@ func (store *store) RunInTx(ctx context.Context, cb func(ctx context.Context) er
|
||||
return cb(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
func (store *store) CreateIntegrationDashboard(ctx context.Context, integrationDashboard *cloudintegrationtypes.StorableIntegrationDashboard) error {
|
||||
_, err := store.store.BunDBCtx(ctx).NewInsert().Model(integrationDashboard).Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (store *store) GetIntegrationDashboardBySlug(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.IntegrationDashboardProviderType, slug string) (*cloudintegrationtypes.StorableIntegrationDashboard, error) {
|
||||
integrationDashboard := new(cloudintegrationtypes.StorableIntegrationDashboard)
|
||||
err := store.store.BunDBCtx(ctx).
|
||||
NewSelect().
|
||||
Model(integrationDashboard).
|
||||
Join("JOIN dashboard AS d ON storable_integration_dashboard.dashboard_id = d.id").
|
||||
Where("d.org_id = ?", orgID).
|
||||
Where("storable_integration_dashboard.provider = ?", provider).
|
||||
Where("storable_integration_dashboard.slug = ?", slug).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, store.store.WrapNotFoundErrf(err, errors.CodeNotFound, "integration dashboard with slug %s not found", slug)
|
||||
}
|
||||
return integrationDashboard, nil
|
||||
}
|
||||
|
||||
func (store *store) ListIntegrationDashboardsBySlugPrefix(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.IntegrationDashboardProviderType, slugPrefix string) ([]*cloudintegrationtypes.StorableIntegrationDashboard, error) {
|
||||
var integrationDashboards []*cloudintegrationtypes.StorableIntegrationDashboard
|
||||
err := store.store.BunDBCtx(ctx).
|
||||
NewSelect().
|
||||
Model(&integrationDashboards).
|
||||
Join("JOIN dashboard AS d ON storable_integration_dashboard.dashboard_id = d.id").
|
||||
Where("d.org_id = ?", orgID).
|
||||
Where("storable_integration_dashboard.provider = ?", provider).
|
||||
Where("storable_integration_dashboard.slug LIKE ?", slugPrefix+"%").
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return integrationDashboards, nil
|
||||
}
|
||||
|
||||
func (store *store) DeleteIntegrationDashboardBySlug(ctx context.Context, provider cloudintegrationtypes.IntegrationDashboardProviderType, slug string) error {
|
||||
_, err := store.store.BunDBCtx(ctx).
|
||||
NewDelete().
|
||||
TableExpr("integration_dashboard").
|
||||
Where("provider = ?", provider).
|
||||
Where("slug = ?", slug).
|
||||
Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ type Module interface {
|
||||
// deletes the public sharing config and disables public sharing for the dashboard
|
||||
DeletePublic(context.Context, valuer.UUID, valuer.UUID) error
|
||||
|
||||
Create(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, data dashboardtypes.PostableDashboard) (*dashboardtypes.Dashboard, error)
|
||||
Create(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, source dashboardtypes.Source, data dashboardtypes.PostableDashboard) (*dashboardtypes.Dashboard, error)
|
||||
|
||||
Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error)
|
||||
|
||||
@@ -46,6 +46,10 @@ type Module interface {
|
||||
|
||||
Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
|
||||
|
||||
// DeleteBySource deletes a dashboard only if its source matches the given source,
|
||||
// bypassing the guards. Intended for internal system callers.
|
||||
DeleteBySource(ctx context.Context, orgID valuer.UUID, id valuer.UUID, source dashboardtypes.Source) error
|
||||
|
||||
GetByMetricNames(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string][]map[string]string, error)
|
||||
|
||||
statsreporter.StatsCollector
|
||||
|
||||
@@ -60,7 +60,7 @@ func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
|
||||
dashboardMigrator.Migrate(ctx, req)
|
||||
}
|
||||
|
||||
dashboard, err := handler.module.Create(ctx, orgID, claims.Email, valuer.MustNewUUID(claims.IdentityID()), req)
|
||||
dashboard, err := handler.module.Create(ctx, orgID, claims.Email, valuer.MustNewUUID(claims.IdentityID()), dashboardtypes.SourceUser, req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
|
||||
@@ -37,8 +37,8 @@ func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, an
|
||||
}
|
||||
}
|
||||
|
||||
func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, postableDashboard dashboardtypes.PostableDashboard) (*dashboardtypes.Dashboard, error) {
|
||||
dashboard, err := dashboardtypes.NewDashboard(orgID, createdBy, postableDashboard)
|
||||
func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, source dashboardtypes.Source, postableDashboard dashboardtypes.PostableDashboard) (*dashboardtypes.Dashboard, error) {
|
||||
dashboard, err := dashboardtypes.NewDashboard(orgID, createdBy, source, postableDashboard)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -53,7 +53,9 @@ func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy s
|
||||
return nil, err
|
||||
}
|
||||
|
||||
module.analytics.TrackUser(ctx, orgID.String(), creator.String(), "Dashboard Created", dashboardtypes.NewStatsFromStorableDashboards([]*dashboardtypes.StorableDashboard{storableDashboard}))
|
||||
if source == dashboardtypes.SourceUser {
|
||||
module.analytics.TrackUser(ctx, orgID.String(), creator.String(), "Dashboard Created", dashboardtypes.NewStatsFromStorableDashboards([]*dashboardtypes.StorableDashboard{storableDashboard}))
|
||||
}
|
||||
return dashboard, nil
|
||||
}
|
||||
|
||||
@@ -72,7 +74,16 @@ func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*dashboard
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dashboardtypes.NewDashboardsFromStorableDashboards(storableDashboards), nil
|
||||
// system dashboards are hidden from the listing endpoint but still gettable by id.
|
||||
filtered := make([]*dashboardtypes.StorableDashboard, 0, len(storableDashboards))
|
||||
for _, storable := range storableDashboards {
|
||||
if storable.Source == dashboardtypes.SourceSystem {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, storable)
|
||||
}
|
||||
|
||||
return dashboardtypes.NewDashboardsFromStorableDashboards(filtered), nil
|
||||
}
|
||||
|
||||
func (module *module) Update(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, updatableDashboard dashboardtypes.UpdatableDashboard, diff int) (*dashboardtypes.Dashboard, error) {
|
||||
@@ -81,6 +92,10 @@ func (module *module) Update(ctx context.Context, orgID valuer.UUID, id valuer.U
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := dashboard.ErrIfNotMutable(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = dashboard.Update(ctx, updatableDashboard, updatedBy, diff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -105,6 +120,10 @@ func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valu
|
||||
return err
|
||||
}
|
||||
|
||||
if err := dashboard.ErrIfNotLockable(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = dashboard.LockUnlock(lock, isAdmin, updatedBy)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -128,6 +147,10 @@ func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.U
|
||||
return err
|
||||
}
|
||||
|
||||
if err := dashboard.ErrIfNotDeletable(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dashboard.Locked {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is locked, please unlock the dashboard to be delete it")
|
||||
}
|
||||
@@ -140,6 +163,19 @@ func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.U
|
||||
return nil
|
||||
}
|
||||
|
||||
func (module *module) DeleteBySource(ctx context.Context, orgID valuer.UUID, id valuer.UUID, source dashboardtypes.Source) error {
|
||||
dashboard, err := module.Get(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dashboard.Source != source {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard source does not match")
|
||||
}
|
||||
|
||||
return module.store.Delete(ctx, orgID, id)
|
||||
}
|
||||
|
||||
func (module *module) GetByMetricNames(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string][]map[string]string, error) {
|
||||
dashboards, err := module.List(ctx, orgID)
|
||||
if err != nil {
|
||||
|
||||
@@ -21,7 +21,7 @@ func NewStore(sqlstore sqlstore.SQLStore) dashboardtypes.Store {
|
||||
func (store *store) Create(ctx context.Context, storabledashboard *dashboardtypes.StorableDashboard) error {
|
||||
_, err := store.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
BunDBCtx(ctx).
|
||||
NewInsert().
|
||||
Model(storabledashboard).
|
||||
Exec(ctx)
|
||||
|
||||
@@ -231,7 +231,7 @@ func (m *module) ListPods(ctx context.Context, orgID valuer.UUID, req *inframoni
|
||||
return nil, err
|
||||
}
|
||||
|
||||
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, req, pageGroups)
|
||||
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -314,19 +314,12 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nodeConditionCounts, err := m.getPerGroupNodeConditionCounts(ctx, req, pageGroups)
|
||||
nodeConditionCounts, err := m.getPerGroupNodeConditionCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Reuse the pods phase-counts CTE function via a temp struct — it reads only
|
||||
// Start/End/Filter/GroupBy from PostablePods.
|
||||
podPhaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, &inframonitoringtypes.PostablePods{
|
||||
Start: req.Start,
|
||||
End: req.End,
|
||||
Filter: req.Filter,
|
||||
GroupBy: req.GroupBy,
|
||||
}, pageGroups)
|
||||
podPhaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -409,14 +402,7 @@ func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Reuse the pods phase-counts CTE function via a temp struct — it reads only
|
||||
// Start/End/Filter/GroupBy from PostablePods.
|
||||
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, &inframonitoringtypes.PostablePods{
|
||||
Start: req.Start,
|
||||
End: req.End,
|
||||
Filter: req.Filter,
|
||||
GroupBy: req.GroupBy,
|
||||
}, pageGroups)
|
||||
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -498,27 +484,14 @@ func (m *module) ListClusters(ctx context.Context, orgID valuer.UUID, req *infra
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Reuse the nodes condition-counts CTE function via a temp struct — it reads only
|
||||
// Start/End/Filter/GroupBy from PostableNodes. With default groupBy
|
||||
// [k8s.cluster.name], counts are bucketed per cluster; with a custom groupBy,
|
||||
// they aggregate across clusters in that group.
|
||||
nodeConditionCountsMap, err := m.getPerGroupNodeConditionCounts(ctx, &inframonitoringtypes.PostableNodes{
|
||||
Start: req.Start,
|
||||
End: req.End,
|
||||
Filter: req.Filter,
|
||||
GroupBy: req.GroupBy,
|
||||
}, pageGroups)
|
||||
// With default groupBy [k8s.cluster.name], counts are bucketed per cluster;
|
||||
// with a custom groupBy, they aggregate across clusters in that group.
|
||||
nodeConditionCountsMap, err := m.getPerGroupNodeConditionCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Same pattern for pod phase counts via PostablePods shim.
|
||||
podPhaseCountsMap, err := m.getPerGroupPodPhaseCounts(ctx, &inframonitoringtypes.PostablePods{
|
||||
Start: req.Start,
|
||||
End: req.End,
|
||||
Filter: req.Filter,
|
||||
GroupBy: req.GroupBy,
|
||||
}, pageGroups)
|
||||
podPhaseCountsMap, err := m.getPerGroupPodPhaseCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -689,14 +662,7 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Reuse the pods phase-counts CTE function via a temp struct — it reads only
|
||||
// Start/End/Filter/GroupBy from PostablePods.
|
||||
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, &inframonitoringtypes.PostablePods{
|
||||
Start: req.Start,
|
||||
End: req.End,
|
||||
Filter: req.Filter,
|
||||
GroupBy: req.GroupBy,
|
||||
}, pageGroups)
|
||||
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -784,16 +750,9 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Reuse the pods phase-counts CTE function via a temp struct — it reads only
|
||||
// Start/End/Filter/GroupBy from PostablePods. Pods owned by a StatefulSet carry
|
||||
// k8s.statefulset.name as a resource attribute, so default-groupBy gives
|
||||
// per-statefulset phase counts automatically.
|
||||
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, &inframonitoringtypes.PostablePods{
|
||||
Start: req.Start,
|
||||
End: req.End,
|
||||
Filter: req.Filter,
|
||||
GroupBy: req.GroupBy,
|
||||
}, pageGroups)
|
||||
// Pods owned by a StatefulSet carry k8s.statefulset.name as a resource attribute,
|
||||
// so default-groupBy gives per-statefulset phase counts automatically.
|
||||
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -881,16 +840,9 @@ func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframoni
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Reuse the pods phase-counts CTE function via a temp struct — it reads only
|
||||
// Start/End/Filter/GroupBy from PostablePods. Pods owned by a Job carry
|
||||
// k8s.job.name as a resource attribute, so default-groupBy gives
|
||||
// per-job phase counts automatically.
|
||||
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, &inframonitoringtypes.PostablePods{
|
||||
Start: req.Start,
|
||||
End: req.End,
|
||||
Filter: req.Filter,
|
||||
GroupBy: req.GroupBy,
|
||||
}, pageGroups)
|
||||
// Pods owned by a Job carry k8s.job.name as a resource attribute, so default-groupBy
|
||||
// gives per-job phase counts automatically.
|
||||
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -978,16 +930,9 @@ func (m *module) ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Reuse the pods phase-counts CTE function via a temp struct — it reads only
|
||||
// Start/End/Filter/GroupBy from PostablePods. Pods owned by a DaemonSet carry
|
||||
// k8s.daemonset.name as a resource attribute, so default-groupBy gives
|
||||
// per-daemonset phase counts automatically.
|
||||
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, &inframonitoringtypes.PostablePods{
|
||||
Start: req.Start,
|
||||
End: req.End,
|
||||
Filter: req.Filter,
|
||||
GroupBy: req.GroupBy,
|
||||
}, pageGroups)
|
||||
// Pods owned by a DaemonSet carry k8s.daemonset.name as a resource attribute,
|
||||
// so default-groupBy gives per-daemonset phase counts automatically.
|
||||
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -170,27 +170,29 @@ func (m *module) getNodesTableMetadata(ctx context.Context, req *inframonitoring
|
||||
// Groups absent from the result map have implicit zero counts (caller default).
|
||||
func (m *module) getPerGroupNodeConditionCounts(
|
||||
ctx context.Context,
|
||||
req *inframonitoringtypes.PostableNodes,
|
||||
start, end int64,
|
||||
filter *qbtypes.Filter,
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
pageGroups []map[string]string,
|
||||
) (map[string]nodeConditionCounts, error) {
|
||||
if len(pageGroups) == 0 || len(req.GroupBy) == 0 {
|
||||
if len(pageGroups) == 0 || len(groupBy) == 0 {
|
||||
return map[string]nodeConditionCounts{}, nil
|
||||
}
|
||||
|
||||
// Merged filter expression (user filter + page-groups IN clauses).
|
||||
reqFilterExpr := ""
|
||||
if req.Filter != nil {
|
||||
reqFilterExpr = req.Filter.Expression
|
||||
// Merge user filter with page-groups IN clauses.
|
||||
userFilterExpr := ""
|
||||
if filter != nil {
|
||||
userFilterExpr = filter.Expression
|
||||
}
|
||||
pageGroupsFilterExpr := buildPageGroupsFilterExpr(pageGroups)
|
||||
filterExpr := mergeFilterExpressions(reqFilterExpr, pageGroupsFilterExpr)
|
||||
mergedFilterExpr := mergeFilterExpressions(userFilterExpr, pageGroupsFilterExpr)
|
||||
|
||||
// Resolve tables. Same convention as pods.
|
||||
adjustedStart, adjustedEnd, _, localTimeSeriesTable := telemetrymetrics.WhichTSTableToUse(
|
||||
uint64(req.Start), uint64(req.End), nil,
|
||||
uint64(start), uint64(end), nil,
|
||||
)
|
||||
samplesTable := telemetrymetrics.WhichSamplesTableToUse(
|
||||
uint64(req.Start), uint64(req.End),
|
||||
uint64(start), uint64(end),
|
||||
metrictypes.UnspecifiedType, metrictypes.TimeAggregationUnspecified, nil,
|
||||
)
|
||||
valueCol := telemetrymetrics.ValueColumnForSamplesTable(samplesTable)
|
||||
@@ -201,7 +203,7 @@ func (m *module) getPerGroupNodeConditionCounts(
|
||||
"fingerprint",
|
||||
fmt.Sprintf("JSONExtractString(labels, %s) AS node_name", timeSeriesFPs.Var(nodeNameAttrKey)),
|
||||
}
|
||||
for _, key := range req.GroupBy {
|
||||
for _, key := range groupBy {
|
||||
timeSeriesFPsSelectCols = append(timeSeriesFPsSelectCols,
|
||||
fmt.Sprintf("JSONExtractString(labels, %s) AS %s", timeSeriesFPs.Var(key.Name), quoteIdentifier(key.Name)),
|
||||
)
|
||||
@@ -213,8 +215,8 @@ func (m *module) getPerGroupNodeConditionCounts(
|
||||
timeSeriesFPs.GE("unix_milli", adjustedStart),
|
||||
timeSeriesFPs.L("unix_milli", adjustedEnd),
|
||||
)
|
||||
if filterExpr != "" {
|
||||
filterClause, err := m.buildFilterClause(ctx, &qbtypes.Filter{Expression: filterExpr}, req.Start, req.End)
|
||||
if mergedFilterExpr != "" {
|
||||
filterClause, err := m.buildFilterClause(ctx, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -223,7 +225,7 @@ func (m *module) getPerGroupNodeConditionCounts(
|
||||
}
|
||||
}
|
||||
timeSeriesFPsGroupBy := []string{"fingerprint", "node_name"}
|
||||
for _, key := range req.GroupBy {
|
||||
for _, key := range groupBy {
|
||||
timeSeriesFPsGroupBy = append(timeSeriesFPsGroupBy, quoteIdentifier(key.Name))
|
||||
}
|
||||
timeSeriesFPs.GroupBy(timeSeriesFPsGroupBy...)
|
||||
@@ -233,7 +235,7 @@ func (m *module) getPerGroupNodeConditionCounts(
|
||||
latestConditionPerNode := sqlbuilder.NewSelectBuilder()
|
||||
latestConditionPerNodeSelectCols := []string{"tsfp.node_name AS node_name"}
|
||||
latestConditionPerNodeGroupBy := []string{"node_name"}
|
||||
for _, key := range req.GroupBy {
|
||||
for _, key := range groupBy {
|
||||
col := quoteIdentifier(key.Name)
|
||||
latestConditionPerNodeSelectCols = append(latestConditionPerNodeSelectCols, fmt.Sprintf("tsfp.%s AS %s", col, col))
|
||||
latestConditionPerNodeGroupBy = append(latestConditionPerNodeGroupBy, col)
|
||||
@@ -248,17 +250,17 @@ func (m *module) getPerGroupNodeConditionCounts(
|
||||
))
|
||||
latestConditionPerNode.Where(
|
||||
latestConditionPerNode.E("samples.metric_name", nodeConditionMetricName),
|
||||
latestConditionPerNode.GE("samples.unix_milli", req.Start),
|
||||
latestConditionPerNode.L("samples.unix_milli", req.End),
|
||||
latestConditionPerNode.GE("samples.unix_milli", start),
|
||||
latestConditionPerNode.L("samples.unix_milli", end),
|
||||
"tsfp.node_name != ''",
|
||||
)
|
||||
latestConditionPerNode.GroupBy(latestConditionPerNodeGroupBy...)
|
||||
latestConditionPerNodeSQL, latestConditionPerNodeArgs := latestConditionPerNode.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
// ----- countNodesPerCondition (outer SELECT) -----
|
||||
countNodesPerConditionSelectCols := make([]string, 0, len(req.GroupBy)+2)
|
||||
countNodesPerConditionGroupBy := make([]string, 0, len(req.GroupBy))
|
||||
for _, key := range req.GroupBy {
|
||||
countNodesPerConditionSelectCols := make([]string, 0, len(groupBy)+2)
|
||||
countNodesPerConditionGroupBy := make([]string, 0, len(groupBy))
|
||||
for _, key := range groupBy {
|
||||
col := quoteIdentifier(key.Name)
|
||||
countNodesPerConditionSelectCols = append(countNodesPerConditionSelectCols, col)
|
||||
countNodesPerConditionGroupBy = append(countNodesPerConditionGroupBy, col)
|
||||
@@ -289,8 +291,8 @@ func (m *module) getPerGroupNodeConditionCounts(
|
||||
|
||||
result := make(map[string]nodeConditionCounts)
|
||||
for rows.Next() {
|
||||
groupVals := make([]string, len(req.GroupBy))
|
||||
scanPtrs := make([]any, 0, len(req.GroupBy)+2)
|
||||
groupVals := make([]string, len(groupBy))
|
||||
scanPtrs := make([]any, 0, len(groupBy)+2)
|
||||
for i := range groupVals {
|
||||
scanPtrs = append(scanPtrs, &groupVals[i])
|
||||
}
|
||||
|
||||
@@ -189,27 +189,29 @@ func (m *module) getPodsTableMetadata(ctx context.Context, req *inframonitoringt
|
||||
// Groups absent from the result map have implicit zero counts (caller default).
|
||||
func (m *module) getPerGroupPodPhaseCounts(
|
||||
ctx context.Context,
|
||||
req *inframonitoringtypes.PostablePods,
|
||||
start, end int64,
|
||||
filter *qbtypes.Filter,
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
pageGroups []map[string]string,
|
||||
) (map[string]podPhaseCounts, error) {
|
||||
if len(pageGroups) == 0 || len(req.GroupBy) == 0 {
|
||||
if len(pageGroups) == 0 || len(groupBy) == 0 {
|
||||
return map[string]podPhaseCounts{}, nil
|
||||
}
|
||||
|
||||
// Merged filter expression (user filter + page-groups IN clauses).
|
||||
reqFilterExpr := ""
|
||||
if req.Filter != nil {
|
||||
reqFilterExpr = req.Filter.Expression
|
||||
// Merge user filter with page-groups IN clauses.
|
||||
userFilterExpr := ""
|
||||
if filter != nil {
|
||||
userFilterExpr = filter.Expression
|
||||
}
|
||||
pageGroupsFilterExpr := buildPageGroupsFilterExpr(pageGroups)
|
||||
filterExpr := mergeFilterExpressions(reqFilterExpr, pageGroupsFilterExpr)
|
||||
mergedFilterExpr := mergeFilterExpressions(userFilterExpr, pageGroupsFilterExpr)
|
||||
|
||||
// Resolve tables. Same convention as hosts (distributed names from helpers).
|
||||
adjustedStart, adjustedEnd, _, localTimeSeriesTable := telemetrymetrics.WhichTSTableToUse(
|
||||
uint64(req.Start), uint64(req.End), nil,
|
||||
uint64(start), uint64(end), nil,
|
||||
)
|
||||
samplesTable := telemetrymetrics.WhichSamplesTableToUse(
|
||||
uint64(req.Start), uint64(req.End),
|
||||
uint64(start), uint64(end),
|
||||
metrictypes.UnspecifiedType, metrictypes.TimeAggregationUnspecified, nil,
|
||||
)
|
||||
valueCol := telemetrymetrics.ValueColumnForSamplesTable(samplesTable)
|
||||
@@ -220,7 +222,7 @@ func (m *module) getPerGroupPodPhaseCounts(
|
||||
"fingerprint",
|
||||
fmt.Sprintf("JSONExtractString(labels, %s) AS pod_uid", timeSeriesFPs.Var(podUIDAttrKey)),
|
||||
}
|
||||
for _, key := range req.GroupBy {
|
||||
for _, key := range groupBy {
|
||||
timeSeriesFPsSelectCols = append(timeSeriesFPsSelectCols,
|
||||
fmt.Sprintf("JSONExtractString(labels, %s) AS %s", timeSeriesFPs.Var(key.Name), quoteIdentifier(key.Name)),
|
||||
)
|
||||
@@ -232,8 +234,8 @@ func (m *module) getPerGroupPodPhaseCounts(
|
||||
timeSeriesFPs.GE("unix_milli", adjustedStart),
|
||||
timeSeriesFPs.L("unix_milli", adjustedEnd),
|
||||
)
|
||||
if filterExpr != "" {
|
||||
filterClause, err := m.buildFilterClause(ctx, &qbtypes.Filter{Expression: filterExpr}, req.Start, req.End)
|
||||
if mergedFilterExpr != "" {
|
||||
filterClause, err := m.buildFilterClause(ctx, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -242,7 +244,7 @@ func (m *module) getPerGroupPodPhaseCounts(
|
||||
}
|
||||
}
|
||||
timeSeriesFPsGroupBy := []string{"fingerprint", "pod_uid"}
|
||||
for _, key := range req.GroupBy {
|
||||
for _, key := range groupBy {
|
||||
timeSeriesFPsGroupBy = append(timeSeriesFPsGroupBy, quoteIdentifier(key.Name))
|
||||
}
|
||||
timeSeriesFPs.GroupBy(timeSeriesFPsGroupBy...)
|
||||
@@ -251,7 +253,7 @@ func (m *module) getPerGroupPodPhaseCounts(
|
||||
latestPhasePerPod := sqlbuilder.NewSelectBuilder()
|
||||
latestPhasePerPodSelectCols := []string{"tsfp.pod_uid AS pod_uid"}
|
||||
latestPhasePerPodGroupBy := []string{"pod_uid"}
|
||||
for _, key := range req.GroupBy {
|
||||
for _, key := range groupBy {
|
||||
col := quoteIdentifier(key.Name)
|
||||
latestPhasePerPodSelectCols = append(latestPhasePerPodSelectCols, fmt.Sprintf("tsfp.%s AS %s", col, col))
|
||||
latestPhasePerPodGroupBy = append(latestPhasePerPodGroupBy, col)
|
||||
@@ -266,17 +268,17 @@ func (m *module) getPerGroupPodPhaseCounts(
|
||||
))
|
||||
latestPhasePerPod.Where(
|
||||
latestPhasePerPod.E("samples.metric_name", podPhaseMetricName),
|
||||
latestPhasePerPod.GE("samples.unix_milli", req.Start),
|
||||
latestPhasePerPod.L("samples.unix_milli", req.End),
|
||||
latestPhasePerPod.GE("samples.unix_milli", start),
|
||||
latestPhasePerPod.L("samples.unix_milli", end),
|
||||
"tsfp.pod_uid != ''",
|
||||
)
|
||||
latestPhasePerPod.GroupBy(latestPhasePerPodGroupBy...)
|
||||
latestPhasePerPodSQL, latestPhasePerPodArgs := latestPhasePerPod.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
// ----- countPodsPerPhase (outer SELECT) -----
|
||||
countPodsPerPhaseSelectCols := make([]string, 0, len(req.GroupBy)+5)
|
||||
countPodsPerPhaseGroupBy := make([]string, 0, len(req.GroupBy))
|
||||
for _, key := range req.GroupBy {
|
||||
countPodsPerPhaseSelectCols := make([]string, 0, len(groupBy)+5)
|
||||
countPodsPerPhaseGroupBy := make([]string, 0, len(groupBy))
|
||||
for _, key := range groupBy {
|
||||
col := quoteIdentifier(key.Name)
|
||||
countPodsPerPhaseSelectCols = append(countPodsPerPhaseSelectCols, col)
|
||||
countPodsPerPhaseGroupBy = append(countPodsPerPhaseGroupBy, col)
|
||||
@@ -310,8 +312,8 @@ func (m *module) getPerGroupPodPhaseCounts(
|
||||
|
||||
result := make(map[string]podPhaseCounts)
|
||||
for rows.Next() {
|
||||
groupVals := make([]string, len(req.GroupBy))
|
||||
scanPtrs := make([]any, 0, len(req.GroupBy)+5)
|
||||
groupVals := make([]string, len(groupBy))
|
||||
scanPtrs := make([]any, 0, len(groupBy)+5)
|
||||
for i := range groupVals {
|
||||
scanPtrs = append(scanPtrs, &groupVals[i])
|
||||
}
|
||||
|
||||
@@ -63,7 +63,6 @@ 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/cloudintegrationtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
@@ -1113,20 +1112,7 @@ func (aH *APIHandler) Get(rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
dashboard := new(dashboardtypes.Dashboard)
|
||||
|
||||
if _, _, _, err := cloudintegrationtypes.ParseCloudIntegrationDashboardID(id); err == nil {
|
||||
cloudIntegrationDashboard, err := aH.Signoz.Modules.CloudIntegration.GetDashboardByID(ctx, orgID, id)
|
||||
if err != nil && !errorsV2.Ast(err, errorsV2.TypeLicenseUnavailable) {
|
||||
render.Error(rw, errorsV2.Wrapf(err, errorsV2.TypeInternal, errorsV2.CodeInternal, "failed to get dashboard"))
|
||||
return
|
||||
}
|
||||
|
||||
if cloudIntegrationDashboard == nil {
|
||||
render.Error(rw, errorsV2.Newf(errorsV2.TypeNotFound, errorsV2.CodeNotFound, "dashboard not found"))
|
||||
return
|
||||
}
|
||||
|
||||
dashboard = cloudIntegrationDashboard
|
||||
} else if aH.IntegrationsController.IsInstalledIntegrationDashboardID(id) {
|
||||
if aH.IntegrationsController.IsInstalledIntegrationDashboardID(id) {
|
||||
integrationDashboard, apiErr := aH.IntegrationsController.GetInstalledIntegrationDashboardById(ctx, orgID, id)
|
||||
if apiErr != nil {
|
||||
render.Error(rw, errorsV2.Wrapf(apiErr, errorsV2.TypeInternal, errorsV2.CodeInternal, "failed to get dashboard"))
|
||||
@@ -1189,15 +1175,6 @@ func (aH *APIHandler) List(rw http.ResponseWriter, r *http.Request) {
|
||||
dashboards = append(dashboards, installedIntegrationDashboards...)
|
||||
}
|
||||
|
||||
cloudIntegrationDashboards, err := aH.Signoz.Modules.CloudIntegration.ListDashboards(ctx, orgID)
|
||||
if err != nil {
|
||||
if !errors.Ast(err, errorsV2.TypeLicenseUnavailable) {
|
||||
aH.logger.ErrorContext(ctx, "failed to get dashboards for cloud integrations", errors.Attr(err))
|
||||
}
|
||||
} else {
|
||||
dashboards = append(dashboards, cloudIntegrationDashboards...)
|
||||
}
|
||||
|
||||
gettableDashboards, err := dashboardtypes.NewGettableDashboardsFromDashboards(dashboards)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
|
||||
@@ -348,7 +348,8 @@ func (m *Manager) GetInstalledIntegrationDashboardById(
|
||||
CreatedBy: author,
|
||||
UpdatedBy: author,
|
||||
},
|
||||
OrgID: orgId,
|
||||
OrgID: orgId,
|
||||
Source: dashboardtypes.SourceIntegration,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -387,7 +388,8 @@ func (m *Manager) GetDashboardsForInstalledIntegrations(
|
||||
CreatedBy: author,
|
||||
UpdatedBy: author,
|
||||
},
|
||||
OrgID: orgId,
|
||||
OrgID: orgId,
|
||||
Source: dashboardtypes.SourceIntegration,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +204,8 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewAddTagsFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewAddRoleCRUDTuplesFactory(sqlstore),
|
||||
sqlmigration.NewAddIntegrationDashboardFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewAddSourceToDashboardFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewMigrateCloudIntegrationDashboardsFactory(sqlstore),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ func New(
|
||||
auditorProviderFactories func(licensing.Licensing) factory.NamedMap[factory.ProviderFactory[auditor.Auditor, auditor.Config]],
|
||||
meterReporterProviderFactories func(context.Context, factory.ProviderSettings, flagger.Flagger, licensing.Licensing, telemetrystore.TelemetryStore, retention.Getter, organization.Getter, zeus.Zeus) (factory.NamedMap[factory.ProviderFactory[meterreporter.Reporter, meterreporter.Config]], string),
|
||||
querierHandlerCallback func(factory.ProviderSettings, querier.Querier, analytics.Analytics) querier.Handler,
|
||||
cloudIntegrationCallback func(sqlstore.SQLStore, global.Global, zeus.Zeus, gateway.Gateway, licensing.Licensing, serviceaccount.Module, cloudintegration.Config) (cloudintegration.Module, error),
|
||||
cloudIntegrationCallback func(sqlstore.SQLStore, dashboard.Module, global.Global, zeus.Zeus, gateway.Gateway, licensing.Licensing, serviceaccount.Module, cloudintegration.Config) (cloudintegration.Module, error),
|
||||
rulerProviderFactories func(cache.Cache, alertmanager.Alertmanager, sqlstore.SQLStore, telemetrystore.TelemetryStore, telemetrytypes.MetadataStore, prometheus.Prometheus, organization.Getter, rulestatehistory.Module, querier.Querier, queryparser.QueryParser) factory.NamedMap[factory.ProviderFactory[ruler.Ruler, ruler.Config]],
|
||||
) (*SigNoz, error) {
|
||||
// Initialize instrumentation
|
||||
@@ -458,7 +458,7 @@ func New(
|
||||
|
||||
serviceAccount := implserviceaccount.NewModule(implserviceaccount.NewStore(sqlstore), authz, cache, analytics, providerSettings, config.ServiceAccount)
|
||||
|
||||
cloudIntegrationModule, err := cloudIntegrationCallback(sqlstore, global, zeus, gateway, licensing, serviceAccount, config.CloudIntegration)
|
||||
cloudIntegrationModule, err := cloudIntegrationCallback(sqlstore, dashboard, global, zeus, gateway, licensing, serviceAccount, config.CloudIntegration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
79
pkg/sqlmigration/085_add_source_to_dashboard.go
Normal file
79
pkg/sqlmigration/085_add_source_to_dashboard.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type addSourceToDashboard struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
sqlschema sqlschema.SQLSchema
|
||||
}
|
||||
|
||||
func NewAddSourceToDashboardFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(
|
||||
factory.MustNewName("add_source_to_dashboard"),
|
||||
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &addSourceToDashboard{sqlstore: sqlstore, sqlschema: sqlschema}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (migration *addSourceToDashboard) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
func (migration *addSourceToDashboard) Up(ctx context.Context, db *bun.DB) error {
|
||||
// dashboard is referenced by public_dashboard and integration_dashboard;
|
||||
// FK enforcement must be off for the SQLite recreate-table fallback.
|
||||
if err := migration.sqlschema.ToggleFKEnforcement(ctx, db, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
table, uniqueConstraints, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("dashboard"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sourceColumn := &sqlschema.Column{
|
||||
Name: sqlschema.ColumnName("source"),
|
||||
DataType: sqlschema.DataTypeText,
|
||||
Nullable: false,
|
||||
}
|
||||
|
||||
// backfill existing rows with 'user' before the NOT NULL flip.
|
||||
sqls := migration.sqlschema.Operator().AddColumn(table, uniqueConstraints, sourceColumn, "user")
|
||||
|
||||
for _, sql := range sqls {
|
||||
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := migration.sqlschema.ToggleFKEnforcement(ctx, db, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *addSourceToDashboard) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,851 @@
|
||||
{
|
||||
"description": "View key AWS ECS metrics with an out of the box dashboard.\n",
|
||||
"image":"data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%3F%3E%3Csvg%20width%3D%2280px%22%20height%3D%2280px%22%20viewBox%3D%220%200%2080%2080%22%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%3C!--%20Generator%3A%20Sketch%2064%20(93537)%20-%20https%3A%2F%2Fsketch.com%20--%3E%3Ctitle%3EIcon-Architecture%2F64%2FArch_Amazon-Elastic-Container-Service_64%3C%2Ftitle%3E%3Cdesc%3ECreated%20with%20Sketch.%3C%2Fdesc%3E%3Cdefs%3E%3ClinearGradient%20x1%3D%220%25%22%20y1%3D%22100%25%22%20x2%3D%22100%25%22%20y2%3D%220%25%22%20id%3D%22linearGradient-1%22%3E%3Cstop%20stop-color%3D%22%23C8511B%22%20offset%3D%220%25%22%3E%3C%2Fstop%3E%3Cstop%20stop-color%3D%22%23FF9900%22%20offset%3D%22100%25%22%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Cg%20id%3D%22Icon-Architecture%2F64%2FArch_Amazon-Elastic-Container-Service_64%22%20stroke%3D%22none%22%20stroke-width%3D%221%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Cg%20id%3D%22Icon-Architecture-BG%2F64%2FContainers%22%20fill%3D%22url(%23linearGradient-1)%22%3E%3Crect%20id%3D%22Rectangle%22%20x%3D%220%22%20y%3D%220%22%20width%3D%2280%22%20height%3D%2280%22%3E%3C%2Frect%3E%3C%2Fg%3E%3Cpath%20d%3D%22M64%2C48.2340095%20L56%2C43.4330117%20L56%2C32.0000169%20C56%2C31.6440171%2055.812%2C31.3150172%2055.504%2C31.1360173%20L44%2C24.4260204%20L44%2C14.7520248%20L64%2C26.5710194%20L64%2C48.2340095%20Z%20M65.509%2C25.13902%20L43.509%2C12.139026%20C43.199%2C11.9560261%2042.818%2C11.9540261%2042.504%2C12.131026%20C42.193%2C12.3090259%2042%2C12.6410257%2042%2C13.0000256%20L42%2C25.0000201%20C42%2C25.3550199%2042.189%2C25.6840198%2042.496%2C25.8640197%20L54%2C32.5740166%20L54%2C44.0000114%20C54%2C44.3510113%2054.185%2C44.6770111%2054.486%2C44.857011%20L64.486%2C50.8570083%20C64.644%2C50.9520082%2064.822%2C51%2065%2C51%20C65.17%2C51%2065.34%2C50.9570082%2065.493%2C50.8700083%20C65.807%2C50.6930084%2066%2C50.3600085%2066%2C50%20L66%2C26.0000196%20C66%2C25.6460198%2065.814%2C25.31902%2065.509%2C25.13902%20L65.509%2C25.13902%20Z%20M40.445%2C66.863001%20L17%2C54.3990067%20L17%2C26.5710194%20L37%2C14.7520248%20L37%2C24.4510204%20L26.463%2C31.1560173%20C26.175%2C31.3400172%2026%2C31.6580171%2026%2C32.0000169%20L26%2C49.0000091%20C26%2C49.373009%2026.208%2C49.7150088%2026.538%2C49.8870087%20L39.991%2C56.8870055%20C40.28%2C57.0370055%2040.624%2C57.0380055%2040.912%2C56.8880055%20L53.964%2C50.1440086%20L61.996%2C54.9640064%20L40.445%2C66.863001%20Z%20M64.515%2C54.1420068%20L54.515%2C48.1420095%20C54.217%2C47.9640096%2053.849%2C47.9520096%2053.541%2C48.1120095%20L40.455%2C54.8730065%20L28%2C48.3930094%20L28%2C32.5490167%20L38.537%2C25.8440197%20C38.825%2C25.6600198%2039%2C25.3420199%2039%2C25.0000201%20L39%2C13.0000256%20C39%2C12.6410257%2038.808%2C12.3090259%2038.496%2C12.131026%20C38.184%2C11.9540261%2037.802%2C11.9560261%2037.491%2C12.139026%20L15.491%2C25.13902%20C15.187%2C25.31902%2015%2C25.6460198%2015%2C26.0000196%20L15%2C55%20C15%2C55.3690062%2015.204%2C55.7090061%2015.53%2C55.883006%20L39.984%2C68.8830001%20C40.131%2C68.961%2040.292%2C69%2040.453%2C69%20C40.62%2C69%2040.786%2C68.958%2040.937%2C68.8750001%20L64.484%2C55.875006%20C64.797%2C55.7020061%2064.993%2C55.3750062%2065.0001416%2C55.0180064%20C65.006%2C54.6600066%2064.821%2C54.3260067%2064.515%2C54.1420068%20L64.515%2C54.1420068%20Z%22%20id%3D%22Amazon-Elastic-Container-Service_Icon_64_Squid%22%20fill%3D%22%23FFFFFF%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E",
|
||||
"layout": [
|
||||
{
|
||||
"h": 6,
|
||||
"i": "f78becf8-0328-48b4-84b6-ff4dac325940",
|
||||
"moved": false,
|
||||
"static": false,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"h": 6,
|
||||
"i": "2b4eac06-b426-4f78-b874-2e1734c4104b",
|
||||
"moved": false,
|
||||
"static": false,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"h": 6,
|
||||
"i": "5bea2bc0-13a2-4937-bccb-60ffe8a43ad5",
|
||||
"moved": false,
|
||||
"static": false,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 6
|
||||
},
|
||||
{
|
||||
"h": 6,
|
||||
"i": "6fac67b0-50ec-4b43-ac4b-320a303d0369",
|
||||
"moved": false,
|
||||
"static": false,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 6
|
||||
}
|
||||
],
|
||||
"panelMap": {},
|
||||
"tags": [],
|
||||
"title": "AWS ECS Overview",
|
||||
"uploadedGrafana": false,
|
||||
"variables": {
|
||||
"51f4fa2b-89c7-47c2-9795-f32cffaab985": {
|
||||
"allSelected": false,
|
||||
"customValue": "",
|
||||
"description": "AWS Account ID",
|
||||
"id": "51f4fa2b-89c7-47c2-9795-f32cffaab985",
|
||||
"key": "51f4fa2b-89c7-47c2-9795-f32cffaab985",
|
||||
"modificationUUID": "7b814d17-8fff-4ed6-a4ea-90e3b1a97584",
|
||||
"multiSelect": false,
|
||||
"name": "Account",
|
||||
"order": 0,
|
||||
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.account.id') AS `cloud.account.id`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_ECS_MemoryUtilization_max' GROUP BY `cloud.account.id`",
|
||||
"showALLOption": false,
|
||||
"sort": "DISABLED",
|
||||
"textboxValue": "",
|
||||
"type": "QUERY"
|
||||
},
|
||||
"9faf0f4b-b245-4b3c-83a3-60cfa76dfeb0": {
|
||||
"allSelected": false,
|
||||
"customValue": "",
|
||||
"description": "Account Region",
|
||||
"id": "9faf0f4b-b245-4b3c-83a3-60cfa76dfeb0",
|
||||
"key": "9faf0f4b-b245-4b3c-83a3-60cfa76dfeb0",
|
||||
"modificationUUID": "3b5f499b-22a3-4c8a-847c-8d3811c9e6b2",
|
||||
"multiSelect": false,
|
||||
"name": "Region",
|
||||
"order": 1,
|
||||
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.region') AS region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_ECS_MemoryUtilization_max' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} GROUP BY region",
|
||||
"showALLOption": false,
|
||||
"sort": "ASC",
|
||||
"textboxValue": "",
|
||||
"type": "QUERY"
|
||||
},
|
||||
"bfbdbcbe-a168-4d81-b108-36339e249116": {
|
||||
"allSelected": true,
|
||||
"customValue": "",
|
||||
"description": "ECS Cluster Name",
|
||||
"id": "bfbdbcbe-a168-4d81-b108-36339e249116",
|
||||
"key": "bfbdbcbe-a168-4d81-b108-36339e249116",
|
||||
"modificationUUID": "9fb0d63c-ac6c-497d-82b3-17d95944e245",
|
||||
"multiSelect": true,
|
||||
"name": "Cluster",
|
||||
"order": 2,
|
||||
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'ClusterName') AS cluster\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_ECS_MemoryUtilization_max' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} AND JSONExtractString(labels, 'cloud.region') IN {{.Region}}\nGROUP BY cluster",
|
||||
"showALLOption": true,
|
||||
"sort": "ASC",
|
||||
"textboxValue": "",
|
||||
"type": "QUERY"
|
||||
}
|
||||
},
|
||||
"version": "v4",
|
||||
"widgets": [
|
||||
{
|
||||
"bucketCount": 30,
|
||||
"bucketWidth": 0,
|
||||
"columnUnits": {},
|
||||
"description": "",
|
||||
"fillSpans": false,
|
||||
"id": "f78becf8-0328-48b4-84b6-ff4dac325940",
|
||||
"isLogScale": false,
|
||||
"isStacked": false,
|
||||
"mergeAllActiveQueries": false,
|
||||
"nullZeroValues": "zero",
|
||||
"opacity": "1",
|
||||
"panelTypes": "graph",
|
||||
"query": {
|
||||
"builder": {
|
||||
"queryData": [
|
||||
{
|
||||
"aggregateAttribute": {
|
||||
"dataType": "float64",
|
||||
"id": "aws_ECS_MemoryUtilization_max--float64--Gauge--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "aws_ECS_MemoryUtilization_max",
|
||||
"type": "Gauge"
|
||||
},
|
||||
"aggregateOperator": "max",
|
||||
"dataSource": "metrics",
|
||||
"disabled": false,
|
||||
"expression": "A",
|
||||
"filters": {
|
||||
"items": [
|
||||
{
|
||||
"id": "26ac617d",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.region--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.region",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Region"
|
||||
},
|
||||
{
|
||||
"id": "57172ed9",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.account.id--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.account.id",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Account"
|
||||
},
|
||||
{
|
||||
"id": "49b9f85e",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "in",
|
||||
"value": [
|
||||
"$Cluster"
|
||||
]
|
||||
}
|
||||
],
|
||||
"op": "AND"
|
||||
},
|
||||
"functions": [],
|
||||
"groupBy": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ServiceName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ServiceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"having": [],
|
||||
"legend": "{{ServiceName}} ({{ClusterName}})",
|
||||
"limit": null,
|
||||
"orderBy": [],
|
||||
"queryName": "A",
|
||||
"reduceTo": "avg",
|
||||
"spaceAggregation": "max",
|
||||
"stepInterval": 60,
|
||||
"timeAggregation": "max"
|
||||
}
|
||||
],
|
||||
"queryFormulas": []
|
||||
},
|
||||
"clickhouse_sql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"id": "56068fdd-d523-4117-92fa-87c6518ad07c",
|
||||
"promql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"queryType": "builder"
|
||||
},
|
||||
"selectedLogFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "body",
|
||||
"type": ""
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "timestamp",
|
||||
"type": ""
|
||||
}
|
||||
],
|
||||
"selectedTracesFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "serviceName--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "serviceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "name--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "name",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "float64",
|
||||
"id": "durationNano--float64--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "durationNano",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "httpMethod--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "httpMethod",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "responseStatusCode--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "responseStatusCode",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"softMax": 0,
|
||||
"softMin": 0,
|
||||
"stackedBarChart": false,
|
||||
"thresholds": [],
|
||||
"timePreferance": "GLOBAL_TIME",
|
||||
"title": "Maximum Memory Utilization",
|
||||
"yAxisUnit": "none"
|
||||
},
|
||||
{
|
||||
"bucketCount": 30,
|
||||
"bucketWidth": 0,
|
||||
"columnUnits": {},
|
||||
"description": "",
|
||||
"fillSpans": false,
|
||||
"id": "2b4eac06-b426-4f78-b874-2e1734c4104b",
|
||||
"isLogScale": false,
|
||||
"isStacked": false,
|
||||
"mergeAllActiveQueries": false,
|
||||
"nullZeroValues": "zero",
|
||||
"opacity": "1",
|
||||
"panelTypes": "graph",
|
||||
"query": {
|
||||
"builder": {
|
||||
"queryData": [
|
||||
{
|
||||
"aggregateAttribute": {
|
||||
"dataType": "float64",
|
||||
"id": "aws_ECS_MemoryUtilization_min--float64--Gauge--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "aws_ECS_MemoryUtilization_min",
|
||||
"type": "Gauge"
|
||||
},
|
||||
"aggregateOperator": "min",
|
||||
"dataSource": "metrics",
|
||||
"disabled": false,
|
||||
"expression": "A",
|
||||
"filters": {
|
||||
"items": [
|
||||
{
|
||||
"id": "cd4b8848",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.region--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.region",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Region"
|
||||
},
|
||||
{
|
||||
"id": "aa5115c6",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.account.id--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.account.id",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Account"
|
||||
},
|
||||
{
|
||||
"id": "f60677b6",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "in",
|
||||
"value": [
|
||||
"$Cluster"
|
||||
]
|
||||
}
|
||||
],
|
||||
"op": "AND"
|
||||
},
|
||||
"functions": [],
|
||||
"groupBy": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ServiceName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ServiceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"having": [],
|
||||
"legend": "{{ServiceName}} ({{ClusterName}})",
|
||||
"limit": null,
|
||||
"orderBy": [],
|
||||
"queryName": "A",
|
||||
"reduceTo": "avg",
|
||||
"spaceAggregation": "min",
|
||||
"stepInterval": 60,
|
||||
"timeAggregation": "min"
|
||||
}
|
||||
],
|
||||
"queryFormulas": []
|
||||
},
|
||||
"clickhouse_sql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"id": "fb19342e-cbde-40d8-b12f-ad108698356b",
|
||||
"promql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"queryType": "builder"
|
||||
},
|
||||
"selectedLogFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "body",
|
||||
"type": ""
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "timestamp",
|
||||
"type": ""
|
||||
}
|
||||
],
|
||||
"selectedTracesFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "serviceName--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "serviceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "name--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "name",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "float64",
|
||||
"id": "durationNano--float64--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "durationNano",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "httpMethod--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "httpMethod",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "responseStatusCode--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "responseStatusCode",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"softMax": 0,
|
||||
"softMin": 0,
|
||||
"stackedBarChart": false,
|
||||
"thresholds": [],
|
||||
"timePreferance": "GLOBAL_TIME",
|
||||
"title": "Minimum Memory Utilization",
|
||||
"yAxisUnit": "none"
|
||||
},
|
||||
{
|
||||
"bucketCount": 30,
|
||||
"bucketWidth": 0,
|
||||
"columnUnits": {},
|
||||
"description": "",
|
||||
"fillSpans": false,
|
||||
"id": "5bea2bc0-13a2-4937-bccb-60ffe8a43ad5",
|
||||
"isLogScale": false,
|
||||
"isStacked": false,
|
||||
"mergeAllActiveQueries": false,
|
||||
"nullZeroValues": "zero",
|
||||
"opacity": "1",
|
||||
"panelTypes": "graph",
|
||||
"query": {
|
||||
"builder": {
|
||||
"queryData": [
|
||||
{
|
||||
"aggregateAttribute": {
|
||||
"dataType": "float64",
|
||||
"id": "aws_ECS_CPUUtilization_max--float64--Gauge--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "aws_ECS_CPUUtilization_max",
|
||||
"type": "Gauge"
|
||||
},
|
||||
"aggregateOperator": "max",
|
||||
"dataSource": "metrics",
|
||||
"disabled": false,
|
||||
"expression": "A",
|
||||
"filters": {
|
||||
"items": [
|
||||
{
|
||||
"id": "2c13c8ee",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.region--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.region",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Region"
|
||||
},
|
||||
{
|
||||
"id": "f489f6a8",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.account.id--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.account.id",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Account"
|
||||
},
|
||||
{
|
||||
"id": "94012320",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "in",
|
||||
"value": [
|
||||
"$Cluster"
|
||||
]
|
||||
}
|
||||
],
|
||||
"op": "AND"
|
||||
},
|
||||
"functions": [],
|
||||
"groupBy": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ServiceName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ServiceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"having": [],
|
||||
"legend": "{{ServiceName}} ({{ClusterName}})",
|
||||
"limit": null,
|
||||
"orderBy": [],
|
||||
"queryName": "A",
|
||||
"reduceTo": "avg",
|
||||
"spaceAggregation": "max",
|
||||
"stepInterval": 60,
|
||||
"timeAggregation": "max"
|
||||
}
|
||||
],
|
||||
"queryFormulas": []
|
||||
},
|
||||
"clickhouse_sql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"id": "273e0a76-c780-4b9a-9b03-2649d4227173",
|
||||
"promql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"queryType": "builder"
|
||||
},
|
||||
"selectedLogFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "body",
|
||||
"type": ""
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "timestamp",
|
||||
"type": ""
|
||||
}
|
||||
],
|
||||
"selectedTracesFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "serviceName--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "serviceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "name--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "name",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "float64",
|
||||
"id": "durationNano--float64--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "durationNano",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "httpMethod--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "httpMethod",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "responseStatusCode--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "responseStatusCode",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"softMax": 0,
|
||||
"softMin": 0,
|
||||
"stackedBarChart": false,
|
||||
"thresholds": [],
|
||||
"timePreferance": "GLOBAL_TIME",
|
||||
"title": "Maximum CPU Utilization",
|
||||
"yAxisUnit": "none"
|
||||
},
|
||||
{
|
||||
"bucketCount": 30,
|
||||
"bucketWidth": 0,
|
||||
"columnUnits": {},
|
||||
"description": "",
|
||||
"fillSpans": false,
|
||||
"id": "6fac67b0-50ec-4b43-ac4b-320a303d0369",
|
||||
"isLogScale": false,
|
||||
"isStacked": false,
|
||||
"mergeAllActiveQueries": false,
|
||||
"nullZeroValues": "zero",
|
||||
"opacity": "1",
|
||||
"panelTypes": "graph",
|
||||
"query": {
|
||||
"builder": {
|
||||
"queryData": [
|
||||
{
|
||||
"aggregateAttribute": {
|
||||
"dataType": "float64",
|
||||
"id": "aws_ECS_CPUUtilization_min--float64--Gauge--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "aws_ECS_CPUUtilization_min",
|
||||
"type": "Gauge"
|
||||
},
|
||||
"aggregateOperator": "min",
|
||||
"dataSource": "metrics",
|
||||
"disabled": false,
|
||||
"expression": "A",
|
||||
"filters": {
|
||||
"items": [
|
||||
{
|
||||
"id": "758ba906",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.region--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.region",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Region"
|
||||
},
|
||||
{
|
||||
"id": "4ffe6bf7",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.account.id--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.account.id",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Account"
|
||||
},
|
||||
{
|
||||
"id": "53d98059",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "in",
|
||||
"value": [
|
||||
"$Cluster"
|
||||
]
|
||||
}
|
||||
],
|
||||
"op": "AND"
|
||||
},
|
||||
"functions": [],
|
||||
"groupBy": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ServiceName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ServiceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"having": [],
|
||||
"legend": "{{ServiceName}} ({{ClusterName}})",
|
||||
"limit": null,
|
||||
"orderBy": [],
|
||||
"queryName": "A",
|
||||
"reduceTo": "avg",
|
||||
"spaceAggregation": "min",
|
||||
"stepInterval": 60,
|
||||
"timeAggregation": "min"
|
||||
}
|
||||
],
|
||||
"queryFormulas": []
|
||||
},
|
||||
"clickhouse_sql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"id": "c89482b3-5a98-4e2c-be0d-ef036d7dac05",
|
||||
"promql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"queryType": "builder"
|
||||
},
|
||||
"selectedLogFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "body",
|
||||
"type": ""
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "timestamp",
|
||||
"type": ""
|
||||
}
|
||||
],
|
||||
"selectedTracesFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "serviceName--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "serviceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "name--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "name",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "float64",
|
||||
"id": "durationNano--float64--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "durationNano",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "httpMethod--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "httpMethod",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "responseStatusCode--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "responseStatusCode",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"softMax": 0,
|
||||
"softMin": 0,
|
||||
"stackedBarChart": false,
|
||||
"thresholds": [],
|
||||
"timePreferance": "GLOBAL_TIME",
|
||||
"title": "Minimum CPU Utilization",
|
||||
"yAxisUnit": "none"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,818 @@
|
||||
{
|
||||
"description": "View key AWS SNS metrics with an out of the box dashboard.",
|
||||
"image": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iODBweCIgaGVpZ2h0PSI4MHB4IiB2aWV3Qm94PSIwIDAgODAgODAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDY0ICg5MzUzNykgLSBodHRwczovL3NrZXRjaC5jb20gLS0+CiAgICA8dGl0bGU+SWNvbi1BcmNoaXRlY3R1cmUvNjQvQXJjaF9BV1MtU2ltcGxlLU5vdGlmaWNhdGlvbi1TZXJ2aWNlXzY0PC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+CiAgICAgICAgPGxpbmVhckdyYWRpZW50IHgxPSIwJSIgeTE9IjEwMCUiIHgyPSIxMDAlIiB5Mj0iMCUiIGlkPSJsaW5lYXJHcmFkaWVudC0xIj4KICAgICAgICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iI0IwMDg0RCIgb2Zmc2V0PSIwJSI+PC9zdG9wPgogICAgICAgICAgICA8c3RvcCBzdG9wLWNvbG9yPSIjRkY0RjhCIiBvZmZzZXQ9IjEwMCUiPjwvc3RvcD4KICAgICAgICA8L2xpbmVhckdyYWRpZW50PgogICAgPC9kZWZzPgogICAgPGcgaWQ9Ikljb24tQXJjaGl0ZWN0dXJlLzY0L0FyY2hfQVdTLVNpbXBsZS1Ob3RpZmljYXRpb24tU2VydmljZV82NCIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPGcgaWQ9Ikljb24tQXJjaGl0ZWN0dXJlLUJHLzY0L0FwcGxpY2F0aW9uLUludGVncmF0aW9uIiBmaWxsPSJ1cmwoI2xpbmVhckdyYWRpZW50LTEpIj4KICAgICAgICAgICAgPHJlY3QgaWQ9IlJlY3RhbmdsZSIgeD0iMCIgeT0iMCIgd2lkdGg9IjgwIiBoZWlnaHQ9IjgwIj48L3JlY3Q+CiAgICAgICAgPC9nPgogICAgICAgIDxwYXRoIGQ9Ik0xNywzOCBDMTguMTAzLDM4IDE5LDM4Ljg5NyAxOSw0MCBDMTksNDEuMTAzIDE4LjEwMyw0MiAxNyw0MiBDMTUuODk3LDQyIDE1LDQxLjEwMyAxNSw0MCBDMTUsMzguODk3IDE1Ljg5NywzOCAxNywzOCBMMTcsMzggWiBNNDEsNjQgQzI5LjMxNCw2NCAxOS4yODksNTUuNDY2IDE3LjE5NCw0My45OCBDMTguOTY1LDQzLjg5NCAyMC40MjcsNDIuNjU5IDIwLjg1Nyw0MSBMMjcsNDEgTDI3LDM5IEwyMC44NTcsMzkgQzIwLjQyNywzNy4zNDIgMTguOTY2LDM2LjEwNyAxNy4xOTUsMzYuMDIgQzE5LjI4NSwyNC43MSAyOS41MTEsMTYgNDEsMTYgQzQ1LjMxMywxNiA0OS44MzIsMTcuNjIyIDU0LjQyOSwyMC44MjEgTDU1LjU3MSwxOS4xNzkgQzUwLjYzMywxNS43NDMgNDUuNzMsMTQgNDEsMTQgQzI4LjI3LDE0IDE2Ljk0OSwyMy44NjUgMTUuMDYzLDM2LjUyMSBDMTMuODM5LDM3LjIwNyAxMywzOC41IDEzLDQwIEMxMyw0MS41IDEzLjgzOSw0Mi43OTMgMTUuMDYzLDQzLjQ3OCBDMTYuOTcsNTYuMzQxIDI4LjA1Niw2NiA0MSw2NiBDNDYuNDA3LDY2IDUxLjk0Miw2NC4xNTcgNTYuNTg1LDYwLjgxMSBMNTUuNDE1LDU5LjE4OSBDNTEuMTEsNjIuMjkyIDQ1Ljk5MSw2NCA0MSw2NCBMNDEsNjQgWiBNMzAuMTAxLDM2LjQ0MiBDMzEuOTU1LDM2Ljg5NSAzNC4yNzUsMzcgMzYsMzcgQzM3LjY0MiwzNyAzOS44MjMsMzYuOTA1IDQxLjYyOSwzNi41MDYgTDM3LjEwNSw0NS41NTMgQzM3LjAzNiw0NS42OTEgMzcsNDUuODQ1IDM3LDQ2IEwzNyw1MC40NTMgQzM2LjE5OSw1MC45NjQgMzQuODMzLDUxLjgxMiAzNCw1MS45ODYgTDM0LDQ2IEMzNCw0NS44NjggMzMuOTc0LDQ1LjczNyAzMy45MjMsNDUuNjE1IEwzMC4xMDEsMzYuNDQyIFogTTM2LDMzIEM0MC4wMjUsMzMgNDIuMTc0LDMzLjYwNCA0Mi44NDEsMzQgQzQyLjE3NCwzNC4zOTYgNDAuMDI1LDM1IDM2LDM1IEMzMS45NzUsMzUgMjkuODI2LDM0LjM5NiAyOS4xNTksMzQgQzI5LjgyNiwzMy42MDQgMzEuOTc1LDMzIDM2LDMzIEwzNiwzMyBaIE0zMyw1NCBMMzQsNTQgQzM0LjA0Myw1NCAzNC4wODYsNTMuOTk3IDM0LjEyOCw1My45OTIgQzM1LjM1Miw1My44MzMgMzYuOTA5LDUyLjg4NyAzOC4yNzIsNTIuMDEzIEwzOC41MzUsNTEuODQ1IEMzOC44MjQsNTEuNjYxIDM5LDUxLjM0MiAzOSw1MSBMMzksNDYuMjM2IEw0NC41NTksMzUuMTIgQzQ0LjgzMywzNC44MDEgNDUsMzQuNDM0IDQ1LDM0IEM0NSwzMS4zOSAzOS4zNjEsMzEgMzYsMzEgQzMyLjYzOSwzMSAyNywzMS4zOSAyNywzNCBDMjcsMzQuMzY2IDI3LjEyLDM0LjY4NCAyNy4zMiwzNC45NjcgTDMyLDQ2LjIgTDMyLDUzIEMzMiw1My41NTIgMzIuNDQ3LDU0IDMzLDU0IEwzMyw1NCBaIE02Miw1MyBDNjMuMTAzLDUzIDY0LDUzLjg5NyA2NCw1NSBDNjQsNTYuMTAzIDYzLjEwMyw1NyA2Miw1NyBDNjAuODk3LDU3IDYwLDU2LjEwMyA2MCw1NSBDNjAsNTMuODk3IDYwLjg5Nyw1MyA2Miw1MyBMNjIsNTMgWiBNNjIsMjMgQzYzLjEwMywyMyA2NCwyMy44OTcgNjQsMjUgQzY0LDI2LjEwMyA2My4xMDMsMjcgNjIsMjcgQzYwLjg5NywyNyA2MCwyNi4xMDMgNjAsMjUgQzYwLDIzLjg5NyA2MC44OTcsMjMgNjIsMjMgTDYyLDIzIFogTTY0LDM4IEM2NS4xMDMsMzggNjYsMzguODk3IDY2LDQwIEM2Niw0MS4xMDMgNjUuMTAzLDQyIDY0LDQyIEM2Mi44OTcsNDIgNjIsNDEuMTAzIDYyLDQwIEM2MiwzOC44OTcgNjIuODk3LDM4IDY0LDM4IEw2NCwzOCBaIE01NCw0MSBMNjAuMTQzLDQxIEM2MC41ODksNDIuNzIgNjIuMTQyLDQ0IDY0LDQ0IEM2Ni4yMDYsNDQgNjgsNDIuMjA2IDY4LDQwIEM2OCwzNy43OTQgNjYuMjA2LDM2IDY0LDM2IEM2Mi4xNDIsMzYgNjAuNTg5LDM3LjI4IDYwLjE0MywzOSBMNTQsMzkgTDU0LDI2IEw1OC4xNDMsMjYgQzU4LjU4OSwyNy43MiA2MC4xNDIsMjkgNjIsMjkgQzY0LjIwNiwyOSA2NiwyNy4yMDYgNjYsMjUgQzY2LDIyLjc5NCA2NC4yMDYsMjEgNjIsMjEgQzYwLjE0MiwyMSA1OC41ODksMjIuMjggNTguMTQzLDI0IEw1MywyNCBDNTIuNDQ3LDI0IDUyLDI0LjQ0OCA1MiwyNSBMNTIsMzkgTDQ1LDM5IEw0NSw0MSBMNTIsNDEgTDUyLDU1IEM1Miw1NS41NTIgNTIuNDQ3LDU2IDUzLDU2IEw1OC4xNDMsNTYgQzU4LjU4OSw1Ny43MiA2MC4xNDIsNTkgNjIsNTkgQzY0LjIwNiw1OSA2Niw1Ny4yMDYgNjYsNTUgQzY2LDUyLjc5NCA2NC4yMDYsNTEgNjIsNTEgQzYwLjE0Miw1MSA1OC41ODksNTIuMjggNTguMTQzLDU0IEw1NCw1NCBMNTQsNDEgWiIgaWQ9IkFXUy1TaW1wbGUtTm90aWZpY2F0aW9uLVNlcnZpY2VfSWNvbl82NF9TcXVpZCIgZmlsbD0iI0ZGRkZGRiI+PC9wYXRoPgogICAgPC9nPgo8L3N2Zz4=",
|
||||
"layout": [
|
||||
{
|
||||
"h": 6,
|
||||
"i": "4eb87f89-0213-4773-9b06-6aecc6701898",
|
||||
"moved": false,
|
||||
"static": false,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"h": 6,
|
||||
"i": "7a010b4e-ea7c-4a45-a9eb-93af650c45b4",
|
||||
"moved": false,
|
||||
"static": false,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"h": 6,
|
||||
"i": "2299d4e3-6c40-4bf2-a550-c7bb8a7acd38",
|
||||
"moved": false,
|
||||
"static": false,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 6
|
||||
},
|
||||
{
|
||||
"h": 6,
|
||||
"i": "16eec8b7-de1a-4039-b180-24c7a6704b6e",
|
||||
"moved": false,
|
||||
"static": false,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 6
|
||||
}
|
||||
],
|
||||
"panelMap": {},
|
||||
"tags": [],
|
||||
"title": "SNS Overview",
|
||||
"uploadedGrafana": false,
|
||||
"variables": {
|
||||
"51f4fa2b-89c7-47c2-9795-f32cffaab985": {
|
||||
"allSelected": false,
|
||||
"customValue": "",
|
||||
"description": "AWS Account ID",
|
||||
"id": "51f4fa2b-89c7-47c2-9795-f32cffaab985",
|
||||
"key": "51f4fa2b-89c7-47c2-9795-f32cffaab985",
|
||||
"modificationUUID": "b7a6b06b-fa1f-4fb8-b70e-6bd9b350f29e",
|
||||
"multiSelect": false,
|
||||
"name": "Account",
|
||||
"order": 0,
|
||||
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.account.id') AS `cloud.account.id`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_SNS_PublishSize_count' GROUP BY `cloud.account.id`",
|
||||
"showALLOption": false,
|
||||
"sort": "DISABLED",
|
||||
"textboxValue": "",
|
||||
"type": "QUERY"
|
||||
},
|
||||
"9faf0f4b-b245-4b3c-83a3-60cfa76dfeb0": {
|
||||
"allSelected": false,
|
||||
"customValue": "",
|
||||
"description": "Account Region",
|
||||
"id": "9faf0f4b-b245-4b3c-83a3-60cfa76dfeb0",
|
||||
"key": "9faf0f4b-b245-4b3c-83a3-60cfa76dfeb0",
|
||||
"modificationUUID": "8428a5de-bfd1-4a69-9601-63e3041cd556",
|
||||
"multiSelect": false,
|
||||
"name": "Region",
|
||||
"order": 1,
|
||||
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.region') AS region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_SNS_PublishSize_count' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} GROUP BY region",
|
||||
"showALLOption": false,
|
||||
"sort": "ASC",
|
||||
"textboxValue": "",
|
||||
"type": "QUERY"
|
||||
},
|
||||
"bfbdbcbe-a168-4d81-b108-36339e249116": {
|
||||
"allSelected": true,
|
||||
"customValue": "",
|
||||
"description": "SNS Topic Name",
|
||||
"id": "bfbdbcbe-a168-4d81-b108-36339e249116",
|
||||
"modificationUUID": "dfed7272-16dc-4eb6-99bf-7c82fc8e04f0",
|
||||
"multiSelect": true,
|
||||
"name": "Topic",
|
||||
"order": 2,
|
||||
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'TopicName') AS topic\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_SNS_PublishSize_count' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} AND JSONExtractString(labels, 'cloud.region') IN {{.Region}}\nGROUP BY topic",
|
||||
"showALLOption": true,
|
||||
"sort": "ASC",
|
||||
"textboxValue": "",
|
||||
"type": "QUERY"
|
||||
}
|
||||
},
|
||||
"version": "v4",
|
||||
"widgets": [
|
||||
{
|
||||
"bucketCount": 30,
|
||||
"bucketWidth": 0,
|
||||
"columnUnits": {},
|
||||
"description": "",
|
||||
"fillSpans": false,
|
||||
"id": "4eb87f89-0213-4773-9b06-6aecc6701898",
|
||||
"isLogScale": false,
|
||||
"isStacked": false,
|
||||
"mergeAllActiveQueries": false,
|
||||
"nullZeroValues": "zero",
|
||||
"opacity": "1",
|
||||
"panelTypes": "graph",
|
||||
"query": {
|
||||
"builder": {
|
||||
"queryData": [
|
||||
{
|
||||
"aggregateAttribute": {
|
||||
"dataType": "float64",
|
||||
"id": "aws_SNS_NumberOfMessagesPublished_max--float64--Gauge--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "aws_SNS_NumberOfMessagesPublished_max",
|
||||
"type": "Gauge"
|
||||
},
|
||||
"aggregateOperator": "max",
|
||||
"dataSource": "metrics",
|
||||
"disabled": false,
|
||||
"expression": "A",
|
||||
"filters": {
|
||||
"items": [
|
||||
{
|
||||
"id": "8fd51b53",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "TopicName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "TopicName",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "in",
|
||||
"value": [
|
||||
"$Topic"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "b18187c3",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.region--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.region",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Region"
|
||||
},
|
||||
{
|
||||
"id": "eebe4578",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.account.id--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.account.id",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Account"
|
||||
}
|
||||
],
|
||||
"op": "AND"
|
||||
},
|
||||
"functions": [],
|
||||
"groupBy": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "TopicName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "TopicName",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"having": [],
|
||||
"legend": "{{TopicName}}",
|
||||
"limit": null,
|
||||
"orderBy": [],
|
||||
"queryName": "A",
|
||||
"reduceTo": "avg",
|
||||
"spaceAggregation": "max",
|
||||
"stepInterval": 60,
|
||||
"timeAggregation": "max"
|
||||
}
|
||||
],
|
||||
"queryFormulas": []
|
||||
},
|
||||
"clickhouse_sql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"id": "9c67615a-55f7-42da-835c-86922f2ff8bb",
|
||||
"promql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"queryType": "builder"
|
||||
},
|
||||
"selectedLogFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "body",
|
||||
"type": ""
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "timestamp",
|
||||
"type": ""
|
||||
}
|
||||
],
|
||||
"selectedTracesFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "serviceName--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "serviceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "name--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "name",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "float64",
|
||||
"id": "durationNano--float64--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "durationNano",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "httpMethod--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "httpMethod",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "responseStatusCode--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "responseStatusCode",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"softMax": 0,
|
||||
"softMin": 0,
|
||||
"stackedBarChart": false,
|
||||
"thresholds": [],
|
||||
"timePreferance": "GLOBAL_TIME",
|
||||
"title": "Number of Messages Published",
|
||||
"yAxisUnit": "none"
|
||||
},
|
||||
{
|
||||
"bucketCount": 30,
|
||||
"bucketWidth": 0,
|
||||
"columnUnits": {},
|
||||
"description": "",
|
||||
"fillSpans": false,
|
||||
"id": "7a010b4e-ea7c-4a45-a9eb-93af650c45b4",
|
||||
"isLogScale": false,
|
||||
"isStacked": false,
|
||||
"mergeAllActiveQueries": false,
|
||||
"nullZeroValues": "zero",
|
||||
"opacity": "1",
|
||||
"panelTypes": "graph",
|
||||
"query": {
|
||||
"builder": {
|
||||
"queryData": [
|
||||
{
|
||||
"aggregateAttribute": {
|
||||
"dataType": "float64",
|
||||
"id": "aws_SNS_PublishSize_max--float64--Gauge--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "aws_SNS_PublishSize_max",
|
||||
"type": "Gauge"
|
||||
},
|
||||
"aggregateOperator": "max",
|
||||
"dataSource": "metrics",
|
||||
"disabled": false,
|
||||
"expression": "A",
|
||||
"filters": {
|
||||
"items": [
|
||||
{
|
||||
"id": "1aa0d1a9",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "TopicName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "TopicName",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "in",
|
||||
"value": [
|
||||
"$Topic"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "62255cff",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.region--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.region",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Region"
|
||||
},
|
||||
{
|
||||
"id": "17c7153e",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.account.id--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.account.id",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Account"
|
||||
}
|
||||
],
|
||||
"op": "AND"
|
||||
},
|
||||
"functions": [],
|
||||
"groupBy": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "TopicName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "TopicName",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"having": [],
|
||||
"legend": "{{TopicName}}",
|
||||
"limit": null,
|
||||
"orderBy": [],
|
||||
"queryName": "A",
|
||||
"reduceTo": "avg",
|
||||
"spaceAggregation": "max",
|
||||
"stepInterval": 60,
|
||||
"timeAggregation": "max"
|
||||
}
|
||||
],
|
||||
"queryFormulas": []
|
||||
},
|
||||
"clickhouse_sql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"id": "a635a15b-dfe6-4617-a82e-29d93e27deaf",
|
||||
"promql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"queryType": "builder"
|
||||
},
|
||||
"selectedLogFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "body",
|
||||
"type": ""
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "timestamp",
|
||||
"type": ""
|
||||
}
|
||||
],
|
||||
"selectedTracesFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "serviceName--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "serviceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "name--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "name",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "float64",
|
||||
"id": "durationNano--float64--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "durationNano",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "httpMethod--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "httpMethod",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "responseStatusCode--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "responseStatusCode",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"softMax": 0,
|
||||
"softMin": 0,
|
||||
"stackedBarChart": false,
|
||||
"thresholds": [],
|
||||
"timePreferance": "GLOBAL_TIME",
|
||||
"title": "Published Message Size",
|
||||
"yAxisUnit": "decbytes"
|
||||
},
|
||||
{
|
||||
"bucketCount": 30,
|
||||
"bucketWidth": 0,
|
||||
"columnUnits": {},
|
||||
"description": "",
|
||||
"fillSpans": false,
|
||||
"id": "2299d4e3-6c40-4bf2-a550-c7bb8a7acd38",
|
||||
"isLogScale": false,
|
||||
"isStacked": false,
|
||||
"mergeAllActiveQueries": false,
|
||||
"nullZeroValues": "zero",
|
||||
"opacity": "1",
|
||||
"panelTypes": "graph",
|
||||
"query": {
|
||||
"builder": {
|
||||
"queryData": [
|
||||
{
|
||||
"aggregateAttribute": {
|
||||
"dataType": "float64",
|
||||
"id": "aws_SNS_NumberOfNotificationsDelivered_max--float64--Gauge--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "aws_SNS_NumberOfNotificationsDelivered_max",
|
||||
"type": "Gauge"
|
||||
},
|
||||
"aggregateOperator": "max",
|
||||
"dataSource": "metrics",
|
||||
"disabled": false,
|
||||
"expression": "A",
|
||||
"filters": {
|
||||
"items": [
|
||||
{
|
||||
"id": "c96a4ac0",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "TopicName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "TopicName",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "in",
|
||||
"value": [
|
||||
"$Topic"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "8ca86829",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.region--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.region",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Region"
|
||||
},
|
||||
{
|
||||
"id": "8a444f66",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.account.id--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.account.id",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Account"
|
||||
}
|
||||
],
|
||||
"op": "AND"
|
||||
},
|
||||
"functions": [],
|
||||
"groupBy": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "TopicName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "TopicName",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"having": [],
|
||||
"legend": "{{TopicName}}",
|
||||
"limit": null,
|
||||
"orderBy": [],
|
||||
"queryName": "A",
|
||||
"reduceTo": "avg",
|
||||
"spaceAggregation": "max",
|
||||
"stepInterval": 60,
|
||||
"timeAggregation": "max"
|
||||
}
|
||||
],
|
||||
"queryFormulas": []
|
||||
},
|
||||
"clickhouse_sql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"id": "0d2fc26c-9b21-4dfc-b631-64b7c8d3bd71",
|
||||
"promql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"queryType": "builder"
|
||||
},
|
||||
"selectedLogFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "body",
|
||||
"type": ""
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "timestamp",
|
||||
"type": ""
|
||||
}
|
||||
],
|
||||
"selectedTracesFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "serviceName--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "serviceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "name--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "name",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "float64",
|
||||
"id": "durationNano--float64--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "durationNano",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "httpMethod--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "httpMethod",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "responseStatusCode--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "responseStatusCode",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"softMax": 0,
|
||||
"softMin": 0,
|
||||
"stackedBarChart": false,
|
||||
"thresholds": [],
|
||||
"timePreferance": "GLOBAL_TIME",
|
||||
"title": "Number of Notifications Delivered",
|
||||
"yAxisUnit": "none"
|
||||
},
|
||||
{
|
||||
"bucketCount": 30,
|
||||
"bucketWidth": 0,
|
||||
"columnUnits": {},
|
||||
"description": "",
|
||||
"fillSpans": false,
|
||||
"id": "16eec8b7-de1a-4039-b180-24c7a6704b6e",
|
||||
"isLogScale": false,
|
||||
"isStacked": false,
|
||||
"mergeAllActiveQueries": false,
|
||||
"nullZeroValues": "zero",
|
||||
"opacity": "1",
|
||||
"panelTypes": "graph",
|
||||
"query": {
|
||||
"builder": {
|
||||
"queryData": [
|
||||
{
|
||||
"aggregateAttribute": {
|
||||
"dataType": "float64",
|
||||
"id": "aws_SNS_NumberOfNotificationsFailed_max--float64--Gauge--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "aws_SNS_NumberOfNotificationsFailed_max",
|
||||
"type": "Gauge"
|
||||
},
|
||||
"aggregateOperator": "max",
|
||||
"dataSource": "metrics",
|
||||
"disabled": false,
|
||||
"expression": "A",
|
||||
"filters": {
|
||||
"items": [
|
||||
{
|
||||
"id": "6175f3d5",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "TopicName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "TopicName",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "in",
|
||||
"value": [
|
||||
"$Topic"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "e2084931",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.region--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.region",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Region"
|
||||
},
|
||||
{
|
||||
"id": "0b05209a",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.account.id--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.account.id",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Account"
|
||||
}
|
||||
],
|
||||
"op": "AND"
|
||||
},
|
||||
"functions": [],
|
||||
"groupBy": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "TopicName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "TopicName",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"having": [],
|
||||
"legend": "{{TopicName}}",
|
||||
"limit": null,
|
||||
"orderBy": [],
|
||||
"queryName": "A",
|
||||
"reduceTo": "avg",
|
||||
"spaceAggregation": "max",
|
||||
"stepInterval": 60,
|
||||
"timeAggregation": "max"
|
||||
}
|
||||
],
|
||||
"queryFormulas": []
|
||||
},
|
||||
"clickhouse_sql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"id": "526247af-6ac9-42ff-83e9-cce0e32a9e63",
|
||||
"promql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"queryType": "builder"
|
||||
},
|
||||
"selectedLogFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "body",
|
||||
"type": ""
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "timestamp",
|
||||
"type": ""
|
||||
}
|
||||
],
|
||||
"selectedTracesFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "serviceName--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "serviceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "name--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "name",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "float64",
|
||||
"id": "durationNano--float64--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "durationNano",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "httpMethod--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "httpMethod",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "responseStatusCode--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "responseStatusCode",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"softMax": 0,
|
||||
"softMin": 0,
|
||||
"stackedBarChart": false,
|
||||
"thresholds": [],
|
||||
"timePreferance": "GLOBAL_TIME",
|
||||
"title": "Number of Notifications Failed",
|
||||
"yAxisUnit": "none"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
284
pkg/sqlmigration/086_migrate_cloud_integration_dashboards.go
Normal file
284
pkg/sqlmigration/086_migrate_cloud_integration_dashboards.go
Normal file
@@ -0,0 +1,284 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
//go:embed 086_cloud_integration_dashboards
|
||||
var cloudIntegrationDashboardFiles embed.FS
|
||||
|
||||
var (
|
||||
cloudProviderAWS = valuer.NewString("aws")
|
||||
cloudProviderAzure = valuer.NewString("azure")
|
||||
integrationSource = valuer.NewString("integration")
|
||||
cloudIntegrationProvider = valuer.NewString("cloud_integration")
|
||||
)
|
||||
|
||||
type migrateCloudIntegrationDashboards struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
}
|
||||
|
||||
type cloudIntegrationAccountRow struct {
|
||||
bun.BaseModel `bun:"table:cloud_integration"`
|
||||
|
||||
ID string `bun:"id"`
|
||||
OrgID string `bun:"org_id"`
|
||||
Provider string `bun:"provider"`
|
||||
}
|
||||
|
||||
type cloudIntegrationServiceRow struct {
|
||||
bun.BaseModel `bun:"table:cloud_integration_service"`
|
||||
|
||||
Type string `bun:"type"`
|
||||
Config string `bun:"config"`
|
||||
CloudIntegrationID string `bun:"cloud_integration_id"`
|
||||
}
|
||||
|
||||
type cloudIntegrationAWSServiceConfig struct {
|
||||
Metrics *cloudIntegrationMetricsConfig `json:"metrics"`
|
||||
}
|
||||
|
||||
type cloudIntegrationAzureServiceConfig struct {
|
||||
Metrics *cloudIntegrationMetricsConfig `json:"metrics"`
|
||||
}
|
||||
|
||||
type cloudIntegrationMetricsConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type cloudIntegrationDashboardRow struct {
|
||||
bun.BaseModel `bun:"table:dashboard"`
|
||||
|
||||
ID string `bun:"id,pk,type:text"`
|
||||
CreatedAt time.Time `bun:"created_at"`
|
||||
UpdatedAt time.Time `bun:"updated_at"`
|
||||
CreatedBy *string `bun:"created_by,type:text"`
|
||||
UpdatedBy *string `bun:"updated_by,type:text"`
|
||||
Data string `bun:"data,type:text"`
|
||||
Locked bool `bun:"locked"`
|
||||
OrgID string `bun:"org_id,type:text"`
|
||||
Source string `bun:"source,type:text"`
|
||||
}
|
||||
|
||||
type cloudIntegrationAccountMeta struct {
|
||||
orgID string
|
||||
provider string
|
||||
}
|
||||
|
||||
type cloudIntegrationOrgService struct {
|
||||
orgID string
|
||||
provider string
|
||||
serviceID string
|
||||
}
|
||||
|
||||
type integrationDashboardRow struct {
|
||||
bun.BaseModel `bun:"table:integration_dashboard"`
|
||||
|
||||
ID string `bun:"id,pk,type:text"`
|
||||
DashboardID string `bun:"dashboard_id,type:text"`
|
||||
Provider string `bun:"provider,type:text"`
|
||||
Slug string `bun:"slug,type:text"`
|
||||
CreatedAt time.Time `bun:"created_at"`
|
||||
UpdatedAt time.Time `bun:"updated_at"`
|
||||
}
|
||||
|
||||
func NewMigrateCloudIntegrationDashboardsFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(
|
||||
// migrate_cloud_integration_dashboards name is intentionally kept short to avoid hitting identifier length limits
|
||||
factory.MustNewName("migrate_ci_dashboards"),
|
||||
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &migrateCloudIntegrationDashboards{sqlstore: sqlstore}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (m *migrateCloudIntegrationDashboards) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(m.Up, m.Down)
|
||||
}
|
||||
|
||||
func (m *migrateCloudIntegrationDashboards) Up(ctx context.Context, db *bun.DB) error {
|
||||
dashboardDefs, err := m.loadDashboardDefs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
var accounts []*cloudIntegrationAccountRow
|
||||
if err := tx.NewSelect().
|
||||
Model(&accounts).
|
||||
Where("removed_at IS NULL").
|
||||
Where("account_id IS NOT NULL").
|
||||
Scan(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
accountsMap := make(map[string]cloudIntegrationAccountMeta, len(accounts))
|
||||
for _, a := range accounts {
|
||||
accountsMap[a.ID] = cloudIntegrationAccountMeta{orgID: a.OrgID, provider: a.Provider}
|
||||
}
|
||||
|
||||
var services []*cloudIntegrationServiceRow
|
||||
if err := tx.NewSelect().Model(&services).Scan(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
var toProvision []cloudIntegrationOrgService
|
||||
|
||||
for _, svc := range services {
|
||||
meta, ok := accountsMap[svc.CloudIntegrationID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if !m.isMetricsEnabled(svc.Config, meta.provider) {
|
||||
continue
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s|%s|%s", meta.orgID, meta.provider, svc.Type)
|
||||
if _, dup := seen[key]; dup {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
toProvision = append(toProvision, cloudIntegrationOrgService{
|
||||
orgID: meta.orgID,
|
||||
provider: meta.provider,
|
||||
serviceID: svc.Type,
|
||||
})
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
for _, service := range toProvision {
|
||||
serviceDashboards, ok := dashboardDefs[service.provider][service.serviceID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for dashName, dashboardJSON := range serviceDashboards {
|
||||
slug := fmt.Sprintf("%s-%s-%s", service.provider, service.serviceID, dashName)
|
||||
|
||||
count, err := tx.NewSelect().
|
||||
TableExpr("integration_dashboard AS id").
|
||||
Join("JOIN dashboard AS d ON id.dashboard_id = d.id").
|
||||
Where("d.org_id = ?", service.orgID).
|
||||
Where("id.provider = ?", "cloud_integration").
|
||||
Where("id.slug = ?", slug).
|
||||
Count(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
dashID := valuer.GenerateUUID().StringValue()
|
||||
|
||||
dashRow := &cloudIntegrationDashboardRow{
|
||||
ID: dashID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Data: string(dashboardJSON),
|
||||
Locked: true,
|
||||
OrgID: service.orgID,
|
||||
Source: integrationSource.StringValue(),
|
||||
}
|
||||
if _, err := tx.NewInsert().Model(dashRow).Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
intRow := &integrationDashboardRow{
|
||||
ID: valuer.GenerateUUID().StringValue(),
|
||||
DashboardID: dashID,
|
||||
Provider: cloudIntegrationProvider.StringValue(),
|
||||
Slug: slug,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if _, err := tx.NewInsert().Model(intRow).Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (m *migrateCloudIntegrationDashboards) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *migrateCloudIntegrationDashboards) loadDashboardDefs() (map[string]map[string]map[string]json.RawMessage, error) {
|
||||
result := make(map[string]map[string]map[string]json.RawMessage)
|
||||
|
||||
err := fs.WalkDir(cloudIntegrationDashboardFiles, "086_cloud_integration_dashboards", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() || filepath.Ext(path) != ".json" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// path: 086_cloud_integration_dashboards/{provider}/{service}/{file}.json
|
||||
rel := strings.TrimPrefix(path, "086_cloud_integration_dashboards/")
|
||||
parts := strings.SplitN(rel, "/", 3)
|
||||
if len(parts) != 3 {
|
||||
return nil
|
||||
}
|
||||
provider := parts[0]
|
||||
serviceID := parts[1]
|
||||
dashName := strings.TrimSuffix(parts[2], ".json")
|
||||
|
||||
data, err := cloudIntegrationDashboardFiles.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result[provider] == nil {
|
||||
result[provider] = make(map[string]map[string]json.RawMessage)
|
||||
}
|
||||
if result[provider][serviceID] == nil {
|
||||
result[provider][serviceID] = make(map[string]json.RawMessage)
|
||||
}
|
||||
result[provider][serviceID][dashName] = json.RawMessage(data)
|
||||
return nil
|
||||
})
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (m *migrateCloudIntegrationDashboards) isMetricsEnabled(configJSON string, provider string) bool {
|
||||
switch provider {
|
||||
case cloudProviderAWS.String():
|
||||
cfg := new(cloudIntegrationAWSServiceConfig)
|
||||
if err := json.Unmarshal([]byte(configJSON), cfg); err != nil {
|
||||
return false
|
||||
}
|
||||
return cfg.Metrics != nil && cfg.Metrics.Enabled
|
||||
case cloudProviderAzure.String():
|
||||
cfg := new(cloudIntegrationAzureServiceConfig)
|
||||
if err := json.Unmarshal([]byte(configJSON), cfg); err != nil {
|
||||
return false
|
||||
}
|
||||
return cfg.Metrics != nil && cfg.Metrics.Enabled
|
||||
}
|
||||
return false
|
||||
}
|
||||
44
pkg/types/cloudintegrationtypes/integration_dashboard.go
Normal file
44
pkg/types/cloudintegrationtypes/integration_dashboard.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package cloudintegrationtypes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type IntegrationDashboardProviderType struct{ valuer.String }
|
||||
|
||||
var IntegrationDashboardProviderCloudIntegration = IntegrationDashboardProviderType{valuer.NewString("cloud_integration")}
|
||||
|
||||
type StorableIntegrationDashboard struct {
|
||||
bun.BaseModel `bun:"table:integration_dashboard"`
|
||||
|
||||
ID string `bun:"id,pk,type:text"`
|
||||
DashboardID string `bun:"dashboard_id,type:text"`
|
||||
Provider string `bun:"provider,type:text"`
|
||||
Slug string `bun:"slug,type:text"`
|
||||
CreatedAt time.Time `bun:"created_at"`
|
||||
UpdatedAt time.Time `bun:"updated_at"`
|
||||
}
|
||||
|
||||
func NewStorableIntegrationDashboard(dashboardID string, provider IntegrationDashboardProviderType, slug string) *StorableIntegrationDashboard {
|
||||
now := time.Now()
|
||||
return &StorableIntegrationDashboard{
|
||||
ID: valuer.GenerateUUID().StringValue(),
|
||||
DashboardID: dashboardID,
|
||||
Provider: provider.StringValue(),
|
||||
Slug: slug,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
}
|
||||
|
||||
func IntegrationDashboardSlug(provider CloudProviderType, serviceID ServiceID, dashName string) string {
|
||||
return fmt.Sprintf("%s-%s-%s", provider.StringValue(), serviceID.StringValue(), dashName)
|
||||
}
|
||||
|
||||
func IntegrationDashboardSlugPrefix(provider CloudProviderType, serviceID ServiceID) string {
|
||||
return fmt.Sprintf("%s-%s-", provider.StringValue(), serviceID.StringValue())
|
||||
}
|
||||
@@ -2,8 +2,6 @@ package cloudintegrationtypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
@@ -318,56 +316,3 @@ func (updatableService *UpdatableService) UnmarshalJSON(data []byte) error {
|
||||
*updatableService = UpdatableService(temp)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UTILITIES
|
||||
|
||||
// GetCloudIntegrationDashboardID returns the dashboard id for a cloud integration, given the cloud provider, service id, and dashboard id.
|
||||
// This is used to generate unique dashboard ids for cloud integration, and also to parse the dashboard id to get the cloud provider and service id when needed.
|
||||
func GetCloudIntegrationDashboardID(cloudProvider CloudProviderType, svcID, dashboardID string) string {
|
||||
return fmt.Sprintf("cloud-integration--%s--%s--%s", cloudProvider.StringValue(), svcID, dashboardID)
|
||||
}
|
||||
|
||||
// ParseCloudIntegrationDashboardID parses a dashboard id generated by GetCloudIntegrationDashboardID
|
||||
// into its constituent parts (cloudProvider, serviceID, dashboardID).
|
||||
func ParseCloudIntegrationDashboardID(id string) (CloudProviderType, string, string, error) {
|
||||
parts := strings.SplitN(id, "--", 4)
|
||||
if len(parts) != 4 || parts[0] != "cloud-integration" {
|
||||
return CloudProviderType{}, "", "", errors.New(errors.TypeNotFound, ErrCodeCloudIntegrationNotFound, "invalid cloud integration dashboard id")
|
||||
}
|
||||
provider, err := NewCloudProvider(parts[1])
|
||||
if err != nil {
|
||||
return CloudProviderType{}, "", "", err
|
||||
}
|
||||
return provider, parts[2], parts[3], nil
|
||||
}
|
||||
|
||||
// GetDashboardsFromAssets returns the list of dashboards for the cloud provider service from definition.
|
||||
func GetDashboardsFromAssets(
|
||||
svcID string,
|
||||
orgID valuer.UUID,
|
||||
cloudProvider CloudProviderType,
|
||||
createdAt time.Time,
|
||||
assets Assets,
|
||||
) []*dashboardtypes.Dashboard {
|
||||
dashboards := make([]*dashboardtypes.Dashboard, 0)
|
||||
|
||||
for _, d := range assets.Dashboards {
|
||||
author := fmt.Sprintf("%s-integration", cloudProvider.StringValue())
|
||||
dashboards = append(dashboards, &dashboardtypes.Dashboard{
|
||||
ID: d.ID,
|
||||
Locked: true,
|
||||
OrgID: orgID,
|
||||
Data: d.Definition,
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: createdAt,
|
||||
},
|
||||
UserAuditable: types.UserAuditable{
|
||||
CreatedBy: author,
|
||||
UpdatedBy: author,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return dashboards
|
||||
}
|
||||
|
||||
@@ -45,7 +45,18 @@ type Store interface {
|
||||
// UpdateService updates an existing cloud integration service
|
||||
UpdateService(ctx context.Context, service *StorableCloudIntegrationService) error
|
||||
|
||||
// DeleteServicesByCloudIntegrationID deletes all services for the given cloud integration id
|
||||
DeleteServicesByCloudIntegrationID(ctx context.Context, cloudIntegrationID valuer.UUID) error
|
||||
|
||||
RunInTx(context.Context, func(ctx context.Context) error) error
|
||||
|
||||
CreateIntegrationDashboard(ctx context.Context, row *StorableIntegrationDashboard) error
|
||||
|
||||
GetIntegrationDashboardBySlug(ctx context.Context, orgID valuer.UUID, provider IntegrationDashboardProviderType, slug string) (*StorableIntegrationDashboard, error)
|
||||
|
||||
ListIntegrationDashboardsBySlugPrefix(ctx context.Context, orgID valuer.UUID, provider IntegrationDashboardProviderType, slugPrefix string) ([]*StorableIntegrationDashboard, error)
|
||||
|
||||
DeleteIntegrationDashboardBySlug(ctx context.Context, provider IntegrationDashboardProviderType, slug string) error
|
||||
}
|
||||
|
||||
type ServiceDefinitionStore interface {
|
||||
|
||||
@@ -19,6 +19,8 @@ var (
|
||||
ErrCodeDashboardNotFound = errors.MustNewCode("dashboard_not_found")
|
||||
ErrCodeDashboardInvalidData = errors.MustNewCode("dashboard_invalid_data")
|
||||
ErrCodeDashboardInvalidWidgetQuery = errors.MustNewCode("dashboard_invalid_widget_query")
|
||||
ErrCodeDashboardInvalidSource = errors.MustNewCode("dashboard_invalid_source")
|
||||
ErrCodeDashboardImmutable = errors.MustNewCode("dashboard_immutable")
|
||||
)
|
||||
|
||||
type StorableDashboard struct {
|
||||
@@ -30,6 +32,7 @@ type StorableDashboard struct {
|
||||
Data StorableDashboardData `bun:"data,type:text,notnull"`
|
||||
Locked bool `bun:"locked,notnull,default:false"`
|
||||
OrgID valuer.UUID `bun:"org_id,notnull"`
|
||||
Source Source `bun:"source,type:text,notnull"`
|
||||
}
|
||||
|
||||
type Dashboard struct {
|
||||
@@ -40,6 +43,7 @@ type Dashboard struct {
|
||||
Data StorableDashboardData `json:"data"`
|
||||
Locked bool `json:"locked"`
|
||||
OrgID valuer.UUID `json:"org_id"`
|
||||
Source Source `json:"source"`
|
||||
}
|
||||
|
||||
type LockUnlockDashboard struct {
|
||||
@@ -64,6 +68,10 @@ func NewStorableDashboardFromDashboard(dashboard *Dashboard) (*StorableDashboard
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid")
|
||||
}
|
||||
|
||||
if !dashboard.Source.IsValid() {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardInvalidSource, "invalid dashboard source %q, must be one of user, system, integration", dashboard.Source.StringValue())
|
||||
}
|
||||
|
||||
return &StorableDashboard{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: dashboardID,
|
||||
@@ -79,11 +87,21 @@ func NewStorableDashboardFromDashboard(dashboard *Dashboard) (*StorableDashboard
|
||||
OrgID: dashboard.OrgID,
|
||||
Data: dashboard.Data,
|
||||
Locked: dashboard.Locked,
|
||||
Source: dashboard.Source,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewDashboard(orgID valuer.UUID, createdBy string, storableDashboardData StorableDashboardData) (*Dashboard, error) {
|
||||
func NewDashboard(orgID valuer.UUID, createdBy string, source Source, storableDashboardData StorableDashboardData) (*Dashboard, error) {
|
||||
if !source.IsValid() {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardInvalidSource, "invalid dashboard source %q, must be one of user, system, integration", source.StringValue())
|
||||
}
|
||||
|
||||
currentTime := time.Now()
|
||||
locked := false
|
||||
|
||||
if source == SourceIntegration {
|
||||
locked = true
|
||||
}
|
||||
|
||||
return &Dashboard{
|
||||
ID: valuer.GenerateUUID().StringValue(),
|
||||
@@ -97,7 +115,8 @@ func NewDashboard(orgID valuer.UUID, createdBy string, storableDashboardData Sto
|
||||
},
|
||||
OrgID: orgID,
|
||||
Data: storableDashboardData,
|
||||
Locked: false,
|
||||
Locked: locked,
|
||||
Source: source,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -115,6 +134,7 @@ func NewDashboardFromStorableDashboard(storableDashboard *StorableDashboard) *Da
|
||||
OrgID: storableDashboard.OrgID,
|
||||
Data: storableDashboard.Data,
|
||||
Locked: storableDashboard.Locked,
|
||||
Source: storableDashboard.Source,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +167,7 @@ func NewGettableDashboardFromDashboard(dashboard *Dashboard) (*GettableDashboard
|
||||
OrgID: dashboard.OrgID,
|
||||
Data: dashboard.Data,
|
||||
Locked: dashboard.Locked,
|
||||
Source: dashboard.Source,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -238,6 +259,43 @@ func (storableDashboardData *StorableDashboardData) GetWidgetIds() []string {
|
||||
return widgetIds
|
||||
}
|
||||
|
||||
func (dashboard *Dashboard) ErrIfNotMutable() error {
|
||||
if dashboard.Source == SourceIntegration {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardImmutable, "integration dashboards cannot be modified")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dashboard *Dashboard) ErrIfNotDeletable() error {
|
||||
if err := dashboard.ErrIfNotMutable(); err != nil {
|
||||
return err
|
||||
}
|
||||
if dashboard.Source == SourceSystem {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardImmutable, "system dashboards cannot be deleted")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dashboard *Dashboard) ErrIfNotLockable() error {
|
||||
if err := dashboard.ErrIfNotMutable(); err != nil {
|
||||
return err
|
||||
}
|
||||
if dashboard.Source == SourceSystem {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardImmutable, "system dashboards cannot be locked or unlocked")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dashboard *Dashboard) ErrIfNotPublishable() error {
|
||||
if err := dashboard.ErrIfNotMutable(); err != nil {
|
||||
return err
|
||||
}
|
||||
if dashboard.Source == SourceSystem {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardImmutable, "system dashboards cannot be made public")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dashboard *Dashboard) CanUpdate(ctx context.Context, data StorableDashboardData, diff int) error {
|
||||
if dashboard.Locked {
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "cannot update a locked dashboard, please unlock the dashboard to update")
|
||||
|
||||
@@ -66,7 +66,7 @@ func TestCanUpdate_MultipleDeletions_ByDiff(t *testing.T) {
|
||||
initial := StorableDashboardData{
|
||||
"widgets": makeTestWidgets("a", "b", "c"),
|
||||
}
|
||||
d, err := NewDashboard(orgID, "tester", initial)
|
||||
d, err := NewDashboard(orgID, "tester", SourceUser, initial)
|
||||
assert.NoError(t, err)
|
||||
|
||||
updated := StorableDashboardData{
|
||||
|
||||
60
pkg/types/dashboardtypes/source.go
Normal file
60
pkg/types/dashboardtypes/source.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Source struct {
|
||||
s valuer.String
|
||||
}
|
||||
|
||||
var (
|
||||
SourceUser = Source{s: valuer.NewString("user")}
|
||||
SourceSystem = Source{s: valuer.NewString("system")}
|
||||
SourceIntegration = Source{s: valuer.NewString("integration")}
|
||||
)
|
||||
|
||||
func (Source) Enum() []any {
|
||||
return []any{SourceUser, SourceSystem, SourceIntegration}
|
||||
}
|
||||
|
||||
func (s Source) IsValid() bool {
|
||||
return slices.ContainsFunc(s.Enum(), func(v any) bool { return v == s })
|
||||
}
|
||||
|
||||
func (s Source) IsZero() bool { return s.s.IsZero() }
|
||||
|
||||
func (s Source) String() string { return s.s.String() }
|
||||
|
||||
func (s Source) StringValue() string { return s.s.StringValue() }
|
||||
|
||||
func (s Source) Value() (driver.Value, error) {
|
||||
if !s.IsValid() {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardInvalidSource, "invalid dashboard source %q, must be one of user, system, integration", s.s.StringValue())
|
||||
}
|
||||
return s.s.Value()
|
||||
}
|
||||
|
||||
func (s *Source) Scan(src any) error {
|
||||
return s.s.Scan(src)
|
||||
}
|
||||
|
||||
func (s Source) MarshalJSON() ([]byte, error) {
|
||||
return s.s.MarshalJSON()
|
||||
}
|
||||
|
||||
func (s *Source) UnmarshalJSON(data []byte) error {
|
||||
return s.s.UnmarshalJSON(data)
|
||||
}
|
||||
|
||||
func NewSource(source string) (Source, error) {
|
||||
candidate := Source{s: valuer.NewString(source)}
|
||||
if !candidate.IsValid() {
|
||||
return Source{}, errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardInvalidSource, "invalid dashboard source %q, must be one of user, system, integration", source)
|
||||
}
|
||||
return candidate, nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user