Compare commits

...

144 Commits

Author SHA1 Message Date
Naman Verma
4cb27e330e Merge branch 'nv/tags' into nv/v2-dashboard-create 2026-05-13 14:22:20 +05:30
Naman Verma
b47bc6bcc4 fix: only ascii in regex 2026-05-13 13:51:02 +05:30
Naman Verma
5321e9ee87 feat: functional unique index in sql schema 2026-05-13 13:27:44 +05:30
Naman Verma
fc4f326953 fix: use sync tags in create api 2026-05-13 12:21:45 +05:30
Naman Verma
2af4cbf0f9 Merge branch 'nv/tags' into nv/v2-dashboard-create 2026-05-13 12:11:30 +05:30
Naman Verma
e82f568a27 chore: remove methods that shouldn't be exposed 2026-05-13 12:11:09 +05:30
Naman Verma
c2aac3a278 fix: add regex for tags 2026-05-13 12:00:26 +05:30
Naman Verma
ddfec3e5f7 fix: add len check on tags keys and values 2026-05-13 11:32:31 +05:30
Naman Verma
af623f66e8 chore: bump migration number 2026-05-13 11:23:03 +05:30
Naman Verma
268e747f5c fix: fix build error 2026-05-13 10:59:12 +05:30
Naman Verma
3fc72329c9 Merge branch 'nv/tags' into nv/v2-dashboard-create 2026-05-13 10:56:15 +05:30
Naman Verma
10ecb7524c fix: add ID in tag relation 2026-05-13 10:33:27 +05:30
Naman Verma
8fc21ca6b9 fix: remove user auditable 2026-05-13 10:19:51 +05:30
Naman Verma
422149369d fix: add org id filter in all list and delete queries 2026-05-13 10:11:55 +05:30
Naman Verma
2063697350 chore: change entity id to resource id 2026-05-13 09:41:01 +05:30
Naman Verma
685faa9211 Merge branch 'main' into nv/tags 2026-05-13 09:23:28 +05:30
Naman Verma
de6fcb9fbb chore: bump migration number 2026-05-12 14:19:47 +05:30
Naman Verma
828619a9e6 Merge branch 'main' into nv/tags 2026-05-12 14:19:17 +05:30
Naman Verma
aff2e1be6b fix: fix build errors in dashboard module 2026-05-12 14:13:40 +05:30
Naman Verma
9728d17a0a fix: remove entity type definition 2026-05-12 14:10:29 +05:30
Naman Verma
ffaf334dfd Merge branch 'nv/tags' into nv/v2-dashboard-create 2026-05-12 14:09:58 +05:30
Naman Verma
d87e7241c0 feat: add SyncTags method that covers creation and linking 2026-05-12 14:09:19 +05:30
Naman Verma
ae184315a9 feat: foreign key on tag id 2026-05-12 13:55:33 +05:30
Naman Verma
29f782a3a0 chore: remove org ID from tag relation 2026-05-12 13:51:14 +05:30
Naman Verma
71d8dafce1 fix: singular table name 2026-05-12 13:45:02 +05:30
Naman Verma
412320d7d9 fix: use coretypes.Kind instead of defining entity type 2026-05-12 13:42:35 +05:30
Naman Verma
9b5d78b5a0 Merge branch 'main' into nv/tags 2026-05-12 13:21:20 +05:30
Naman Verma
444464ae15 fix: created and updated by schema 2026-05-12 13:04:48 +05:30
Naman Verma
d5841f8daa fix: correct pk in bun model for tag relations 2026-05-12 12:29:29 +05:30
Naman Verma
c344cd256f fix: diff error codes for invalid keys and values 2026-05-12 12:27:46 +05:30
Naman Verma
2f6b7b6260 Merge branch 'nv/tags' into nv/v2-dashboard-create 2026-05-11 23:42:21 +05:30
Naman Verma
5e61be1606 feat: method to build postable tags from tags 2026-05-11 23:42:08 +05:30
Naman Verma
1f7032953c Merge branch 'nv/tags' into nv/v2-dashboard-create 2026-05-11 22:34:02 +05:30
Naman Verma
173037d3be fix: extend bun in joinedRow 2026-05-11 22:33:37 +05:30
Naman Verma
4713fd4839 chore: generate api spec 2026-05-11 14:47:02 +05:30
Naman Verma
4ad872b722 fix: add back api endpoint 2026-05-11 14:24:53 +05:30
Naman Verma
642fb66831 Merge branch 'nv/tags' into nv/v2-dashboard-create 2026-05-11 14:11:28 +05:30
Naman Verma
d12c846212 chore: change where tag module is instantiated 2026-05-11 14:10:07 +05:30
Naman Verma
4e5bd7cf6f fix: lint fix regarding nil, nil return in test file 2026-05-11 14:06:08 +05:30
Naman Verma
3982cce603 chore: merge conflicts error fixing pt 1 2026-05-11 13:59:41 +05:30
Naman Verma
1a43c85cb8 Merge branch 'nv/tags' into nv/v2-dashboard-create 2026-05-11 13:53:21 +05:30
Naman Verma
bd11e985e1 feat: new module for tags 2026-05-11 13:45:10 +05:30
Naman Verma
3e849ee2d3 feat: reserved DSL key validation for tags 2026-05-08 18:10:28 +05:30
Naman Verma
f7d9a57637 fix: pass entity type in create many 2026-05-08 17:56:58 +05:30
Naman Verma
fceb770337 Merge branch 'nv/tags-for-dashboard-create' into nv/v2-dashboard-create 2026-05-08 15:48:58 +05:30
Naman Verma
44496d9d8d feat: entity type column in tags 2026-05-08 15:48:51 +05:30
Naman Verma
398943fe41 Merge branch 'nv/tags-for-dashboard-create' into nv/v2-dashboard-create 2026-05-08 13:38:22 +05:30
Naman Verma
a17debc61b feat: move tags to key:value pairs model 2026-05-08 13:38:13 +05:30
Naman Verma
3113b82904 Merge branch 'nv/tags-for-dashboard-create' into nv/v2-dashboard-create 2026-05-07 12:06:33 +05:30
Naman Verma
71c60c3f2a test: fix mock interface in test 2026-05-07 12:06:27 +05:30
Naman Verma
3b824d50a3 Merge branch 'nv/tags-for-dashboard-create' into nv/v2-dashboard-create 2026-05-07 02:25:09 +05:30
Naman Verma
d0a693b034 feat: method to fetch tags for multiple entries at once 2026-05-07 02:24:42 +05:30
Naman Verma
90377f8116 Merge branch 'nv/tags-for-dashboard-create' into nv/v2-dashboard-create 2026-05-07 02:18:54 +05:30
Naman Verma
cabfd7271b Merge branch 'nv/dashboardv2' into nv/tags-for-dashboard-create 2026-05-07 02:18:50 +05:30
Naman Verma
750d63cf6b test: unit test fixes 2026-05-07 02:16:05 +05:30
Naman Verma
e4c4acb5df Merge branch 'nv/tags-for-dashboard-create' into nv/v2-dashboard-create 2026-05-06 11:25:36 +05:30
Naman Verma
c9235cd3d2 Merge branch 'nv/dashboardv2' into nv/tags-for-dashboard-create 2026-05-06 11:22:25 +05:30
Naman Verma
ec837c7006 fix: allow only 1 query in a panel 2026-05-06 11:21:49 +05:30
Naman Verma
a1f73655ca Merge branch 'nv/tags-for-dashboard-create' into nv/v2-dashboard-create 2026-05-05 16:39:35 +05:30
Naman Verma
0d6081d0d0 feat: consolidate tag module and tagtypes changes from downstream branches 2026-05-05 16:39:13 +05:30
Naman Verma
54832cad34 Merge branch 'nv/tags-for-dashboard-create' into nv/v2-dashboard-create 2026-05-05 11:54:38 +05:30
Naman Verma
a45178d709 Merge branch 'nv/dashboardv2' into nv/tags-for-dashboard-create 2026-05-05 11:54:21 +05:30
Naman Verma
c4224ecf72 Merge branch 'main' into nv/dashboardv2 2026-05-05 11:53:56 +05:30
Naman Verma
ff578f7d92 Merge branch 'nv/tags-for-dashboard-create' into nv/v2-dashboard-create 2026-05-04 19:26:49 +05:30
Naman Verma
cd630b1152 Merge branch 'nv/dashboardv2' into nv/tags-for-dashboard-create 2026-05-04 19:26:36 +05:30
Naman Verma
bd0842ac17 fix: query-less panels not allowed 2026-05-04 19:25:49 +05:30
Naman Verma
97b85c386a fix: no v2 package and its consequences 2026-05-04 17:27:58 +05:30
Naman Verma
00bdf50c1c fix: no v2 package and its consequences 2026-05-04 17:26:12 +05:30
Naman Verma
5dec4ec580 Merge branch 'nv/tags-for-dashboard-create' into nv/v2-dashboard-create 2026-05-04 17:18:39 +05:30
Naman Verma
325767c240 Merge branch 'nv/dashboardv2' into nv/tags-for-dashboard-create 2026-05-04 17:17:32 +05:30
Naman Verma
5fed2a4585 chore: no v2 subpackage 2026-05-04 17:16:39 +05:30
Naman Verma
664337ae0f Merge branch 'nv/tags-for-dashboard-create' into nv/v2-dashboard-create 2026-05-04 16:19:29 +05:30
Naman Verma
a0ea276681 Merge branch 'nv/dashboardv2' into nv/tags-for-dashboard-create 2026-05-04 16:18:03 +05:30
Naman Verma
2dc8699f08 fix: wrap errors 2026-05-04 14:55:38 +05:30
Naman Verma
ed81ed8ab5 fix: no need for copying textboxvariablespec 2026-05-04 14:44:42 +05:30
Naman Verma
48c9da19df fix: return 500 err if spec is nil for composite kind w/ code comment 2026-05-04 14:34:16 +05:30
Naman Verma
eb9663d518 fix: remove extra (un)marshal cycle 2026-05-04 14:18:37 +05:30
Naman Verma
a56a862338 fix: add allowed values in err messages 2026-05-04 14:16:22 +05:30
Naman Verma
021f33f65e Merge branch 'main' into nv/dashboardv2 2026-05-04 12:52:31 +05:30
Naman Verma
4d9386f418 fix: merge conflicts 2026-04-29 14:36:39 +05:30
Naman Verma
737473521d Merge branch 'nv/tags-for-dashboard-create' into nv/v2-dashboard-create 2026-04-29 14:33:25 +05:30
Naman Verma
1863db8ba8 Merge branch 'nv/dashboardv2' into nv/tags-for-dashboard-create 2026-04-29 14:32:14 +05:30
Naman Verma
661af09a13 Merge branch 'main' into nv/dashboardv2 2026-04-29 14:31:59 +05:30
Naman Verma
6024fa2b91 fix: remove extra spec from builder query marshalling 2026-04-29 14:31:16 +05:30
Naman Verma
8996a96387 chore: use existing mapper 2026-04-29 14:09:34 +05:30
Naman Verma
d6db5c2aab test: integration test fixes 2026-04-29 12:56:14 +05:30
Naman Verma
709590ea1b test: integration tests for create API 2026-04-29 12:23:12 +05:30
Naman Verma
1add46b4c5 fix: module should also validate postable dashboard 2026-04-28 20:05:38 +05:30
Naman Verma
8401261e20 Merge branch 'nv/tags-for-dashboard-create' into nv/v2-dashboard-create 2026-04-28 20:04:33 +05:30
Naman Verma
0ff34a7274 Merge branch 'nv/dashboardv2' into nv/tags-for-dashboard-create 2026-04-28 20:04:08 +05:30
Naman Verma
44e3bd9608 chore: separate method for validation 2026-04-28 20:03:48 +05:30
Naman Verma
c3944d779e fix: more dashboard request validations 2026-04-28 19:59:11 +05:30
Naman Verma
f5ec783a53 fix: go lint fix 2026-04-28 19:33:28 +05:30
Naman Verma
35b729c425 Merge branch 'nv/tags-for-dashboard-create' into nv/v2-dashboard-create 2026-04-28 19:30:42 +05:30
Naman Verma
4f43c3d803 fix: use existing tag's casing if new tag is a prefix of an existing tag 2026-04-28 19:30:07 +05:30
Naman Verma
5dbde6c64d fix: only return name of a tag in dashboard response 2026-04-28 19:13:03 +05:30
Naman Verma
fb6fdd54ec feat: v2 create dashboard API 2026-04-28 15:05:29 +05:30
Naman Verma
64b8ba62da Merge branch 'nv/dashboardv2' into nv/tags-for-dashboard-create 2026-04-28 15:04:11 +05:30
Naman Verma
7c66df408b Merge branch 'main' into nv/dashboardv2 2026-04-28 15:04:03 +05:30
Naman Verma
54049de391 chore: follow proper unmarshal json method structure 2026-04-28 15:02:49 +05:30
Naman Verma
a82f4237c8 Merge branch 'nv/dashboardv2' into nv/tags-for-dashboard-create 2026-04-28 09:52:23 +05:30
Naman Verma
89606b6238 Merge branch 'main' into nv/dashboardv2 2026-04-28 09:52:13 +05:30
Naman Verma
db5ce958eb Merge branch 'nv/dashboardv2' into nv/tags-for-dashboard-create 2026-04-28 09:49:01 +05:30
Naman Verma
c8d3a9a54b feat: enum for entity type that other modules can register 2026-04-28 09:47:24 +05:30
Naman Verma
637870b1fc feat: define tags module for v2 dashboard creation 2026-04-27 22:14:47 +05:30
Naman Verma
d46a7e24c9 Merge branch 'main' into nv/dashboardv2 2026-04-27 22:12:12 +05:30
Naman Verma
2a451e1c31 test: test for drift detection mechanics 2026-04-27 18:57:41 +05:30
Naman Verma
60b6d1d890 chore: better method name extractKindAndSpec 2026-04-27 18:42:31 +05:30
Naman Verma
36f755b232 chore: cleanup comments 2026-04-27 18:39:41 +05:30
Naman Verma
c1b3e3683a chore: code movement 2026-04-27 18:37:57 +05:30
Naman Verma
4c68544b1a chore: go lint fix (godot) 2026-04-27 18:37:05 +05:30
Naman Verma
90d9ab95f9 chore: code movement 2026-04-27 18:35:18 +05:30
Naman Verma
065e712e0c chore: code movement 2026-04-27 18:33:48 +05:30
Naman Verma
50db309ecd chore: code movement 2026-04-27 18:32:41 +05:30
Naman Verma
261bc552b0 chore: cleanup testing code 2026-04-27 18:24:52 +05:30
Naman Verma
bab720e98b Merge branch 'main' into nv/dashboardv2 2026-04-27 18:21:09 +05:30
Naman Verma
71fef6636b chore: better method name 2026-04-27 18:18:14 +05:30
Naman Verma
fc3cdecbbb chore: cleaner comment 2026-04-27 18:15:21 +05:30
Naman Verma
860fcfa641 chore: cleaner comment 2026-04-27 18:14:27 +05:30
Naman Verma
a090e3a4aa chore: cleaner comment 2026-04-27 18:14:02 +05:30
Naman Verma
6cf73e2ade chore: better comment to explain what restrictKindToLiteral does 2026-04-27 18:13:34 +05:30
Naman Verma
bbcb6a45d6 chore: renames and code rearrangement 2026-04-27 17:53:54 +05:30
Naman Verma
d13934febc fix: remove textbox plugin from openapi spec 2026-04-27 17:29:36 +05:30
Naman Verma
d5a7b7523d fix: strict decode variable spec as well 2026-04-27 17:27:51 +05:30
Naman Verma
5b8984f131 Merge branch 'main' into nv/dashboardv2 2026-04-27 17:18:44 +05:30
Naman Verma
6ddc5f1f12 chore: better error messages 2026-04-27 17:18:11 +05:30
Naman Verma
055968bfad fix: dot at the end of a comment 2026-04-27 17:07:58 +05:30
Naman Verma
1bf0f38ed9 fix: js lint errors 2026-04-27 17:07:38 +05:30
Naman Verma
842125e20a chore: too many comments 2026-04-27 16:50:41 +05:30
Naman Verma
6dab35caf8 chore: better file name 2026-04-27 16:43:42 +05:30
Naman Verma
047e9e2001 chore: better file names 2026-04-27 16:42:31 +05:30
Naman Verma
45eaa7db58 test: add tests for spec wrappers 2026-04-27 16:36:27 +05:30
Naman Verma
8a3d894eba chore: comment cleanup 2026-04-27 16:32:29 +05:30
Naman Verma
5239060b53 chore: move plugin maps to correct file 2026-04-27 16:30:33 +05:30
Naman Verma
42c6f507ac test: more descriptive test file name 2026-04-27 15:42:54 +05:30
Naman Verma
1b695a0b80 chore: separate file for perses replicas 2026-04-27 15:42:21 +05:30
Naman Verma
438cfab155 chore: comment clean up 2026-04-27 15:39:46 +05:30
Naman Verma
69f7617e01 Merge branch 'main' into nv/dashboardv2 2026-04-27 15:36:58 +05:30
Naman Verma
4420a7e1fc test: much bigger json for data column 2026-04-24 22:16:03 +05:30
Naman Verma
b4bc68c5c5 test: data column in perf tests should match real data 2026-04-24 17:17:37 +05:30
Naman Verma
eb9eb317cc test: perf test script for both sql flavours 2026-04-23 17:14:33 +05:30
Naman Verma
0b1eb16a42 test: fixes in dashboard perf testing data generator 2026-04-23 15:42:58 +05:30
Naman Verma
05a4d12183 test: script to generate test dashboard data in a sql db 2026-04-23 14:19:58 +05:30
Naman Verma
bbaf64c4f0 feat: openapi spec generation 2026-04-21 13:41:06 +05:30
28 changed files with 3573 additions and 18 deletions

