Compare commits

..

32 Commits

Author SHA1 Message Date
swapnil-signoz
40811b4cad feat(integrations): persist installed integration dashboards in DB
Provisions dashboard DB rows when an integration is installed and
deprovisions them on uninstall. Adds a backfill migration (087) for
users with already-installed integrations. Removes the on-the-fly
filesystem serving path from http_handler in favor of the standard
dashboard module.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 11:48:41 +05:30
swapnil-signoz
3ab7ab612c ci: golangci-lint fix 2026-05-21 23:41:00 +05:30
swapnil-signoz
3d9ab97856 Merge branch 'main' into feat/cloudintegrations-dashboards-migration 2026-05-21 23:36:03 +05:30
swapnil-signoz
4e6384c0c1 feat: adding ListSharedServices store method 2026-05-21 23:25:54 +05:30
SagarRajput-7
9c6656d6b9 fix(user-info): surfaced errors for reset password and fixed issues (#11389)
* fix(user-info): surfaced errors for reset password and fixed issues

* fix(user-info): removed notification from atnd and used toast and showerrormodal in userinfo

* fix(user-info): refactor and added tests

* fix(user-info): code refactor
2026-05-21 17:24:31 +00:00
Nikhil Mantri
5c54a2537c chore: arrays non-nullable (#11388) 2026-05-21 17:22:25 +00:00
Nikhil Mantri
bf201710a7 feat(infra-monitoring): allow order by primary name column in v2 apis (#11264)
Some checks failed
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
* chore: baseline setup

* chore: endpoint detail update

* chore: added logic for hosts v3 api

* fix: bug fix

* chore: disk usage

* chore: added validate function

* chore: added some unit tests

* chore: return status as a string

* chore: yarn generate api

* chore: removed isSendingK8sAgentsMetricsCode

* chore: moved funcs

* chore: added validation on order by

* chore: added pods list logic

* chore: updated openapi yml

* chore: updated spec

* chore: pods api meta start time

* chore: nil pointer check

* chore: nil pointer dereference fix in req.Filter

* chore: added temporalities of metrics

* chore: added pods metrics temporality

* chore: unified composite key function

* chore: code improvements

* chore: added pods list api updates

* chore: hostStatusNone added for clarity that this field can be left empty as well in payload

* chore: yarn generate api

* chore: return errors from getMetadata and lint fix

* chore: return errors from getMetadata and lint fix

* chore: added hostName logic

* chore: modified getMetadata query

* chore: add type for response and files rearrange

* chore: warnings added passing from queryResponse warning to host lists response struct

* chore: added better metrics existence check

* chore: added a TODO remark

* chore: added required metrics check

* chore: distributed samples table to local table change for get metadata

* chore: frontend fix

* chore: endpoint correction

* chore: endpoint modification openapi

* chore: escape backtick to prevent sql injection

* chore: rearrage

* chore: improvements

* chore: validate order by to validate function

* chore: improved description

* chore: added TODOs and made filterByStatus a part of filter struct

* chore: ignore empty string hosts in get active hosts

* feat(infra-monitoring): v2 hosts list - return counts of active & inactive hosts for custom group by attributes (#10956)

* chore: add functionality for showing active and inactive counts in custom group by

* chore: bug fix

* chore: added subquery for active and total count

* chore: ignore empty string hosts in get active hosts

* fix: sinceUnixMilli for determining active hosts compute once per request

* chore: refactor code

* chore: rename HostsList -> ListHosts

* chore: rearrangement

* chore: inframonitoring types renaming

* chore: added types package

* chore: file structure further breakdown for clarity

* chore: comments correction

* chore: removed temporalities

* chore: pods code restructuring

* chore: comments resolve

* chore: added json tag required: true

* chore: removed pod metric temporalities

* chore: removed internal server error

* chore: added status unauthorized

* chore: remove a defensive nil map check, the function ensure non-nil map when err nil

* chore: cleanup and rename

* chore: make sort stable in case of tiebreaker by comparing composite group by keys

* chore: regen api client for inframonitoring

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: added phase counts feature

* chore: added queries for pod phase counts in custom group by

* chore: added required tags

* chore: added support for pod phase unknown

* chore: removed pods - order by phase

* chore: improved api description to document -1 as no data in numeric fields

* fix: rebase fixes

* chore: added unknown phase count

* fix: isPodUIDInGroupBy in buildPodRecords

* chore: 3 cte --> 2 cte

* chore: pod phase with local table of time series as counts

* chore: comment correction

* chore: corrected comment

* chore: value column for samples table added

* chore: removed query G for phase counts

* chore: rename variable

* chore: added PodPhaseNum constants to types

* feat(infra-monitoring): v2 pods list apis - phase counts when custom grouping (#11088)

* chore: added phase counts feature

* chore: added queries for pod phase counts in custom group by

* chore: added unknown phase count

* fix: isPodUIDInGroupBy in buildPodRecords

* chore: 3 cte --> 2 cte

* chore: pod phase with local table of time series as counts

* chore: comment correction

* chore: corrected comment

* chore: value column for samples table added

* chore: removed query G for phase counts

* chore: rename variable

* chore: added PodPhaseNum constants to types

* chore: nodes list v2 full blown

* chore: metadata fix

* chore: updated comment

* chore: namespaces code

* chore: v2 nodes api

* chore: rename

* chore: v2 clusters list api

* chore: namespaces code

* chore: rename

* chore: review clusters PR

* chore: pvcs code added

* chore: updated endpoint and spec

* chore: pvcs todo

* chore: added condition

* chore: added filter

* chore: added code for deployments

* chore: query nit

* chore: statefulsets code added

* chore: base filter added

* chore: added base deployments change

* chore: added base condition

* chore: v2 jobs list api added

* chore: added daemonsets api

* chore: added pod phase counts

* chore: for pods and nodes, replace none with no_data

* chore: node and pod counts structs added

* chore: namespace record uses PodCountsByPhase

* chore: cluster record uses PodCountsByPhase, NodeCountsByReadiness

* chore: deployment record uses PodCountsByPhase

* chore: statefulset record uses PodCountsByPhase

* chore: job record uses PodCountsByPhase

* chore: daemonset record uses PodCountsByPhase

* chore: added remaining metrics to check

* chore: metrics existence check

* chore: statefulset metrics added

* chore: added jobs metrics

* chore: added metrics

* chore: feature added

* chore: cosmetic changes

* chore: replaced common order by key with entity specific attr key

* chore: moved paginateByName to types and added unit tests

* chore: added pageGroups

* chore: assert added instead of require

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Ashwin Bhatkal <ashwin96@gmail.com>
2026-05-21 14:55:14 +00:00
swapnil-signoz
b05d16242a ci: py fmt lint fixes 2026-05-21 19:57:06 +05:30
swapnil-signoz
fb2edfd770 refactor: renaming migration and adding integration tests 2026-05-21 19:52:26 +05:30
swapnil-signoz
34a2767396 ci: lint staticcheck fix 2026-05-21 18:12:30 +05:30
swapnil-signoz
43c72b8f4d refactor: simplify comments 2026-05-21 18:05:51 +05:30
swapnil-signoz
95be5f12f3 refactor: review changes and update service flow change 2026-05-21 17:58:31 +05:30
Aditya Singh
a5adc52276 feat(trace-details): promote ExpandableValue to periscope + dropdown z-index fix (#11393)
* feat: add expandable value component around status message

* feat: add minor change

* feat: style fix

* feat: remove comment
2026-05-21 12:19:51 +00:00
Aditya Singh
5ddcf33811 Fix drilldown on service details page (#11338)
* feat: fix drilldown on service details page

* feat: hide edit btn if no dashboard
2026-05-21 10:51:36 +00:00
swapnil-signoz
ce141576d6 refactor: adding DeleteBySource on dashboard module 2026-05-21 00:16:20 +05:30
swapnil-signoz
fab7fd001f refactor: removing loose strings 2026-05-20 23:41:55 +05:30
swapnil-signoz
02634894d9 Merge branch 'main' into feat/cloudintegrations-dashboards-migration 2026-05-20 23:24:12 +05:30
swapnil-signoz
83f1070b8a refactor: dashboard creation and listing flow change 2026-05-20 23:23:01 +05:30
swapnil-signoz
147005077c Merge branch 'main' into feat/cloudintegrations-dashboards-migration 2026-05-20 19:22:52 +05:30
swapnil-signoz
d720f76819 Merge branch 'main' into feat/cloudintegrations-dashboards-migration 2026-05-20 13:39:13 +05:30
swapnil-signoz
d76e15d444 Merge branch 'feat/add-integration-dashboards-table' into feat/cloudintegrations-dashboards-migration 2026-05-19 11:15:18 +05:30
swapnil-signoz
dc1a5cce76 chore: file rename 2026-05-19 11:14:23 +05:30
swapnil-signoz
37c97528f3 refactor: rename and restructure cloud integration dashboard migration types 2026-05-19 11:13:36 +05:30
swapnil-signoz
6a8e6e94e9 Merge branch 'feat/add-integration-dashboards-table' into feat/cloudintegrations-dashboards-migration 2026-05-18 23:23:17 +05:30
swapnil-signoz
5c480272f3 refactor: renaming table name 2026-05-18 23:02:18 +05:30
swapnil-signoz
ab26fd3d8c Merge branch 'feat/add-integration-dashboards-table' into feat/cloudintegrations-dashboards-migration 2026-05-18 20:16:55 +05:30
swapnil-signoz
80c0801b2e chore: adding comment for fk 2026-05-18 20:01:44 +05:30
swapnil-signoz
bd190b8d88 Merge branch 'main' into feat/add-integration-dashboards-table 2026-05-18 19:57:07 +05:30
swapnil-signoz
7e5f5bfac4 chore(sqlmigration): clean up stale 079 artifacts, add 079 schema migration
Remove the pre-rename 079_migrate_cloud_integration_dashboards.go and
079_cloud_integration_dashboards/ directory that were left behind when
the backfill migration was renumbered to 080. Add the missing
079_add_integration_dashboards.go (schema-only migration creating the
integration_dashboards table) which provider.go already references.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 18:45:42 +05:30
swapnil-signoz
7f72ca19d3 feat(sqlmigration): backfill cloud integration dashboards to DB (migration 080)
One-time idempotent migration that provisions dashboard rows for all
orgs with existing cloud integration services where metrics are enabled.
Each dashboard is inserted into the `dashboard` table with
source="integration" and locked=true, and a companion row is added to
`integration_dashboards` with provider="cloud_integrations" and
slug="{provider}-{service}-{dashboard}" (e.g. aws-alb-overview).
Idempotency is enforced by checking (org_id, provider, slug) on
integration_dashboards before each insert.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 18:41:39 +05:30
swapnil-signoz
231229d73e feat(sqlmigration): add integration_dashboards table (migration 079)
Adds the `integration_dashboards` relations table that stores the
integration-specific identity for dashboards provisioned from cloud
or builtin integrations. Columns: id, org_id, dashboard_id, provider,
slug, created_at, updated_at. Includes a unique index on dashboard_id.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 18:41:16 +05:30
swapnil-signoz
bb88cde296 chore: added migration setup 2026-05-18 10:56:12 +05:30
138 changed files with 52499 additions and 564 deletions

View File

@@ -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]] {

View File

@@ -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))

View File

@@ -2689,7 +2689,6 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesClusterRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -2759,7 +2758,6 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesDaemonSetRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -2829,7 +2827,6 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesDeploymentRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -2908,7 +2905,6 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesHostRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -2984,7 +2980,6 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesJobRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -3032,7 +3027,6 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesNamespaceRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -3110,7 +3104,6 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesNodeRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -3209,7 +3202,6 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesPodRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -3554,7 +3546,6 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesStatefulSetRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -3615,7 +3606,6 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesVolumeRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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,41 @@ 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
}
sharedServices, err := module.store.ListSharedServices(ctx, orgID, provider, accountID)
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
}
if isServiceSharedWithMetricsEnabled(provider, sharedServices[svc.Type]) {
continue
}
if err := module.deprovisionDashboards(ctx, orgID, provider, svc.Type); err != nil {
return err
}
}
if err := module.store.DeleteServicesByCloudIntegrationID(ctx, orgID, 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 +368,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 +398,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 +433,28 @@ 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")
}
if metricsEnabled {
return module.provisionDashboards(ctx, orgID, createdBy, creator, provider, integrationService, serviceDefinition)
}
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())
}
sharedServices, err := module.store.ListSharedServices(ctx, orgID, provider, integrationService.CloudIntegrationID)
if err != nil {
return err
}
if isServiceSharedWithMetricsEnabled(provider, sharedServices[integrationService.Type]) {
return nil
}
return module.listDashboards(ctx, orgID)
return module.deprovisionDashboards(ctx, orgID, provider, integrationService.Type)
})
}
func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
@@ -493,52 +530,89 @@ 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.
// make sure to call this within a transaction.
func (module *module) deprovisionDashboards(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, serviceID cloudintegrationtypes.ServiceID) error {
slugPrefix := cloudintegrationtypes.IntegrationDashboardSlugPrefix(provider, serviceID)
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, orgID, cloudintegrationtypes.IntegrationDashboardProviderCloudIntegration, row.Slug); err != nil {
return err
}
if err := module.dashboardModule.DeleteUnsafe(ctx, orgID, dashID); err != nil {
return err
}
}
return nil
}
// isServiceSharedWithMetricsEnabled returns true if any of the provided services has metrics enabled.
// It is used to determine whether dashboards for a service type should be deprovisioned when
// an account is disconnected or a service is updated.
func isServiceSharedWithMetricsEnabled(provider cloudintegrationtypes.CloudProviderType, services []*cloudintegrationtypes.StorableCloudIntegrationService) bool {
for _, svc := range services {
cfg, err := cloudintegrationtypes.NewServiceConfigFromJSON(provider, svc.Config)
if err != nil {
continue
}
if cfg.IsMetricsEnabled(provider) {
return true
}
}
return false
}
// 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
}

View File

@@ -162,24 +162,11 @@ func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.U
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is locked, please unlock the dashboard to be delete it")
}
err = module.store.RunInTx(ctx, func(ctx context.Context) error {
err := module.store.DeletePublic(ctx, id.String())
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return err
}
return module.delete(ctx, orgID, id)
}
err = module.store.Delete(ctx, orgID, id)
if err != nil {
return err
}
return nil
})
if err != nil {
return err
}
return nil
func (module *module) DeleteUnsafe(ctx context.Context, orgID, id valuer.UUID) error {
return module.delete(ctx, orgID, id)
}
func (module *module) DeletePublic(ctx context.Context, orgID valuer.UUID, dashboardID valuer.UUID) error {
@@ -221,8 +208,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) {
@@ -244,3 +231,12 @@ func (module *module) Update(ctx context.Context, orgID valuer.UUID, id valuer.U
func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, isAdmin bool, lock bool) error {
return module.pkgDashboardModule.LockUnlock(ctx, orgID, id, updatedBy, isAdmin, lock)
}
func (module *module) delete(ctx context.Context, orgID, id valuer.UUID) error {
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)
})
}

View File