View File

@@ -29,6 +29,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/tag"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/retention"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
@@ -103,8 +104,8 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, authtypes.NewRegistry()), nil
},
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing) dashboard.Module {
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser)
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing, tagModule tag.Module) dashboard.Module {
return impldashboard.NewModule(impldashboard.NewStore(store), store, settings, analytics, orgGetter, queryParser, tagModule)
},
func(_ licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
return noopgateway.NewProviderFactory()

View File

@@ -48,6 +48,7 @@ import (
pkgcloudintegration "github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/tag"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/retention"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
@@ -151,8 +152,8 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
}
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, licensing, onBeforeRoleDelete, authtypes.NewRegistry()), nil
},
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, querier, licensing)
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, tagModule tag.Module) dashboard.Module {
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), store, settings, analytics, orgGetter, queryParser, querier, licensing, tagModule)
},
func(licensing licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
return httpgateway.NewProviderFactory(licensing)

View File

@@ -1199,6 +1199,18 @@ components:
required:
- config
type: object
CommonDisplay:
properties:
description:
type: string
name:
type: string
type: object
CommonJSONRef:
properties:
$ref:
type: string
type: object
ConfigAuthorization:
properties:
credentials:
@@ -2208,6 +2220,138 @@ components:
- metaresource
- metaresources
type: string
DashboardGridItem:
properties:
content:
$ref: '#/components/schemas/CommonJSONRef'
height:
type: integer
width:
type: integer
x:
type: integer
"y":
type: integer
type: object
DashboardGridLayoutCollapse:
properties:
open:
type: boolean
type: object
DashboardGridLayoutDisplay:
properties:
collapse:
$ref: '#/components/schemas/DashboardGridLayoutCollapse'
title:
type: string
type: object
DashboardGridLayoutSpec:
properties:
display:
$ref: '#/components/schemas/DashboardGridLayoutDisplay'
items:
items:
$ref: '#/components/schemas/DashboardGridItem'
nullable: true
type: array
repeatVariable:
type: string
type: object
DashboardTextVariableSpec:
properties:
constant:
type: boolean
display:
$ref: '#/components/schemas/VariableDisplay'
name:
type: string
value:
type: string
type: object
DashboardtypesAxes:
properties:
isLogScale:
type: boolean
softMax:
nullable: true
type: number
softMin:
nullable: true
type: number
type: object
DashboardtypesBarChartPanelSpec:
properties:
axes:
$ref: '#/components/schemas/DashboardtypesAxes'
formatting:
$ref: '#/components/schemas/DashboardtypesPanelFormatting'
legend:
$ref: '#/components/schemas/DashboardtypesLegend'
thresholds:
items:
$ref: '#/components/schemas/DashboardtypesThresholdWithLabel'
nullable: true
type: array
visualization:
$ref: '#/components/schemas/DashboardtypesBarChartVisualization'
type: object
DashboardtypesBarChartVisualization:
properties:
fillSpans:
type: boolean
stackedBarChart:
type: boolean
timePreference:
$ref: '#/components/schemas/DashboardtypesTimePreference'
type: object
DashboardtypesBasicVisualization:
properties:
timePreference:
$ref: '#/components/schemas/DashboardtypesTimePreference'
type: object
DashboardtypesBuilderQuerySpec:
oneOf:
- $ref: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5LogAggregation'
- $ref: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5MetricAggregation'
- $ref: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5TraceAggregation'
DashboardtypesComparisonOperator:
enum:
- '>'
- <
- '>='
- <=
- =
- above
- below
- above_or_equal
- below_or_equal
- equal
- not_equal
type: string
DashboardtypesComparisonThreshold:
properties:
color:
type: string
format:
$ref: '#/components/schemas/DashboardtypesThresholdFormat'
operator:
$ref: '#/components/schemas/DashboardtypesComparisonOperator'
unit:
type: string
value:
format: double
type: number
required:
- value
- color
type: object
DashboardtypesCustomVariableSpec:
properties:
customValue:
type: string
required:
- customValue
type: object
DashboardtypesDashboard:
properties:
createdAt:
@@ -2229,6 +2373,126 @@ components:
updatedBy:
type: string
type: object
DashboardtypesDashboardData:
properties:
datasources:
additionalProperties:
$ref: '#/components/schemas/DashboardtypesDatasourceSpec'
type: object
display:
$ref: '#/components/schemas/CommonDisplay'
duration:
type: string
layouts:
items:
$ref: '#/components/schemas/DashboardtypesLayout'
nullable: true
type: array
links:
items:
$ref: '#/components/schemas/V1Link'
type: array
panels:
additionalProperties:
$ref: '#/components/schemas/DashboardtypesPanel'
nullable: true
type: object
refreshInterval:
type: string
variables:
items:
$ref: '#/components/schemas/DashboardtypesVariable'
type: array
type: object
DashboardtypesDashboardMetadata:
properties:
image:
type: string
schemaVersion:
type: string
uploadedGrafana:
type: boolean
type: object
DashboardtypesDatasourcePlugin:
oneOf:
- $ref: '#/components/schemas/DashboardtypesDatasourcePluginVariantStruct'
DashboardtypesDatasourcePluginKind:
enum:
- signoz/Datasource
type: string
DashboardtypesDatasourcePluginVariantStruct:
properties:
kind:
enum:
- signoz/Datasource
type: string
spec:
nullable: true
type: object
required:
- kind
- spec
type: object
DashboardtypesDatasourceSpec:
properties:
default:
type: boolean
display:
$ref: '#/components/schemas/CommonDisplay'
plugin:
$ref: '#/components/schemas/DashboardtypesDatasourcePlugin'
type: object
DashboardtypesDynamicVariableSpec:
properties:
name:
type: string
signal:
$ref: '#/components/schemas/TelemetrytypesSignal'
required:
- name
type: object
DashboardtypesFillMode:
enum:
- solid
- gradient
- none
type: string
DashboardtypesGettableDashboardInfo:
properties:
data:
$ref: '#/components/schemas/DashboardtypesDashboardData'
metadata:
$ref: '#/components/schemas/DashboardtypesDashboardMetadata'
tags:
items:
$ref: '#/components/schemas/TagtypesPostableTag'
type: array
type: object
DashboardtypesGettableDashboardV2:
properties:
createdAt:
format: date-time
type: string
createdBy:
type: string
id:
type: string
info:
$ref: '#/components/schemas/DashboardtypesGettableDashboardInfo'
locked:
type: boolean
orgId:
type: string
publicConfig:
$ref: '#/components/schemas/DashboardtypesGettablePublicDasbhboard'
updatedAt:
format: date-time
type: string
updatedBy:
type: string
required:
- id
type: object
DashboardtypesGettablePublicDasbhboard:
properties:
defaultTimeRange:
@@ -2245,6 +2509,259 @@ components:
publicDashboard:
$ref: '#/components/schemas/DashboardtypesGettablePublicDasbhboard'
type: object
DashboardtypesHistogramBuckets:
properties:
bucketCount:
nullable: true
type: number
bucketWidth:
nullable: true
type: number
mergeAllActiveQueries:
type: boolean
type: object
DashboardtypesHistogramPanelSpec:
properties:
histogramBuckets:
$ref: '#/components/schemas/DashboardtypesHistogramBuckets'
legend:
$ref: '#/components/schemas/DashboardtypesLegend'
type: object
DashboardtypesLayout:
oneOf:
- $ref: '#/components/schemas/DashboardtypesLayoutEnvelopeGithubComPersesPersesPkgModelApiV1DashboardGridLayoutSpec'
DashboardtypesLayoutEnvelopeGithubComPersesPersesPkgModelApiV1DashboardGridLayoutSpec:
properties:
kind:
enum:
- Grid
type: string
spec:
$ref: '#/components/schemas/DashboardGridLayoutSpec'
required:
- kind
- spec
type: object
DashboardtypesLegend:
properties:
customColors:
additionalProperties:
type: string
nullable: true
type: object
position:
$ref: '#/components/schemas/DashboardtypesLegendPosition'
type: object
DashboardtypesLegendPosition:
enum:
- bottom
- right
type: string
DashboardtypesLineInterpolation:
enum:
- linear
- spline
- step_after
- step_before
type: string
DashboardtypesLineStyle:
enum:
- solid
- dashed
type: string
DashboardtypesListPanelSpec:
properties:
selectFields:
items:
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
type: array
type: object
DashboardtypesListVariableSpec:
properties:
allowAllValue:
type: boolean
allowMultiple:
type: boolean
capturingRegexp:
type: string
customAllValue:
type: string
defaultValue:
$ref: '#/components/schemas/VariableDefaultValue'
display:
$ref: '#/components/schemas/VariableDisplay'
name:
type: string
plugin:
$ref: '#/components/schemas/DashboardtypesVariablePlugin'
sort:
nullable: true
type: string
type: object
DashboardtypesNumberPanelSpec:
properties:
formatting:
$ref: '#/components/schemas/DashboardtypesPanelFormatting'
thresholds:
items:
$ref: '#/components/schemas/DashboardtypesComparisonThreshold'
nullable: true
type: array
visualization:
$ref: '#/components/schemas/DashboardtypesBasicVisualization'
type: object
DashboardtypesPanel:
properties:
kind:
type: string
spec:
$ref: '#/components/schemas/DashboardtypesPanelSpec'
type: object
DashboardtypesPanelFormatting:
properties:
decimalPrecision:
$ref: '#/components/schemas/DashboardtypesPrecisionOption'
unit:
type: string
type: object
DashboardtypesPanelPlugin:
oneOf:
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesNumberPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesPieChartPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTablePanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHistogramPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesListPanelSpec'
DashboardtypesPanelPluginKind:
enum:
- signoz/TimeSeriesPanel
- signoz/BarChartPanel
- signoz/NumberPanel
- signoz/PieChartPanel
- signoz/TablePanel
- signoz/HistogramPanel
- signoz/ListPanel
type: string
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpec:
properties:
kind:
enum:
- signoz/BarChartPanel
type: string
spec:
$ref: '#/components/schemas/DashboardtypesBarChartPanelSpec'
required:
- kind
- spec
type: object
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHistogramPanelSpec:
properties:
kind:
enum:
- signoz/HistogramPanel
type: string
spec:
$ref: '#/components/schemas/DashboardtypesHistogramPanelSpec'
required:
- kind
- spec
type: object
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesListPanelSpec:
properties:
kind:
enum:
- signoz/ListPanel
type: string
spec:
$ref: '#/components/schemas/DashboardtypesListPanelSpec'
required:
- kind
- spec
type: object
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesNumberPanelSpec:
properties:
kind:
enum:
- signoz/NumberPanel
type: string
spec:
$ref: '#/components/schemas/DashboardtypesNumberPanelSpec'
required:
- kind
- spec
type: object
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesPieChartPanelSpec:
properties:
kind:
enum:
- signoz/PieChartPanel
type: string
spec:
$ref: '#/components/schemas/DashboardtypesPieChartPanelSpec'
required:
- kind
- spec
type: object
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTablePanelSpec:
properties:
kind:
enum:
- signoz/TablePanel
type: string
spec:
$ref: '#/components/schemas/DashboardtypesTablePanelSpec'
required:
- kind
- spec
type: object
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpec:
properties:
kind:
enum:
- signoz/TimeSeriesPanel
type: string
spec:
$ref: '#/components/schemas/DashboardtypesTimeSeriesPanelSpec'
required:
- kind
- spec
type: object
DashboardtypesPanelSpec:
properties:
display:
$ref: '#/components/schemas/V1PanelDisplay'
links:
items:
$ref: '#/components/schemas/V1Link'
type: array
plugin:
$ref: '#/components/schemas/DashboardtypesPanelPlugin'
queries:
items:
$ref: '#/components/schemas/DashboardtypesQuery'
type: array
type: object
DashboardtypesPieChartPanelSpec:
properties:
formatting:
$ref: '#/components/schemas/DashboardtypesPanelFormatting'
legend:
$ref: '#/components/schemas/DashboardtypesLegend'
visualization:
$ref: '#/components/schemas/DashboardtypesBasicVisualization'
type: object
DashboardtypesPostableDashboardV2:
properties:
data:
$ref: '#/components/schemas/DashboardtypesDashboardData'
metadata:
$ref: '#/components/schemas/DashboardtypesDashboardMetadata'
tags:
items:
$ref: '#/components/schemas/TagtypesPostableTag'
type: array
type: object
DashboardtypesPostablePublicDashboard:
properties:
defaultTimeRange:
@@ -2252,9 +2769,249 @@ components:
timeRangeEnabled:
type: boolean
type: object
DashboardtypesPrecisionOption:
enum:
- "0"
- "1"
- "2"
- "3"
- "4"
- full
type: string
DashboardtypesQuery:
properties:
kind:
type: string
spec:
$ref: '#/components/schemas/DashboardtypesQuerySpec'
type: object
DashboardtypesQueryPlugin:
oneOf:
- $ref: '#/components/schemas/DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBuilderQuerySpec'
- $ref: '#/components/schemas/DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5CompositeQuery'
- $ref: '#/components/schemas/DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5QueryBuilderFormula'
- $ref: '#/components/schemas/DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5PromQuery'
- $ref: '#/components/schemas/DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5ClickHouseQuery'
- $ref: '#/components/schemas/DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5QueryBuilderTraceOperator'
DashboardtypesQueryPluginKind:
enum:
- signoz/BuilderQuery
- signoz/CompositeQuery
- signoz/Formula
- signoz/PromQLQuery
- signoz/ClickHouseSQL
- signoz/TraceOperator
type: string
DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBuilderQuerySpec:
properties:
kind:
enum:
- signoz/BuilderQuery
type: string
spec:
$ref: '#/components/schemas/DashboardtypesBuilderQuerySpec'
required:
- kind
- spec
type: object
DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5ClickHouseQuery:
properties:
kind:
enum:
- signoz/ClickHouseSQL
type: string
spec:
$ref: '#/components/schemas/Querybuildertypesv5ClickHouseQuery'
required:
- kind
- spec
type: object
DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5CompositeQuery:
properties:
kind:
enum:
- signoz/CompositeQuery
type: string
spec:
$ref: '#/components/schemas/Querybuildertypesv5CompositeQuery'
required:
- kind
- spec
type: object
DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5PromQuery:
properties:
kind:
enum:
- signoz/PromQLQuery
type: string
spec:
$ref: '#/components/schemas/Querybuildertypesv5PromQuery'
required:
- kind
- spec
type: object
DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5QueryBuilderFormula:
properties:
kind:
enum:
- signoz/Formula
type: string
spec:
$ref: '#/components/schemas/Querybuildertypesv5QueryBuilderFormula'
required:
- kind
- spec
type: object
DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5QueryBuilderTraceOperator:
properties:
kind:
enum:
- signoz/TraceOperator
type: string
spec:
$ref: '#/components/schemas/Querybuildertypesv5QueryBuilderTraceOperator'
required:
- kind
- spec
type: object
DashboardtypesQuerySpec:
properties:
name:
type: string
plugin:
$ref: '#/components/schemas/DashboardtypesQueryPlugin'
type: object
DashboardtypesQueryVariableSpec:
properties:
queryValue:
type: string
required:
- queryValue
type: object
DashboardtypesSpanGaps:
properties:
fillLessThan:
type: string
fillOnlyBelow:
type: boolean
type: object
DashboardtypesStorableDashboardData:
additionalProperties: {}
type: object
DashboardtypesTableFormatting:
properties:
columnUnits:
additionalProperties:
type: string
nullable: true
type: object
decimalPrecision:
$ref: '#/components/schemas/DashboardtypesPrecisionOption'
type: object
DashboardtypesTablePanelSpec:
properties:
formatting:
$ref: '#/components/schemas/DashboardtypesTableFormatting'
thresholds:
items:
$ref: '#/components/schemas/DashboardtypesTableThreshold'
nullable: true
type: array
visualization:
$ref: '#/components/schemas/DashboardtypesBasicVisualization'
type: object
DashboardtypesTableThreshold:
properties:
color:
type: string
columnName:
type: string
format:
$ref: '#/components/schemas/DashboardtypesThresholdFormat'
operator:
$ref: '#/components/schemas/DashboardtypesComparisonOperator'
unit:
type: string
value:
format: double
type: number
required:
- value
- color
- columnName
type: object
DashboardtypesThresholdFormat:
enum:
- text
- background
type: string
DashboardtypesThresholdWithLabel:
properties:
color:
type: string
label:
type: string
unit:
type: string
value:
format: double
type: number
required:
- value
- color
- label
type: object
DashboardtypesTimePreference:
enum:
- global_time
- last_5_min
- last_15_min
- last_30_min
- last_1_hr
- last_6_hr
- last_1_day
- last_3_days
- last_1_week
- last_1_month
type: string
DashboardtypesTimeSeriesChartAppearance:
properties:
fillMode:
$ref: '#/components/schemas/DashboardtypesFillMode'
lineInterpolation:
$ref: '#/components/schemas/DashboardtypesLineInterpolation'
lineStyle:
$ref: '#/components/schemas/DashboardtypesLineStyle'
showPoints:
type: boolean
spanGaps:
$ref: '#/components/schemas/DashboardtypesSpanGaps'
type: object
DashboardtypesTimeSeriesPanelSpec:
properties:
axes:
$ref: '#/components/schemas/DashboardtypesAxes'
chartAppearance:
$ref: '#/components/schemas/DashboardtypesTimeSeriesChartAppearance'
formatting:
$ref: '#/components/schemas/DashboardtypesPanelFormatting'
legend:
$ref: '#/components/schemas/DashboardtypesLegend'
thresholds:
items:
$ref: '#/components/schemas/DashboardtypesThresholdWithLabel'
nullable: true
type: array
visualization:
$ref: '#/components/schemas/DashboardtypesTimeSeriesVisualization'
type: object
DashboardtypesTimeSeriesVisualization:
properties:
fillSpans:
type: boolean
timePreference:
$ref: '#/components/schemas/DashboardtypesTimePreference'
type: object
DashboardtypesUpdatablePublicDashboard:
properties:
defaultTimeRange:
@@ -2262,6 +3019,81 @@ components:
timeRangeEnabled:
type: boolean
type: object
DashboardtypesVariable:
oneOf:
- $ref: '#/components/schemas/DashboardtypesVariableEnvelopeGithubComSigNozSignozPkgTypesDashboardtypesListVariableSpec'
- $ref: '#/components/schemas/DashboardtypesVariableEnvelopeGithubComPersesPersesPkgModelApiV1DashboardTextVariableSpec'
DashboardtypesVariableEnvelopeGithubComPersesPersesPkgModelApiV1DashboardTextVariableSpec:
properties:
kind:
enum:
- TextVariable
type: string
spec:
$ref: '#/components/schemas/DashboardTextVariableSpec'
required:
- kind
- spec
type: object
DashboardtypesVariableEnvelopeGithubComSigNozSignozPkgTypesDashboardtypesListVariableSpec:
properties:
kind:
enum:
- ListVariable
type: string
spec:
$ref: '#/components/schemas/DashboardtypesListVariableSpec'
required:
- kind
- spec
type: object
DashboardtypesVariablePlugin:
oneOf:
- $ref: '#/components/schemas/DashboardtypesVariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDynamicVariableSpec'
- $ref: '#/components/schemas/DashboardtypesVariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesQueryVariableSpec'
- $ref: '#/components/schemas/DashboardtypesVariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesCustomVariableSpec'
DashboardtypesVariablePluginKind:
enum:
- signoz/DynamicVariable
- signoz/QueryVariable
- signoz/CustomVariable
type: string
DashboardtypesVariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesCustomVariableSpec:
properties:
kind:
enum:
- signoz/CustomVariable
type: string
spec:
$ref: '#/components/schemas/DashboardtypesCustomVariableSpec'
required:
- kind
- spec
type: object
DashboardtypesVariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDynamicVariableSpec:
properties:
kind:
enum:
- signoz/DynamicVariable
type: string
spec:
$ref: '#/components/schemas/DashboardtypesDynamicVariableSpec'
required:
- kind
- spec
type: object
DashboardtypesVariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesQueryVariableSpec:
properties:
kind:
enum:
- signoz/QueryVariable
type: string
spec:
$ref: '#/components/schemas/DashboardtypesQueryVariableSpec'
required:
- kind
- spec
type: object
ErrorsJSON:
properties:
code:
@@ -5423,6 +6255,16 @@ components:
nullable: true
type: string
type: object
TagtypesPostableTag:
properties:
key:
type: string
value:
type: string
required:
- key
- value
type: object
TelemetrytypesFieldContext:
enum:
- metric
@@ -5879,6 +6721,37 @@ components:
required:
- id
type: object
V1Link:
properties:
name:
type: string
renderVariables:
type: boolean
targetBlank:
type: boolean
tooltip:
type: string
url:
type: string
type: object
V1PanelDisplay:
properties:
description:
type: string
name:
type: string
type: object
VariableDefaultValue:
type: object
VariableDisplay:
properties:
description:
type: string
hidden:
type: boolean
name:
type: string
type: object
ZeustypesGettableHost:
properties:
hosts:
@@ -11397,6 +12270,58 @@ paths:
summary: Update user preference
tags:
- preferences
/api/v2/dashboards:
post:
deprecated: false
description: This endpoint creates a v2-shape dashboard with structured metadata,
a typed data tree, and resolved tags.
operationId: CreateDashboardV2
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/DashboardtypesPostableDashboardV2'
responses:
"201":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/DashboardtypesGettableDashboardV2'
status:
type: string
required:
- status
- data
type: object
description: Created
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- EDITOR
- tokenizer:
- EDITOR
summary: Create dashboard (v2)
tags:
- dashboard
/api/v2/factor_password/forgot:
post:
deprecated: false

View File

@@ -11,8 +11,10 @@ import (
"github.com/SigNoz/signoz/pkg/modules/dashboard"
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/tag"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
@@ -30,9 +32,9 @@ type module struct {
licensing licensing.Licensing
}
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
func NewModule(store dashboardtypes.Store, sqlstore sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, tagModule tag.Module) dashboard.Module {
scopedProviderSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard")
pkgDashboardModule := pkgimpldashboard.NewModule(store, settings, analytics, orgGetter, queryParser)
pkgDashboardModule := pkgimpldashboard.NewModule(store, sqlstore, settings, analytics, orgGetter, queryParser, tagModule)
return &module{
pkgDashboardModule: pkgDashboardModule,
@@ -197,6 +199,10 @@ func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy s
return module.pkgDashboardModule.Create(ctx, orgID, createdBy, creator, data)
}
func (module *module) CreateV2(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, postable dashboardtypes.PostableDashboardV2) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.CreateV2(ctx, orgID, createdBy, creator, postable)
}
func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error) {
return module.pkgDashboardModule.Get(ctx, orgID, id)
}

View File

@@ -18,8 +18,10 @@ import type {
} from 'react-query';
import type {
CreateDashboardV2201,
CreatePublicDashboard201,
CreatePublicDashboardPathParameters,
DashboardtypesPostableDashboardV2DTO,
DashboardtypesPostablePublicDashboardDTO,
DashboardtypesUpdatablePublicDashboardDTO,
DeletePublicDashboardPathParameters,
@@ -634,3 +636,88 @@ export const invalidateGetPublicDashboardWidgetQueryRange = async (
return queryClient;
};
/**
* This endpoint creates a v2-shape dashboard with structured metadata, a typed data tree, and resolved tags.
* @summary Create dashboard (v2)
*/
export const createDashboardV2 = (
dashboardtypesPostableDashboardV2DTO: BodyType<DashboardtypesPostableDashboardV2DTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateDashboardV2201>({
url: `/api/v2/dashboards`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: dashboardtypesPostableDashboardV2DTO,
signal,
});
};
export const getCreateDashboardV2MutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createDashboardV2>>,
TError,
{ data: BodyType<DashboardtypesPostableDashboardV2DTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createDashboardV2>>,
TError,
{ data: BodyType<DashboardtypesPostableDashboardV2DTO> },
TContext
> => {
const mutationKey = ['createDashboardV2'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createDashboardV2>>,
{ data: BodyType<DashboardtypesPostableDashboardV2DTO> }
> = (props) => {
const { data } = props ?? {};
return createDashboardV2(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateDashboardV2MutationResult = NonNullable<
Awaited<ReturnType<typeof createDashboardV2>>
>;
export type CreateDashboardV2MutationBody =
BodyType<DashboardtypesPostableDashboardV2DTO>;
export type CreateDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create dashboard (v2)
*/
export const useCreateDashboardV2 = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createDashboardV2>>,
TError,
{ data: BodyType<DashboardtypesPostableDashboardV2DTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createDashboardV2>>,
TError,
{ data: BodyType<DashboardtypesPostableDashboardV2DTO> },
TContext
> => {
const mutationOptions = getCreateDashboardV2MutationOptions(options);
return useMutation(mutationOptions);
};

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,23 @@ import (
)
func (provider *provider) addDashboardRoutes(router *mux.Router) error {
if err := router.Handle("/api/v2/dashboards", handler.New(provider.authzMiddleware.EditAccess(provider.dashboardHandler.CreateV2), handler.OpenAPIDef{
ID: "CreateDashboardV2",
Tags: []string{"dashboard"},
Summary: "Create dashboard (v2)",
Description: "This endpoint creates a v2-shape dashboard with structured metadata, a typed data tree, and resolved tags.",
Request: new(dashboardtypes.PostableDashboardV2),
RequestContentType: "application/json",
Response: new(dashboardtypes.GettableDashboardV2),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/dashboards/{id}/public", handler.New(provider.authzMiddleware.AdminAccess(provider.dashboardHandler.CreatePublic), handler.OpenAPIDef{
ID: "CreatePublicDashboard",
Tags: []string{"dashboard"},

View File

@@ -49,6 +49,11 @@ type Module interface {
GetByMetricNames(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string][]map[string]string, error)
statsreporter.StatsCollector
// ════════════════════════════════════════════════════════════════════════
// v2 dashboard methods
// ════════════════════════════════════════════════════════════════════════
CreateV2(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, postable dashboardtypes.PostableDashboardV2) (*dashboardtypes.DashboardV2, error)
}
type Handler interface {
@@ -71,4 +76,9 @@ type Handler interface {
LockUnlock(http.ResponseWriter, *http.Request)
Delete(http.ResponseWriter, *http.Request)
// ════════════════════════════════════════════════════════════════════════
// v2 dashboard methods
// ════════════════════════════════════════════════════════════════════════
CreateV2(http.ResponseWriter, *http.Request)
}

View File

@@ -10,7 +10,9 @@ import (
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/tag"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
@@ -20,20 +22,24 @@ import (
type module struct {
store dashboardtypes.Store
sqlstore sqlstore.SQLStore
settings factory.ScopedProviderSettings
analytics analytics.Analytics
orgGetter organization.Getter
queryParser queryparser.QueryParser
tagModule tag.Module
}
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser) dashboard.Module {
func NewModule(store dashboardtypes.Store, sqlstore sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, tagModule tag.Module) dashboard.Module {
scopedProviderSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard")
return &module{
store: store,
sqlstore: sqlstore,
settings: scopedProviderSettings,
analytics: analytics,
orgGetter: orgGetter,
queryParser: queryParser,
tagModule: tagModule,
}
}

View File

@@ -0,0 +1,44 @@
package impldashboard
import (
"context"
"encoding/json"
"net/http"
"time"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
func (handler *handler) CreateV2(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
req := dashboardtypes.PostableDashboardV2{}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
render.Error(rw, err)
return
}
dashboard, err := handler.module.CreateV2(ctx, orgID, claims.Email, valuer.MustNewUUID(claims.IdentityID()), req)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusCreated, dashboardtypes.NewGettableDashboardV2FromDashboardV2(dashboard))
}

View File

@@ -0,0 +1,39 @@
package impldashboard
import (
"context"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
func (module *module) CreateV2(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, postable dashboardtypes.PostableDashboardV2) (*dashboardtypes.DashboardV2, error) {
if err := postable.Validate(); err != nil {
return nil, err
}
dashboard := dashboardtypes.NewDashboardV2(orgID, createdBy, postable, nil)
var storableDashboard *dashboardtypes.StorableDashboard
err := module.sqlstore.RunInTxCtx(ctx, nil, func(ctx context.Context) error {
resolvedTags, err := module.tagModule.SyncTags(ctx, orgID, coretypes.KindDashboard, dashboard.ID, postable.Tags)
if err != nil {
return err
}
dashboard.Info.Tags = resolvedTags
storable, err := dashboard.ToStorableDashboard()
if err != nil {
return err
}
storableDashboard = storable
return module.store.Create(ctx, storable)
})
if err != nil {
return nil, err
}
module.analytics.TrackUser(ctx, orgID.String(), creator.String(), "Dashboard Created", dashboardtypes.NewStatsFromStorableDashboards([]*dashboardtypes.StorableDashboard{storableDashboard}))
return dashboard, nil
}

View File

@@ -0,0 +1,76 @@
package impltag
import (
"context"
"github.com/SigNoz/signoz/pkg/modules/tag"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/tagtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type module struct {
store tagtypes.Store
}
func NewModule(store tagtypes.Store) tag.Module {
return &module{store: store}
}
func (m *module) SyncTags(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceID valuer.UUID, postable []tagtypes.PostableTag) ([]*tagtypes.Tag, error) {
var tags []*tagtypes.Tag
err := m.store.RunInTx(ctx, func(ctx context.Context) error {
resolved, err := m.createMany(ctx, orgID, kind, postable)
if err != nil {
return err
}
tagIDs := make([]valuer.UUID, len(resolved))
for i, t := range resolved {
tagIDs[i] = t.ID
}
if err := m.syncLinksForResource(ctx, orgID, kind, resourceID, tagIDs); err != nil {
return err
}
tags = resolved
return nil
})
if err != nil {
return nil, err
}
return tags, nil
}
func (m *module) createMany(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, postable []tagtypes.PostableTag) ([]*tagtypes.Tag, error) {
if len(postable) == 0 {
return []*tagtypes.Tag{}, nil
}
toCreate, matched, err := tagtypes.Resolve(ctx, m.store, orgID, kind, postable)
if err != nil {
return nil, err
}
created, err := m.store.Create(ctx, toCreate)
if err != nil {
return nil, err
}
return append(matched, created...), nil
}
func (m *module) syncLinksForResource(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceID valuer.UUID, tagIDs []valuer.UUID) error {
return m.store.RunInTx(ctx, func(ctx context.Context) error {
if err := m.store.CreateRelations(ctx, tagtypes.NewTagRelations(kind, resourceID, tagIDs)); err != nil {
return err
}
return m.store.DeleteRelationsExcept(ctx, orgID, kind, resourceID, tagIDs)
})
}
func (m *module) ListForResource(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceID valuer.UUID) ([]*tagtypes.Tag, error) {
return m.store.ListByResource(ctx, orgID, kind, resourceID)
}
func (m *module) ListForResources(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceIDs []valuer.UUID) (map[valuer.UUID][]*tagtypes.Tag, error) {
return m.store.ListByResources(ctx, orgID, kind, resourceIDs)
}

View File

@@ -0,0 +1,148 @@
package impltag
import (
"context"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/tagtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
type store struct {
sqlstore sqlstore.SQLStore
}
func NewStore(sqlstore sqlstore.SQLStore) tagtypes.Store {
return &store{sqlstore: sqlstore}
}
func (s *store) List(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind) ([]*tagtypes.Tag, error) {
tags := make([]*tagtypes.Tag, 0)
err := s.sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(&tags).
Where("org_id = ?", orgID).
Where("kind = ?", kind).
Scan(ctx)
if err != nil {
return nil, err
}
return tags, nil
}
func (s *store) ListByResource(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceID valuer.UUID) ([]*tagtypes.Tag, error) {
tags := make([]*tagtypes.Tag, 0)
err := s.sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(&tags).
Join("JOIN tag_relation AS tr ON tr.tag_id = tag.id").
Where("tr.kind = ?", kind).
Where("tr.resource_id = ?", resourceID).
Where("tag.org_id = ?", orgID).
Scan(ctx)
if err != nil {
return nil, err
}
return tags, nil
}
func (s *store) ListByResources(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceIDs []valuer.UUID) (map[valuer.UUID][]*tagtypes.Tag, error) {
if len(resourceIDs) == 0 {
return map[valuer.UUID][]*tagtypes.Tag{}, nil
}
type joinedRow struct {
tagtypes.Tag `bun:",extend"`
ResourceID valuer.UUID `bun:"resource_id"`
}
rows := make([]*joinedRow, 0)
err := s.sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(&rows).
ColumnExpr("tag.*, tr.resource_id").
Join("JOIN tag_relation AS tr ON tr.tag_id = tag.id").
Where("tr.kind = ?", kind).
Where("tr.resource_id IN (?)", bun.In(resourceIDs)).
Where("tag.org_id = ?", orgID).
Scan(ctx)
if err != nil {
return nil, err
}
out := make(map[valuer.UUID][]*tagtypes.Tag)
for _, r := range rows {
tag := r.Tag
out[r.ResourceID] = append(out[r.ResourceID], &tag)
}
return out, nil
}
func (s *store) Create(ctx context.Context, tags []*tagtypes.Tag) ([]*tagtypes.Tag, error) {
if len(tags) == 0 {
return tags, nil
}
// DO UPDATE on a self-set is a deliberate no-op write whose only purpose
// is to make RETURNING fire on conflicting rows. Without it, RETURNING is
// silent on the conflict path and we'd have to refetch by (key, value) to
// learn the existing rows' IDs after a concurrent-insert race. Setting
// key = tag.key (the existing row's value) preserves the first writer's
// casing on case-only collisions.
err := s.sqlstore.
BunDBCtx(ctx).
NewInsert().
Model(&tags).
On("CONFLICT (org_id, kind, (LOWER(key)), (LOWER(value))) DO UPDATE").
Set("key = tag.key").
Returning("*").
Scan(ctx)
if err != nil {
return nil, err
}
return tags, nil
}
func (s *store) CreateRelations(ctx context.Context, relations []*tagtypes.TagRelation) error {
if len(relations) == 0 {
return nil
}
_, err := s.sqlstore.
BunDBCtx(ctx).
NewInsert().
Model(&relations).
On("CONFLICT (kind, resource_id, tag_id) DO NOTHING").
Exec(ctx)
return err
}
func (s *store) DeleteRelationsExcept(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceID valuer.UUID, keepTagIDs []valuer.UUID) error {
// Scope the delete to the caller's org via a subquery on tag — bun's
// DELETE-with-JOIN syntax isn't uniformly portable across Postgres/SQLite.
tagIDsToDelete := s.sqlstore.
BunDBCtx(ctx).
NewSelect().
TableExpr("tag").
Column("id").
Where("org_id = ?", orgID)
if len(keepTagIDs) > 0 {
tagIDsToDelete = tagIDsToDelete.Where("id NOT IN (?)", bun.In(keepTagIDs))
}
_, err := s.sqlstore.
BunDBCtx(ctx).
NewDelete().
Model((*tagtypes.TagRelation)(nil)).
Where("kind = ?", kind).
Where("resource_id = ?", resourceID).
Where("tag_id IN (?)", tagIDsToDelete).
Exec(ctx)
return err
}
func (s *store) RunInTx(ctx context.Context, cb func(ctx context.Context) error) error {
return s.sqlstore.RunInTxCtx(ctx, nil, cb)
}

View File

@@ -0,0 +1,147 @@
package impltag
import (
"context"
"path/filepath"
"strings"
"testing"
"time"
"github.com/SigNoz/signoz/pkg/factory/factorytest"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/sqlstore/sqlitesqlstore"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/tagtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/uptrace/bun"
)
func newTestStore(t *testing.T) sqlstore.SQLStore {
t.Helper()
dbPath := filepath.Join(t.TempDir(), "test.db")
store, err := sqlitesqlstore.New(context.Background(), factorytest.NewSettings(), sqlstore.Config{
Provider: "sqlite",
Connection: sqlstore.ConnectionConfig{
MaxOpenConns: 1,
MaxConnLifetime: 0,
},
Sqlite: sqlstore.SqliteConfig{
Path: dbPath,
Mode: "wal",
BusyTimeout: 5 * time.Second,
TransactionMode: "deferred",
},
})
require.NoError(t, err)
_, err = store.BunDB().NewCreateTable().
Model((*tagtypes.Tag)(nil)).
IfNotExists().
Exec(context.Background())
require.NoError(t, err)
_, err = store.BunDB().Exec(`CREATE UNIQUE INDEX IF NOT EXISTS uq_tag_org_kind_lower_key_lower_value ON tag (org_id, kind, LOWER(key), LOWER(value))`)
require.NoError(t, err)
return store
}
var dashboardKind = coretypes.KindDashboard
func tagsByLowerKeyValue(t *testing.T, db *bun.DB) map[string]*tagtypes.Tag {
t.Helper()
all := make([]*tagtypes.Tag, 0)
require.NoError(t, db.NewSelect().Model(&all).Scan(context.Background()))
out := map[string]*tagtypes.Tag{}
for _, tag := range all {
out[strings.ToLower(tag.Key)+"\x00"+strings.ToLower(tag.Value)] = tag
}
return out
}
func TestStore_Create_PopulatesIDsOnFreshInsert(t *testing.T) {
ctx := context.Background()
sqlstore := newTestStore(t)
s := NewStore(sqlstore)
orgID := valuer.GenerateUUID()
tagA := tagtypes.NewTag(orgID, dashboardKind, "tag", "Database")
tagB := tagtypes.NewTag(orgID, dashboardKind, "team", "BLR")
preIDA := tagA.ID
preIDB := tagB.ID
got, err := s.Create(ctx, []*tagtypes.Tag{tagA, tagB})
require.NoError(t, err)
require.Len(t, got, 2)
// No race → pre-generated IDs stand. The slice is what we passed in,
// confirming Scan didn't reallocate.
assert.Equal(t, preIDA, got[0].ID)
assert.Equal(t, preIDB, got[1].ID)
// And the rows are in the DB.
stored := tagsByLowerKeyValue(t, sqlstore.BunDB())
require.Contains(t, stored, "tag\x00database")
require.Contains(t, stored, "team\x00blr")
assert.Equal(t, preIDA, stored["tag\x00database"].ID)
assert.Equal(t, preIDB, stored["team\x00blr"].ID)
}
func TestStore_Create_ConflictReturnsExistingRowID(t *testing.T) {
ctx := context.Background()
sqlstore := newTestStore(t)
s := NewStore(sqlstore)
orgID := valuer.GenerateUUID()
// Simulate a concurrent insert: someone else has already inserted "tag:Database".
winner := tagtypes.NewTag(orgID, dashboardKind, "tag", "Database")
_, err := s.Create(ctx, []*tagtypes.Tag{winner})
require.NoError(t, err)
winnerID := winner.ID
// Now our request runs with a different pre-generated ID for the same
// (key, value) — case differs but the functional unique index collapses
// them. RETURNING should overwrite our stale ID with winner's ID.
loser := tagtypes.NewTag(orgID, dashboardKind, "TAG", "DATABASE")
loserPreID := loser.ID
require.NotEqual(t, winnerID, loserPreID, "pre-generated IDs must differ for this test to be meaningful")
got, err := s.Create(ctx, []*tagtypes.Tag{loser})
require.NoError(t, err)
require.Len(t, got, 1)
assert.Equal(t, winnerID, got[0].ID, "returned slice should carry the existing row's ID, not our stale one")
assert.Equal(t, winnerID, loser.ID, "input slice element is mutated in place")
// And the DB still has exactly one row for that (lower(key), lower(value)) — winner's, with winner's casing.
stored := tagsByLowerKeyValue(t, sqlstore.BunDB())
require.Len(t, stored, 1)
assert.Equal(t, winnerID, stored["tag\x00database"].ID)
assert.Equal(t, "tag", stored["tag\x00database"].Key, "winner's casing preserved in key")
assert.Equal(t, "Database", stored["tag\x00database"].Value, "winner's casing preserved in value")
}
func TestStore_Create_MixedFreshAndConflict(t *testing.T) {
ctx := context.Background()
sqlstore := newTestStore(t)
s := NewStore(sqlstore)
orgID := valuer.GenerateUUID()
pre := tagtypes.NewTag(orgID, dashboardKind, "tag", "Database")
_, err := s.Create(ctx, []*tagtypes.Tag{pre})
require.NoError(t, err)
preExistingID := pre.ID
conflict := tagtypes.NewTag(orgID, dashboardKind, "tag", "Database")
fresh := tagtypes.NewTag(orgID, dashboardKind, "team", "BLR")
freshPreID := fresh.ID
got, err := s.Create(ctx, []*tagtypes.Tag{conflict, fresh})
require.NoError(t, err)
require.Len(t, got, 2)
assert.Equal(t, preExistingID, got[0].ID, "conflicting row's ID overwritten with the existing row's")
assert.Equal(t, freshPreID, got[1].ID, "fresh row's pre-generated ID is preserved")
}

20
pkg/modules/tag/tag.go Normal file
View File

@@ -0,0 +1,20 @@
package tag
import (
"context"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/tagtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Module interface {
// SyncTags resolves the given postable tags (creating new rows as needed)
// and reconciles the resource's links to exactly that set, all in one transaction.
SyncTags(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceID valuer.UUID, postable []tagtypes.PostableTag) ([]*tagtypes.Tag, error)
ListForResource(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceID valuer.UUID) ([]*tagtypes.Tag, error)
// Resources with no tags are absent from the returned map.
ListForResources(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceIDs []valuer.UUID) (map[valuer.UUID][]*tagtypes.Tag, error)
}

View File

@@ -17,6 +17,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
"github.com/SigNoz/signoz/pkg/modules/retention/implretention"
"github.com/SigNoz/signoz/pkg/modules/tag/impltag"
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/queryparser"
@@ -45,7 +46,8 @@ func TestNewHandlers(t *testing.T) {
emailing := emailingtest.New()
queryParser := queryparser.New(providerSettings)
require.NoError(t, err)
dashboardModule := impldashboard.NewModule(impldashboard.NewStore(sqlstore), providerSettings, nil, orgGetter, queryParser)
tagModule := impltag.NewModule(impltag.NewStore(sqlstore))
dashboardModule := impldashboard.NewModule(impldashboard.NewStore(sqlstore), sqlstore, providerSettings, nil, orgGetter, queryParser, tagModule)
flagger, err := flagger.New(context.Background(), instrumentationtest.New().ToProviderSettings(), flagger.Config{}, flagger.MustNewRegistry())
require.NoError(t, err)
@@ -53,8 +55,9 @@ func TestNewHandlers(t *testing.T) {
userRoleStore := impluser.NewUserRoleStore(sqlstore, providerSettings)
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings), userRoleStore, flagger)
retentionGetter := implretention.NewGetter(implretention.NewStore(sqlstore))
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, userRoleStore, nil, nil, retentionGetter, flagger)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, userRoleStore, nil, nil, retentionGetter, flagger, tagModule)
querierHandler := querier.NewHandler(providerSettings, nil, nil)
registryHandler := factory.NewHandler(nil)

View File

@@ -45,6 +45,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/spanmapper/implspanmapper"
"github.com/SigNoz/signoz/pkg/modules/spanpercentile"
"github.com/SigNoz/signoz/pkg/modules/spanpercentile/implspanpercentile"
"github.com/SigNoz/signoz/pkg/modules/tag"
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
"github.com/SigNoz/signoz/pkg/modules/tracedetail/impltracedetail"
"github.com/SigNoz/signoz/pkg/modules/tracefunnel"
@@ -88,6 +89,7 @@ type Modules struct {
TraceDetail tracedetail.Module
SpanMapper spanmapper.Module
LLMPricingRule llmpricingrule.Module
Tag tag.Module
}
func NewModules(
@@ -113,6 +115,7 @@ func NewModules(
cloudIntegrationModule cloudintegration.Module,
retentionGetter retention.Getter,
fl flagger.Flagger,
tagModule tag.Module,
) Modules {
quickfilter := implquickfilter.NewModule(implquickfilter.NewStore(sqlstore))
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter)
@@ -145,5 +148,6 @@ func NewModules(
TraceDetail: impltracedetail.NewModule(impltracedetail.NewTraceStore(telemetryStore), providerSettings, config.TraceDetail),
SpanMapper: implspanmapper.NewModule(implspanmapper.NewStore(sqlstore)),
LLMPricingRule: impllmpricingrule.NewModule(impllmpricingrule.NewStore(sqlstore)),
Tag: tagModule,
}
}

View File

@@ -19,6 +19,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/retention/implretention"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount/implserviceaccount"
"github.com/SigNoz/signoz/pkg/modules/tag/impltag"
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/sharder"
@@ -46,7 +47,8 @@ func TestNewModules(t *testing.T) {
emailing := emailingtest.New()
queryParser := queryparser.New(providerSettings)
require.NoError(t, err)
dashboardModule := impldashboard.NewModule(impldashboard.NewStore(sqlstore), providerSettings, nil, orgGetter, queryParser)
tagModule := impltag.NewModule(impltag.NewStore(sqlstore))
dashboardModule := impldashboard.NewModule(impldashboard.NewStore(sqlstore), sqlstore, providerSettings, nil, orgGetter, queryParser, tagModule)
flagger, err := flagger.New(context.Background(), instrumentationtest.New().ToProviderSettings(), flagger.Config{}, flagger.MustNewRegistry())
require.NoError(t, err)
@@ -58,7 +60,8 @@ func TestNewModules(t *testing.T) {
serviceAccount := implserviceaccount.NewModule(implserviceaccount.NewStore(sqlstore), nil, nil, nil, providerSettings, serviceaccount.Config{})
retentionGetter := implretention.NewGetter(implretention.NewStore(sqlstore))
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, userRoleStore, serviceAccount, implcloudintegration.NewModule(), retentionGetter, flagger)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, userRoleStore, serviceAccount, implcloudintegration.NewModule(), retentionGetter, flagger, tagModule)
reflectVal := reflect.ValueOf(modules)
for i := 0; i < reflectVal.NumField(); i++ {

View File

@@ -200,6 +200,7 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewAddServiceAccountManagedRoleTransactionsFactory(sqlstore),
sqlmigration.NewAddSpanMapperFactory(sqlstore, sqlschema),
sqlmigration.NewAddLLMPricingRulesFactory(sqlstore, sqlschema),
sqlmigration.NewAddTagsFactory(sqlstore, sqlschema),
)
}

View File

@@ -32,6 +32,8 @@ import (
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount/implserviceaccount"
"github.com/SigNoz/signoz/pkg/modules/tag"
"github.com/SigNoz/signoz/pkg/modules/tag/impltag"
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/querier"
@@ -105,7 +107,7 @@ func New(
telemetrystoreProviderFactories factory.NamedMap[factory.ProviderFactory[telemetrystore.TelemetryStore, telemetrystore.Config]],
authNsCallback func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error),
authzCallback func(context.Context, sqlstore.SQLStore, authz.Config, licensing.Licensing, []authz.OnBeforeRoleDelete) (factory.ProviderFactory[authz.AuthZ, authz.Config], error),
dashboardModuleCallback func(sqlstore.SQLStore, factory.ProviderSettings, analytics.Analytics, organization.Getter, queryparser.QueryParser, querier.Querier, licensing.Licensing) dashboard.Module,
dashboardModuleCallback func(sqlstore.SQLStore, factory.ProviderSettings, analytics.Analytics, organization.Getter, queryparser.QueryParser, querier.Querier, licensing.Licensing, tag.Module) dashboard.Module,
gatewayProviderFactory func(licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config],
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),
@@ -332,8 +334,13 @@ func New(
// Initialize query parser (needed for dashboard module)
queryParser := queryparser.New(providerSettings)
// Initialize dashboard module
dashboard := dashboardModuleCallback(sqlstore, providerSettings, analytics, orgGetter, queryParser, querier, licensing)
// Initialize tag module — shared across modules that link entities to tags
// (currently dashboard; future: alerts, RBAC). Built once here and injected
// where needed.
tagModule := impltag.NewModule(impltag.NewStore(sqlstore))
// Initialize dashboard module (needed for authz registry)
dashboard := dashboardModuleCallback(sqlstore, providerSettings, analytics, orgGetter, queryParser, querier, licensing, tagModule)
// Initialize user getter
userGetter := impluser.NewGetter(userStore, userRoleStore, flagger)
@@ -455,7 +462,7 @@ func New(
}
// Initialize all modules
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, userGetter, userRoleStore, serviceAccount, cloudIntegrationModule, retentionGetter, flagger)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, userGetter, userRoleStore, serviceAccount, cloudIntegrationModule, retentionGetter, flagger, tagModule)
// Initialize ruler from the variant-specific provider factories
rulerInstance, err := factory.NewProviderFromNamedMap(ctx, providerSettings, config.Ruler, rulerProviderFactories(cache, alertmanager, sqlstore, telemetrystore, telemetryMetadataStore, prometheus, orgGetter, modules.RuleStateHistory, querier, queryParser), "signoz")

View File

@@ -0,0 +1,114 @@
package sqlmigration
import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type addTags struct {
sqlstore sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
}
func NewAddTagsFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("add_tags"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &addTags{
sqlstore: sqlstore,
sqlschema: sqlschema,
}, nil
})
}
func (migration *addTags) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
func (migration *addTags) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
sqls := [][]byte{}
tagTableSQLs := migration.sqlschema.Operator().CreateTable(&sqlschema.Table{
Name: "tag",
Columns: []*sqlschema.Column{
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "key", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "value", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "org_id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "kind", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
{Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
},
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{ColumnNames: []sqlschema.ColumnName{"id"}},
ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{
{
ReferencingColumnName: sqlschema.ColumnName("org_id"),
ReferencedTableName: sqlschema.TableName("organizations"),
ReferencedColumnName: sqlschema.ColumnName("id"),
},
},
})
sqls = append(sqls, tagTableSQLs...)
// Case-insensitive uniqueness on (org_id, kind, key, value) — both Postgres
// and SQLite (modernc 3.50.x) support expression indexes.
tagUniqueIndexSQLs := migration.sqlschema.Operator().CreateIndex(
(&sqlschema.FunctionalUniqueIndex{
TableName: "tag",
Expressions: []string{"org_id", "kind", "LOWER(key)", "LOWER(value)"},
ReferencedColumns: []sqlschema.ColumnName{"org_id", "kind", "key", "value"},
}).Named("uq_tag_org_kind_lower_key_lower_value"),
)
sqls = append(sqls, tagUniqueIndexSQLs...)
tagRelationsTableSQLs := migration.sqlschema.Operator().CreateTable(&sqlschema.Table{
Name: "tag_relation",
Columns: []*sqlschema.Column{
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "kind", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "resource_id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "tag_id", DataType: sqlschema.DataTypeText, Nullable: false},
},
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{ColumnNames: []sqlschema.ColumnName{"id"}},
ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{
{
ReferencingColumnName: sqlschema.ColumnName("tag_id"),
ReferencedTableName: sqlschema.TableName("tag"),
ReferencedColumnName: sqlschema.ColumnName("id"),
},
},
})
sqls = append(sqls, tagRelationsTableSQLs...)
tagRelationUniqueIndexSQLs := migration.sqlschema.Operator().CreateIndex(
&sqlschema.UniqueIndex{
TableName: "tag_relation",
ColumnNames: []sqlschema.ColumnName{"kind", "resource_id", "tag_id"},
},
)
sqls = append(sqls, tagRelationUniqueIndexSQLs...)
for _, sql := range sqls {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
return tx.Commit()
}
func (migration *addTags) Down(_ context.Context, _ *bun.DB) error {
return nil
}

View File

@@ -1,6 +1,8 @@
package sqlschema
import (
"fmt"
"hash/fnv"
"slices"
"strings"
@@ -8,9 +10,10 @@ import (
)
var (
IndexTypeUnique = IndexType{s: valuer.NewString("uq")}
IndexTypeIndex = IndexType{s: valuer.NewString("ix")}
IndexTypePartialUnique = IndexType{s: valuer.NewString("puq")}
IndexTypeUnique = IndexType{s: valuer.NewString("uq")}
IndexTypeIndex = IndexType{s: valuer.NewString("ix")}
IndexTypePartialUnique = IndexType{s: valuer.NewString("puq")}
IndexTypeFunctionalUnique = IndexType{s: valuer.NewString("fnq")}
)
type IndexType struct{ s valuer.String }
@@ -232,3 +235,111 @@ func (index *PartialUniqueIndex) ToDropSQL(fmter SQLFormatter) []byte {
return sql
}
// FunctionalUniqueIndex is a unique index whose key parts may be arbitrary SQL
// expressions (e.g. LOWER(key)) rather than plain column names. The strings in
// Expressions are emitted into the generated SQL exactly as supplied — they are
// not identifier-quoted by the formatter — so the caller is responsible for
// well-formed expressions. Auto-generated names are opaque (`fnq_<table>_<hash>`);
// prefer calling Named(...) to give the index a readable name.
//
// ReferencedColumns is informational: it lets consumers (drift detection,
// "which columns are indexed" lookups) answer that question without having to
// parse Expressions. It does not affect the emitted SQL or Equals.
type FunctionalUniqueIndex struct {
TableName TableName
Expressions []string
ReferencedColumns []ColumnName
name string
}
func (index *FunctionalUniqueIndex) Name() string {
if index.name != "" {
return index.name
}
// column names can't be used cuz same columns can have different functional indices
// and the expressions can't be used directly cuz they will have disallowed characters.
hasher := fnv.New32a()
_, _ = hasher.Write([]byte(strings.Join(index.Expressions, "\x00")))
var b strings.Builder
b.WriteString(IndexTypeFunctionalUnique.String())
b.WriteString("_")
b.WriteString(string(index.TableName))
b.WriteString("_")
fmt.Fprintf(&b, "%08x", hasher.Sum32())
return b.String()
}
func (index *FunctionalUniqueIndex) Named(name string) Index {
copyOfExpressions := make([]string, len(index.Expressions))
copy(copyOfExpressions, index.Expressions)
copyOfReferencedColumns := make([]ColumnName, len(index.ReferencedColumns))
copy(copyOfReferencedColumns, index.ReferencedColumns)
return &FunctionalUniqueIndex{
TableName: index.TableName,
Expressions: copyOfExpressions,
ReferencedColumns: copyOfReferencedColumns,
name: name,
}
}
func (index *FunctionalUniqueIndex) IsNamed() bool {
return index.name != ""
}
func (*FunctionalUniqueIndex) Type() IndexType {
return IndexTypeFunctionalUnique
}
// Columns returns the caller-declared ReferencedColumns. Equality of two
// functional indexes still goes through Equals (which diffs Expressions);
// Columns is purely informational.
func (index *FunctionalUniqueIndex) Columns() []ColumnName {
return index.ReferencedColumns
}
func (index *FunctionalUniqueIndex) Equals(other Index) bool {
if other.Type() != IndexTypeFunctionalUnique {
return false
}
otherFunctional, ok := other.(*FunctionalUniqueIndex)
if !ok {
return false
}
return index.Name() == other.Name() && index.TableName == otherFunctional.TableName && slices.Equal(index.Expressions, otherFunctional.Expressions) && slices.Equal(index.ReferencedColumns, otherFunctional.ReferencedColumns)
}
func (index *FunctionalUniqueIndex) ToCreateSQL(fmter SQLFormatter) []byte {
sql := []byte{}
sql = append(sql, "CREATE UNIQUE INDEX IF NOT EXISTS "...)
sql = fmter.AppendIdent(sql, index.Name())
sql = append(sql, " ON "...)
sql = fmter.AppendIdent(sql, string(index.TableName))
sql = append(sql, " ("...)
for i, expr := range index.Expressions {
if i > 0 {
sql = append(sql, ", "...)
}
sql = append(sql, expr...)
}
sql = append(sql, ")"...)
return sql
}
func (index *FunctionalUniqueIndex) ToDropSQL(fmter SQLFormatter) []byte {
sql := []byte{}
sql = append(sql, "DROP INDEX IF EXISTS "...)
sql = fmter.AppendIdent(sql, index.Name())
return sql
}

View File

@@ -0,0 +1,194 @@
package dashboardtypes
import (
"bytes"
"encoding/json"
"strings"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/tagtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
const (
SchemaVersion = "v6"
MaxTagsPerDashboard = 5
)
type DSLKey string
const (
DSLKeyName DSLKey = "name"
DSLKeyDescription DSLKey = "description"
DSLKeyCreatedAt DSLKey = "created_at"
DSLKeyUpdatedAt DSLKey = "updated_at"
DSLKeyCreatedBy DSLKey = "created_by"
DSLKeyLocked DSLKey = "locked"
DSLKeyPublic DSLKey = "public"
)
// reservedDSLKeys are dashboard column-level filter names in the list-query DSL.
// A tag whose key collides with one of these would make the DSL ambiguous, so
// they're rejected (case-insensitively) at write time.
var reservedDSLKeys = map[DSLKey]struct{}{
DSLKeyName: {},
DSLKeyDescription: {},
DSLKeyCreatedAt: {},
DSLKeyUpdatedAt: {},
DSLKeyCreatedBy: {},
DSLKeyLocked: {},
DSLKeyPublic: {},
}
type DashboardV2 struct {
types.Identifiable
types.TimeAuditable
types.UserAuditable
OrgID valuer.UUID `json:"orgId"`
Locked bool `json:"locked"`
Info DashboardInfo `json:"info"`
PublicConfig *PublicDashboard `json:"publicConfig,omitempty"`
}
// DashboardInfo is the serializable view of a dashboard's contents — what the UI renders as "the dashboard JSON".
type DashboardInfo struct {
StoredDashboardInfo
Tags []*tagtypes.Tag `json:"tags,omitempty"`
}
// StoredDashboardInfo is exactly what serializes into the dashboard.data column.
type StoredDashboardInfo struct {
Metadata DashboardMetadata `json:"metadata"`
Data DashboardData `json:"data"`
}
type DashboardMetadata struct {
SchemaVersion string `json:"schemaVersion"`
Image string `json:"image,omitempty"`
UploadedGrafana bool `json:"uploadedGrafana"`
}
type PostableDashboardV2 struct {
StoredDashboardInfo
Tags []tagtypes.PostableTag `json:"tags,omitempty"`
}
func (p *PostableDashboardV2) UnmarshalJSON(data []byte) error {
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()
type alias PostableDashboardV2
var tmp alias
if err := dec.Decode(&tmp); err != nil {
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "%s", err.Error())
}
*p = PostableDashboardV2(tmp)
return p.Validate()
}
func (p *PostableDashboardV2) Validate() error {
if p.Metadata.SchemaVersion != SchemaVersion {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "metadata.schemaVersion must be %q, got %q", SchemaVersion, p.Metadata.SchemaVersion)
}
if p.Data.Display == nil || p.Data.Display.Name == "" {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "data.display.name is required")
}
if err := p.validateTags(); err != nil {
return err
}
return p.Data.Validate()
}
func (p *PostableDashboardV2) validateTags() error {
if len(p.Tags) > MaxTagsPerDashboard {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "a dashboard can have at most %d tags", MaxTagsPerDashboard)
}
for _, tag := range p.Tags {
if _, reserved := reservedDSLKeys[DSLKey(strings.ToLower(strings.TrimSpace(tag.Key)))]; reserved {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "tag key %q is reserved", tag.Key)
}
}
return nil
}
type GettableDashboardV2 struct {
types.Identifiable
types.TimeAuditable
types.UserAuditable
OrgID valuer.UUID `json:"orgId"`
Locked bool `json:"locked"`
Info GettableDashboardInfo `json:"info"`
PublicConfig *GettablePublicDasbhboard `json:"publicConfig,omitempty"`
}
type GettableDashboardInfo struct {
StoredDashboardInfo
Tags []*tagtypes.GettableTag `json:"tags,omitempty"`
}
func NewGettableDashboardV2FromDashboardV2(dashboard *DashboardV2) *GettableDashboardV2 {
gettable := &GettableDashboardV2{
Identifiable: dashboard.Identifiable,
TimeAuditable: dashboard.TimeAuditable,
UserAuditable: dashboard.UserAuditable,
OrgID: dashboard.OrgID,
Locked: dashboard.Locked,
Info: GettableDashboardInfo{
StoredDashboardInfo: dashboard.Info.StoredDashboardInfo,
Tags: tagtypes.NewGettableTagsFromTags(dashboard.Info.Tags),
},
}
if dashboard.PublicConfig != nil {
gettable.PublicConfig = NewGettablePublicDashboard(dashboard.PublicConfig)
}
return gettable
}
func NewDashboardV2(orgID valuer.UUID, createdBy string, postable PostableDashboardV2, resolvedTags []*tagtypes.Tag) *DashboardV2 {
now := time.Now()
return &DashboardV2{
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
TimeAuditable: types.TimeAuditable{CreatedAt: now, UpdatedAt: now},
UserAuditable: types.UserAuditable{CreatedBy: createdBy, UpdatedBy: createdBy},
OrgID: orgID,
Locked: false,
Info: DashboardInfo{
StoredDashboardInfo: StoredDashboardInfo{
Metadata: postable.Metadata,
Data: postable.Data,
},
Tags: resolvedTags,
},
}
}
func (d *DashboardV2) ToStorableDashboard() (*StorableDashboard, error) {
data, err := d.Info.toStorableDashboardData()
if err != nil {
return nil, err
}
return &StorableDashboard{
Identifiable: types.Identifiable{ID: d.ID},
TimeAuditable: d.TimeAuditable,
UserAuditable: d.UserAuditable,
OrgID: d.OrgID,
Locked: d.Locked,
Data: data,
}, nil
}
func (s StoredDashboardInfo) toStorableDashboardData() (StorableDashboardData, error) {
raw, err := json.Marshal(s)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "marshal v2 dashboard data")
}
out := StorableDashboardData{}
if err := json.Unmarshal(raw, &out); err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "unmarshal v2 dashboard data")
}
return out, nil
}

View File

@@ -0,0 +1,30 @@
package tagtypes
import (
"context"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Store interface {
List(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind) ([]*Tag, error)
ListByResource(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceID valuer.UUID) ([]*Tag, error)
ListByResources(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceIDs []valuer.UUID) (map[valuer.UUID][]*Tag, error)
// Create upserts the given tags and returns them with authoritative IDs.
// On conflict on (org_id, kind, LOWER(key), LOWER(value)) — which
// happens only when a concurrent insert raced ours, including casing-only
// collisions — the returned entry carries the existing row's ID rather
// than the pre-generated one in the input.
Create(ctx context.Context, tags []*Tag) ([]*Tag, error)
// CreateRelations inserts tag-resource relations. Conflicts on the composite primary key are ignored.
CreateRelations(ctx context.Context, relations []*TagRelation) error
DeleteRelationsExcept(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceID valuer.UUID, keepTagIDs []valuer.UUID) error
RunInTx(ctx context.Context, cb func(ctx context.Context) error) error
}

162
pkg/types/tagtypes/tag.go Normal file
View File

@@ -0,0 +1,162 @@
package tagtypes
import (
"context"
"regexp"
"strings"
"time"
"unicode/utf8"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
const (
MAX_LEN_TAG_KEY = 32
MAX_LEN_TAG_VALUE = 32
)
var (
tagKeyRegex = regexp.MustCompile(`^[a-zA-Z$_@{#][a-zA-Z0-9$_@#{}:/-]*$`)
tagValueRegex = regexp.MustCompile(`^[a-zA-Z0-9$_@#{}:.+=/-]*$`)
ErrCodeTagInvalidKey = errors.MustNewCode("tag_invalid_key")
ErrCodeTagInvalidValue = errors.MustNewCode("tag_invalid_value")
ErrCodeTagNotFound = errors.MustNewCode("tag_not_found")
)
type Tag struct {
bun.BaseModel `bun:"table:tag,alias:tag"`
types.Identifiable
types.TimeAuditable
Key string `json:"key" required:"true" bun:"key,type:text,notnull"`
Value string `json:"value" required:"true" bun:"value,type:text,notnull"`
OrgID valuer.UUID `json:"orgId" required:"true" bun:"org_id,type:text,notnull"`
Kind coretypes.Kind `json:"kind" required:"true" bun:"kind,type:text,notnull"`
}
type PostableTag struct {
Key string `json:"key" required:"true"`
Value string `json:"value" required:"true"`
}
type GettableTag = PostableTag
func NewGettableTagFromTag(tag *Tag) *GettableTag {
return &GettableTag{Key: tag.Key, Value: tag.Value}
}
func NewGettableTagsFromTags(tags []*Tag) []*GettableTag {
out := make([]*GettableTag, len(tags))
for i, t := range tags {
out[i] = NewGettableTagFromTag(t)
}
return out
}
func NewPostableTagFromTag(tag *Tag) PostableTag {
return PostableTag{Key: tag.Key, Value: tag.Value}
}
func NewPostableTagsFromTags(tags []*Tag) []PostableTag {
out := make([]PostableTag, len(tags))
for i, t := range tags {
out[i] = NewPostableTagFromTag(t)
}
return out
}
func NewTag(orgID valuer.UUID, kind coretypes.Kind, key, value string) *Tag {
now := time.Now()
return &Tag{
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
TimeAuditable: types.TimeAuditable{
CreatedAt: now,
UpdatedAt: now,
},
Key: key,
Value: value,
OrgID: orgID,
Kind: kind,
}
}
// Resolve canonicalizes a batch of user-supplied (key, value) tag pairs against
// the existing tags for an org. Lookup is case-insensitive on both key and
// value (matching the storage uniqueness rule); when an existing row matches,
// its display casing is reused. Inputs are deduped on (LOWER(key), LOWER(value));
// the first input's casing wins on collisions. Returns:
// - toCreate: new Tag rows the caller should insert (with pre-generated IDs)
// - matched: existing rows the caller's input already pointed to. They
// already carry authoritative IDs from the store.
func Resolve(ctx context.Context, store Store, orgID valuer.UUID, kind coretypes.Kind, postable []PostableTag) ([]*Tag, []*Tag, error) {
if len(postable) == 0 {
return nil, nil, nil
}
existing, err := store.List(ctx, orgID, kind)
if err != nil {
return nil, nil, err
}
lowercaseTagsMap := make(map[string]*Tag, len(existing))
for _, t := range existing {
mapKey := strings.ToLower(t.Key) + "\x00" + strings.ToLower(t.Value)
lowercaseTagsMap[mapKey] = t
}
seenInRequestAlready := make(map[string]struct{}, len(postable)) // postable can have the same tag multiple times
toCreate := make([]*Tag, 0)
matched := make([]*Tag, 0)
for _, p := range postable {
key, value, err := validatePostableTag(p)
if err != nil {
return nil, nil, err
}
lookup := strings.ToLower(key) + "\x00" + strings.ToLower(value)
if _, dup := seenInRequestAlready[lookup]; dup {
continue
}
seenInRequestAlready[lookup] = struct{}{}
if existingTag, ok := lowercaseTagsMap[lookup]; ok {
matched = append(matched, existingTag)
continue
}
toCreate = append(toCreate, NewTag(orgID, kind, key, value))
}
return toCreate, matched, nil
}
// Entity-specific reserved-key checks (e.g. dashboard column names that would
// collide with the list-query DSL) are the caller's responsibility — perform
// them before calling into the tag module.
func validatePostableTag(p PostableTag) (string, string, error) {
key := strings.TrimSpace(p.Key)
value := strings.TrimSpace(p.Value)
if key == "" {
return "", "", errors.Newf(errors.TypeInvalidInput, ErrCodeTagInvalidKey, "tag key cannot be empty")
}
if value == "" {
return "", "", errors.Newf(errors.TypeInvalidInput, ErrCodeTagInvalidValue, "tag value cannot be empty")
}
if !tagKeyRegex.MatchString(key) {
return "", "", errors.Newf(errors.TypeInvalidInput, ErrCodeTagInvalidKey, "tag key %q contains disallowed characters", key)
}
if !tagValueRegex.MatchString(value) {
return "", "", errors.Newf(errors.TypeInvalidInput, ErrCodeTagInvalidValue, "tag value %q contains disallowed characters", value)
}
if utf8.RuneCountInString(key) > MAX_LEN_TAG_KEY {
return "", "", errors.Newf(errors.TypeInvalidInput, ErrCodeTagInvalidKey, "tag key %q exceeds the %d-character limit", key, MAX_LEN_TAG_KEY)
}
if utf8.RuneCountInString(value) > MAX_LEN_TAG_VALUE {
return "", "", errors.Newf(errors.TypeInvalidInput, ErrCodeTagInvalidValue, "tag value %q exceeds the %d-character limit", value, MAX_LEN_TAG_VALUE)
}
return key, value, nil
}

View File

@@ -0,0 +1,34 @@
package tagtypes
import (
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
type TagRelation struct {
bun.BaseModel `bun:"table:tag_relation,alias:tag_relation"`
types.Identifiable
Kind coretypes.Kind `json:"kind" required:"true" bun:"kind,type:text,notnull"`
ResourceID valuer.UUID `json:"resourceId" required:"true" bun:"resource_id,type:text,notnull"`
TagID valuer.UUID `json:"tagId" required:"true" bun:"tag_id,type:text,notnull"`
}
func NewTagRelation(kind coretypes.Kind, resourceID valuer.UUID, tagID valuer.UUID) *TagRelation {
return &TagRelation{
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
Kind: kind,
ResourceID: resourceID,
TagID: tagID,
}
}
func NewTagRelations(kind coretypes.Kind, resourceID valuer.UUID, tagIDs []valuer.UUID) []*TagRelation {
relations := make([]*TagRelation, 0, len(tagIDs))
for _, tagID := range tagIDs {
relations = append(relations, NewTagRelation(kind, resourceID, tagID))
}
return relations
}

View File

@@ -0,0 +1,196 @@
package tagtypes
import (
"context"
"strings"
"testing"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestValidatePostableTag(t *testing.T) {
tests := []struct {
name string
input PostableTag
wantKey string
wantValue string
wantError bool
}{
{name: "simple pair", input: PostableTag{Key: "team", Value: "pulse"}, wantKey: "team", wantValue: "pulse"},
{name: "preserves casing", input: PostableTag{Key: "Team", Value: "Pulse"}, wantKey: "Team", wantValue: "Pulse"},
{name: "trims key", input: PostableTag{Key: " team ", Value: "pulse"}, wantKey: "team", wantValue: "pulse"},
{name: "trims value", input: PostableTag{Key: "team", Value: " pulse "}, wantKey: "team", wantValue: "pulse"},
{name: "empty key rejected", input: PostableTag{Key: "", Value: "pulse"}, wantError: true},
{name: "empty value rejected", input: PostableTag{Key: "team", Value: ""}, wantError: true},
{name: "whitespace-only key rejected", input: PostableTag{Key: " ", Value: "pulse"}, wantError: true},
{name: "whitespace-only value rejected", input: PostableTag{Key: "team", Value: " "}, wantError: true},
{name: "slash accepted", input: PostableTag{Key: "team/eng", Value: "pulse/events"}, wantKey: "team/eng", wantValue: "pulse/events"},
{name: "colon accepted", input: PostableTag{Key: "team:eng", Value: "env:prod"}, wantKey: "team:eng", wantValue: "env:prod"},
{name: "extra punctuation accepted in both", input: PostableTag{Key: "a_b-c@d#e$f{g}h", Value: "a_b-c@d#e$f{g}h"}, wantKey: "a_b-c@d#e$f{g}h", wantValue: "a_b-c@d#e$f{g}h"},
// Key is strict; value allows the extra `. + =` plus leading digits.
{name: "dot in key rejected", input: PostableTag{Key: "team.eng", Value: "pulse"}, wantError: true},
{name: "dot in value accepted", input: PostableTag{Key: "team", Value: "pulse.events"}, wantKey: "team", wantValue: "pulse.events"},
{name: "plus in key rejected", input: PostableTag{Key: "team+eng", Value: "pulse"}, wantError: true},
{name: "plus in value accepted", input: PostableTag{Key: "team", Value: "a+b"}, wantKey: "team", wantValue: "a+b"},
{name: "equals in key rejected", input: PostableTag{Key: "team=eng", Value: "pulse"}, wantError: true},
{name: "equals in value accepted", input: PostableTag{Key: "team", Value: "a=b"}, wantKey: "team", wantValue: "a=b"},
{name: "leading digit in key rejected", input: PostableTag{Key: "2024team", Value: "pulse"}, wantError: true},
{name: "leading digit in value accepted", input: PostableTag{Key: "team", Value: "2024_team"}, wantKey: "team", wantValue: "2024_team"},
{name: "unicode letter in key rejected", input: PostableTag{Key: "チーム", Value: "pulse"}, wantError: true},
{name: "unicode letter in value rejected", input: PostableTag{Key: "team", Value: "東京"}, wantError: true},
{name: "internal space in key rejected", input: PostableTag{Key: "team eng", Value: "pulse"}, wantError: true},
{name: "internal space in value rejected", input: PostableTag{Key: "team", Value: "pulse two"}, wantError: true},
{name: "disallowed char in key rejected", input: PostableTag{Key: "team!eng", Value: "pulse"}, wantError: true},
{name: "disallowed char in value rejected", input: PostableTag{Key: "team", Value: "pulse!one"}, wantError: true},
{name: "control char rejected", input: PostableTag{Key: "team\tone", Value: "pulse"}, wantError: true},
{name: "key at the 32-char limit accepted", input: PostableTag{Key: "abcdefghijklmnopabcdefghijklmnop", Value: "pulse"}, wantKey: "abcdefghijklmnopabcdefghijklmnop", wantValue: "pulse"},
{name: "value at the 32-char limit accepted", input: PostableTag{Key: "team", Value: "abcdefghijklmnopabcdefghijklmnop"}, wantKey: "team", wantValue: "abcdefghijklmnopabcdefghijklmnop"},
{name: "key over the 32-char limit rejected", input: PostableTag{Key: "abcdefghijklmnopabcdefghijklmnopq", Value: "pulse"}, wantError: true},
{name: "value over the 32-char limit rejected", input: PostableTag{Key: "team", Value: "abcdefghijklmnopabcdefghijklmnopq"}, wantError: true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
gotKey, gotValue, err := validatePostableTag(tc.input)
if tc.wantError {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tc.wantKey, gotKey)
assert.Equal(t, tc.wantValue, gotValue)
})
}
}
var testKind = coretypes.KindDashboard
type fakeStore struct {
tags []*Tag
listCallCount int
}
func (f *fakeStore) List(_ context.Context, _ valuer.UUID, _ coretypes.Kind) ([]*Tag, error) {
f.listCallCount++
out := make([]*Tag, len(f.tags))
copy(out, f.tags)
return out, nil
}
func (f *fakeStore) Create(_ context.Context, tags []*Tag) ([]*Tag, error) {
return tags, nil
}
func (f *fakeStore) CreateRelations(_ context.Context, _ []*TagRelation) error {
return nil
}
func (f *fakeStore) ListByResource(_ context.Context, _ valuer.UUID, _ coretypes.Kind, _ valuer.UUID) ([]*Tag, error) {
return []*Tag{}, nil
}
func (f *fakeStore) ListByResources(_ context.Context, _ valuer.UUID, _ coretypes.Kind, _ []valuer.UUID) (map[valuer.UUID][]*Tag, error) {
return map[valuer.UUID][]*Tag{}, nil
}
func (f *fakeStore) DeleteRelationsExcept(_ context.Context, _ valuer.UUID, _ coretypes.Kind, _ valuer.UUID, _ []valuer.UUID) error {
return nil
}
func (f *fakeStore) RunInTx(ctx context.Context, cb func(ctx context.Context) error) error {
return cb(ctx)
}
func TestResolve(t *testing.T) {
t.Run("empty input does not hit store", func(t *testing.T) {
store := &fakeStore{}
toCreate, matched, err := Resolve(context.Background(), store, valuer.GenerateUUID(), testKind, nil)
require.NoError(t, err)
assert.Empty(t, toCreate)
assert.Empty(t, matched)
assert.Zero(t, store.listCallCount, "should not hit store when input is empty")
})
t.Run("creates missing pairs and reuses existing", func(t *testing.T) {
orgID := valuer.GenerateUUID()
dbTag := NewTag(orgID, testKind, "team", "Pulse")
dbTag2 := NewTag(orgID, testKind, "Database", "redis")
store := &fakeStore{tags: []*Tag{dbTag, dbTag2}}
toCreate, matched, err := Resolve(context.Background(), store, orgID, testKind, []PostableTag{
{Key: "team", Value: "events"}, // new
{Key: "DATABASE", Value: "REDIS"}, // case-only conflict
{Key: "Brand", Value: "New"}, // new
})
require.NoError(t, err)
createdLowerKVs := []string{}
for _, tg := range toCreate {
createdLowerKVs = append(createdLowerKVs, strings.ToLower(tg.Key)+"\x00"+strings.ToLower(tg.Value))
}
assert.ElementsMatch(t, []string{"team\x00events", "brand\x00new"}, createdLowerKVs,
"only the two missing pairs should be returned for insertion")
require.Len(t, matched, 1, "DATABASE:REDIS should hit the existing 'Database:redis' tag")
assert.Same(t, dbTag2, matched[0], "matched should return the existing pointer with its authoritative ID")
})
t.Run("dedupes inputs that map to the same lower(key)+lower(value)", func(t *testing.T) {
orgID := valuer.GenerateUUID()
store := &fakeStore{}
toCreate, matched, err := Resolve(context.Background(), store, orgID, testKind, []PostableTag{
{Key: "Foo", Value: "Bar"},
{Key: "foo", Value: "bar"},
{Key: "FOO", Value: "BAR"},
})
require.NoError(t, err)
require.Empty(t, matched)
require.Len(t, toCreate, 1, "duplicate inputs must collapse into a single insert")
assert.Equal(t, "Foo", toCreate[0].Key, "first input's casing wins")
assert.Equal(t, "Bar", toCreate[0].Value, "first input's casing wins")
})
t.Run("preserves existing casing on case-only match", func(t *testing.T) {
orgID := valuer.GenerateUUID()
dbTag := NewTag(orgID, testKind, "Team", "Pulse")
store := &fakeStore{tags: []*Tag{dbTag}}
toCreate, matched, err := Resolve(context.Background(), store, orgID, testKind, []PostableTag{
{Key: "team", Value: "PULSE"},
})
require.NoError(t, err)
assert.Empty(t, toCreate)
require.Len(t, matched, 1)
assert.Equal(t, "Team", matched[0].Key)
assert.Equal(t, "Pulse", matched[0].Value)
})
t.Run("propagates validation error from any input", func(t *testing.T) {
store := &fakeStore{}
_, _, err := Resolve(context.Background(), store, valuer.GenerateUUID(), testKind, []PostableTag{
{Key: "team", Value: "pulse"},
{Key: "", Value: "x"},
})
require.Error(t, err)
})
t.Run("propagates regex validation error", func(t *testing.T) {
store := &fakeStore{}
_, _, err := Resolve(context.Background(), store, valuer.GenerateUUID(), testKind, []PostableTag{
{Key: "team!eng", Value: "pulse"},
})
require.Error(t, err)
})
}

View File

@@ -0,0 +1,191 @@
import json
from collections.abc import Callable
from http import HTTPStatus
from pathlib import Path
import requests
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.types import Operation, SigNoz
_PERSES_FIXTURE = (
Path(__file__).parents[4]
/ "pkg/types/dashboardtypes/dashboardtypesv2/testdata/perses.json"
)
def _post_dashboard(signoz: SigNoz, token: str, body: dict) -> requests.Response:
return requests.post(
signoz.self.host_configs["8080"].get("/api/v2/dashboards"),
json=body,
headers={"Authorization": f"Bearer {token}"},
timeout=2,
)
def test_empty_body_rejected_for_missing_schema_version(
signoz: SigNoz,
create_user_admin: Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = _post_dashboard(signoz, admin_token, {})
assert response.status_code == HTTPStatus.BAD_REQUEST
body = response.json()
assert body["status"] == "error"
assert body["error"]["code"] == "dashboard_invalid_input"
assert body["error"]["message"] == 'metadata.schemaVersion must be "v6", got ""'
def test_missing_display_name_rejected(
signoz: SigNoz,
create_user_admin: Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = _post_dashboard(signoz, admin_token, {"metadata": {"schemaVersion": "v6"}})
assert response.status_code == HTTPStatus.BAD_REQUEST
body = response.json()
assert body["status"] == "error"
assert body["error"]["code"] == "dashboard_invalid_input"
assert body["error"]["message"] == "data.display.name is required"
def test_minimal_valid_body_creates_dashboard(
signoz: SigNoz,
create_user_admin: Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = _post_dashboard(
signoz,
admin_token,
{
"metadata": {"schemaVersion": "v6"},
"data": {"display": {"name": "test name"}},
},
)
assert response.status_code == HTTPStatus.CREATED
body = response.json()
assert body["status"] == "success"
data = body["data"]
assert data["info"]["data"]["display"]["name"] == "test name"
assert data["info"]["metadata"]["schemaVersion"] == "v6"
def test_unknown_root_field_rejected(
signoz: SigNoz,
create_user_admin: Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = _post_dashboard(
signoz,
admin_token,
{
"metadata": {"schemaVersion": "v6"},
"data": {"display": {"name": "test name"}},
"unknownfieldattheroot": "shouldgiveanerror",
},
)
assert response.status_code == HTTPStatus.BAD_REQUEST
body = response.json()
assert body["status"] == "error"
assert body["error"]["code"] == "dashboard_invalid_input"
assert body["error"]["message"] == 'json: unknown field "unknownfieldattheroot"'
def test_unknown_nested_field_rejected(
signoz: SigNoz,
create_user_admin: Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = _post_dashboard(
signoz,
admin_token,
{
"metadata": {"schemaVersion": "v6"},
"data": {
"display": {
"name": "test name",
"unknownfieldinside": "shouldgiveanerror",
},
},
},
)
assert response.status_code == HTTPStatus.BAD_REQUEST
body = response.json()
assert body["status"] == "error"
assert body["error"]["code"] == "dashboard_invalid_input"
assert body["error"]["message"] == 'json: unknown field "unknownfieldinside"'
def test_perses_fixture_creates_dashboard(
signoz: SigNoz,
create_user_admin: Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
"""The perses.json fixture is the kitchen-sink dashboard the schema tests
use; round-tripping it through the create API exercises the full plugin
surface end-to-end."""
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
data = json.loads(_PERSES_FIXTURE.read_text())
response = _post_dashboard(
signoz,
admin_token,
{"metadata": {"schemaVersion": "v6"}, "data": data},
)
assert response.status_code == HTTPStatus.CREATED
body = response.json()
assert body["status"] == "success"
assert body["data"]["info"]["data"]["display"]["name"] == data["display"]["name"]
def test_tag_casing_is_inherited_from_existing_parent(
signoz: SigNoz,
create_user_admin: Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
"""A second dashboard tagged with a sibling under a casing-variant parent
path should adopt the existing parent's casing while keeping the
user-supplied casing for the new leaf segment."""
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
first = _post_dashboard(
signoz,
admin_token,
{
"metadata": {"schemaVersion": "v6"},
"data": {"display": {"name": "dac"}},
"tags": [{"name": "engineering/US/NYC"}],
},
)
assert first.status_code == HTTPStatus.CREATED
first_tags = first.json()["data"]["info"]["tags"]
assert first_tags == [{"name": "engineering/US/NYC"}]
second = _post_dashboard(
signoz,
admin_token,
{
"metadata": {"schemaVersion": "v6"},
"data": {"display": {"name": "dac"}},
"tags": [{"name": "engineering/us/SF"}],
},
)
assert second.status_code == HTTPStatus.CREATED
second_tags = second.json()["data"]["info"]["tags"]
assert second_tags == [{"name": "engineering/US/SF"}]