@@ -86,7 +86,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
// initiate opamp
opAmpModel.Init(signoz.SQLStore, signoz.Instrumentation.Logger(), signoz.Modules.OrgGetter)
integrationsController, err := integrations.NewController(signoz.SQLStore)
integrationsController, err := integrations.NewController(signoz.SQLStore, signoz.Modules.Dashboard)
if err != nil {
return nil, fmt.Errorf(
"couldn't create integrations controller: %w", err,

View File

@@ -3488,9 +3488,9 @@ export interface InframonitoringtypesClustersDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array,null
* @type array
*/
records: InframonitoringtypesClusterRecordDTO[] | null;
records: InframonitoringtypesClusterRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -3566,9 +3566,9 @@ export interface InframonitoringtypesDaemonSetsDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array,null
* @type array
*/
records: InframonitoringtypesDaemonSetRecordDTO[] | null;
records: InframonitoringtypesDaemonSetRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -3644,9 +3644,9 @@ export interface InframonitoringtypesDeploymentsDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array,null
* @type array
*/
records: InframonitoringtypesDeploymentRecordDTO[] | null;
records: InframonitoringtypesDeploymentRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -3730,9 +3730,9 @@ export interface InframonitoringtypesHostsDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array,null
* @type array
*/
records: InframonitoringtypesHostRecordDTO[] | null;
records: InframonitoringtypesHostRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -3816,9 +3816,9 @@ export interface InframonitoringtypesJobsDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array,null
* @type array
*/
records: InframonitoringtypesJobRecordDTO[] | null;
records: InframonitoringtypesJobRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -3866,9 +3866,9 @@ export interface InframonitoringtypesNamespacesDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array,null
* @type array
*/
records: InframonitoringtypesNamespaceRecordDTO[] | null;
records: InframonitoringtypesNamespaceRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -3933,9 +3933,9 @@ export interface InframonitoringtypesNodesDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array,null
* @type array
*/
records: InframonitoringtypesNodeRecordDTO[] | null;
records: InframonitoringtypesNodeRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -4017,9 +4017,9 @@ export interface InframonitoringtypesPodsDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array,null
* @type array
*/
records: InframonitoringtypesPodRecordDTO[] | null;
records: InframonitoringtypesPodRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -4437,9 +4437,9 @@ export interface InframonitoringtypesStatefulSetsDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array,null
* @type array
*/
records: InframonitoringtypesStatefulSetRecordDTO[] | null;
records: InframonitoringtypesStatefulSetRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -4506,9 +4506,9 @@ export interface InframonitoringtypesVolumesDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array,null
* @type array
*/
records: InframonitoringtypesVolumeRecordDTO[] | null;
records: InframonitoringtypesVolumeRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer

View File

@@ -13,7 +13,7 @@ export function NoAuthBanner(): JSX.Element {
Impersonation mode: authentication is disabled. Anyone with access to this
instance has admin privileges.{' '}
<a
href="https://signoz.io/docs/manage/administrator-guide/configuration/no-auth-mode/"
href="https://signoz.io/docs/manage/administrator-guide/configuration/impersonation-mode/"
target="_blank"
rel="noreferrer"
>

View File

@@ -0,0 +1,147 @@
import { renderHook } from '@testing-library/react';
import { UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { usePanelContextMenu } from '../usePanelContextMenu';
// The hook composes `useCoordinates` (popover state) and `useGraphContextMenu`
// (menu items). We mock both so the test focuses on the `enableDrillDown` gate
// rather than the implementation of the menu wiring itself.
const onClickMock = jest.fn();
jest.mock('periscope/components/ContextMenu', () => ({
useCoordinates: (): unknown => ({
coordinates: null,
popoverPosition: null,
clickedData: null,
onClose: jest.fn(),
subMenu: null,
onClick: onClickMock,
setSubMenu: jest.fn(),
}),
}));
jest.mock('container/QueryTable/Drilldown/useGraphContextMenu', () => ({
__esModule: true,
default: (): { menuItemsConfig: { header: string; items: string } } => ({
menuItemsConfig: { header: 'menu-header', items: 'menu-items' },
}),
}));
jest.mock('container/QueryTable/Drilldown/drilldownUtils', () => ({
getUplotClickData: jest.fn(() => ({
coord: { x: 1, y: 2 },
record: { queryName: 'A', filters: [] },
label: 'lbl',
seriesColor: '#abc',
})),
}));
jest.mock('container/PanelWrapper/utils', () => ({
isApmMetric: jest.fn(() => false),
getTimeRangeFromStepInterval: jest.fn(() => ({ start: 0, end: 0 })),
}));
const mockWidget = { id: 'w-1', query: {} } as unknown as Widgets;
const mockQueryResponse = {
data: undefined,
isLoading: false,
} as unknown as UseQueryResult<
SuccessResponse<MetricRangePayloadProps, unknown>,
Error
>;
describe('usePanelContextMenu', () => {
beforeEach(() => {
onClickMock.mockClear();
});
it('returns empty menuItemsConfig when enableDrillDown is false', () => {
const { result } = renderHook(() =>
usePanelContextMenu({
widget: mockWidget,
queryResponse: mockQueryResponse,
enableDrillDown: false,
}),
);
expect(result.current.menuItemsConfig).toStrictEqual({});
});
it('returns wired menuItemsConfig when enableDrillDown is true', () => {
const { result } = renderHook(() =>
usePanelContextMenu({
widget: mockWidget,
queryResponse: mockQueryResponse,
enableDrillDown: true,
}),
);
expect(result.current.menuItemsConfig).toStrictEqual({
header: 'menu-header',
items: 'menu-items',
});
});
it('clickHandlerWithContextMenu is a no-op when enableDrillDown is false', () => {
const { result } = renderHook(() =>
usePanelContextMenu({
widget: mockWidget,
queryResponse: mockQueryResponse,
enableDrillDown: false,
}),
);
result.current.clickHandlerWithContextMenu(
100, // xValue
200, // yValue
0, // mouseX
0, // mouseY
{ serviceName: 'svc' }, // metric
{ queryName: 'A', inFocusOrNot: true }, // queryData
10, // absoluteMouseX
20, // absoluteMouseY
{}, // axesData
{ seriesIndex: 0, seriesName: 'A', value: 1, color: '#abc' }, // focusedSeries
);
expect(onClickMock).not.toHaveBeenCalled();
});
it('clickHandlerWithContextMenu opens popover when enableDrillDown is true', () => {
const { result } = renderHook(() =>
usePanelContextMenu({
widget: mockWidget,
queryResponse: mockQueryResponse,
enableDrillDown: true,
}),
);
result.current.clickHandlerWithContextMenu(
100,
200,
0,
0,
{ serviceName: 'svc' },
{ queryName: 'A', inFocusOrNot: true },
10,
20,
{},
{ seriesIndex: 0, seriesName: 'A', value: 1, color: '#abc' },
);
expect(onClickMock).toHaveBeenCalledTimes(1);
});
it('defaults to disabled when enableDrillDown is not provided', () => {
const { result } = renderHook(() =>
usePanelContextMenu({
widget: mockWidget,
queryResponse: mockQueryResponse,
}),
);
expect(result.current.menuItemsConfig).toStrictEqual({});
});
});

View File

@@ -21,11 +21,13 @@ interface UseTimeSeriesContextMenuParams {
SuccessResponse<MetricRangePayloadProps, unknown>,
Error
>;
enableDrillDown?: boolean;
}
export const usePanelContextMenu = ({
widget,
queryResponse,
enableDrillDown = false,
}: UseTimeSeriesContextMenuParams): {
coordinates: { x: number; y: number } | null;
popoverPosition: PopoverPosition | null;
@@ -61,6 +63,9 @@ export const usePanelContextMenu = ({
const clickHandlerWithContextMenu = useCallback(
(...args: any[]) => {
if (!enableDrillDown) {
return;
}
const [
xValue,
_yvalue,
@@ -112,14 +117,14 @@ export const usePanelContextMenu = ({
});
}
},
[onClick, queryResponse],
[enableDrillDown, onClick, queryResponse],
);
return {
coordinates,
popoverPosition,
onClose,
menuItemsConfig,
menuItemsConfig: enableDrillDown ? menuItemsConfig : {},
clickHandlerWithContextMenu,
};
};

View File

@@ -31,6 +31,7 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
isFullViewMode,
onToggleModelHandler,
groupByPerQuery,
enableDrillDown = false,
} = props;
const uPlotRef = useRef<uPlot | null>(null);
const graphRef = useRef<HTMLDivElement>(null);
@@ -61,6 +62,7 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
} = usePanelContextMenu({
widget,
queryResponse,
enableDrillDown,
});
const config = useMemo(() => {

View File

@@ -31,6 +31,7 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
isFullViewMode,
onToggleModelHandler,
groupByPerQuery,
enableDrillDown = false,
} = props;
const graphRef = useRef<HTMLDivElement>(null);
const [minTimeScale, setMinTimeScale] = useState<number>();
@@ -60,6 +61,7 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
} = usePanelContextMenu({
widget,
queryResponse,
enableDrillDown,
});
const chartData = useMemo(() => {

View File

@@ -292,6 +292,8 @@ function FullView({
return <Spinner height="100%" size="large" tip="Loading..." />;
}
const showEditBtn = editWidget && dashboardEditView;
return (
<div className="full-view-container">
<OverlayScrollbar>
@@ -306,7 +308,7 @@ function FullView({
Reset Query
</Button>
)}
{editWidget && (
{showEditBtn && (
<Button
className="switch-edit-btn"
disabled={response.isFetching || response.isLoading}

View File

@@ -30,7 +30,12 @@ import { v4 as uuid } from 'uuid';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { GraphTitle, MENU_ITEMS, SERVICE_CHART_ID } from '../constant';
import {
GraphTitle,
MENU_ITEMS,
SERVICE_CHART_ID,
SERVICE_DETAIL_DRILLDOWN_ENABLED,
} from '../constant';
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
import { Card, GraphContainer, Row } from '../styles';
import { Button } from './styles';
@@ -206,6 +211,7 @@ function DBCall(): JSX.Element {
}}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
</GraphContainer>
</Card>
@@ -244,6 +250,7 @@ function DBCall(): JSX.Element {
}}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
</GraphContainer>
</Card>

View File

@@ -32,7 +32,12 @@ import { v4 as uuid } from 'uuid';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { GraphTitle, legend, MENU_ITEMS } from '../constant';
import {
GraphTitle,
legend,
MENU_ITEMS,
SERVICE_DETAIL_DRILLDOWN_ENABLED,
} from '../constant';
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
import { Card, GraphContainer, Row } from '../styles';
import GraphControlsPanel from './Overview/GraphControlsPanel/GraphControlsPanel';
@@ -279,6 +284,7 @@ function External(): JSX.Element {
}}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
</GraphContainer>
</Card>
@@ -322,6 +328,7 @@ function External(): JSX.Element {
}}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
</GraphContainer>
</Card>
@@ -366,6 +373,7 @@ function External(): JSX.Element {
}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
</GraphContainer>
</Card>
@@ -409,6 +417,7 @@ function External(): JSX.Element {
}}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
</GraphContainer>
</Card>

View File

@@ -15,6 +15,7 @@ import DisplayThreshold from 'container/GridCardLayout/WidgetHeader/DisplayThres
import {
GraphTitle,
SERVICE_CHART_ID,
SERVICE_DETAIL_DRILLDOWN_ENABLED,
} from 'container/MetricsApplication/constant';
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
import { apDexMetricsQueryBuilderQueries } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries';
@@ -105,6 +106,7 @@ function ApDexMetrics({
threshold={threshold}
isQueryEnabled={isQueryEnabled}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
);
}

View File

@@ -8,6 +8,7 @@ import Graph from 'container/GridCardLayout/GridCard';
import {
GraphTitle,
SERVICE_CHART_ID,
SERVICE_DETAIL_DRILLDOWN_ENABLED,
} from 'container/MetricsApplication/constant';
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
import { latency } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries';
@@ -138,6 +139,7 @@ function ServiceOverview({
onClickHandler={handleGraphClick('Service')}
isQueryEnabled={isQueryEnabled}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
)}
</GraphContainer>

View File

@@ -4,6 +4,7 @@ import axios from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { ENTITY_VERSION_V4 } from 'constants/app';
import Graph from 'container/GridCardLayout/GridCard';
import { SERVICE_DETAIL_DRILLDOWN_ENABLED } from 'container/MetricsApplication/constant';
import { Card, GraphContainer } from 'container/MetricsApplication/styles';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { Widgets } from 'types/api/dashboard/getAll';
@@ -43,6 +44,7 @@ function TopLevelOperation({
onDragSelect={onDragSelect}
isQueryEnabled={!topLevelOperationsIsLoading}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
)}
</GraphContainer>

View File

@@ -25,6 +25,8 @@ export const OPERATION_LEGENDS = ['Operations'];
export const MENU_ITEMS = [MenuItemKeys.View, MenuItemKeys.CreateAlerts];
export const SERVICE_DETAIL_DRILLDOWN_ENABLED = true;
export enum FORMULA {
ERROR_PERCENTAGE = 'A*100/B',
DATABASE_CALLS_AVG_DURATION = 'A/B',

View File

@@ -127,6 +127,12 @@
flex-direction: column;
gap: 8px;
padding-bottom: 16px;
.password-error-text {
font-size: var(--font-size-xs);
color: var(--bg-cherry-400);
margin-top: 2px;
}
}
.ant-color-picker-trigger {

View File

@@ -1,25 +1,27 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Input, Modal } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import {
updateMyPassword,
useUpdateMyUserV2,
} from 'api/generated/services/users';
import { useNotifications } from 'hooks/useNotifications';
import { toast } from '@signozhq/ui/sonner';
import { Check, FileTerminal, Mail, User } from '@signozhq/icons';
import { useAppContext } from 'providers/App/App';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { ErrorV2Resp } from 'types/api';
import { AxiosError } from 'axios';
import '../MySettings.styles.scss';
import './UserInfo.styles.scss';
function UserInfo(): JSX.Element {
const { user, org, updateUser } = useAppContext();
const { t } = useTranslation(['routes', 'settings', 'common']);
const { notifications } = useNotifications();
const { showErrorModal } = useErrorModal();
const { mutateAsync: updateMyUser } = useUpdateMyUserV2();
const [currentPassword, setCurrentPassword] = useState<string>('');
@@ -47,6 +49,8 @@ function UserInfo(): JSX.Element {
const hideResetPasswordModal = (): void => {
setIsResetPasswordModalOpen(false);
setCurrentPassword('');
setUpdatePassword('');
};
const onChangePasswordClickHandler = async (): Promise<void> => {
@@ -57,27 +61,29 @@ function UserInfo(): JSX.Element {
newPassword: updatePassword,
oldPassword: currentPassword,
});
notifications.success({
message: t('success', {
ns: 'common',
}),
});
toast.success('Password updated successfully');
hideResetPasswordModal();
setIsLoading(false);
} catch (error) {
setIsLoading(false);
notifications.error({
message: (error as APIError).error.error.code,
description: (error as APIError).error.error.message,
});
try {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
} catch (apiError) {
showErrorModal(apiError as APIError);
}
}
};
const passwordsMatch =
currentPassword.length > 0 &&
updatePassword.length > 0 &&
currentPassword === updatePassword;
const isResetPasswordDisabled =
isLoading ||
currentPassword.length === 0 ||
updatePassword.length === 0 ||
currentPassword === updatePassword;
passwordsMatch;
const onSaveHandler = async (): Promise<void> => {
void logEvent('Account Settings: Name Updated', {
@@ -94,11 +100,7 @@ function UserInfo(): JSX.Element {
setIsLoading(true);
await updateMyUser({ data: { displayName: changedName } });
notifications.success({
message: t('success', {
ns: 'common',
}),
});
toast.success('Name updated successfully');
updateUser({
...user,
displayName: changedName,
@@ -106,10 +108,11 @@ function UserInfo(): JSX.Element {
setIsLoading(false);
hideUpdateNameModal();
} catch (error) {
notifications.error({
message: (error as APIError).getErrorCode(),
description: (error as APIError).getErrorMessage(),
});
try {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
} catch (apiError) {
showErrorModal(apiError as APIError);
}
}
setIsLoading(false);
};
@@ -166,7 +169,7 @@ function UserInfo(): JSX.Element {
type="primary"
icon={<Check size={16} />}
onClick={onSaveHandler}
disabled={isLoading}
loading={isLoading}
data-testid="update-name-btn"
>
Update name
@@ -178,7 +181,11 @@ function UserInfo(): JSX.Element {
<Input
placeholder="e.g. John Doe"
value={changedName}
disabled={isLoading}
onChange={(e): void => setChangedName(e.target.value)}
onPressEnter={(): void => {
void onSaveHandler();
}}
/>
</div>
</Modal>
@@ -188,6 +195,7 @@ function UserInfo(): JSX.Element {
title={<span className="title">Reset password</span>}
open={isResetPasswordModalOpen}
closable
destroyOnClose
onCancel={hideResetPasswordModal}
footer={[
<Button
@@ -197,7 +205,8 @@ function UserInfo(): JSX.Element {
}`}
icon={<Check size={16} />}
onClick={onChangePasswordClickHandler}
disabled={isLoading || isResetPasswordDisabled}
loading={isLoading}
disabled={isResetPasswordDisabled}
data-testid="reset-password-btn"
>
Reset password
@@ -218,6 +227,11 @@ function UserInfo(): JSX.Element {
type="password"
autoComplete="off"
visibilityToggle
onPressEnter={(): void => {
if (!isResetPasswordDisabled) {
void onChangePasswordClickHandler();
}
}}
/>
</div>
@@ -235,7 +249,18 @@ function UserInfo(): JSX.Element {
type="password"
autoComplete="off"
visibilityToggle={false}
status={passwordsMatch ? 'error' : ''}
onPressEnter={(): void => {
if (!isResetPasswordDisabled) {
void onChangePasswordClickHandler();
}
}}
/>
{passwordsMatch && (
<span className="password-error-text">
New password must be different from current password
</span>
)}
</div>
</div>
</Modal>

View File

@@ -8,11 +8,23 @@ import {
waitFor,
within,
} from 'tests/test-utils';
import APIError from 'types/api/error';
import { toast } from '@signozhq/ui/sonner';
const toggleThemeFunction = jest.fn();
const logEventFunction = jest.fn();
const copyToClipboardFn = jest.fn();
const editUserFn = jest.fn();
const updateMyPasswordFn = jest.fn();
const showErrorModalFn = jest.fn();
jest.mock('@signozhq/ui/sonner', () => ({
...jest.requireActual('@signozhq/ui/sonner'),
toast: {
success: jest.fn(),
error: jest.fn(),
},
}));
jest.mock('react-use', () => ({
__esModule: true,
@@ -24,12 +36,21 @@ jest.mock('react-use', () => ({
jest.mock('api/generated/services/users', () => ({
...jest.requireActual('api/generated/services/users'),
updateMyPassword: (...args: unknown[]): Promise<unknown> =>
updateMyPasswordFn(...args),
useUpdateMyUserV2: jest.fn(() => ({
mutateAsync: (...args: unknown[]): Promise<unknown> => editUserFn(...args),
isLoading: false,
})),
}));
jest.mock('providers/ErrorModalProvider', () => ({
...jest.requireActual('providers/ErrorModalProvider'),
useErrorModal: jest.fn(() => ({
showErrorModal: showErrorModalFn,
})),
}));
jest.mock('hooks/useDarkMode', () => ({
__esModule: true,
useIsDarkMode: jest.fn(() => true),
@@ -65,12 +86,12 @@ const NEW_PASSWORD_TEST_ID = 'new-password-textbox';
const UPDATE_NAME_BUTTON_TEST_ID = 'update-name-btn';
const RESET_PASSWORD_BUTTON_TEST_ID = 'reset-password-btn';
const UPDATE_NAME_BUTTON_TEXT = 'Update name';
const PASSWORD_VALIDATION_MESSAGE_TEST_ID = 'password-validation-message';
describe('MySettings Flows', () => {
beforeEach(() => {
jest.clearAllMocks();
editUserFn.mockResolvedValue({});
updateMyPasswordFn.mockResolvedValue({});
render(<MySettingsContainer />);
});
@@ -152,9 +173,7 @@ describe('MySettings Flows', () => {
fireEvent.click(modalUpdateNameButton);
await waitFor(() =>
expect(successNotification).toHaveBeenCalledWith({
message: 'success',
}),
expect(toast.success).toHaveBeenCalledWith('Name updated successfully'),
);
});
});
@@ -181,22 +200,131 @@ describe('MySettings Flows', () => {
expect(screen.getByTestId(NEW_PASSWORD_TEST_ID)).toBeInTheDocument();
});
it('Should display validation error if password is less than 8 characters', async () => {
it('Should show inline error when new password matches current password', async () => {
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
fireEvent.click(resetPasswordButtons[0]);
const currentPasswordTextbox = screen.getByTestId(CURRENT_PASSWORD_TEST_ID);
act(() => {
fireEvent.change(currentPasswordTextbox, { target: { value: '123' } });
fireEvent.change(screen.getByTestId(CURRENT_PASSWORD_TEST_ID), {
target: { value: 'samePassword1' },
});
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
target: { value: 'samePassword1' },
});
});
expect(
screen.getByText('New password must be different from current password'),
).toBeInTheDocument();
expect(screen.getByTestId(RESET_PASSWORD_BUTTON_TEST_ID)).toBeDisabled();
});
it('Should hide inline error when passwords are changed to be different', async () => {
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
fireEvent.click(resetPasswordButtons[0]);
act(() => {
fireEvent.change(screen.getByTestId(CURRENT_PASSWORD_TEST_ID), {
target: { value: 'samePassword1' },
});
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
target: { value: 'samePassword1' },
});
});
act(() => {
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
target: { value: 'differentPassword1' },
});
});
expect(
screen.queryByText('New password must be different from current password'),
).not.toBeInTheDocument();
});
it('Should show error modal when password reset API returns an error', async () => {
updateMyPasswordFn.mockRejectedValue(
new Error('Current password is incorrect'),
);
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
fireEvent.click(resetPasswordButtons[0]);
act(() => {
fireEvent.change(screen.getByTestId(CURRENT_PASSWORD_TEST_ID), {
target: { value: 'oldPassword1' },
});
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
target: { value: 'newPassword1' },
});
});
fireEvent.click(screen.getByTestId(RESET_PASSWORD_BUTTON_TEST_ID));
await waitFor(() => {
// Use getByTestId for the validation message (if present in your modal/component)
if (screen.queryByTestId(PASSWORD_VALIDATION_MESSAGE_TEST_ID)) {
expect(
screen.getByTestId(PASSWORD_VALIDATION_MESSAGE_TEST_ID),
).toBeInTheDocument();
}
expect(showErrorModalFn).toHaveBeenCalledWith(expect.any(APIError));
});
});
it('Should show success toast and close modal on successful password reset', async () => {
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
fireEvent.click(resetPasswordButtons[0]);
act(() => {
fireEvent.change(screen.getByTestId(CURRENT_PASSWORD_TEST_ID), {
target: { value: 'oldPassword1' },
});
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
target: { value: 'newPassword1' },
});
});
fireEvent.click(screen.getByTestId(RESET_PASSWORD_BUTTON_TEST_ID));
await waitFor(() => {
expect(toast.success).toHaveBeenCalledWith('Password updated successfully');
expect(
screen.queryByTestId(CURRENT_PASSWORD_TEST_ID),
).not.toBeInTheDocument();
});
});
it('Should clear password fields when modal is cancelled', async () => {
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
fireEvent.click(resetPasswordButtons[0]);
act(() => {
fireEvent.change(screen.getByTestId(CURRENT_PASSWORD_TEST_ID), {
target: { value: 'somePassword' },
});
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
target: { value: 'otherPassword' },
});
});
expect(screen.getByTestId(CURRENT_PASSWORD_TEST_ID)).toHaveValue(
'somePassword',
);
// Close the modal
const closeButton = document.querySelector(
'.reset-password-modal .ant-modal-close',
) as HTMLElement;
fireEvent.click(closeButton);
// Reopen the modal
await waitFor(() => {
expect(
screen.queryByTestId(CURRENT_PASSWORD_TEST_ID),
).not.toBeInTheDocument();
});
fireEvent.click(screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT)[0]);
await waitFor(() => {
expect(screen.getByTestId(CURRENT_PASSWORD_TEST_ID)).toHaveValue('');
expect(screen.getByTestId(NEW_PASSWORD_TEST_ID)).toHaveValue('');
});
});

View File

@@ -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

View File

@@ -169,9 +169,10 @@ describe('drilldownUtils', () => {
// Verify transformations were applied
if (filterExpression) {
// Rule 2: operation → name
expect(filterExpression).toContain(`name = 'GET'`);
// `operation` rewrites to `name` via source-side pass, then `name`
// is dropped by the logs target-side pass (logs has no span-name).
expect(filterExpression).not.toContain(`operation = 'GET'`);
expect(filterExpression).not.toContain(`name = 'GET'`);
// Rule 3: span.kind → kind
expect(filterExpression).toContain(`${spanKindKey} = '${spanKindServer}'`);
@@ -262,8 +263,9 @@ describe('drilldownUtils', () => {
const filterExpression = result?.builder.queryData[0]?.filter?.expression;
if (filterExpression) {
// All transformations should be applied
expect(filterExpression).toContain(`name = 'POST'`);
// `operation` rewrites to `name` then drops for logs target.
expect(filterExpression).not.toContain(`operation = 'POST'`);
expect(filterExpression).not.toContain(`name = 'POST'`);
expect(filterExpression).toContain(`${spanKindKey} = '${spanKindClient}'`);
expect(filterExpression).toContain(`status_code_string = 'Error'`);
expect(filterExpression).toContain(`http.status_code = 500`);
@@ -410,8 +412,9 @@ describe('drilldownUtils', () => {
const filterExpression = result?.builder.queryData[0]?.filter?.expression;
if (filterExpression) {
// Transformed attributes
expect(filterExpression).toContain(`name = 'GET'`);
// `operation` rewrites to `name` then drops for logs target.
expect(filterExpression).not.toContain(`operation = 'GET'`);
expect(filterExpression).not.toContain(`name = 'GET'`);
expect(filterExpression).toContain(`${spanKindKey} = '${spanKindServer}'`);
// Preserved non-metric attributes
@@ -499,4 +502,189 @@ describe('drilldownUtils', () => {
});
});
});
describe('getViewQuery target-aware sanitisation (serviceName / name)', () => {
const makeQuery = (
expression: string,
dataSource: 'traces' | 'logs' | 'metrics' = 'traces',
): Query => ({
id: 'src-query',
queryType: 'builder' as any,
builder: {
queryData: [
{
queryName: 'src',
dataSource: dataSource as any,
aggregations: [{ metricName: 'non_apm_metric' }] as any,
groupBy: [],
expression: '',
disabled: false,
functions: [],
legend: '',
having: [],
limit: null,
stepInterval: undefined,
orderBy: [],
filter: { expression },
},
],
queryFormulas: [],
queryTraceOperator: [],
},
promql: [],
clickhouse_sql: [],
});
it('rewrites serviceName -> service.name when drilling to logs', () => {
const result = getViewQuery(
makeQuery(`serviceName = 'svc'`),
[],
'view_logs',
'src',
);
const expr = result?.builder.queryData[0]?.filter?.expression || '';
expect(expr).toContain(`service.name = 'svc'`);
expect(expr).not.toContain('serviceName');
});
it('rewrites serviceName -> service.name when drilling to traces', () => {
const result = getViewQuery(
makeQuery(`serviceName = 'svc'`),
[],
'view_traces',
'src',
);
const expr = result?.builder.queryData[0]?.filter?.expression || '';
expect(expr).toContain(`service.name = 'svc'`);
expect(expr).not.toContain('serviceName');
});
it('drops `name` clause when drilling to logs', () => {
const result = getViewQuery(
makeQuery(`name = 'GET /api'`),
[],
'view_logs',
'src',
);
const expr = result?.builder.queryData[0]?.filter?.expression || '';
expect(expr).not.toContain(`name = 'GET /api'`);
});
it('keeps `name` clause when drilling to traces', () => {
const result = getViewQuery(
makeQuery(`name = 'GET /api'`),
[],
'view_traces',
'src',
);
const expr = result?.builder.queryData[0]?.filter?.expression || '';
expect(expr).toContain(`name = 'GET /api'`);
});
it('combined: drilling to logs rewrites serviceName and drops name', () => {
const result = getViewQuery(
makeQuery(`serviceName = 'svc' AND name = 'GET /api'`),
[],
'view_logs',
'src',
);
const expr = result?.builder.queryData[0]?.filter?.expression || '';
expect(expr).toContain(`service.name = 'svc'`);
expect(expr).not.toContain('serviceName');
expect(expr).not.toContain(`name = 'GET /api'`);
});
it('combined: drilling to traces rewrites serviceName and keeps name', () => {
const result = getViewQuery(
makeQuery(`serviceName = 'svc' AND name = 'GET /api'`),
[],
'view_traces',
'src',
);
const expr = result?.builder.queryData[0]?.filter?.expression || '';
expect(expr).toContain(`service.name = 'svc'`);
expect(expr).toContain(`name = 'GET /api'`);
expect(expr).not.toContain('serviceName');
});
it('metric-APM source -> traces target preserves existing operation -> name rewrite', () => {
const metricsQuery: Query = {
id: 'apm-metrics',
queryType: 'builder' as any,
builder: {
queryData: [
{
queryName: 'm',
dataSource: 'metrics' as any,
aggregations: [{ metricName: 'signoz_calls_total' }] as any,
groupBy: [],
expression: '',
disabled: false,
functions: [],
legend: '',
having: [],
limit: null,
stepInterval: undefined,
orderBy: [],
filter: { expression: `operation = 'GET'` },
},
],
queryFormulas: [],
queryTraceOperator: [],
},
promql: [],
clickhouse_sql: [],
};
const result = getViewQuery(metricsQuery, [], 'view_traces', 'm');
const expr = result?.builder.queryData[0]?.filter?.expression || '';
expect(expr).toContain(`name = 'GET'`);
expect(expr).not.toContain(`operation = 'GET'`);
});
it('metric-APM source -> logs target: operation rewrites to name, then dropped', () => {
const metricsQuery: Query = {
id: 'apm-metrics',
queryType: 'builder' as any,
builder: {
queryData: [
{
queryName: 'm',
dataSource: 'metrics' as any,
aggregations: [{ metricName: 'signoz_calls_total' }] as any,
groupBy: [],
expression: '',
disabled: false,
functions: [],
legend: '',
having: [],
limit: null,
stepInterval: undefined,
orderBy: [],
filter: { expression: `operation = 'GET'` },
},
],
queryFormulas: [],
queryTraceOperator: [],
},
promql: [],
clickhouse_sql: [],
};
const result = getViewQuery(metricsQuery, [], 'view_logs', 'm');
const expr = result?.builder.queryData[0]?.filter?.expression || '';
expect(expr).not.toContain(`operation = 'GET'`);
expect(expr).not.toContain(`name = 'GET'`);
});
it('drilling to metrics does not apply target-side sanitisation', () => {
const result = getViewQuery(
makeQuery(`serviceName = 'svc' AND name = 'GET /api'`),
[],
'view_metrics',
'src',
);
const expr = result?.builder.queryData[0]?.filter?.expression || '';
expect(expr).toContain('serviceName');
expect(expr).toContain(`name = 'GET /api'`);
});
});
});

View File

@@ -7,8 +7,10 @@ import {
import ROUTES from 'constants/routes';
import { isApmMetric } from 'container/PanelWrapper/utils';
import {
applyMappingsToExpression,
DRILLDOWN_TO_LOGS_MAPPINGS,
DRILLDOWN_TO_TRACES_MAPPINGS,
METRIC_TO_LOGS_TRACES_MAPPINGS,
replaceKeysAndValuesInExpression,
} from 'container/QueryTable/Drilldown/metricsCorrelationUtils';
import cloneDeep from 'lodash-es/cloneDeep';
import {
@@ -347,27 +349,41 @@ export const getViewQuery = (
newQuery.builder.queryData[0].filter = newFilterExpression;
try {
// ===========================================
// TEMP LOGIC - TO BE REMOVED LATER
// ===========================================
// Apply metric-to-logs/traces transformations
// Drill-down filter sanitisation. Two stages:
// 1. Source-side: rewrite metric-APM-specific keys (operation, span.kind,
// status.code) so they map onto trace/log columns.
// 2. Target-side: normalise legacy keys to OTel-canonical (`serviceName`
// -> `service.name`) and drop keys with no equivalent in the target
// datasource (e.g. `name` for logs).
let expression = newFilterExpression?.expression || '';
const specificQuery = getQueryData(query, queryName);
const isMetricQuery = specificQuery?.dataSource === 'metrics';
const metricName = (specificQuery?.aggregations?.[0] as MetricAggregation)
?.metricName;
if (isMetricQuery && isApmMetric(metricName || '')) {
const transformedExpression = replaceKeysAndValuesInExpression(
newFilterExpression?.expression || '',
expression = applyMappingsToExpression(
expression,
METRIC_TO_LOGS_TRACES_MAPPINGS,
);
newQuery.builder.queryData[0].filter = {
expression: transformedExpression || '',
};
}
// ===========================================
if (key === 'view_logs') {
expression = applyMappingsToExpression(
expression,
DRILLDOWN_TO_LOGS_MAPPINGS,
);
} else if (key === 'view_traces') {
expression = applyMappingsToExpression(
expression,
DRILLDOWN_TO_TRACES_MAPPINGS,
);
}
newQuery.builder.queryData[0].filter = { expression };
} catch (error) {
console.error('Error transforming metrics to logs/traces:', error);
console.error('Error sanitising drilldown filter expression:', error);
}
return newQuery;

View File

@@ -1,5 +1,8 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import {
formatValueForExpression,
removeKeysFromExpression,
} from 'components/QueryBuilderV2/utils';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { IQueryPair } from 'types/antlrQueryTypes';
import { extractQueryPairs } from 'utils/queryContextUtils';
@@ -8,7 +11,7 @@ import { isFunctionOperator, isNonValueOperator } from 'utils/tokenUtils';
type KeyValueMapping = {
attribute: string;
newAttribute: string;
newAttribute: string | null;
valueMappings: Record<string, string>;
};
@@ -40,8 +43,33 @@ export const METRIC_TO_LOGS_TRACES_MAPPINGS: KeyValueMapping[] = [
},
];
export const DRILLDOWN_TO_LOGS_MAPPINGS: KeyValueMapping[] = [
{
attribute: 'serviceName',
newAttribute: 'service.name',
valueMappings: {},
},
{
attribute: 'name',
newAttribute: null,
valueMappings: {},
},
];
export const DRILLDOWN_TO_TRACES_MAPPINGS: KeyValueMapping[] = [
{
attribute: 'serviceName',
newAttribute: 'service.name',
valueMappings: {},
},
];
// Logic for rewriting key/values in an expression using provided mappings.
function modifyKeyVal(pair: IQueryPair, mapping: KeyValueMapping): string {
// Callers must pre-filter mappings to ensure newAttribute is non-null.
function modifyKeyVal(
pair: IQueryPair,
mapping: KeyValueMapping & { newAttribute: string },
): string {
const newKey = mapping.newAttribute;
const op = pair.operator;
@@ -107,8 +135,18 @@ export function replaceKeysAndValuesInExpression(
return expression;
}
const attributeToMapping = new Map<string, KeyValueMapping>(
mappingList.map((m) => [m.attribute.trim().toLowerCase(), m]),
// Only rewrite mappings (newAttribute non-null) are processed here.
// Drops are handled separately by applyMappingsToExpression via removeKeysFromExpression.
const attributeToMapping = new Map<
string,
KeyValueMapping & { newAttribute: string }
>(
mappingList
.filter(
(m): m is KeyValueMapping & { newAttribute: string } =>
m.newAttribute !== null,
)
.map((m) => [m.attribute.trim().toLowerCase(), m]),
);
const pairs: IQueryPair[] = extractQueryPairs(expression);
@@ -179,3 +217,26 @@ export function replaceKeysAndValuesInExpression(
return resultParts.join('');
}
// Apply a list of mappings to a filter expression. Rewrites are applied first
// (newAttribute is a string), then drops (newAttribute is null) via the
// ANTLR-parser-based removeKeysFromExpression which handles AND/OR/NOT/paren
// elision correctly.
export function applyMappingsToExpression(
expression: string,
mappings: KeyValueMapping[],
): string {
if (!expression || !mappings || mappings.length === 0) {
return expression;
}
const dropKeys = mappings
.filter((m) => m.newAttribute === null)
.map((m) => m.attribute);
let result = replaceKeysAndValuesInExpression(expression, mappings);
if (dropKeys.length > 0) {
result = removeKeysFromExpression(result, dropKeys);
}
return result;
}

View File

@@ -119,6 +119,12 @@
flex-shrink: 0;
}
.statusMessageBadge {
width: 100%;
min-width: 0;
box-sizing: border-box;
}
.traceId {
color: var(--accent-primary);
overflow: hidden;

View File

@@ -1,5 +1,6 @@
import { ReactNode } from 'react';
import { Badge } from '@signozhq/ui/badge';
import ExpandableValue from 'periscope/components/ExpandableValue';
import { SpanV3 } from 'types/api/trace/getTraceV3';
import styles from './SpanDetailsPanel.module.scss';
@@ -48,7 +49,15 @@ export const HIGHLIGHTED_OPTIONS: HighlightedOption[] = [
label: 'STATUS MESSAGE',
render: (span): ReactNode | null =>
span.status_message ? (
<Badge color="vanilla">{span.status_message}</Badge>
<ExpandableValue value={span.status_message} title="Status message">
<Badge
color="vanilla"
textEllipsis="end"
className={styles.statusMessageBadge}
>
{span.status_message}
</Badge>
</ExpandableValue>
) : null,
},
];

View File

@@ -0,0 +1,3 @@
.traceOptionsDropdown {
z-index: 1100;
}

View File

@@ -6,6 +6,8 @@ import { Ellipsis } from '@signozhq/icons';
import { useTraceStore } from '../stores/traceStore';
import styles from './TraceOptionsMenu.module.scss';
interface TraceOptionsMenuProps {
showTraceDetails: boolean;
onToggleTraceDetails: () => void;
@@ -82,7 +84,11 @@ function TraceOptionsMenu({
]);
return (
<Dropdown menu={{ items: menuItems }} align="start">
<Dropdown
menu={{ items: menuItems }}
align="start"
className={styles.traceOptionsDropdown}
>
<Button
variant="ghost"
size="icon"

View File

@@ -0,0 +1,49 @@
.trigger {
display: block;
min-width: 0;
max-width: 100%;
overflow: hidden;
[data-truncated='true'] {
pointer-events: none;
}
}
.tooltipContent {
display: flex;
flex-direction: column;
gap: 8px;
max-width: 480px;
padding: 8px;
}
.preview {
margin: 0;
max-height: 220px;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
font-family: var(--font-family-mono, monospace);
font-size: 12px;
line-height: 1.4;
}
.expandButton {
align-self: flex-end;
}
.dialog {
max-width: 80vw;
width: 80vw;
}
.fullValue {
margin: 0;
max-height: 70vh;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
font-family: var(--font-family-mono, monospace);
font-size: 13px;
line-height: 1.5;
}

View File

@@ -0,0 +1,78 @@
import { ReactNode, useState } from 'react';
import { Button } from '@signozhq/ui/button';
import { DialogWrapper } from '@signozhq/ui/dialog';
import {
TooltipContent,
TooltipProvider,
TooltipRoot,
TooltipTrigger,
} from '@signozhq/ui/tooltip';
import { Fullscreen } from '@signozhq/icons';
import styles from './ExpandableValue.module.scss';
const DEFAULT_THRESHOLD = 100;
const DEFAULT_DIALOG_TITLE = 'Value';
const DEFAULT_Z_INDEX = 1100;
interface ExpandableValueProps {
value: string;
title?: string;
threshold?: number;
zIndex?: number;
children: ReactNode;
}
function ExpandableValue({
value,
title = DEFAULT_DIALOG_TITLE,
threshold = DEFAULT_THRESHOLD,
zIndex = DEFAULT_Z_INDEX,
children,
}: ExpandableValueProps): JSX.Element {
const [isDialogOpen, setIsDialogOpen] = useState(false);
if (value.length <= threshold) {
return <>{children}</>;
}
return (
<TooltipProvider>
<TooltipRoot>
<TooltipTrigger asChild>
<span className={styles.trigger}>{children}</span>
</TooltipTrigger>
<TooltipContent
className={styles.tooltipContent}
side="top"
style={{ zIndex }}
>
<pre className={styles.preview}>{value}</pre>
<Button
variant="outlined"
color="secondary"
size="sm"
prefix={<Fullscreen size={14} />}
onClick={(): void => setIsDialogOpen(true)}
className={styles.expandButton}
>
Expand
</Button>
</TooltipContent>
</TooltipRoot>
<DialogWrapper
title={title}
open={isDialogOpen}
onOpenChange={setIsDialogOpen}
className={styles.dialog}
style={{ zIndex }}
>
<pre className={styles.fullValue}>{value}</pre>
</DialogWrapper>
</TooltipProvider>
);
}
export default ExpandableValue;

View File

@@ -0,0 +1 @@
export { default } from './ExpandableValue';

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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")
}

View File

@@ -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"
@@ -186,6 +187,41 @@ func (store *store) ListServices(ctx context.Context, cloudIntegrationID valuer.
return services, nil
}
func (store *store) ListSharedServices(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, cloudIntegrationID valuer.UUID) (map[cloudintegrationtypes.ServiceID][]*cloudintegrationtypes.StorableCloudIntegrationService, error) {
// Subquery: service types that belong to the given account
ownTypes := store.store.BunDBCtx(ctx).
NewSelect().
TableExpr("cloud_integration_service").
ColumnExpr("type").
Where("cloud_integration_id = ?", cloudIntegrationID)
var services []*cloudintegrationtypes.StorableCloudIntegrationService
err := store.
store.
BunDBCtx(ctx).
NewSelect().
TableExpr("cloud_integration_service AS cis").
ColumnExpr("cis.*").
Join("JOIN cloud_integration AS ci ON ci.id = cis.cloud_integration_id").
Where("ci.org_id = ?", orgID).
Where("ci.provider = ?", provider).
Where("ci.removed_at IS NULL").
Where("ci.account_id IS NOT NULL").
Where("ci.last_agent_report IS NOT NULL").
Where("cis.cloud_integration_id != ?", cloudIntegrationID).
Where("cis.type IN (?)", ownTypes).
Scan(ctx, &services)
if err != nil {
return nil, err
}
result := make(map[cloudintegrationtypes.ServiceID][]*cloudintegrationtypes.StorableCloudIntegrationService)
for _, svc := range services {
result[svc.Type] = append(result[svc.Type], svc)
}
return result, nil
}
func (store *store) CreateService(ctx context.Context, service *cloudintegrationtypes.StorableCloudIntegrationService) error {
_, err := store.
store.
@@ -200,6 +236,24 @@ func (store *store) CreateService(ctx context.Context, service *cloudintegration
return nil
}
func (store *store) DeleteServicesByCloudIntegrationID(ctx context.Context, orgID, cloudIntegrationID valuer.UUID) error {
cte := store.store.BunDBCtx(ctx).
NewSelect().
TableExpr("cloud_integration_service AS cis_inner").
ColumnExpr("cis_inner.id").
Join("JOIN cloud_integration AS ci ON cis_inner.cloud_integration_id = ci.id").
Where("ci.org_id = ?", orgID).
Where("cis_inner.cloud_integration_id = ?", cloudIntegrationID)
_, err := store.store.BunDBCtx(ctx).
NewDelete().
Model(new(cloudintegrationtypes.StorableCloudIntegrationService)).
With("target", cte).
Where("id IN (SELECT id FROM target)").
Exec(ctx)
return err
}
func (store *store) UpdateService(ctx context.Context, service *cloudintegrationtypes.StorableCloudIntegrationService) error {
_, err := store.
store.
@@ -213,6 +267,62 @@ func (store *store) UpdateService(ctx context.Context, service *cloudintegration
return err
}
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, orgID valuer.UUID, provider cloudintegrationtypes.IntegrationDashboardProviderType, slug string) error {
cte := store.store.BunDBCtx(ctx).
NewSelect().
TableExpr("integration_dashboard AS id_inner").
ColumnExpr("id_inner.id").
Join("JOIN dashboard AS d ON id_inner.dashboard_id = d.id").
Where("d.org_id = ?", orgID).
Where("id_inner.provider = ?", provider).
Where("id_inner.slug = ?", slug)
_, err := store.store.BunDBCtx(ctx).
NewDelete().
Model(new(cloudintegrationtypes.StorableIntegrationDashboard)).
With("target", cte).
Where("id IN (SELECT id FROM target)").
Exec(ctx)
return err
}
func (store *store) RunInTx(ctx context.Context, cb func(ctx context.Context) error) error {
return store.store.RunInTxCtx(ctx, nil, func(ctx context.Context) error {
return cb(ctx)

View File

@@ -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,9 @@ type Module interface {
Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
// DeleteUnsafe deletes a dashboard bypassing the guards. Intended for internal system callers.
DeleteUnsafe(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
GetByMetricNames(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string][]map[string]string, error)
statsreporter.StatsCollector

View File

@@ -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

View File

@@ -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, dashboardtypes.SourceUser, 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
}
@@ -161,6 +161,10 @@ func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.U
return nil
}
func (module *module) DeleteUnsafe(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
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 {

View File

@@ -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)

View File

@@ -26,7 +26,7 @@ func buildClusterRecords(
records := make([]inframonitoringtypes.ClusterRecord, 0, len(pageGroups))
for _, labels := range pageGroups {
compositeKey := compositeKeyFromLabels(labels, groupBy)
clusterName := labels[clusterNameAttrKey]
clusterName := labels[inframonitoringtypes.ClusterNameAttrKey]
record := inframonitoringtypes.ClusterRecord{ // initialize with default values
ClusterName: clusterName,
@@ -87,6 +87,9 @@ func (m *module) getTopClusterGroups(
metadataMap map[string]map[string]string,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.ClusterNameAttrKey {
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.ClusterNameAttrKey), nil
}
queryNamesForOrderBy := orderByToClustersQueryNames[orderByKey]
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]

View File

@@ -7,14 +7,9 @@ import (
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
// TODO(nikhilmantri0902): change to k8s.cluster.uid after showing the missing
// data banner. Carried forward from v1 (see k8sClusterUIDAttrKey in
// pkg/query-service/app/inframetrics/clusters.go).
const clusterNameAttrKey = "k8s.cluster.name"
var clusterNameGroupByKey = qbtypes.GroupByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: clusterNameAttrKey,
Name: inframonitoringtypes.ClusterNameAttrKey,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},

View File

@@ -25,7 +25,7 @@ func buildDaemonSetRecords(
records := make([]inframonitoringtypes.DaemonSetRecord, 0, len(pageGroups))
for _, labels := range pageGroups {
compositeKey := compositeKeyFromLabels(labels, groupBy)
daemonSetName := labels[daemonSetNameAttrKey]
daemonSetName := labels[inframonitoringtypes.DaemonSetNameAttrKey]
record := inframonitoringtypes.DaemonSetRecord{ // initialize with default values
DaemonSetName: daemonSetName,
@@ -95,6 +95,9 @@ func (m *module) getTopDaemonSetGroups(
metadataMap map[string]map[string]string,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.DaemonSetNameAttrKey {
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.DaemonSetNameAttrKey), nil
}
queryNamesForOrderBy := orderByToDaemonSetsQueryNames[orderByKey]
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]

View File

@@ -7,14 +7,11 @@ import (
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
const (
daemonSetNameAttrKey = "k8s.daemonset.name"
daemonSetsBaseFilterExpr = "k8s.daemonset.name != ''"
)
const daemonSetsBaseFilterExpr = "k8s.daemonset.name != ''"
var daemonSetNameGroupByKey = qbtypes.GroupByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: daemonSetNameAttrKey,
Name: inframonitoringtypes.DaemonSetNameAttrKey,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},

View File

@@ -25,7 +25,7 @@ func buildDeploymentRecords(
records := make([]inframonitoringtypes.DeploymentRecord, 0, len(pageGroups))
for _, labels := range pageGroups {
compositeKey := compositeKeyFromLabels(labels, groupBy)
deploymentName := labels[deploymentNameAttrKey]
deploymentName := labels[inframonitoringtypes.DeploymentNameAttrKey]
record := inframonitoringtypes.DeploymentRecord{ // initialize with default values
DeploymentName: deploymentName,
@@ -95,6 +95,9 @@ func (m *module) getTopDeploymentGroups(
metadataMap map[string]map[string]string,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.DeploymentNameAttrKey {
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.DeploymentNameAttrKey), nil
}
queryNamesForOrderBy := orderByToDeploymentsQueryNames[orderByKey]
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]

View File

@@ -7,14 +7,11 @@ import (
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
const (
deploymentNameAttrKey = "k8s.deployment.name"
deploymentsBaseFilterExpr = "k8s.deployment.name != ''"
)
const deploymentsBaseFilterExpr = "k8s.deployment.name != ''"
var deploymentNameGroupByKey = qbtypes.GroupByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: deploymentNameAttrKey,
Name: inframonitoringtypes.DeploymentNameAttrKey,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},

View File

@@ -38,7 +38,7 @@ func (m *module) getPerGroupHostStatusCounts(
uint64(req.Start), uint64(req.End), nil,
)
hostNameExpr := fmt.Sprintf("JSONExtractString(labels, '%s')", hostNameAttrKey)
hostNameExpr := fmt.Sprintf("JSONExtractString(labels, '%s')", inframonitoringtypes.HostNameAttrKey)
sb := sqlbuilder.NewSelectBuilder()
selectCols := make([]string, 0, len(req.GroupBy)+2)
@@ -48,7 +48,7 @@ func (m *module) getPerGroupHostStatusCounts(
)
}
activeHostsSQ := m.getActiveHostsQuery(metricNames, hostNameAttrKey, sinceUnixMilli)
activeHostsSQ := m.getActiveHostsQuery(metricNames, inframonitoringtypes.HostNameAttrKey, sinceUnixMilli)
selectCols = append(selectCols,
fmt.Sprintf("uniqExactIf(%s, %s GLOBAL IN (%s)) AS active_host_count", hostNameExpr, hostNameExpr, sb.Var(activeHostsSQ)),
fmt.Sprintf("uniqExactIf(%s, %s != '') AS total_host_count", hostNameExpr, hostNameExpr),
@@ -142,7 +142,7 @@ func buildHostRecords(
records := make([]inframonitoringtypes.HostRecord, 0, len(pageGroups))
for _, labels := range pageGroups {
compositeKey := compositeKeyFromLabels(labels, groupBy)
hostName := labels[hostNameAttrKey]
hostName := labels[inframonitoringtypes.HostNameAttrKey]
activeStatus := inframonitoringtypes.HostStatusNone
activeHostCount := 0
@@ -216,6 +216,9 @@ func (m *module) getTopHostGroups(
metadataMap map[string]map[string]string,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.HostNameAttrKey {
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.HostNameAttrKey), nil
}
queryNamesForOrderBy := orderByToHostsQueryNames[orderByKey]
// The last entry is the formula/query whose value we sort by.
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
@@ -281,7 +284,7 @@ func (m *module) applyHostsActiveStatusFilter(req *inframonitoringtypes.Postable
if req.Filter.FilterByStatus == inframonitoringtypes.HostStatusInactive {
op = "NOT IN"
}
statusClause := fmt.Sprintf("%s %s (%s)", hostNameAttrKey, op, strings.Join(activeHosts, ", "))
statusClause := fmt.Sprintf("%s %s (%s)", inframonitoringtypes.HostNameAttrKey, op, strings.Join(activeHosts, ", "))
req.Filter.Expression = mergeFilterExpressions(req.Filter.Expression, statusClause)
return false
}

View File

@@ -7,14 +7,10 @@ import (
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
const (
hostNameAttrKey = "host.name"
)
// Helper group-by key used across all queries.
var hostNameGroupByKey = qbtypes.GroupByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: hostNameAttrKey,
Name: inframonitoringtypes.HostNameAttrKey,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},

View File

@@ -25,7 +25,7 @@ func buildJobRecords(
records := make([]inframonitoringtypes.JobRecord, 0, len(pageGroups))
for _, labels := range pageGroups {
compositeKey := compositeKeyFromLabels(labels, groupBy)
jobName := labels[jobNameAttrKey]
jobName := labels[inframonitoringtypes.JobNameAttrKey]
record := inframonitoringtypes.JobRecord{ // initialize with default values
JobName: jobName,
@@ -103,6 +103,9 @@ func (m *module) getTopJobGroups(
metadataMap map[string]map[string]string,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.JobNameAttrKey {
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.JobNameAttrKey), nil
}
queryNamesForOrderBy := orderByToJobsQueryNames[orderByKey]
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]

View File

@@ -7,14 +7,11 @@ import (
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
const (
jobNameAttrKey = "k8s.job.name"
jobsBaseFilterExpr = "k8s.job.name != ''"
)
const jobsBaseFilterExpr = "k8s.job.name != ''"
var jobNameGroupByKey = qbtypes.GroupByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: jobNameAttrKey,
Name: inframonitoringtypes.JobNameAttrKey,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},

View File

@@ -100,7 +100,7 @@ func (m *module) ListHosts(ctx context.Context, orgID valuer.UUID, req *inframon
// Determine active hosts: those with metrics reported in the last 10 minutes.
// Compute the cutoff once so every downstream query/subquery agrees on what "active" means.
sinceUnixMilli := time.Now().Add(-10 * time.Minute).UTC().UnixMilli()
activeHostsMap, err := m.getActiveHosts(ctx, hostsTableMetricNamesList, hostNameAttrKey, sinceUnixMilli)
activeHostsMap, err := m.getActiveHosts(ctx, hostsTableMetricNamesList, inframonitoringtypes.HostNameAttrKey, sinceUnixMilli)
if err != nil {
return nil, err
}
@@ -146,7 +146,7 @@ func (m *module) ListHosts(ctx context.Context, orgID valuer.UUID, req *inframon
// When host.name is not in groupBy, we need to run an additional query to get the counts per group for the current page,
// using the same filter expression as the main query (including user filters + page groups IN clause).
hostCounts := make(map[string]groupHostStatusCounts)
isHostNameInGroupBy := isKeyInGroupByAttrs(req.GroupBy, hostNameAttrKey)
isHostNameInGroupBy := isKeyInGroupByAttrs(req.GroupBy, inframonitoringtypes.HostNameAttrKey)
if !isHostNameInGroupBy {
hostCounts, err = m.getPerGroupHostStatusCounts(ctx, req, hostsTableMetricNamesList, pageGroups, sinceUnixMilli)
if err != nil {
@@ -324,7 +324,7 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
return nil, err
}
isNodeNameInGroupBy := isKeyInGroupByAttrs(req.GroupBy, nodeNameAttrKey)
isNodeNameInGroupBy := isKeyInGroupByAttrs(req.GroupBy, inframonitoringtypes.NodeNameAttrKey)
resp.Records = buildNodeRecords(isNodeNameInGroupBy, queryResp, pageGroups, req.GroupBy, metadataMap, nodeConditionCounts, podPhaseCounts)
resp.Warning = queryResp.Warning

View File

@@ -24,7 +24,7 @@ func buildNamespaceRecords(
records := make([]inframonitoringtypes.NamespaceRecord, 0, len(pageGroups))
for _, labels := range pageGroups {
compositeKey := compositeKeyFromLabels(labels, groupBy)
namespaceName := labels[namespaceNameAttrKey]
namespaceName := labels[inframonitoringtypes.NamespaceNameAttrKey]
record := inframonitoringtypes.NamespaceRecord{ // initialize with default values
NamespaceName: namespaceName,
@@ -70,6 +70,9 @@ func (m *module) getTopNamespaceGroups(
metadataMap map[string]map[string]string,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.NamespaceNameAttrKey {
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.NamespaceNameAttrKey), nil
}
queryNamesForOrderBy := orderByToNamespacesQueryNames[orderByKey]
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]

View File

@@ -7,13 +7,9 @@ import (
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
const (
namespaceNameAttrKey = "k8s.namespace.name"
)
var namespaceNameGroupByKey = qbtypes.GroupByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: namespaceNameAttrKey,
Name: inframonitoringtypes.NamespaceNameAttrKey,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},

View File

@@ -33,7 +33,7 @@ func buildNodeRecords(
records := make([]inframonitoringtypes.NodeRecord, 0, len(pageGroups))
for _, labels := range pageGroups {
compositeKey := compositeKeyFromLabels(labels, groupBy)
nodeName := labels[nodeNameAttrKey]
nodeName := labels[inframonitoringtypes.NodeNameAttrKey]
record := inframonitoringtypes.NodeRecord{ // initialize with default values
NodeName: nodeName,
@@ -105,6 +105,9 @@ func (m *module) getTopNodeGroups(
metadataMap map[string]map[string]string,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.NodeNameAttrKey {
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.NodeNameAttrKey), nil
}
queryNamesForOrderBy := orderByToNodesQueryNames[orderByKey]
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
@@ -201,7 +204,7 @@ func (m *module) getPerGroupNodeConditionCounts(
timeSeriesFPs := sqlbuilder.NewSelectBuilder()
timeSeriesFPsSelectCols := []string{
"fingerprint",
fmt.Sprintf("JSONExtractString(labels, %s) AS node_name", timeSeriesFPs.Var(nodeNameAttrKey)),
fmt.Sprintf("JSONExtractString(labels, %s) AS node_name", timeSeriesFPs.Var(inframonitoringtypes.NodeNameAttrKey)),
}
for _, key := range groupBy {
timeSeriesFPsSelectCols = append(timeSeriesFPsSelectCols,

View File

@@ -7,14 +7,11 @@ import (
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
const (
nodeNameAttrKey = "k8s.node.name"
nodeConditionMetricName = "k8s.node.condition_ready"
)
const nodeConditionMetricName = "k8s.node.condition_ready"
var nodeNameGroupByKey = qbtypes.GroupByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: nodeNameAttrKey,
Name: inframonitoringtypes.NodeNameAttrKey,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},

View File

@@ -124,6 +124,9 @@ func (m *module) getTopPodGroups(
metadataMap map[string]map[string]string,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.PodNameAttrKey {
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.PodNameAttrKey), nil
}
queryNamesForOrderBy := orderByToPodsQueryNames[orderByKey]
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]

View File

@@ -25,7 +25,7 @@ func buildStatefulSetRecords(
records := make([]inframonitoringtypes.StatefulSetRecord, 0, len(pageGroups))
for _, labels := range pageGroups {
compositeKey := compositeKeyFromLabels(labels, groupBy)
statefulSetName := labels[statefulSetNameAttrKey]
statefulSetName := labels[inframonitoringtypes.StatefulSetNameAttrKey]
record := inframonitoringtypes.StatefulSetRecord{ // initialize with default values
StatefulSetName: statefulSetName,
@@ -95,6 +95,9 @@ func (m *module) getTopStatefulSetGroups(
metadataMap map[string]map[string]string,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.StatefulSetNameAttrKey {
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.StatefulSetNameAttrKey), nil
}
queryNamesForOrderBy := orderByToStatefulSetsQueryNames[orderByKey]
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]

View File

@@ -7,14 +7,11 @@ import (
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
const (
statefulSetNameAttrKey = "k8s.statefulset.name"
statefulSetsBaseFilterExpr = "k8s.statefulset.name != ''"
)
const statefulSetsBaseFilterExpr = "k8s.statefulset.name != ''"
var statefulSetNameGroupByKey = qbtypes.GroupByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: statefulSetNameAttrKey,
Name: inframonitoringtypes.StatefulSetNameAttrKey,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},

View File

@@ -23,7 +23,7 @@ func buildVolumeRecords(
records := make([]inframonitoringtypes.VolumeRecord, 0, len(pageGroups))
for _, labels := range pageGroups {
compositeKey := compositeKeyFromLabels(labels, groupBy)
pvcName := labels[persistentVolumeClaimNameAttrKey]
pvcName := labels[inframonitoringtypes.PersistentVolumeClaimNameAttrKey]
record := inframonitoringtypes.VolumeRecord{ // initialize with default values
PersistentVolumeClaimName: pvcName,
@@ -75,6 +75,9 @@ func (m *module) getTopVolumeGroups(
metadataMap map[string]map[string]string,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.PersistentVolumeClaimNameAttrKey {
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.PersistentVolumeClaimNameAttrKey), nil
}
queryNamesForOrderBy := orderByToVolumesQueryNames[orderByKey]
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]

View File

@@ -7,14 +7,11 @@ import (
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
const (
persistentVolumeClaimNameAttrKey = "k8s.persistentvolumeclaim.name"
volumesBaseFilterExpr = "k8s.persistentvolumeclaim.name != ''"
)
const volumesBaseFilterExpr = "k8s.persistentvolumeclaim.name != ''"
var pvcNameGroupByKey = qbtypes.GroupByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: persistentVolumeClaimNameAttrKey,
Name: inframonitoringtypes.PersistentVolumeClaimNameAttrKey,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},

View File

@@ -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"
@@ -1111,40 +1110,15 @@ func (aH *APIHandler) Get(rw http.ResponseWriter, r *http.Request) {
return
}
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) {
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"))
return
}
dashboard = integrationDashboard
} else {
dashboardID, err := valuer.NewUUID(id)
if err != nil {
render.Error(rw, err)
return
}
sqlDashboard, err := aH.Signoz.Modules.Dashboard.Get(ctx, orgID, dashboardID)
if err != nil {
render.Error(rw, err)
return
}
dashboard = sqlDashboard
dashboardID, err := valuer.NewUUID(id)
if err != nil {
render.Error(rw, err)
return
}
dashboard, err := aH.Signoz.Modules.Dashboard.Get(ctx, orgID, dashboardID)
if err != nil {
render.Error(rw, err)
return
}
gettableDashboard, err := dashboardtypes.NewGettableDashboardFromDashboard(dashboard)
@@ -1172,31 +1146,11 @@ func (aH *APIHandler) List(rw http.ResponseWriter, r *http.Request) {
return
}
dashboards := make([]*dashboardtypes.Dashboard, 0)
sqlDashboards, err := aH.Signoz.Modules.Dashboard.List(ctx, orgID)
dashboards, err := aH.Signoz.Modules.Dashboard.List(ctx, orgID)
if err != nil && !errorsV2.Ast(err, errorsV2.TypeNotFound) {
render.Error(rw, err)
return
}
if sqlDashboards != nil {
dashboards = append(dashboards, sqlDashboards...)
}
installedIntegrationDashboards, apiErr := aH.IntegrationsController.GetDashboardsForInstalledIntegrations(ctx, orgID)
if apiErr != nil {
aH.logger.ErrorContext(ctx, "failed to get dashboards for installed integrations", errors.Attr(apiErr))
} else {
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 {
@@ -3286,7 +3240,7 @@ func (aH *APIHandler) InstallIntegration(w http.ResponseWriter, r *http.Request)
}
integration, apiErr := aH.IntegrationsController.Install(
r.Context(), claims.OrgID, &req,
r.Context(), claims.OrgID, &req, claims.Email, valuer.MustNewUUID(claims.IdentityID()),
)
if apiErr != nil {
RespondError(w, apiErr, nil)

View File

@@ -4,10 +4,10 @@ import (
"context"
"fmt"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -16,8 +16,8 @@ type Controller struct {
mgr *Manager
}
func NewController(sqlStore sqlstore.SQLStore) (*Controller, error) {
mgr, err := NewManager(sqlStore)
func NewController(sqlStore sqlstore.SQLStore, dashboardModule dashboard.Module) (*Controller, error) {
mgr, err := NewManager(sqlStore, dashboardModule)
if err != nil {
return nil, fmt.Errorf("couldn't create integrations manager: %w", err)
}
@@ -74,9 +74,9 @@ type InstallIntegrationRequest struct {
Config map[string]interface{} `json:"config"`
}
func (c *Controller) Install(ctx context.Context, orgId string, req *InstallIntegrationRequest) (*IntegrationsListItem, *model.ApiError) {
func (c *Controller) Install(ctx context.Context, orgId string, req *InstallIntegrationRequest, createdBy string, creator valuer.UUID) (*IntegrationsListItem, *model.ApiError) {
res, apiErr := c.mgr.InstallIntegration(
ctx, orgId, req.IntegrationId, req.Config,
ctx, orgId, req.IntegrationId, req.Config, createdBy, creator,
)
if apiErr != nil {
return nil, apiErr
@@ -109,15 +109,3 @@ func (c *Controller) Uninstall(ctx context.Context, orgId string, req *Uninstall
func (c *Controller) GetPipelinesForInstalledIntegrations(ctx context.Context, orgId string) ([]pipelinetypes.GettablePipeline, error) {
return c.mgr.GetPipelinesForInstalledIntegrations(ctx, orgId)
}
func (c *Controller) GetDashboardsForInstalledIntegrations(ctx context.Context, orgId valuer.UUID) ([]*dashboardtypes.Dashboard, *model.ApiError) {
return c.mgr.GetDashboardsForInstalledIntegrations(ctx, orgId)
}
func (c *Controller) GetInstalledIntegrationDashboardById(ctx context.Context, orgId valuer.UUID, dashboardUuid string) (*dashboardtypes.Dashboard, *model.ApiError) {
return c.mgr.GetInstalledIntegrationDashboardById(ctx, orgId, dashboardUuid)
}
func (c *Controller) IsInstalledIntegrationDashboardID(dashboardUuid string) bool {
return c.mgr.IsInstalledIntegrationDashboardUuid(dashboardUuid)
}

View File

@@ -6,6 +6,7 @@ import (
"slices"
"strings"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/query-service/utils"
"github.com/SigNoz/signoz/pkg/sqlstore"
@@ -114,9 +115,10 @@ type Integration struct {
type Manager struct {
availableIntegrationsRepo AvailableIntegrationsRepo
installedIntegrationsRepo InstalledIntegrationsRepo
dashboardModule dashboard.Module
}
func NewManager(store sqlstore.SQLStore) (*Manager, error) {
func NewManager(store sqlstore.SQLStore, dashboardModule dashboard.Module) (*Manager, error) {
iiRepo, err := NewInstalledIntegrationsSqliteRepo(store)
if err != nil {
return nil, fmt.Errorf(
@@ -127,6 +129,7 @@ func NewManager(store sqlstore.SQLStore) (*Manager, error) {
return &Manager{
availableIntegrationsRepo: &BuiltInIntegrations{},
installedIntegrationsRepo: iiRepo,
dashboardModule: dashboardModule,
}, nil
}
@@ -225,12 +228,19 @@ func (m *Manager) InstallIntegration(
orgId string,
integrationId string,
config integrationtypes.InstalledIntegrationConfig,
createdBy string,
creator valuer.UUID,
) (*IntegrationsListItem, *model.ApiError) {
integrationDetails, apiErr := m.getIntegrationDetails(ctx, integrationId)
if apiErr != nil {
return nil, apiErr
}
orgUUID, err := valuer.NewUUID(orgId)
if err != nil {
return nil, model.BadRequest(fmt.Errorf("invalid org id: %w", err))
}
_, apiErr = m.installedIntegrationsRepo.upsert(
ctx, orgId, integrationId, config,
)
@@ -240,6 +250,10 @@ func (m *Manager) InstallIntegration(
)
}
if err := m.provisionDashboards(ctx, orgUUID, createdBy, creator, integrationId, integrationDetails); err != nil {
return nil, model.InternalError(fmt.Errorf("could not provision dashboards: %w", err))
}
return &IntegrationsListItem{
IntegrationSummary: integrationDetails.IntegrationSummary,
IsInstalled: true,
@@ -251,9 +265,78 @@ func (m *Manager) UninstallIntegration(
orgId string,
integrationId string,
) *model.ApiError {
orgUUID, err := valuer.NewUUID(orgId)
if err != nil {
return model.BadRequest(fmt.Errorf("invalid org id: %w", err))
}
if err := m.deprovisionDashboards(ctx, orgUUID, integrationId); err != nil {
return model.InternalError(fmt.Errorf("could not deprovision dashboards: %w", err))
}
return m.installedIntegrationsRepo.delete(ctx, orgId, integrationId)
}
func (m *Manager) provisionDashboards(
ctx context.Context,
orgID valuer.UUID,
createdBy string,
creator valuer.UUID,
integrationID string,
integration *IntegrationDetails,
) error {
for _, dd := range integration.Assets.Dashboards {
dashName, _ := dd["id"].(string)
if dashName == "" {
continue
}
slug := integrationtypes.InstalledIntegrationDashboardSlug(integrationID, dashName)
existing, err := m.installedIntegrationsRepo.getIntegrationDashboardBySlug(ctx, orgID.StringValue(), slug)
if err == nil && existing != nil {
continue
}
createdDashboard, err := m.dashboardModule.Create(ctx, orgID, createdBy, creator, dashboardtypes.SourceIntegration, dashboardtypes.PostableDashboard(dd))
if err != nil {
return fmt.Errorf("could not create dashboard for slug %s: %w", slug, err)
}
row := integrationtypes.NewStorableIntegrationDashboard(createdDashboard.ID, slug)
if err := m.installedIntegrationsRepo.createIntegrationDashboard(ctx, row); err != nil {
return fmt.Errorf("could not create integration_dashboard row for slug %s: %w", slug, err)
}
}
return nil
}
func (m *Manager) deprovisionDashboards(
ctx context.Context,
orgID valuer.UUID,
integrationID string,
) error {
slugPrefix := integrationtypes.InstalledIntegrationDashboardSlugPrefix(integrationID)
rows, err := m.installedIntegrationsRepo.listIntegrationDashboardsBySlugPrefix(ctx, orgID.StringValue(), slugPrefix)
if err != nil {
return err
}
for _, row := range rows {
if err := m.installedIntegrationsRepo.deleteIntegrationDashboardBySlug(ctx, orgID.StringValue(), row.Slug); err != nil {
return err
}
dashID, err := valuer.NewUUID(row.DashboardID)
if err != nil {
return err
}
if err := m.dashboardModule.DeleteUnsafe(ctx, orgID, dashID); err != nil {
return err
}
}
return nil
}
func (m *Manager) GetPipelinesForInstalledIntegrations(
ctx context.Context,
orgId string,
@@ -289,115 +372,6 @@ func (m *Manager) GetPipelinesForInstalledIntegrations(
return gettablePipelines, nil
}
func (m *Manager) dashboardUuid(integrationId string, dashboardId string) string {
return strings.Join([]string{"integration", integrationId, dashboardId}, "--")
}
func (m *Manager) parseDashboardUuid(dashboardUuid string) (
integrationId string, dashboardId string, apiErr *model.ApiError,
) {
parts := strings.SplitN(dashboardUuid, "--", 3)
if len(parts) != 3 || parts[0] != "integration" {
return "", "", model.BadRequest(fmt.Errorf(
"invalid installed integration dashboard id",
))
}
return parts[1], parts[2], nil
}
func (m *Manager) IsInstalledIntegrationDashboardUuid(dashboardUuid string) bool {
_, _, apiErr := m.parseDashboardUuid(dashboardUuid)
return apiErr == nil
}
func (m *Manager) GetInstalledIntegrationDashboardById(
ctx context.Context,
orgId valuer.UUID,
dashboardUuid string,
) (*dashboardtypes.Dashboard, *model.ApiError) {
integrationId, dashboardId, apiErr := m.parseDashboardUuid(dashboardUuid)
if apiErr != nil {
return nil, apiErr
}
integration, apiErr := m.GetIntegration(ctx, orgId.StringValue(), integrationId)
if apiErr != nil {
return nil, apiErr
}
if integration.Installation == nil {
return nil, model.BadRequest(fmt.Errorf(
"integration with id %s is not installed", integrationId,
))
}
for _, dd := range integration.IntegrationDetails.Assets.Dashboards {
if dId, exists := dd["id"]; exists {
if id, ok := dId.(string); ok && id == dashboardId {
author := "integration"
return &dashboardtypes.Dashboard{
ID: m.dashboardUuid(integrationId, string(dashboardId)),
Locked: true,
Data: dd,
TimeAuditable: types.TimeAuditable{
CreatedAt: integration.Installation.InstalledAt,
UpdatedAt: integration.Installation.InstalledAt,
},
UserAuditable: types.UserAuditable{
CreatedBy: author,
UpdatedBy: author,
},
OrgID: orgId,
Source: dashboardtypes.SourceIntegration,
}, nil
}
}
}
return nil, model.NotFoundError(fmt.Errorf(
"integration dashboard with id %s not found", dashboardUuid,
))
}
func (m *Manager) GetDashboardsForInstalledIntegrations(
ctx context.Context,
orgId valuer.UUID,
) ([]*dashboardtypes.Dashboard, *model.ApiError) {
installedIntegrations, apiErr := m.getInstalledIntegrations(ctx, orgId.StringValue())
if apiErr != nil {
return nil, apiErr
}
result := []*dashboardtypes.Dashboard{}
for _, ii := range installedIntegrations {
for _, dd := range ii.Assets.Dashboards {
if dId, exists := dd["id"]; exists {
if dashboardId, ok := dId.(string); ok {
author := "integration"
result = append(result, &dashboardtypes.Dashboard{
ID: m.dashboardUuid(ii.IntegrationSummary.Id, dashboardId),
Locked: true,
Data: dd,
TimeAuditable: types.TimeAuditable{
CreatedAt: ii.Installation.InstalledAt,
UpdatedAt: ii.Installation.InstalledAt,
},
UserAuditable: types.UserAuditable{
CreatedBy: author,
UpdatedBy: author,
},
OrgID: orgId,
Source: dashboardtypes.SourceIntegration,
})
}
}
}
}
return result, nil
}
// Helpers.
func (m *Manager) getIntegrationDetails(

View File

@@ -22,6 +22,14 @@ type InstalledIntegrationsRepo interface {
) (*integrationtypes.InstalledIntegration, *model.ApiError)
delete(ctx context.Context, orgId string, integrationType string) *model.ApiError
createIntegrationDashboard(ctx context.Context, row *integrationtypes.StorableIntegrationDashboard) error
getIntegrationDashboardBySlug(ctx context.Context, orgID string, slug string) (*integrationtypes.StorableIntegrationDashboard, error)
listIntegrationDashboardsBySlugPrefix(ctx context.Context, orgID string, slugPrefix string) ([]*integrationtypes.StorableIntegrationDashboard, error)
deleteIntegrationDashboardBySlug(ctx context.Context, orgID string, slug string) error
}
type AvailableIntegrationsRepo interface {

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
@@ -129,3 +130,67 @@ func (r *InstalledIntegrationsSqliteRepo) delete(
return nil
}
func (r *InstalledIntegrationsSqliteRepo) createIntegrationDashboard(
ctx context.Context, row *integrationtypes.StorableIntegrationDashboard,
) error {
_, err := r.store.BunDBCtx(ctx).NewInsert().Model(row).Exec(ctx)
return err
}
func (r *InstalledIntegrationsSqliteRepo) getIntegrationDashboardBySlug(
ctx context.Context, orgID string, slug string,
) (*integrationtypes.StorableIntegrationDashboard, error) {
row := new(integrationtypes.StorableIntegrationDashboard)
err := r.store.BunDBCtx(ctx).
NewSelect().
Model(row).
Join("JOIN dashboard AS d ON storable_integration_dashboard.dashboard_id = d.id").
Where("d.org_id = ?", orgID).
Where("storable_integration_dashboard.provider = ?", integrationtypes.InstalledIntegrationProvider).
Where("storable_integration_dashboard.slug = ?", slug).
Scan(ctx)
if err != nil {
return nil, r.store.WrapNotFoundErrf(err, errors.CodeNotFound, "integration dashboard with slug %s not found", slug)
}
return row, nil
}
func (r *InstalledIntegrationsSqliteRepo) listIntegrationDashboardsBySlugPrefix(
ctx context.Context, orgID string, slugPrefix string,
) ([]*integrationtypes.StorableIntegrationDashboard, error) {
var rows []*integrationtypes.StorableIntegrationDashboard
err := r.store.BunDBCtx(ctx).
NewSelect().
Model(&rows).
Join("JOIN dashboard AS d ON storable_integration_dashboard.dashboard_id = d.id").
Where("d.org_id = ?", orgID).
Where("storable_integration_dashboard.provider = ?", integrationtypes.InstalledIntegrationProvider).
Where("storable_integration_dashboard.slug LIKE ?", slugPrefix+"%").
Scan(ctx)
if err != nil {
return nil, err
}
return rows, nil
}
func (r *InstalledIntegrationsSqliteRepo) deleteIntegrationDashboardBySlug(
ctx context.Context, orgID string, slug string,
) error {
cte := r.store.BunDBCtx(ctx).
NewSelect().
TableExpr("integration_dashboard AS id_inner").
ColumnExpr("id_inner.id").
Join("JOIN dashboard AS d ON id_inner.dashboard_id = d.id").
Where("d.org_id = ?", orgID).
Where("id_inner.provider = ?", integrationtypes.InstalledIntegrationProvider).
Where("id_inner.slug = ?", slug)
_, err := r.store.BunDBCtx(ctx).
NewDelete().
Model(new(integrationtypes.StorableIntegrationDashboard)).
With("target", cte).
Where("id IN (SELECT id FROM target)").
Exec(ctx)
return err
}

View File

@@ -56,7 +56,7 @@ type Server struct {
// NewServer creates and initializes Server
func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
integrationsController, err := integrations.NewController(signoz.SQLStore)
integrationsController, err := integrations.NewController(signoz.SQLStore, signoz.Modules.Dashboard)
if err != nil {
return nil, err
}

View File

@@ -205,6 +205,8 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewAddRoleCRUDTuplesFactory(sqlstore),
sqlmigration.NewAddIntegrationDashboardFactory(sqlstore, sqlschema),
sqlmigration.NewAddSourceToDashboardFactory(sqlstore, sqlschema),
sqlmigration.NewMigrateCloudIntegrationDashboardsFactory(sqlstore),
sqlmigration.NewMigrateInstalledIntegrationDashboardsFactory(sqlstore),
)
}

View File

@@ -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
}

View 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_migrate_ci_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_migrate_ci_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_migrate_ci_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
}

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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,197 @@
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 087_migrate_installed_integration_dashboards
var installedIntegrationDashboardFiles embed.FS
type migrateInstalledIntegrationDashboards struct {
sqlstore sqlstore.SQLStore
}
type installedIntegrationRow struct {
bun.BaseModel `bun:"table:installed_integration"`
Type string `bun:"type"`
OrgID string `bun:"org_id"`
}
type installedIntegrationOrgKey struct {
orgID string
integrationType string
}
type installedIntDashboardRow 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 installedIntDashboardLinkRow 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 NewMigrateInstalledIntegrationDashboardsFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(
factory.MustNewName("migrate_installed_integration_dashboards"),
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &migrateInstalledIntegrationDashboards{sqlstore: sqlstore}, nil
},
)
}
func (m *migrateInstalledIntegrationDashboards) Register(migrations *migrate.Migrations) error {
return migrations.Register(m.Up, m.Down)
}
func (m *migrateInstalledIntegrationDashboards) 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 installations []*installedIntegrationRow
if err := tx.NewSelect().Model(&installations).Scan(ctx); err != nil {
return err
}
seen := make(map[installedIntegrationOrgKey]struct{})
now := time.Now()
for _, inst := range installations {
key := installedIntegrationOrgKey{orgID: inst.OrgID, integrationType: inst.Type}
if _, dup := seen[key]; dup {
continue
}
seen[key] = struct{}{}
defs, ok := dashboardDefs[inst.Type]
if !ok {
continue
}
for dashName, dashboardJSON := range defs {
slug := fmt.Sprintf("%s-%s", inst.Type, 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 = ?", inst.OrgID).
Where("id.provider = ?", "installed_integration").
Where("id.slug = ?", slug).
Count(ctx)
if err != nil {
return err
}
if count > 0 {
continue
}
dashID := valuer.GenerateUUID().StringValue()
dashRow := &installedIntDashboardRow{
ID: dashID,
CreatedAt: now,
UpdatedAt: now,
Data: string(dashboardJSON),
Locked: true,
OrgID: inst.OrgID,
Source: "integration",
}
if _, err := tx.NewInsert().Model(dashRow).Exec(ctx); err != nil {
return err
}
intRow := &installedIntDashboardLinkRow{
ID: valuer.GenerateUUID().StringValue(),
DashboardID: dashID,
Provider: "installed_integration",
Slug: slug,
CreatedAt: now,
UpdatedAt: now,
}
if _, err := tx.NewInsert().Model(intRow).Exec(ctx); err != nil {
return err
}
}
}
return tx.Commit()
}
func (m *migrateInstalledIntegrationDashboards) Down(context.Context, *bun.DB) error {
return nil
}
// loadDashboardDefs returns map[integrationID]map[dashName]rawJSON.
// Only non-_dot dashboard files are loaded (the standard variants).
func (m *migrateInstalledIntegrationDashboards) loadDashboardDefs() (map[string]map[string]json.RawMessage, error) {
result := make(map[string]map[string]json.RawMessage)
err := fs.WalkDir(installedIntegrationDashboardFiles, "087_migrate_installed_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: 087_migrate_installed_integration_dashboards/{integrationID}/{dashName}.json
rel := strings.TrimPrefix(path, "087_migrate_installed_integration_dashboards/")
parts := strings.SplitN(rel, "/", 2)
if len(parts) != 2 {
return nil
}
integrationID := parts[0]
dashName := strings.TrimSuffix(parts[1], ".json")
data, err := installedIntegrationDashboardFiles.ReadFile(path)
if err != nil {
return err
}
if result[integrationID] == nil {
result[integrationID] = make(map[string]json.RawMessage)
}
result[integrationID][dashName] = json.RawMessage(data)
return nil
})
return result, err
}

View File

@@ -0,0 +1,631 @@
{
"description": "This dashboard provides a high-level overview of your MongoDB. It includes read/write performance, most-used replicas, collection metrics etc...",
"id": "mongo-overview",
"layout": [
{
"h": 3,
"i": "0c3d2b15-89be-4d62-a821-b26d93332ed3",
"moved": false,
"static": false,
"w": 6,
"x": 6,
"y": 3
},
{
"h": 3,
"i": "14504a3c-4a05-4d22-bab3-e22e94f51380",
"moved": false,
"static": false,
"w": 6,
"x": 0,
"y": 6
},
{
"h": 3,
"i": "dcfb3829-c3f2-44bb-907d-8dc8a6dc4aab",
"moved": false,
"static": false,
"w": 6,
"x": 0,
"y": 3
},
{
"h": 3,
"i": "bfc9e80b-02bf-4122-b3da-3dd943d35012",
"moved": false,
"static": false,
"w": 6,
"x": 6,
"y": 0
},
{
"h": 3,
"i": "4c07a7d2-893a-46c2-bcdb-a19b6efeac3a",
"moved": false,
"static": false,
"w": 6,
"x": 0,
"y": 0
},
{
"h": 3,
"i": "a5a64eec-1034-4aa6-8cb1-05673c4426c6",
"moved": false,
"static": false,
"w": 6,
"x": 6,
"y": 6
},
{
"h": 3,
"i": "503af589-ef4d-4fe3-8934-c8f7eb480d9a",
"moved": false,
"static": false,
"w": 6,
"x": 0,
"y": 9
}
],
"name": "",
"tags": [
"mongo",
"database"
],
"title": "Mongo overview",
"variables": {
"a2c21714-a814-4d31-9b56-7367c3208801": {
"allSelected": true,
"customValue": "",
"description": "List of hosts sending mongo metrics",
"id": "a2c21714-a814-4d31-9b56-7367c3208801",
"modificationUUID": "448e675a-4531-45b1-b434-a9ee809470d6",
"multiSelect": true,
"name": "host.name",
"order": 0,
"queryValue": "SELECT JSONExtractString(labels, 'host.name') AS `host.name`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'mongodb_memory_usage'\nGROUP BY `host.name`",
"selectedValue": [
"Srikanths-MacBook-Pro.local"
],
"showALLOption": true,
"sort": "ASC",
"textboxValue": "",
"type": "QUERY"
}
},
"version": "v5",
"widgets": [
{
"description": "Total number of operations",
"fillSpans": false,
"id": "4c07a7d2-893a-46c2-bcdb-a19b6efeac3a",
"isStacked": false,
"nullZeroValues": "zero",
"opacity": "1",
"panelTypes": "graph",
"query": {
"builder": {
"queryData": [
{
"aggregations": [
{
"metricName": "mongodb.operation.count",
"reduceTo": "sum",
"spaceAggregation": "sum",
"temporality": null,
"timeAggregation": "rate"
}
],
"dataSource": "metrics",
"disabled": false,
"expression": "A",
"filter": {
"expression": "host.name IN $host.name"
},
"groupBy": [
{
"dataType": "string",
"id": "operation--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "operation",
"type": "tag"
}
],
"having": {
"expression": ""
},
"legend": "{{operation}}",
"limit": null,
"orderBy": [],
"queryName": "A",
"stepInterval": 60
}
],
"queryFormulas": []
},
"clickhouse_sql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"id": "7da5d899-8b06-4139-9a89-47baf9551ff8",
"promql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"queryType": "builder"
},
"softMax": null,
"softMin": null,
"thresholds": [],
"timePreferance": "GLOBAL_TIME",
"title": "Operations count",
"yAxisUnit": "none"
},
{
"description": "The total time spent performing operations.",
"fillSpans": false,
"id": "bfc9e80b-02bf-4122-b3da-3dd943d35012",
"isStacked": false,
"nullZeroValues": "zero",
"opacity": "1",
"panelTypes": "graph",
"query": {
"builder": {
"queryData": [
{
"aggregations": [
{
"metricName": "mongodb.operation.time",
"reduceTo": "sum",
"spaceAggregation": "sum",
"temporality": null,
"timeAggregation": "rate"
}
],
"dataSource": "metrics",
"disabled": false,
"expression": "A",
"filter": {
"expression": "host.name IN $host.name"
},
"groupBy": [
{
"dataType": "string",
"id": "operation--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "operation",
"type": "tag"
}
],
"having": {
"expression": ""
},
"legend": "{{operation}}",
"limit": null,
"orderBy": [],
"queryName": "A",
"stepInterval": 60
}
],
"queryFormulas": []
},
"clickhouse_sql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"id": "2ca35957-894a-46ae-a2a6-95d7e400d8e1",
"promql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"queryType": "builder"
},
"softMax": null,
"softMin": null,
"thresholds": [],
"timePreferance": "GLOBAL_TIME",
"title": "Total operations time",
"yAxisUnit": "ms"
},
{
"description": "The number of cache operations",
"fillSpans": false,
"id": "dcfb3829-c3f2-44bb-907d-8dc8a6dc4aab",
"isStacked": false,
"nullZeroValues": "zero",
"opacity": "1",
"panelTypes": "graph",
"query": {
"builder": {
"queryData": [
{
"aggregations": [
{
"metricName": "mongodb.cache.operations",
"reduceTo": "sum",
"spaceAggregation": "sum",
"temporality": null,
"timeAggregation": "rate"
}
],
"dataSource": "metrics",
"disabled": false,
"expression": "A",
"filter": {
"expression": "host.name IN $host.name"
},
"groupBy": [
{
"dataType": "string",
"id": "type--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "type",
"type": "tag"
}
],
"having": {
"expression": ""
},
"legend": "{{type}}",
"limit": null,
"orderBy": [],
"queryName": "A",
"stepInterval": 60
}
],
"queryFormulas": []
},
"clickhouse_sql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"id": "bb439198-dcf5-4767-b0d0-ab5785159b8d",
"promql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"queryType": "builder"
},
"softMax": null,
"softMin": null,
"thresholds": [],
"timePreferance": "GLOBAL_TIME",
"title": "Cache operations",
"yAxisUnit": "none"
},
{
"description": "",
"fillSpans": false,
"id": "14504a3c-4a05-4d22-bab3-e22e94f51380",
"isStacked": false,
"nullZeroValues": "zero",
"opacity": "1",
"panelTypes": "graph",
"query": {
"builder": {
"queryData": [
{
"aggregations": [
{
"metricName": "mongodb.operation.latency.time",
"reduceTo": "sum",
"spaceAggregation": "max",
"temporality": null,
"timeAggregation": "max"
}
],
"dataSource": "metrics",
"disabled": false,
"expression": "A",
"filter": {
"expression": "(operation = 'read' AND host.name IN $host.name)"
},
"groupBy": [],
"having": {
"expression": ""
},
"legend": "Latency",
"limit": null,
"orderBy": [],
"queryName": "A",
"stepInterval": 60
}
],
"queryFormulas": []
},
"clickhouse_sql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"id": "4a9cafe8-778b-476c-b825-c04e165bf285",
"promql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"queryType": "builder"
},
"softMax": null,
"softMin": null,
"thresholds": [],
"timePreferance": "GLOBAL_TIME",
"title": "Read latency",
"yAxisUnit": "µs"
},
{
"description": "",
"fillSpans": false,
"id": "a5a64eec-1034-4aa6-8cb1-05673c4426c6",
"isStacked": false,
"nullZeroValues": "zero",
"opacity": "1",
"panelTypes": "graph",
"query": {
"builder": {
"queryData": [
{
"aggregations": [
{
"metricName": "mongodb.operation.latency.time",
"reduceTo": "sum",
"spaceAggregation": "max",
"temporality": null,
"timeAggregation": "max"
}
],
"dataSource": "metrics",
"disabled": false,
"expression": "A",
"filter": {
"expression": "(host.name IN $host.name AND operation = 'write')"
},
"groupBy": [],
"having": {
"expression": ""
},
"legend": "Latency",
"limit": null,
"orderBy": [],
"queryName": "A",
"stepInterval": 60
}
],
"queryFormulas": []
},
"clickhouse_sql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"id": "446827eb-a4f2-4ff3-966b-fb65288c983b",
"promql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"queryType": "builder"
},
"softMax": null,
"softMin": null,
"thresholds": [],
"timePreferance": "GLOBAL_TIME",
"title": "Write latency",
"yAxisUnit": "µs"
},
{
"description": "",
"fillSpans": false,
"id": "503af589-ef4d-4fe3-8934-c8f7eb480d9a",
"isStacked": false,
"nullZeroValues": "zero",
"opacity": "1",
"panelTypes": "graph",
"query": {
"builder": {
"queryData": [
{
"aggregations": [
{
"metricName": "mongodb.operation.latency.time",
"reduceTo": "sum",
"spaceAggregation": "max",
"temporality": null,
"timeAggregation": "max"
}
],
"dataSource": "metrics",
"disabled": false,
"expression": "A",
"filter": {
"expression": "(host.name IN $host.name AND operation = 'command')"
},
"groupBy": [],
"having": {
"expression": ""
},
"legend": "Latency",
"limit": null,
"orderBy": [],
"queryName": "A",
"stepInterval": 60
}
],
"queryFormulas": []
},
"clickhouse_sql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"id": "7b7b977d-0921-4552-8cfe-d82dfde63ef4",
"promql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"queryType": "builder"
},
"softMax": null,
"softMin": null,
"thresholds": [],
"timePreferance": "GLOBAL_TIME",
"title": "Command latency",
"yAxisUnit": "µs"
},
{
"description": "",
"fillSpans": false,
"id": "0c3d2b15-89be-4d62-a821-b26d93332ed3",
"isStacked": false,
"nullZeroValues": "zero",
"opacity": "1",
"panelTypes": "graph",
"query": {
"builder": {
"queryData": [
{
"aggregations": [
{
"metricName": "mongodb.network.io.receive",
"reduceTo": "sum",
"spaceAggregation": "avg",
"temporality": null,
"timeAggregation": "avg"
}
],
"dataSource": "metrics",
"disabled": false,
"expression": "A",
"filter": {
"expression": "host.name IN $host.name"
},
"groupBy": [
{
"dataType": "string",
"id": "host.name--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "host.name",
"type": "tag"
}
],
"having": {
"expression": ""
},
"legend": "Bytes received :: {{host.name}}",
"limit": null,
"orderBy": [],
"queryName": "A",
"stepInterval": 60
},
{
"aggregations": [
{
"metricName": "mongodb.network.io.transmit",
"reduceTo": "sum",
"spaceAggregation": "avg",
"temporality": null,
"timeAggregation": "avg"
}
],
"dataSource": "metrics",
"disabled": false,
"expression": "B",
"filter": {
"expression": "host.name IN $host.name"
},
"groupBy": [
{
"dataType": "string",
"id": "host.name--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "host.name",
"type": "tag"
}
],
"having": {
"expression": ""
},
"legend": "Bytes transmitted :: {{host.name}}",
"limit": null,
"orderBy": [],
"queryName": "B",
"stepInterval": 60
}
],
"queryFormulas": []
},
"clickhouse_sql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"id": "41eea5bc-f9cf-45c2-92fb-ef226d6b540b",
"promql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"queryType": "builder"
},
"softMax": null,
"softMin": null,
"thresholds": [],
"timePreferance": "GLOBAL_TIME",
"title": "Network IO",
"yAxisUnit": "bytes"
}
]
}

View File

@@ -0,0 +1,779 @@
{
"description": "This dashboard shows the Redis instance overview. It includes latency, hit/miss rate, connections, and memory information.\n",
"id": "redis-overview",
"layout": [
{
"h": 3,
"i": "d4c164bc-8fc2-4dbc-aadd-8d17479ca649",
"moved": false,
"static": false,
"w": 6,
"x": 0,
"y": 9
},
{
"h": 3,
"i": "2fbaef0d-3cdb-4ce3-aa3c-9bbbb41786d9",
"moved": false,
"static": false,
"w": 6,
"x": 3,
"y": 6
},
{
"h": 3,
"i": "f5ee1511-0d2b-4404-9ce0-e991837decc2",
"moved": false,
"static": false,
"w": 6,
"x": 6,
"y": 3
},
{
"h": 3,
"i": "b19c7058-b806-4ea2-974a-ca555b168991",
"moved": false,
"static": false,
"w": 6,
"x": 0,
"y": 3
},
{
"h": 3,
"i": "bf0deeeb-e926-4234-944c-82bacd96af47",
"moved": false,
"static": false,
"w": 6,
"x": 6,
"y": 0
},
{
"h": 3,
"i": "a77227c7-16f5-4353-952e-b183c715a61c",
"moved": false,
"static": false,
"w": 6,
"x": 0,
"y": 0
},
{
"h": 3,
"i": "9698cee2-b1f3-4c0b-8c9f-3da4f0e05f17",
"moved": false,
"static": false,
"w": 6,
"x": 6,
"y": 9
},
{
"h": 3,
"i": "64a5f303-d7db-44ff-9a0e-948e5c653320",
"moved": false,
"static": false,
"w": 6,
"x": 0,
"y": 12
},
{
"h": 3,
"i": "3e80a918-69af-4c9a-bc57-a94e1d41b05c",
"moved": false,
"static": false,
"w": 6,
"x": 6,
"y": 12
}
],
"name": "",
"tags": [
"redis",
"database"
],
"title": "Redis overview",
"variables": {
"94f19b3c-ad9f-4b47-a9b2-f312c09fa965": {
"allSelected": true,
"customValue": "",
"description": "List of hosts sending Redis metrics",
"id": "94f19b3c-ad9f-4b47-a9b2-f312c09fa965",
"key": "94f19b3c-ad9f-4b47-a9b2-f312c09fa965",
"modificationUUID": "4c5b0c03-9cbc-425b-8d8e-7152e5c39ba8",
"multiSelect": true,
"name": "host.name",
"order": 0,
"queryValue": "SELECT JSONExtractString(labels, 'host.name') AS `host.name`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'redis.cpu.time'\nGROUP BY `host.name`",
"selectedValue": [
"Srikanths-MacBook-Pro.local"
],
"showALLOption": true,
"sort": "ASC",
"textboxValue": "",
"type": "QUERY"
}
},
"version": "v5",
"widgets": [
{
"description": "Rate successful lookup of keys in the main dictionary",
"fillSpans": false,
"id": "a77227c7-16f5-4353-952e-b183c715a61c",
"isStacked": false,
"nullZeroValues": "zero",
"opacity": "1",
"panelTypes": "graph",
"query": {
"builder": {
"queryData": [
{
"aggregations": [
{
"metricName": "redis.keyspace.hits",
"reduceTo": "sum",
"spaceAggregation": "sum",
"temporality": null,
"timeAggregation": "rate"
}
],
"dataSource": "metrics",
"disabled": false,
"expression": "A",
"filter": {
"expression": "host.name IN $host.name"
},
"groupBy": [],
"having": {
"expression": ""
},
"legend": "Hit/s across all hosts",
"limit": null,
"orderBy": [],
"queryName": "A",
"stepInterval": 60
}
],
"queryFormulas": []
},
"clickhouse_sql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"id": "42c9c117-bfaf-49f7-b528-aad099392295",
"promql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"queryType": "builder"
},
"softMax": null,
"softMin": null,
"thresholds": [],
"timePreferance": "GLOBAL_TIME",
"title": "Hits/s",
"yAxisUnit": "none"
},
{
"description": "Number of clients pending on a blocking call",
"fillSpans": false,
"id": "bf0deeeb-e926-4234-944c-82bacd96af47",
"isStacked": false,
"nullZeroValues": "zero",
"opacity": "1",
"panelTypes": "graph",
"query": {
"builder": {
"queryData": [
{
"aggregations": [
{
"metricName": "redis.clients.blocked",
"reduceTo": "sum",
"spaceAggregation": "sum",
"temporality": null,
"timeAggregation": "sum"
}
],
"dataSource": "metrics",
"disabled": false,
"expression": "A",
"filter": {
"expression": "host.name IN $host.name"
},
"groupBy": [],
"having": {
"expression": ""
},
"legend": "Blocked clients across all hosts",
"limit": null,
"orderBy": [],
"queryName": "A",
"stepInterval": 60
}
],
"queryFormulas": []
},
"clickhouse_sql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"id": "b77a9e11-fb98-4a95-88a8-c3ad25c14369",
"promql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"queryType": "builder"
},
"softMax": null,
"softMin": null,
"thresholds": [],
"timePreferance": "GLOBAL_TIME",
"title": "Clients blocked",
"yAxisUnit": "none"
},
{
"description": "",
"fillSpans": false,
"id": "b19c7058-b806-4ea2-974a-ca555b168991",
"isStacked": false,
"nullZeroValues": "zero",
"opacity": "1",
"panelTypes": "graph",
"query": {
"builder": {
"queryData": [
{
"aggregations": [
{
"metricName": "redis.db.keys",
"reduceTo": "sum",
"spaceAggregation": "sum",
"temporality": null,
"timeAggregation": "sum"
}
],
"dataSource": "metrics",
"disabled": false,
"expression": "A",
"groupBy": [],
"having": {
"expression": ""
},
"legend": "",
"limit": null,
"orderBy": [],
"queryName": "A",
"stepInterval": 60
}
],
"queryFormulas": []
},
"clickhouse_sql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"id": "b77a9e11-fb98-4a95-88a8-c3ad25c14369",
"promql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"queryType": "builder"
},
"softMax": null,
"softMin": null,
"thresholds": [],
"timePreferance": "GLOBAL_TIME",
"title": "Keyspace Keys",
"yAxisUnit": "none"
},
{
"description": "Number of changes since the last dump",
"fillSpans": false,
"id": "f5ee1511-0d2b-4404-9ce0-e991837decc2",
"isStacked": false,
"nullZeroValues": "zero",
"opacity": "1",
"panelTypes": "graph",
"query": {
"builder": {
"queryData": [
{
"aggregations": [
{
"metricName": "redis.rdb.changes_since_last_save",
"reduceTo": "sum",
"spaceAggregation": "sum",
"temporality": null,
"timeAggregation": "sum"
}
],
"dataSource": "metrics",
"disabled": false,
"expression": "A",
"filter": {
"expression": "host.name IN $host.name"
},
"groupBy": [],
"having": {
"expression": ""
},
"legend": "Number of unsaved changes",
"limit": null,
"orderBy": [],
"queryName": "A",
"stepInterval": 60
}
],
"queryFormulas": []
},
"clickhouse_sql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"id": "32cedddf-606d-4de1-8c1d-4b7049e6430c",
"promql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"queryType": "builder"
},
"softMax": null,
"softMin": null,
"thresholds": [],
"timePreferance": "GLOBAL_TIME",
"title": "Unsaved changes",
"yAxisUnit": "none"
},
{
"description": "",
"fillSpans": false,
"id": "2fbaef0d-3cdb-4ce3-aa3c-9bbbb41786d9",
"isStacked": false,
"nullZeroValues": "zero",
"opacity": "1",
"panelTypes": "graph",
"query": {
"builder": {
"queryData": [
{
"aggregations": [
{
"metricName": "redis.commands",
"reduceTo": "sum",
"spaceAggregation": "sum",
"temporality": null,
"timeAggregation": "sum"
}
],
"dataSource": "metrics",
"disabled": false,
"expression": "A",
"filter": {
"expression": "host.name IN $host.name"
},
"groupBy": [],
"having": {
"expression": ""
},
"legend": "ops/s",
"limit": null,
"orderBy": [],
"queryName": "A",
"stepInterval": 60
}
],
"queryFormulas": []
},
"clickhouse_sql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"id": "c70de4dd-a68a-42df-a249-6610c296709c",
"promql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"queryType": "builder"
},
"softMax": null,
"softMin": null,
"thresholds": [],
"timePreferance": "GLOBAL_TIME",
"title": "Command/s",
"yAxisUnit": "ops"
},
{
"description": "",
"fillSpans": false,
"id": "d4c164bc-8fc2-4dbc-aadd-8d17479ca649",
"isStacked": false,
"nullZeroValues": "zero",
"opacity": "1",
"panelTypes": "graph",
"query": {
"builder": {
"queryData": [
{
"aggregations": [
{
"metricName": "redis.memory.used",
"reduceTo": "sum",
"spaceAggregation": "sum",
"temporality": null,
"timeAggregation": "sum"
}
],
"dataSource": "metrics",
"disabled": false,
"expression": "A",
"filter": {
"expression": "host.name IN $host.name"
},
"groupBy": [
{
"dataType": "string",
"id": "host.name--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "host.name",
"type": "tag"
}
],
"having": {
"expression": ""
},
"legend": "Used::{{host.name}}",
"limit": null,
"orderBy": [],
"queryName": "A",
"stepInterval": 60
},
{
"aggregations": [
{
"metricName": "redis.maxmemory",
"reduceTo": "sum",
"spaceAggregation": "max",
"temporality": null,
"timeAggregation": "max"
}
],
"dataSource": "metrics",
"disabled": false,
"expression": "B",
"filter": {
"expression": "host.name IN $host.name"
},
"groupBy": [
{
"dataType": "string",
"id": "host.name--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "host.name",
"type": "tag"
}
],
"having": {
"expression": ""
},
"legend": "Max::{{host.name}}",
"limit": null,
"orderBy": [],
"queryName": "B",
"stepInterval": 60
}
],
"queryFormulas": []
},
"clickhouse_sql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"id": "2f47df76-f09e-4152-8623-971f0fe66bfe",
"promql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"queryType": "builder"
},
"softMax": null,
"softMin": null,
"thresholds": [],
"timePreferance": "GLOBAL_TIME",
"title": "Memory usage",
"yAxisUnit": "bytes"
},
{
"description": "",
"fillSpans": false,
"id": "9698cee2-b1f3-4c0b-8c9f-3da4f0e05f17",
"isStacked": false,
"nullZeroValues": "zero",
"opacity": "1",
"panelTypes": "graph",
"query": {
"builder": {
"queryData": [
{
"aggregations": [
{
"metricName": "redis.memory.rss",
"reduceTo": "sum",
"spaceAggregation": "sum",
"temporality": null,
"timeAggregation": "sum"
}
],
"dataSource": "metrics",
"disabled": false,
"expression": "A",
"filter": {
"expression": "host.name IN $host.name"
},
"groupBy": [
{
"dataType": "string",
"id": "host.name--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "host.name",
"type": "tag"
}
],
"having": {
"expression": ""
},
"legend": "Rss::{{host.name}}",
"limit": null,
"orderBy": [],
"queryName": "A",
"stepInterval": 60
}
],
"queryFormulas": []
},
"clickhouse_sql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"id": "fddd043c-1385-481c-9f4c-381f261e1dd9",
"promql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"queryType": "builder"
},
"softMax": null,
"softMin": null,
"thresholds": [],
"timePreferance": "GLOBAL_TIME",
"title": "RSS Memory",
"yAxisUnit": "bytes"
},
{
"description": "",
"fillSpans": false,
"id": "64a5f303-d7db-44ff-9a0e-948e5c653320",
"isStacked": false,
"nullZeroValues": "zero",
"opacity": "1",
"panelTypes": "graph",
"query": {
"builder": {
"queryData": [
{
"aggregations": [
{
"metricName": "redis.memory.fragmentation_ratio",
"reduceTo": "sum",
"spaceAggregation": "avg",
"temporality": null,
"timeAggregation": "avg"
}
],
"dataSource": "metrics",
"disabled": false,
"expression": "A",
"filter": {
"expression": "host.name IN $host.name"
},
"groupBy": [
{
"dataType": "string",
"id": "host.name--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "host.name",
"type": "tag"
}
],
"having": {
"expression": ""
},
"legend": "Rss::{{host.name}}",
"limit": null,
"orderBy": [],
"queryName": "A",
"stepInterval": 60
}
],
"queryFormulas": []
},
"clickhouse_sql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"id": "3e802b07-0249-4d79-a5c7-6580ab535ad0",
"promql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"queryType": "builder"
},
"softMax": null,
"softMin": null,
"thresholds": [],
"timePreferance": "GLOBAL_TIME",
"title": "Fragmentation ratio",
"yAxisUnit": "short"
},
{
"description": "Number of evicted keys due to maxmemory limit",
"fillSpans": false,
"id": "3e80a918-69af-4c9a-bc57-a94e1d41b05c",
"isStacked": false,
"nullZeroValues": "zero",
"opacity": "1",
"panelTypes": "graph",
"query": {
"builder": {
"queryData": [
{
"aggregations": [
{
"metricName": "redis.keys.evicted",
"reduceTo": "sum",
"spaceAggregation": "sum",
"temporality": null,
"timeAggregation": "rate"
}
],
"dataSource": "metrics",
"disabled": false,
"expression": "A",
"filter": {
"expression": "host.name IN $host.name"
},
"groupBy": [
{
"dataType": "string",
"id": "host.name--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "host.name",
"type": "tag"
}
],
"having": {
"expression": ""
},
"legend": "Rss::{{host.name}}",
"limit": null,
"orderBy": [],
"queryName": "A",
"stepInterval": 60
}
],
"queryFormulas": []
},
"clickhouse_sql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"id": "15d1d9d7-eb10-464b-aa7b-33ff211996f7",
"promql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"queryType": "builder"
},
"softMax": null,
"softMin": null,
"thresholds": [],
"timePreferance": "GLOBAL_TIME",
"title": "Eviction rate",
"yAxisUnit": "short"
}
]
}

View File

@@ -74,7 +74,7 @@ func (h *provider) BeforeQuery(ctx context.Context, _ *telemetrystore.QueryEvent
// TODO(srikanthccv): enable it when the "Cannot read all data" issue is fixed
// https://github.com/ClickHouse/ClickHouse/issues/82283
settings["secondary_indices_enable_bulk_filtering"] = true
settings["secondary_indices_enable_bulk_filtering"] = false
ctx = clickhouse.Context(ctx, clickhouse.WithSettings(settings))
return ctx

Some files were not shown because too many files have changed in this diff Show More