Compare commits

..

23 Commits

Author SHA1 Message Date
Abhi Kumar
5d789ca38e fix: added fix for the UI 2026-03-18 14:39:47 +05:30
Abhi Kumar
8e28899626 chore: updated rightcontainer 2026-03-18 03:50:57 +05:30
Abhi Kumar
dc4a735e7f chore: added spanGaps selection component 2026-03-18 03:50:11 +05:30
Abhi Kumar
50d88fd783 chore: fixed disconnect points logic 2026-03-18 00:33:46 +05:30
Abhi Kumar
b473f55444 Merge branch 'main' of https://github.com/SigNoz/signoz into feat/gaps-thresholds 2026-03-17 22:06:11 +05:30
Naman Verma
24b72084ac fix: return not-found error with diagnostic info for absent metrics (#10560)
* fix: check for metric type without query range constraint

* revert: revert check for metric type without query range constraint

* chore: move temporality+type fetcher to the case where it is actually used

* fix: don't send absent metrics to query builder

* chore: better package import name

* test: unit test add mock for metadata call (which is expected in the test's scenario)

* revert: revert seeding of absent metrics

* fix: throw a not found err if metric data is missing

* test: unit test add mock for metadata call (which is expected in the test's scenario)

* revert: no need for special err handling in threshold rule

* chore: add last seen info in err message

* test: fix broken dashboard test

* test: integration test for short time range query

* chore: python lint issue
2026-03-17 16:15:32 +00:00
Pandey
2db83b453d refactor: merge roletypes into authtypes (#10614)
* refactor: merge roletypes into authtypes

* refactor: merge roletypes into authtypes

* refactor: update openapi spec

* feat: split CI

* fix: fix tsc of frontend
2026-03-17 15:43:58 +00:00
Amaresh S M
2f012715b4 fix(frontend/vite): avoid inlining whole process.env into bundle (#10605) 2026-03-17 11:51:30 +00:00
Vikrant Gupta
aa05a7bf14 chore(identn): add me as codeowner for identn (#10612) 2026-03-17 11:29:34 +00:00
Vikrant Gupta
99327960b0 feat(authn): move identn to factory and config (#10608)
* feat(authn): move identn to factory and config

* feat(authn): add support for enabling identNs

* feat(authn): add support for enabling identNs
2026-03-17 11:22:26 +00:00
Pandey
12b02a1002 feat(sqlschema): add support for partial unique indexes (#10604)
* feat(sqlschema): add support for partial unique indexes

* feat(sqlschema): add support for multiple indexes

* feat(sqlschema): add support for multiple indexes

* feat(sqlschema): move normalizer to its own struct

* feat(sqlschema): move normalizer tests to normalizer

* feat(sqlschema): move normalizer tests to normalizer

* feat(sqlschema): add more index tests from docs
2026-03-17 11:22:11 +00:00
Abhi Kumar
7233af57aa chore: minor cleanup 2026-03-17 12:45:50 +05:30
Abhi Kumar
30ce22511a Merge branch 'main' of https://github.com/SigNoz/signoz into feat/gaps-thresholds 2026-03-17 12:42:01 +05:30
Abhi Kumar
dd8d236647 Merge branch 'refactor/rightcontainer-breakup' of https://github.com/SigNoz/signoz into feat/gaps-thresholds 2026-03-13 02:29:56 +05:30
Abhi Kumar
68e4a2c5de chore: broke down rightcontainer component into sub-components 2026-03-13 02:27:22 +05:30
Abhi Kumar
4affdeda56 feat: added fill mode in timeseries 2026-03-13 00:37:25 +05:30
Abhi Kumar
99944cc1de feat: added chart appearance settings in panel 2026-03-12 21:25:58 +05:30
Abhi Kumar
d1bd36e88a chore: updated the categorisation 2026-03-12 19:33:54 +05:30
Abhi Kumar
d26d4ebd31 fix: minor style fixes 2026-03-12 18:44:20 +05:30
Abhi Kumar
771e5bd287 fix: fixed failing tests 2026-03-12 17:22:15 +05:30
Abhi Kumar
bd33304912 chore: minor changes 2026-03-12 17:18:19 +05:30
Abhi Kumar
ec35ef86cf feat: added changes for spangaps thresholds 2026-03-12 17:13:03 +05:30
Abhi Kumar
ca1cc0a4ac feat: added section in panel settings 2026-03-12 15:41:56 +05:30
93 changed files with 2586 additions and 1337 deletions

4
.github/CODEOWNERS vendored
View File

@@ -105,6 +105,10 @@ go.mod @therealpandey
/pkg/modules/authdomain/ @vikrantgupta25
/pkg/modules/role/ @vikrantgupta25
# IdentN Owners
/pkg/identn/ @vikrantgupta25
/pkg/http/middleware/identn.go @vikrantgupta25
# Integration tests
/tests/integration/ @vikrantgupta25

View File

@@ -102,13 +102,3 @@ jobs:
run: |
go run cmd/enterprise/*.go generate openapi
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in openapi spec. Run go run cmd/enterprise/*.go generate openapi locally and commit."; exit 1)
- name: node-install
uses: actions/setup-node@v5
with:
node-version: "22"
- name: install-frontend
run: cd frontend && yarn install
- name: generate-api-clients
run: |
cd frontend && yarn generate:api
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in generated api clients. Run yarn generate:api in frontend/ locally and commit."; exit 1)

View File

@@ -52,16 +52,16 @@ jobs:
with:
PRIMUS_REF: main
JS_SRC: frontend
md-languages:
languages:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
steps:
- name: checkout
- name: self-checkout
uses: actions/checkout@v4
- name: validate md languages
- name: run
run: bash frontend/scripts/validate-md-languages.sh
authz:
if: |
@@ -70,44 +70,55 @@ jobs:
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
steps:
- name: Checkout code
- name: self-checkout
uses: actions/checkout@v5
- name: Set up Node.js
- name: node-install
uses: actions/setup-node@v5
with:
node-version: "22"
- name: Install frontend dependencies
- name: deps-install
working-directory: ./frontend
run: |
yarn install
- name: Install uv
- name: uv-install
uses: astral-sh/setup-uv@v5
- name: Install Python dependencies
- name: uv-deps
working-directory: ./tests/integration
run: |
uv sync
- name: Start test environment
- name: setup-test
run: |
make py-test-setup
- name: Generate permissions.type.ts
- name: generate
working-directory: ./frontend
run: |
yarn generate:permissions-type
- name: Teardown test environment
- name: teardown-test
if: always()
run: |
make py-test-teardown
- name: Check for changes
- name: validate
run: |
if ! git diff --exit-code frontend/src/hooks/useAuthZ/permissions.type.ts; then
echo "::error::frontend/src/hooks/useAuthZ/permissions.type.ts is out of date. Please run the generator locally and commit the changes: npm run generate:permissions-type (from the frontend directory)"
exit 1
fi
openapi:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
steps:
- name: self-checkout
uses: actions/checkout@v4
- name: node-install
uses: actions/setup-node@v5
with:
node-version: "22"
- name: install-frontend
run: cd frontend && yarn install
- name: generate-api-clients
run: |
cd frontend && yarn generate:api
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in generated api clients. Run yarn generate:api in frontend/ locally and commit."; exit 1)

View File

@@ -12,14 +12,6 @@
"editor.formatOnSave": true,
"editor.defaultFormatter": "golang.go"
},
"go.buildTags": "chdb",
"go.testFlags": ["-tags=chdb"],
"go.toolsEnvVars": {
"GOFLAGS": "-tags=chdb"
},
"gopls": {
"build.buildFlags": ["-tags=chdb"]
},
"[sql]": {
"editor.defaultFormatter": "adpyke.vscode-sql-formatter"
},

View File

@@ -321,3 +321,19 @@ user:
org:
name: default
id: 00000000-0000-0000-0000-000000000000
##################### IdentN #####################
identn:
tokenizer:
# toggle the identN resolver
enabled: true
# headers to use for tokenizer identN resolver
headers:
- Authorization
- Sec-WebSocket-Protocol
apikey:
# toggle the identN resolver
enabled: true
# headers to use for apikey identN resolver
headers:
- SIGNOZ-API-KEY

View File

@@ -220,6 +220,13 @@ components:
- additions
- deletions
type: object
AuthtypesPatchableRole:
properties:
description:
type: string
required:
- description
type: object
AuthtypesPostableAuthDomain:
properties:
config:
@@ -236,6 +243,15 @@ components:
password:
type: string
type: object
AuthtypesPostableRole:
properties:
description:
type: string
name:
type: string
required:
- name
type: object
AuthtypesPostableRotateToken:
properties:
refreshToken:
@@ -251,6 +267,31 @@ components:
- name
- type
type: object
AuthtypesRole:
properties:
createdAt:
format: date-time
type: string
description:
type: string
id:
type: string
name:
type: string
orgId:
type: string
type:
type: string
updatedAt:
format: date-time
type: string
required:
- id
- name
- description
- type
- orgId
type: object
AuthtypesRoleMapping:
properties:
defaultRole:
@@ -1722,47 +1763,6 @@ components:
- status
- error
type: object
RoletypesPatchableRole:
properties:
description:
type: string
required:
- description
type: object
RoletypesPostableRole:
properties:
description:
type: string
name:
type: string
required:
- name
type: object
RoletypesRole:
properties:
createdAt:
format: date-time
type: string
description:
type: string
id:
type: string
name:
type: string
orgId:
type: string
type:
type: string
updatedAt:
format: date-time
type: string
required:
- id
- name
- description
- type
- orgId
type: object
ServiceaccounttypesFactorAPIKey:
properties:
createdAt:
@@ -4234,7 +4234,7 @@ paths:
properties:
data:
items:
$ref: '#/components/schemas/RoletypesRole'
$ref: '#/components/schemas/AuthtypesRole'
type: array
status:
type: string
@@ -4277,7 +4277,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/RoletypesPostableRole'
$ref: '#/components/schemas/AuthtypesPostableRole'
responses:
"201":
content:
@@ -4422,7 +4422,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/RoletypesRole'
$ref: '#/components/schemas/AuthtypesRole'
status:
type: string
required:
@@ -4470,7 +4470,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/RoletypesPatchableRole'
$ref: '#/components/schemas/AuthtypesPatchableRole'
responses:
"204":
content:

View File

@@ -13,7 +13,6 @@ import (
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
openfgapkgtransformer "github.com/openfga/language/pkg/go/transformer"
@@ -23,7 +22,7 @@ type provider struct {
pkgAuthzService authz.AuthZ
openfgaServer *openfgaserver.Server
licensing licensing.Licensing
store roletypes.Store
store authtypes.RoleStore
registry []authz.RegisterTypeable
}
@@ -82,23 +81,23 @@ func (provider *provider) Write(ctx context.Context, additions []*openfgav1.Tupl
return provider.openfgaServer.Write(ctx, additions, deletions)
}
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*roletypes.Role, error) {
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.Role, error) {
return provider.pkgAuthzService.Get(ctx, orgID, id)
}
func (provider *provider) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*roletypes.Role, error) {
func (provider *provider) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*authtypes.Role, error) {
return provider.pkgAuthzService.GetByOrgIDAndName(ctx, orgID, name)
}
func (provider *provider) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes.Role, error) {
func (provider *provider) List(ctx context.Context, orgID valuer.UUID) ([]*authtypes.Role, error) {
return provider.pkgAuthzService.List(ctx, orgID)
}
func (provider *provider) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*roletypes.Role, error) {
func (provider *provider) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*authtypes.Role, error) {
return provider.pkgAuthzService.ListByOrgIDAndNames(ctx, orgID, names)
}
func (provider *provider) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*roletypes.Role, error) {
func (provider *provider) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*authtypes.Role, error) {
return provider.pkgAuthzService.ListByOrgIDAndIDs(ctx, orgID, ids)
}
@@ -114,7 +113,7 @@ func (provider *provider) Revoke(ctx context.Context, orgID valuer.UUID, names [
return provider.pkgAuthzService.Revoke(ctx, orgID, names, subject)
}
func (provider *provider) CreateManagedRoles(ctx context.Context, orgID valuer.UUID, managedRoles []*roletypes.Role) error {
func (provider *provider) CreateManagedRoles(ctx context.Context, orgID valuer.UUID, managedRoles []*authtypes.Role) error {
return provider.pkgAuthzService.CreateManagedRoles(ctx, orgID, managedRoles)
}
@@ -136,16 +135,16 @@ func (provider *provider) CreateManagedUserRoleTransactions(ctx context.Context,
return provider.Write(ctx, tuples, nil)
}
func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) error {
func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) error {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
return provider.store.Create(ctx, roletypes.NewStorableRoleFromRole(role))
return provider.store.Create(ctx, authtypes.NewStorableRoleFromRole(role))
}
func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) (*roletypes.Role, error) {
func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) (*authtypes.Role, error) {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
@@ -159,10 +158,10 @@ func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, ro
}
if existingRole != nil {
return roletypes.NewRoleFromStorableRole(existingRole), nil
return authtypes.NewRoleFromStorableRole(existingRole), nil
}
err = provider.store.Create(ctx, roletypes.NewStorableRoleFromRole(role))
err = provider.store.Create(ctx, authtypes.NewStorableRoleFromRole(role))
if err != nil {
return nil, err
}
@@ -217,13 +216,13 @@ func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id
return objects, nil
}
func (provider *provider) Patch(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) error {
func (provider *provider) Patch(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) error {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
return provider.store.Update(ctx, orgID, roletypes.NewStorableRoleFromRole(role))
return provider.store.Update(ctx, orgID, authtypes.NewStorableRoleFromRole(role))
}
func (provider *provider) PatchObjects(ctx context.Context, orgID valuer.UUID, name string, relation authtypes.Relation, additions, deletions []*authtypes.Object) error {
@@ -232,12 +231,12 @@ func (provider *provider) PatchObjects(ctx context.Context, orgID valuer.UUID, n
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
additionTuples, err := roletypes.GetAdditionTuples(name, orgID, relation, additions)
additionTuples, err := authtypes.GetAdditionTuples(name, orgID, relation, additions)
if err != nil {
return err
}
deletionTuples, err := roletypes.GetDeletionTuples(name, orgID, relation, deletions)
deletionTuples, err := authtypes.GetDeletionTuples(name, orgID, relation, deletions)
if err != nil {
return err
}
@@ -261,7 +260,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
return err
}
role := roletypes.NewRoleFromStorableRole(storableRole)
role := authtypes.NewRoleFromStorableRole(storableRole)
err = role.ErrIfManaged()
if err != nil {
return err
@@ -271,7 +270,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
}
func (provider *provider) MustGetTypeables() []authtypes.Typeable {
return []authtypes.Typeable{authtypes.TypeableRole, roletypes.TypeableResourcesRoles}
return []authtypes.Typeable{authtypes.TypeableRole, authtypes.TypeableResourcesRoles}
}
func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID valuer.UUID) ([]*openfgav1.TupleKey, error) {
@@ -283,7 +282,7 @@ func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID va
adminSubject,
authtypes.RelationAssignee,
[]authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozAdminRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
},
orgID,
)
@@ -298,7 +297,7 @@ func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID va
anonymousSubject,
authtypes.RelationAssignee,
[]authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozAnonymousRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAnonymousRoleName),
},
orgID,
)

View File

@@ -19,7 +19,6 @@ import (
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
"github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -224,7 +223,7 @@ func (module *module) MustGetTypeables() []authtypes.Typeable {
func (module *module) MustGetManagedRoleTransactions() map[string][]*authtypes.Transaction {
return map[string][]*authtypes.Transaction{
roletypes.SigNozAnonymousRoleName: {
authtypes.SigNozAnonymousRoleName: {
{
ID: valuer.GenerateUUID(),
Relation: authtypes.RelationRead,

View File

@@ -80,6 +80,21 @@ func TestManager_TestNotification_SendUnmatched_ThresholdRule(t *testing.T) {
alertDataRows := cmock.NewRows(cols, tc.Values)
mock := telemetryStore.Mock()
// Mock metadata queries for FetchTemporalityAndTypeMulti
// First query: fetchMetricsTemporalityAndType (from signoz_metrics time series table)
metadataCols := []cmock.ColumnType{
{Name: "metric_name", Type: "String"},
{Name: "temporality", Type: "String"},
{Name: "type", Type: "String"},
{Name: "is_monotonic", Type: "Bool"},
}
metadataRows := cmock.NewRows(metadataCols, [][]any{
{"probe_success", metrictypes.Unspecified, metrictypes.GaugeType, false},
})
mock.ExpectQuery("*distributed_time_series_v4*").WithArgs(nil, nil, nil).WillReturnRows(metadataRows)
// Second query: fetchMeterSourceMetricsTemporalityAndType (from signoz_meter table)
emptyMetadataRows := cmock.NewRows(metadataCols, [][]any{})
mock.ExpectQuery("*meter*").WithArgs(nil).WillReturnRows(emptyMetadataRows)
// Generate query arguments for the metric query
evalTime := time.Now().UTC()

View File

@@ -223,7 +223,8 @@ SELECT
i.indisunique AS unique,
i.indisprimary AS primary,
a.attname AS column_name,
array_position(i.indkey, a.attnum) AS column_position
array_position(i.indkey, a.attnum) AS column_position,
pg_get_expr(i.indpred, i.indrelid) AS predicate
FROM
pg_index i
LEFT JOIN pg_class ct ON ct.oid = i.indrelid
@@ -246,7 +247,12 @@ ORDER BY index_name, column_position`, string(name))
}
}()
uniqueIndicesMap := make(map[string]*sqlschema.UniqueIndex)
type indexEntry struct {
columns []sqlschema.ColumnName
predicate *string
}
uniqueIndicesMap := make(map[string]*indexEntry)
for rows.Next() {
var (
tableName string
@@ -256,30 +262,50 @@ ORDER BY index_name, column_position`, string(name))
columnName string
// starts from 0 and is unused in this function, this is to ensure that the column names are in the correct order
columnPosition int
predicate *string
)
if err := rows.Scan(&tableName, &indexName, &unique, &primary, &columnName, &columnPosition); err != nil {
if err := rows.Scan(&tableName, &indexName, &unique, &primary, &columnName, &columnPosition, &predicate); err != nil {
return nil, err
}
if unique {
if _, ok := uniqueIndicesMap[indexName]; !ok {
uniqueIndicesMap[indexName] = &sqlschema.UniqueIndex{
TableName: name,
ColumnNames: []sqlschema.ColumnName{sqlschema.ColumnName(columnName)},
uniqueIndicesMap[indexName] = &indexEntry{
columns: []sqlschema.ColumnName{sqlschema.ColumnName(columnName)},
predicate: predicate,
}
} else {
uniqueIndicesMap[indexName].ColumnNames = append(uniqueIndicesMap[indexName].ColumnNames, sqlschema.ColumnName(columnName))
uniqueIndicesMap[indexName].columns = append(uniqueIndicesMap[indexName].columns, sqlschema.ColumnName(columnName))
}
}
}
indices := make([]sqlschema.Index, 0)
for indexName, index := range uniqueIndicesMap {
if index.Name() == indexName {
indices = append(indices, index)
for indexName, entry := range uniqueIndicesMap {
if entry.predicate != nil {
index := &sqlschema.PartialUniqueIndex{
TableName: name,
ColumnNames: entry.columns,
Where: *entry.predicate,
}
if index.Name() == indexName {
indices = append(indices, index)
} else {
indices = append(indices, index.Named(indexName))
}
} else {
indices = append(indices, index.Named(indexName))
index := &sqlschema.UniqueIndex{
TableName: name,
ColumnNames: entry.columns,
}
if index.Name() == indexName {
indices = append(indices, index)
} else {
indices = append(indices, index.Named(indexName))
}
}
}

View File

@@ -24,7 +24,8 @@ const config: Config.InitialOptions = {
'<rootDir>/node_modules/@signozhq/icons/dist/index.esm.js',
'^react-syntax-highlighter/dist/esm/(.*)$':
'<rootDir>/node_modules/react-syntax-highlighter/dist/cjs/$1',
'^@signozhq/([^/]+)$': '<rootDir>/node_modules/@signozhq/$1/dist/$1.js',
'^@signozhq/(?!ui$)([^/]+)$':
'<rootDir>/node_modules/@signozhq/$1/dist/$1.js',
},
extensionsToTreatAsEsm: ['.ts'],
testMatch: ['<rootDir>/src/**/*?(*.)(test).(ts|js)?(x)'],

View File

@@ -11,6 +11,7 @@
"prettify": "prettier --write .",
"fmt": "prettier --check .",
"lint": "eslint ./src",
"lint:generated": "eslint ./src/api/generated --fix",
"lint:fix": "eslint ./src --fix",
"jest": "jest",
"jest:coverage": "jest --coverage",
@@ -66,6 +67,7 @@
"@signozhq/table": "0.3.7",
"@signozhq/toggle-group": "0.0.1",
"@signozhq/tooltip": "0.0.2",
"@signozhq/ui": "0.0.4",
"@tanstack/react-table": "8.20.6",
"@tanstack/react-virtual": "3.11.2",
"@uiw/codemirror-theme-copilot": "4.23.11",
@@ -283,4 +285,4 @@
"tmp": "0.2.4",
"vite": "npm:rolldown-vite@7.3.1"
}
}
}

View File

@@ -25,7 +25,7 @@ echo "\n✅ Prettier formatting successful"
# Fix linting issues
echo "\n\n---\nRunning eslint...\n"
if ! yarn lint --fix --quiet src/api/generated; then
if ! yarn lint:generated; then
echo "ESLint check failed! Please fix linting errors before proceeding."
exit 1
fi

View File

@@ -21,6 +21,8 @@ import type { BodyType, ErrorType } from '../../../generatedAPIInstance';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type {
AuthtypesPatchableObjectsDTO,
AuthtypesPatchableRoleDTO,
AuthtypesPostableRoleDTO,
CreateRole201,
DeleteRolePathParameters,
GetObjects200,
@@ -31,8 +33,6 @@ import type {
PatchObjectsPathParameters,
PatchRolePathParameters,
RenderErrorResponseDTO,
RoletypesPatchableRoleDTO,
RoletypesPostableRoleDTO,
} from '../sigNoz.schemas';
/**
@@ -118,14 +118,14 @@ export const invalidateListRoles = async (
* @summary Create role
*/
export const createRole = (
roletypesPostableRoleDTO: BodyType<RoletypesPostableRoleDTO>,
authtypesPostableRoleDTO: BodyType<AuthtypesPostableRoleDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateRole201>({
url: `/api/v1/roles`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: roletypesPostableRoleDTO,
data: authtypesPostableRoleDTO,
signal,
});
};
@@ -137,13 +137,13 @@ export const getCreateRoleMutationOptions = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createRole>>,
TError,
{ data: BodyType<RoletypesPostableRoleDTO> },
{ data: BodyType<AuthtypesPostableRoleDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createRole>>,
TError,
{ data: BodyType<RoletypesPostableRoleDTO> },
{ data: BodyType<AuthtypesPostableRoleDTO> },
TContext
> => {
const mutationKey = ['createRole'];
@@ -157,7 +157,7 @@ export const getCreateRoleMutationOptions = <
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createRole>>,
{ data: BodyType<RoletypesPostableRoleDTO> }
{ data: BodyType<AuthtypesPostableRoleDTO> }
> = (props) => {
const { data } = props ?? {};
@@ -170,7 +170,7 @@ export const getCreateRoleMutationOptions = <
export type CreateRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof createRole>>
>;
export type CreateRoleMutationBody = BodyType<RoletypesPostableRoleDTO>;
export type CreateRoleMutationBody = BodyType<AuthtypesPostableRoleDTO>;
export type CreateRoleMutationError = ErrorType<RenderErrorResponseDTO>;
/**
@@ -183,13 +183,13 @@ export const useCreateRole = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createRole>>,
TError,
{ data: BodyType<RoletypesPostableRoleDTO> },
{ data: BodyType<AuthtypesPostableRoleDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createRole>>,
TError,
{ data: BodyType<RoletypesPostableRoleDTO> },
{ data: BodyType<AuthtypesPostableRoleDTO> },
TContext
> => {
const mutationOptions = getCreateRoleMutationOptions(options);
@@ -370,13 +370,13 @@ export const invalidateGetRole = async (
*/
export const patchRole = (
{ id }: PatchRolePathParameters,
roletypesPatchableRoleDTO: BodyType<RoletypesPatchableRoleDTO>,
authtypesPatchableRoleDTO: BodyType<AuthtypesPatchableRoleDTO>,
) => {
return GeneratedAPIInstance<string>({
url: `/api/v1/roles/${id}`,
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
data: roletypesPatchableRoleDTO,
data: authtypesPatchableRoleDTO,
});
};
@@ -389,7 +389,7 @@ export const getPatchRoleMutationOptions = <
TError,
{
pathParams: PatchRolePathParameters;
data: BodyType<RoletypesPatchableRoleDTO>;
data: BodyType<AuthtypesPatchableRoleDTO>;
},
TContext
>;
@@ -398,7 +398,7 @@ export const getPatchRoleMutationOptions = <
TError,
{
pathParams: PatchRolePathParameters;
data: BodyType<RoletypesPatchableRoleDTO>;
data: BodyType<AuthtypesPatchableRoleDTO>;
},
TContext
> => {
@@ -415,7 +415,7 @@ export const getPatchRoleMutationOptions = <
Awaited<ReturnType<typeof patchRole>>,
{
pathParams: PatchRolePathParameters;
data: BodyType<RoletypesPatchableRoleDTO>;
data: BodyType<AuthtypesPatchableRoleDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
@@ -429,7 +429,7 @@ export const getPatchRoleMutationOptions = <
export type PatchRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof patchRole>>
>;
export type PatchRoleMutationBody = BodyType<RoletypesPatchableRoleDTO>;
export type PatchRoleMutationBody = BodyType<AuthtypesPatchableRoleDTO>;
export type PatchRoleMutationError = ErrorType<RenderErrorResponseDTO>;
/**
@@ -444,7 +444,7 @@ export const usePatchRole = <
TError,
{
pathParams: PatchRolePathParameters;
data: BodyType<RoletypesPatchableRoleDTO>;
data: BodyType<AuthtypesPatchableRoleDTO>;
},
TContext
>;
@@ -453,7 +453,7 @@ export const usePatchRole = <
TError,
{
pathParams: PatchRolePathParameters;
data: BodyType<RoletypesPatchableRoleDTO>;
data: BodyType<AuthtypesPatchableRoleDTO>;
},
TContext
> => {

View File

@@ -278,6 +278,13 @@ export interface AuthtypesPatchableObjectsDTO {
deletions: AuthtypesGettableObjectsDTO[] | null;
}
export interface AuthtypesPatchableRoleDTO {
/**
* @type string
*/
description: string;
}
export interface AuthtypesPostableAuthDomainDTO {
config?: AuthtypesAuthDomainConfigDTO;
/**
@@ -301,6 +308,17 @@ export interface AuthtypesPostableEmailPasswordSessionDTO {
password?: string;
}
export interface AuthtypesPostableRoleDTO {
/**
* @type string
*/
description?: string;
/**
* @type string
*/
name: string;
}
export interface AuthtypesPostableRotateTokenDTO {
/**
* @type string
@@ -319,6 +337,39 @@ export interface AuthtypesResourceDTO {
type: string;
}
export interface AuthtypesRoleDTO {
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
description: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
*/
orgId: string;
/**
* @type string
*/
type: string;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
}
/**
* @nullable
*/
@@ -2039,57 +2090,6 @@ export interface RenderErrorResponseDTO {
status: string;
}
export interface RoletypesPatchableRoleDTO {
/**
* @type string
*/
description: string;
}
export interface RoletypesPostableRoleDTO {
/**
* @type string
*/
description?: string;
/**
* @type string
*/
name: string;
}
export interface RoletypesRoleDTO {
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
description: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
*/
orgId: string;
/**
* @type string
*/
type: string;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
}
export interface ServiceaccounttypesFactorAPIKeyDTO {
/**
* @type string
@@ -3163,7 +3163,7 @@ export type ListRoles200 = {
/**
* @type array
*/
data: RoletypesRoleDTO[];
data: AuthtypesRoleDTO[];
/**
* @type string
*/
@@ -3185,7 +3185,7 @@ export type GetRolePathParameters = {
id: string;
};
export type GetRole200 = {
data: RoletypesRoleDTO;
data: AuthtypesRoleDTO;
/**
* @type string
*/

View File

@@ -30,3 +30,4 @@ import '@signozhq/switch';
import '@signozhq/table';
import '@signozhq/toggle-group';
import '@signozhq/tooltip';
import '@signozhq/ui';

View File

@@ -123,7 +123,7 @@ export const prepareUPlotConfig = ({
drawStyle: hasSingleValidPoint ? DrawStyle.Points : DrawStyle.Line,
label: label,
colorMapping: widget.customLegendColors ?? {},
spanGaps: true,
spanGaps: widget.spanGaps ?? true,
lineStyle: widget.lineStyle || LineStyle.Solid,
lineInterpolation: widget.lineInterpolation || LineInterpolation.Spline,
showPoints:

View File

@@ -30,6 +30,17 @@
width: 100%;
}
.section-heading {
font-family: 'Space Mono';
color: var(--bg-vanilla-400);
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 138.462% */
letter-spacing: 0.52px;
text-transform: uppercase;
}
.header {
display: flex;
padding: 14px 14px 14px 12px;

View File

@@ -7,9 +7,10 @@ import {
} from 'lib/uPlotV2/config/types';
import { Paintbrush } from 'lucide-react';
import { FillModeSelector } from '../../components/FillModeSelector/FillModeSelector';
import { LineInterpolationSelector } from '../../components/LineInterpolationSelector/LineInterpolationSelector';
import { LineStyleSelector } from '../../components/LineStyleSelector/LineStyleSelector';
import DisconnectValuesSelector from '../../components/DisconnectValuesSelector/DisconnectValuesSelector';
import FillModeSelector from '../../components/FillModeSelector/FillModeSelector';
import LineInterpolationSelector from '../../components/LineInterpolationSelector/LineInterpolationSelector';
import LineStyleSelector from '../../components/LineStyleSelector/LineStyleSelector';
import SettingsSection from '../../components/SettingsSection/SettingsSection';
interface ChartAppearanceSectionProps {
@@ -21,10 +22,14 @@ interface ChartAppearanceSectionProps {
setLineInterpolation: Dispatch<SetStateAction<LineInterpolation>>;
showPoints: boolean;
setShowPoints: Dispatch<SetStateAction<boolean>>;
spanGaps: boolean | number;
setSpanGaps: Dispatch<SetStateAction<boolean | number>>;
allowFillMode: boolean;
allowLineStyle: boolean;
allowLineInterpolation: boolean;
allowShowPoints: boolean;
allowSpanGaps: boolean;
stepInterval: number;
}
export default function ChartAppearanceSection({
@@ -36,10 +41,14 @@ export default function ChartAppearanceSection({
setLineInterpolation,
showPoints,
setShowPoints,
spanGaps,
setSpanGaps,
allowFillMode,
allowLineStyle,
allowLineInterpolation,
allowShowPoints,
allowSpanGaps,
stepInterval,
}: ChartAppearanceSectionProps): JSX.Element {
return (
<SettingsSection title="Chart Appearance" icon={<Paintbrush size={14} />}>
@@ -66,6 +75,13 @@ export default function ChartAppearanceSection({
<Switch size="small" checked={showPoints} onChange={setShowPoints} />
</section>
)}
{allowSpanGaps && (
<DisconnectValuesSelector
value={spanGaps}
minValue={stepInterval}
onChange={setSpanGaps}
/>
)}
</SettingsSection>
);
}

View File

@@ -178,6 +178,8 @@ describe('RightContainer - Alerts Section', () => {
setLineStyle: jest.fn(),
showPoints: false,
setShowPoints: jest.fn(),
spanGaps: false,
setSpanGaps: jest.fn(),
};
beforeEach(() => {

View File

@@ -0,0 +1,44 @@
import { ToggleGroup, ToggleGroupItem } from '@signozhq/toggle-group';
import { Typography } from 'antd';
import { DisconnectedValuesMode } from 'lib/uPlotV2/config/types';
interface DisconnectValuesModeToggleProps {
value: DisconnectedValuesMode;
onChange: (value: DisconnectedValuesMode) => void;
}
export default function DisconnectValuesModeToggle({
value,
onChange,
}: DisconnectValuesModeToggleProps): JSX.Element {
return (
<ToggleGroup
type="single"
value={value}
variant="outline"
size="lg"
onValueChange={(newValue): void => {
if (newValue) {
onChange(newValue as DisconnectedValuesMode);
}
}}
>
<ToggleGroupItem
value={DisconnectedValuesMode.Never}
aria-label="Never"
title="Never"
>
<Typography.Text className="section-heading-small">Never</Typography.Text>
</ToggleGroupItem>
<ToggleGroupItem
value={DisconnectedValuesMode.Threshold}
aria-label="Threshold"
title="Threshold"
>
<Typography.Text className="section-heading-small">
Threshold
</Typography.Text>
</ToggleGroupItem>
</ToggleGroup>
);
}

View File

@@ -0,0 +1,21 @@
.disconnect-values-selector {
.disconnect-values-input-wrapper {
display: flex;
flex-direction: column;
gap: 16px;
.disconnect-values-threshold-wrapper {
display: flex;
flex-direction: column;
gap: 8px;
.disconnect-values-threshold-input {
max-width: 160px;
height: auto;
.disconnect-values-threshold-prefix {
padding: 0 8px;
font-size: 20px;
}
}
}
}
}

View File

@@ -0,0 +1,91 @@
import { useEffect, useState } from 'react';
import { Typography } from 'antd';
import { DisconnectedValuesMode } from 'lib/uPlotV2/config/types';
import DisconnectValuesModeToggle from './DisconnectValuesModeToggle';
import DisconnectValuesThresholdInput from './DisconnectValuesThresholdInput';
import './DisconnectValuesSelector.styles.scss';
const DEFAULT_THRESHOLD_SECONDS = 60;
interface DisconnectValuesSelectorProps {
value: boolean | number;
minValue?: number;
onChange: (value: boolean | number) => void;
}
export default function DisconnectValuesSelector({
value,
minValue,
onChange,
}: DisconnectValuesSelectorProps): JSX.Element {
const [mode, setMode] = useState<DisconnectedValuesMode>(() => {
if (typeof value === 'number') {
return DisconnectedValuesMode.Threshold;
}
return DisconnectedValuesMode.Never;
});
const [thresholdSeconds, setThresholdSeconds] = useState<number>(
typeof value === 'number' ? value : minValue ?? DEFAULT_THRESHOLD_SECONDS,
);
useEffect(() => {
if (typeof value === 'boolean') {
setMode(DisconnectedValuesMode.Never);
} else if (typeof value === 'number') {
setMode(DisconnectedValuesMode.Threshold);
setThresholdSeconds(value);
}
}, [value]);
useEffect(() => {
if (minValue !== undefined) {
setThresholdSeconds(minValue);
if (mode === DisconnectedValuesMode.Threshold) {
onChange(minValue);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [minValue]);
const handleModeChange = (newMode: DisconnectedValuesMode): void => {
setMode(newMode);
switch (newMode) {
case DisconnectedValuesMode.Never:
onChange(true);
break;
case DisconnectedValuesMode.Threshold:
onChange(thresholdSeconds);
break;
}
};
const handleThresholdChange = (seconds: number): void => {
setThresholdSeconds(seconds);
onChange(seconds);
};
return (
<section className="disconnect-values-selector control-container">
<Typography.Text className="section-heading">
Disconnect values
</Typography.Text>
<div className="disconnect-values-input-wrapper">
<DisconnectValuesModeToggle value={mode} onChange={handleModeChange} />
{mode === DisconnectedValuesMode.Threshold && (
<section className="control-container">
<Typography.Text className="section-heading">
Threshold Value
</Typography.Text>
<DisconnectValuesThresholdInput
value={thresholdSeconds}
minValue={minValue}
onChange={handleThresholdChange}
/>
</section>
)}
</div>
</section>
);
}

View File

@@ -0,0 +1,87 @@
import { useEffect, useState } from 'react';
import { rangeUtil } from '@grafana/data';
import { Callout } from '@signozhq/callout';
import { Input } from '@signozhq/ui';
interface DisconnectValuesThresholdInputProps {
value: number;
onChange: (seconds: number) => void;
minValue?: number;
}
export default function DisconnectValuesThresholdInput({
value,
onChange,
minValue,
}: DisconnectValuesThresholdInputProps): JSX.Element {
const [inputValue, setInputValue] = useState<string>(
rangeUtil.secondsToHms(value),
);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
setInputValue(rangeUtil.secondsToHms(value));
setError(null);
}, [value]);
const commitValue = (txt: string): void => {
if (!txt) {
return;
}
try {
let seconds: number;
if (rangeUtil.isValidTimeSpan(txt)) {
seconds = rangeUtil.intervalToSeconds(txt);
} else {
const parsed = Number(txt);
if (Number.isNaN(parsed) || parsed <= 0) {
setError('Enter a valid duration (e.g. 1h, 10m, 1d)');
return;
}
seconds = parsed;
}
if (minValue !== undefined && seconds <= minValue) {
setError(`Threshold should be > ${rangeUtil.secondsToHms(minValue)}`);
return;
}
setError(null);
setInputValue(txt);
onChange(seconds);
} catch {
setError('Invalid threshold value');
}
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
if (e.key === 'Enter') {
commitValue(e.currentTarget.value);
}
};
const handleBlur = (e: React.FocusEvent<HTMLInputElement>): void => {
commitValue(e.currentTarget.value);
};
return (
<div className="disconnect-values-threshold-wrapper">
<Input
name="disconnect-values-threshold"
type="text"
className="disconnect-values-threshold-input"
prefix={<span className="disconnect-values-threshold-prefix">&gt;</span>}
value={inputValue}
onChange={(e): void => {
setInputValue(e.currentTarget.value);
if (error) {
setError(null);
}
}}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
autoFocus={false}
aria-invalid={!!error}
aria-describedby={error ? 'threshold-error' : undefined}
/>
{error && <Callout type="error" size="small" showIcon description={error} />}
</div>
);
}

View File

@@ -9,7 +9,7 @@ interface FillModeSelectorProps {
onChange: (value: FillMode) => void;
}
export function FillModeSelector({
export default function FillModeSelector({
value,
onChange,
}: FillModeSelectorProps): JSX.Element {

View File

@@ -9,7 +9,7 @@ interface LineInterpolationSelectorProps {
onChange: (value: LineInterpolation) => void;
}
export function LineInterpolationSelector({
export default function LineInterpolationSelector({
value,
onChange,
}: LineInterpolationSelectorProps): JSX.Element {

View File

@@ -9,7 +9,7 @@ interface LineStyleSelectorProps {
onChange: (value: LineStyle) => void;
}
export function LineStyleSelector({
export default function LineStyleSelector({
value,
onChange,
}: LineStyleSelectorProps): JSX.Element {

View File

@@ -262,3 +262,17 @@ export const panelTypeVsShowPoints: {
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
} as const;
export const panelTypeVsSpanGaps: {
[key in PANEL_TYPES]: boolean;
} = {
[PANEL_TYPES.TIME_SERIES]: true,
[PANEL_TYPES.VALUE]: false,
[PANEL_TYPES.TABLE]: false,
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: false,
[PANEL_TYPES.BAR]: false,
[PANEL_TYPES.HISTOGRAM]: false,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
} as const;

View File

@@ -1,6 +1,7 @@
import { Dispatch, SetStateAction, useMemo } from 'react';
import { UseQueryResult } from 'react-query';
import { Typography } from 'antd';
import { ExecStats } from 'api/v5/v5';
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
import { PANEL_TYPES, PanelDisplay } from 'constants/queryBuilder';
import { PanelTypesWithData } from 'container/DashboardContainer/PanelTypeSelectionModal/menuItems';
@@ -11,6 +12,7 @@ import {
LineInterpolation,
LineStyle,
} from 'lib/uPlotV2/config/types';
import get from 'lodash-es/get';
import { SuccessResponse } from 'types/api';
import {
ColumnUnit,
@@ -36,6 +38,7 @@ import {
panelTypeVsPanelTimePreferences,
panelTypeVsShowPoints,
panelTypeVsSoftMinMax,
panelTypeVsSpanGaps,
panelTypeVsStackingChartPreferences,
panelTypeVsThreshold,
panelTypeVsYAxisUnit,
@@ -68,6 +71,8 @@ function RightContainer({
setLineStyle,
showPoints,
setShowPoints,
spanGaps,
setSpanGaps,
bucketCount,
bucketWidth,
stackedBarChart,
@@ -138,6 +143,7 @@ function RightContainer({
const allowLineStyle = panelTypeVsLineStyle[selectedGraph];
const allowFillMode = panelTypeVsFillMode[selectedGraph];
const allowShowPoints = panelTypeVsShowPoints[selectedGraph];
const allowSpanGaps = panelTypeVsSpanGaps[selectedGraph];
const decimapPrecisionOptions = useMemo(
() => [
@@ -176,10 +182,26 @@ function RightContainer({
(allowFillMode ||
allowLineStyle ||
allowLineInterpolation ||
allowShowPoints),
[allowFillMode, allowLineStyle, allowLineInterpolation, allowShowPoints],
allowShowPoints ||
allowSpanGaps),
[
allowFillMode,
allowLineStyle,
allowLineInterpolation,
allowShowPoints,
allowSpanGaps,
],
);
const stepInterval = useMemo(() => {
const stepIntervals: ExecStats['stepIntervals'] = get(
queryResponse,
'data.payload.data.newResult.meta.stepIntervals',
{},
);
return Math.min(...Object.values(stepIntervals));
}, [queryResponse]);
return (
<div className="right-container">
<section className="header">
@@ -237,10 +259,14 @@ function RightContainer({
setLineInterpolation={setLineInterpolation}
showPoints={showPoints}
setShowPoints={setShowPoints}
spanGaps={spanGaps}
setSpanGaps={setSpanGaps}
allowFillMode={allowFillMode}
allowLineStyle={allowLineStyle}
allowLineInterpolation={allowLineInterpolation}
allowShowPoints={allowShowPoints}
allowSpanGaps={allowSpanGaps}
stepInterval={stepInterval}
/>
)}
@@ -364,6 +390,8 @@ export interface RightContainerProps {
setLineStyle: Dispatch<SetStateAction<LineStyle>>;
showPoints: boolean;
setShowPoints: Dispatch<SetStateAction<boolean>>;
spanGaps: boolean | number;
setSpanGaps: Dispatch<SetStateAction<boolean | number>>;
}
RightContainer.defaultProps = {

View File

@@ -220,6 +220,9 @@ function NewWidget({
const [showPoints, setShowPoints] = useState<boolean>(
selectedWidget?.showPoints ?? false,
);
const [spanGaps, setSpanGaps] = useState<boolean | number>(
selectedWidget?.spanGaps ?? false,
);
const [customLegendColors, setCustomLegendColors] = useState<
Record<string, string>
>(selectedWidget?.customLegendColors || {});
@@ -289,6 +292,7 @@ function NewWidget({
fillMode,
lineStyle,
showPoints,
spanGaps,
columnUnits,
bucketCount,
stackedBarChart,
@@ -328,6 +332,7 @@ function NewWidget({
fillMode,
lineStyle,
showPoints,
spanGaps,
customLegendColors,
contextLinks,
selectedWidget.columnWidths,
@@ -541,6 +546,7 @@ function NewWidget({
softMin: selectedWidget?.softMin || 0,
softMax: selectedWidget?.softMax || 0,
fillSpans: selectedWidget?.fillSpans,
spanGaps: selectedWidget?.spanGaps ?? true,
isLogScale: selectedWidget?.isLogScale || false,
bucketWidth: selectedWidget?.bucketWidth || 0,
bucketCount: selectedWidget?.bucketCount || 0,
@@ -572,6 +578,7 @@ function NewWidget({
softMin: selectedWidget?.softMin || 0,
softMax: selectedWidget?.softMax || 0,
fillSpans: selectedWidget?.fillSpans,
spanGaps: selectedWidget?.spanGaps ?? true,
isLogScale: selectedWidget?.isLogScale || false,
bucketWidth: selectedWidget?.bucketWidth || 0,
bucketCount: selectedWidget?.bucketCount || 0,
@@ -889,6 +896,8 @@ function NewWidget({
setLineStyle={setLineStyle}
showPoints={showPoints}
setShowPoints={setShowPoints}
spanGaps={spanGaps}
setSpanGaps={setSpanGaps}
opacity={opacity}
yAxisUnit={yAxisUnit}
columnUnits={columnUnits}

View File

@@ -13,8 +13,8 @@ import {
usePatchRole,
} from 'api/generated/services/role';
import {
AuthtypesPostableRoleDTO,
RenderErrorResponseDTO,
RoletypesPostableRoleDTO,
} from 'api/generated/services/sigNoz.schemas';
import { ErrorType } from 'api/generatedAPIInstance';
import ROUTES from 'constants/routes';
@@ -114,7 +114,7 @@ function CreateRoleModal({
data: { description: values.description || '' },
});
} else {
const data: RoletypesPostableRoleDTO = {
const data: AuthtypesPostableRoleDTO = {
name: values.name,
...(values.description ? { description: values.description } : {}),
};

View File

@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo } from 'react';
import { useHistory } from 'react-router-dom';
import { Pagination, Skeleton } from 'antd';
import { useListRoles } from 'api/generated/services/role';
import { RoletypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
import { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import ROUTES from 'constants/routes';
@@ -20,7 +20,7 @@ const PAGE_SIZE = 20;
type DisplayItem =
| { type: 'section'; label: string; count?: number }
| { type: 'role'; role: RoletypesRoleDTO };
| { type: 'role'; role: AuthtypesRoleDTO };
interface RolesListingTableProps {
searchQuery: string;
@@ -187,7 +187,7 @@ function RolesListingTable({
};
// todo: use table from periscope when its available for consumption
const renderRow = (role: RoletypesRoleDTO): JSX.Element => (
const renderRow = (role: AuthtypesRoleDTO): JSX.Element => (
<div
key={role.id}
className={`roles-table-row ${

View File

@@ -7,6 +7,7 @@ import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFall
import uPlot, { AlignedData, Options } from 'uplot';
import { usePlotContext } from '../context/PlotContext';
import { applySpanGapsToAlignedData } from '../utils/dataUtils';
import { UPlotChartProps } from './types';
/**
@@ -84,7 +85,13 @@ export default function UPlotChart({
} as Options;
// Create new plot instance
const plot = new uPlot(plotConfig, data as AlignedData, containerRef.current);
const seriesSpanGaps = config.getSeriesSpanGapsOptions();
const preparedData =
seriesSpanGaps.length > 0
? applySpanGapsToAlignedData(data, seriesSpanGaps)
: (data as AlignedData);
const plot = new uPlot(plotConfig, preparedData, containerRef.current);
if (plotRef) {
plotRef(plot);
@@ -162,7 +169,13 @@ export default function UPlotChart({
}
// Update data if only data changed
else if (!sameData(prevProps, currentProps) && plotInstanceRef.current) {
plotInstanceRef.current.setData(data as AlignedData);
const seriesSpanGaps = config.getSeriesSpanGapsOptions?.() ?? [];
const preparedData =
seriesSpanGaps.length > 0
? applySpanGapsToAlignedData(data as AlignedData, seriesSpanGaps)
: (data as AlignedData);
plotInstanceRef.current.setData(preparedData as AlignedData);
}
prevPropsRef.current = currentProps;

View File

@@ -86,6 +86,7 @@ const createMockConfig = (): UPlotConfigBuilder => {
}),
getId: jest.fn().mockReturnValue(undefined),
getShouldSaveSelectionPreference: jest.fn().mockReturnValue(false),
getSeriesSpanGapsOptions: jest.fn().mockReturnValue([]),
} as unknown) as UPlotConfigBuilder;
};
@@ -328,6 +329,78 @@ describe('UPlotChart', () => {
});
});
describe('spanGaps data transformation', () => {
it('inserts null break points before passing data to uPlot when a gap exceeds the numeric threshold', () => {
const config = createMockConfig();
// gap 0→100 = 100 > threshold 50 → null inserted at midpoint x=50
(config.getSeriesSpanGapsOptions as jest.Mock).mockReturnValue([
{ spanGaps: 50 },
]);
const data: AlignedData = [
[0, 100],
[1, 2],
];
render(<UPlotChart config={config} data={data} width={600} height={400} />, {
wrapper: Wrapper,
});
const [, receivedData] = mockUPlotConstructor.mock.calls[0];
expect(receivedData[0]).toEqual([0, 50, 100]);
expect(receivedData[1]).toEqual([1, null, 2]);
});
it('passes data through unchanged when no gap exceeds the numeric threshold', () => {
const config = createMockConfig();
// all gaps = 10, threshold = 50 → no insertions, same reference returned
(config.getSeriesSpanGapsOptions as jest.Mock).mockReturnValue([
{ spanGaps: 50 },
]);
const data: AlignedData = [
[0, 10, 20],
[1, 2, 3],
];
render(<UPlotChart config={config} data={data} width={600} height={400} />, {
wrapper: Wrapper,
});
const [, receivedData] = mockUPlotConstructor.mock.calls[0];
expect(receivedData).toBe(data);
});
it('transforms data passed to setData when data updates and a new gap exceeds the threshold', () => {
const config = createMockConfig();
(config.getSeriesSpanGapsOptions as jest.Mock).mockReturnValue([
{ spanGaps: 50 },
]);
// initial render: gap 10 < 50, no transformation
const initialData: AlignedData = [
[0, 10],
[1, 2],
];
// updated data: gap 100 > 50 → null inserted at midpoint x=50
const newData: AlignedData = [
[0, 100],
[3, 4],
];
const { rerender } = render(
<UPlotChart config={config} data={initialData} width={600} height={400} />,
{ wrapper: Wrapper },
);
rerender(
<UPlotChart config={config} data={newData} width={600} height={400} />,
);
const receivedData = instances[0].setData.mock.calls[0][0];
expect(receivedData[0]).toEqual([0, 50, 100]);
expect(receivedData[1]).toEqual([3, null, 4]);
});
});
describe('prop updates', () => {
it('calls setData without recreating the plot when only data changes', () => {
const config = createMockConfig();

View File

@@ -14,6 +14,7 @@ import {
STEP_INTERVAL_MULTIPLIER,
} from '../constants';
import { calculateWidthBasedOnStepInterval } from '../utils';
import { SeriesSpanGapsOption } from '../utils/dataUtils';
import {
ConfigBuilder,
ConfigBuilderProps,
@@ -161,6 +162,13 @@ export class UPlotConfigBuilder extends ConfigBuilder<
this.series.push(new UPlotSeriesBuilder(props));
}
getSeriesSpanGapsOptions(): SeriesSpanGapsOption[] {
return this.series.map((s) => {
const { spanGaps } = s.props;
return { spanGaps };
});
}
/**
* Add a hook for extensibility
*/

View File

@@ -212,7 +212,12 @@ export class UPlotSeriesBuilder extends ConfigBuilder<SeriesProps, Series> {
return {
scale: scaleKey,
label,
spanGaps: typeof spanGaps === 'boolean' ? spanGaps : false,
// When spanGaps is numeric, we always disable uPlot's internal
// spanGaps behavior and rely on data-prep to implement the
// threshold-based null handling. When spanGaps is boolean we
// map it directly. When spanGaps is undefined we fall back to
// the default of false.
spanGaps: typeof spanGaps === 'number' ? false : !!spanGaps,
value: (): string => '',
pxAlign: true,
show,

View File

@@ -40,6 +40,37 @@ describe('UPlotSeriesBuilder', () => {
expect(typeof config.value).toBe('function');
});
it('maps boolean spanGaps directly to uPlot spanGaps', () => {
const trueBuilder = new UPlotSeriesBuilder(
createBaseProps({
spanGaps: true,
}),
);
const falseBuilder = new UPlotSeriesBuilder(
createBaseProps({
spanGaps: false,
}),
);
const trueConfig = trueBuilder.getConfig();
const falseConfig = falseBuilder.getConfig();
expect(trueConfig.spanGaps).toBe(true);
expect(falseConfig.spanGaps).toBe(false);
});
it('disables uPlot spanGaps when spanGaps is a number', () => {
const builder = new UPlotSeriesBuilder(
createBaseProps({
spanGaps: 10000,
}),
);
const config = builder.getConfig();
expect(config.spanGaps).toBe(false);
});
it('uses explicit lineColor when provided, regardless of mapping', () => {
const builder = new UPlotSeriesBuilder(
createBaseProps({

View File

@@ -99,6 +99,11 @@ export interface ScaleProps {
distribution?: DistributionType;
}
export enum DisconnectedValuesMode {
Never = 'never',
Threshold = 'threshold',
}
/**
* Props for configuring a series
*/
@@ -175,7 +180,16 @@ export interface SeriesProps extends LineConfig, PointsConfig, BarConfig {
pointsFilter?: Series.Points.Filter;
pointsBuilder?: Series.Points.Show;
show?: boolean;
spanGaps?: boolean;
/**
* Controls how nulls are treated for this series.
*
* - boolean: mapped directly to uPlot's spanGaps behavior
* - number: interpreted as an X-axis threshold (same unit as ref values),
* where gaps smaller than this threshold are spanned by
* converting short null runs to undefined during data prep
* while uPlot's internal spanGaps is kept disabled.
*/
spanGaps?: boolean | number;
fillColor?: string;
fillMode?: FillMode;
isDarkMode?: boolean;

View File

@@ -1,4 +1,12 @@
import { isInvalidPlotValue, normalizePlotValue } from '../dataUtils';
import uPlot from 'uplot';
import {
applySpanGapsToAlignedData,
insertLargeGapNullsIntoAlignedData,
isInvalidPlotValue,
normalizePlotValue,
SeriesSpanGapsOption,
} from '../dataUtils';
describe('dataUtils', () => {
describe('isInvalidPlotValue', () => {
@@ -59,4 +67,217 @@ describe('dataUtils', () => {
expect(normalizePlotValue(42.5)).toBe(42.5);
});
});
describe('insertLargeGapNullsIntoAlignedData', () => {
it('returns original data unchanged when no gap exceeds the threshold', () => {
// all gaps = 10, threshold = 25 → no insertions
const data: uPlot.AlignedData = [
[0, 10, 20, 30],
[1, 2, 3, 4],
];
const options: SeriesSpanGapsOption[] = [{ spanGaps: 25 }];
const result = insertLargeGapNullsIntoAlignedData(data, options);
expect(result).toBe(data);
});
it('does not insert when the gap equals the threshold exactly', () => {
// gap = 50, threshold = 50 → condition is gap > threshold, not >=
const data: uPlot.AlignedData = [
[0, 50],
[1, 2],
];
const options: SeriesSpanGapsOption[] = [{ spanGaps: 50 }];
const result = insertLargeGapNullsIntoAlignedData(data, options);
expect(result).toBe(data);
});
it('inserts a null at the midpoint when a single gap exceeds the threshold', () => {
// gap 0→100 = 100 > 50 → insert null at x=50
const data: uPlot.AlignedData = [
[0, 100],
[1, 2],
];
const options: SeriesSpanGapsOption[] = [{ spanGaps: 50 }];
const result = insertLargeGapNullsIntoAlignedData(data, options);
expect(result[0]).toEqual([0, 50, 100]);
expect(result[1]).toEqual([1, null, 2]);
});
it('inserts nulls at every gap that exceeds the threshold', () => {
// gaps: 0→100=100, 100→110=10, 110→210=100; threshold=50
// → insert at 0→100 and 110→210
const data: uPlot.AlignedData = [
[0, 100, 110, 210],
[1, 2, 3, 4],
];
const options: SeriesSpanGapsOption[] = [{ spanGaps: 50 }];
const result = insertLargeGapNullsIntoAlignedData(data, options);
expect(result[0]).toEqual([0, 50, 100, 110, 160, 210]);
expect(result[1]).toEqual([1, null, 2, 3, null, 4]);
});
it('inserts null for all series at a gap triggered by any one series', () => {
// series 0: threshold=50, gap=100 → triggers insertion
// series 1: threshold=200, gap=100 → would not trigger alone
// result: both series get null at the inserted x because the x-axis is shared
const data: uPlot.AlignedData = [
[0, 100],
[1, 2],
[3, 4],
];
const options: SeriesSpanGapsOption[] = [
{ spanGaps: 50 },
{ spanGaps: 200 },
];
const result = insertLargeGapNullsIntoAlignedData(data, options);
expect(result[0]).toEqual([0, 50, 100]);
expect(result[1]).toEqual([1, null, 2]);
expect(result[2]).toEqual([3, null, 4]);
});
it('ignores boolean spanGaps options (only numeric values trigger insertion)', () => {
const data: uPlot.AlignedData = [
[0, 100],
[1, 2],
];
const options: SeriesSpanGapsOption[] = [{ spanGaps: true }];
const result = insertLargeGapNullsIntoAlignedData(data, options);
expect(result).toBe(data);
});
it('returns original data when series options array is empty', () => {
const data: uPlot.AlignedData = [
[0, 100],
[1, 2],
];
const result = insertLargeGapNullsIntoAlignedData(data, []);
expect(result).toBe(data);
});
it('returns original data when there is only one x point', () => {
const data: uPlot.AlignedData = [[0], [1]];
const options: SeriesSpanGapsOption[] = [{ spanGaps: 10 }];
const result = insertLargeGapNullsIntoAlignedData(data, options);
expect(result).toBe(data);
});
it('preserves existing null values in the series alongside inserted ones', () => {
// original series already has a null; gap 0→100 also triggers insertion
const data: uPlot.AlignedData = [
[0, 100, 110],
[1, null, 2],
];
const options: SeriesSpanGapsOption[] = [{ spanGaps: 50 }];
const result = insertLargeGapNullsIntoAlignedData(data, options);
expect(result[0]).toEqual([0, 50, 100, 110]);
expect(result[1]).toEqual([1, null, null, 2]);
});
});
describe('applySpanGapsToAlignedData', () => {
const xs: uPlot.AlignedData[0] = [0, 10, 20, 30];
it('returns original data when there are no series', () => {
const data: uPlot.AlignedData = [xs];
const result = applySpanGapsToAlignedData(data, []);
expect(result).toBe(data);
});
it('leaves data unchanged when spanGaps is undefined', () => {
const ys = [1, null, 2, null];
const data: uPlot.AlignedData = [xs, ys];
const options: SeriesSpanGapsOption[] = [{}];
const result = applySpanGapsToAlignedData(data, options);
expect(result[1]).toEqual(ys);
});
it('converts nulls to undefined when spanGaps is true', () => {
const ys = [1, null, 2, null];
const data: uPlot.AlignedData = [xs, ys];
const options: SeriesSpanGapsOption[] = [{ spanGaps: true }];
const result = applySpanGapsToAlignedData(data, options);
expect(result[1]).toEqual([1, undefined, 2, undefined]);
});
it('leaves data unchanged when spanGaps is false', () => {
const ys = [1, null, 2, null];
const data: uPlot.AlignedData = [xs, ys];
const options: SeriesSpanGapsOption[] = [{ spanGaps: false }];
const result = applySpanGapsToAlignedData(data, options);
expect(result[1]).toEqual(ys);
});
it('inserts a null break point when a gap exceeds the numeric threshold', () => {
// gap 0→100 = 100 > 50 → null inserted at midpoint x=50
const data: uPlot.AlignedData = [
[0, 100, 110],
[1, 2, 3],
];
const options: SeriesSpanGapsOption[] = [{ spanGaps: 50 }];
const result = applySpanGapsToAlignedData(data, options);
expect(result[0]).toEqual([0, 50, 100, 110]);
expect(result[1]).toEqual([1, null, 2, 3]);
});
it('returns original data when no gap exceeds the numeric threshold', () => {
// all gaps = 10, threshold = 25 → no insertions
const data: uPlot.AlignedData = [xs, [1, 2, 3, 4]];
const options: SeriesSpanGapsOption[] = [{ spanGaps: 25 }];
const result = applySpanGapsToAlignedData(data, options);
expect(result).toBe(data);
});
it('applies both numeric gap insertion and boolean null-to-undefined in one pass', () => {
// series 0: spanGaps: 50 → gap 0→100 triggers a null break at midpoint x=50
// series 1: spanGaps: true → the inserted null at x=50 becomes undefined,
// so the line spans over it rather than breaking
const data: uPlot.AlignedData = [
[0, 100],
[1, 2],
[3, 4],
];
const options: SeriesSpanGapsOption[] = [
{ spanGaps: 50 },
{ spanGaps: true },
];
const result = applySpanGapsToAlignedData(data, options);
// x-axis extended with the inserted midpoint
expect(result[0]).toEqual([0, 50, 100]);
// series 0: null at midpoint breaks the line
expect(result[1]).toEqual([1, null, 2]);
// series 1: null at midpoint converted to undefined → line spans over it
expect(result[2]).toEqual([3, undefined, 4]);
});
});
});

View File

@@ -51,3 +51,163 @@ export function normalizePlotValue(
// Already a valid number
return value as number;
}
export interface SeriesSpanGapsOption {
spanGaps?: boolean | number;
}
// Internal type alias: a series value array that may contain nulls/undefineds.
// uPlot uses null to draw a visible gap and undefined to represent "no sample"
// (the line continues across undefined points but breaks at null ones).
type SeriesArray = Array<number | null | undefined>;
/**
* Returns true if the given gap size exceeds the numeric spanGaps threshold
* of at least one series. Used to decide whether to insert a null break point.
*/
function gapExceedsThreshold(
gapSize: number,
seriesOptions: SeriesSpanGapsOption[],
): boolean {
return seriesOptions.some(
({ spanGaps }) =>
typeof spanGaps === 'number' && spanGaps > 0 && gapSize > spanGaps,
);
}
/**
* For each series with a numeric spanGaps threshold, insert a null data point
* between consecutive x timestamps whose gap exceeds the threshold.
*
* Why: uPlot draws a continuous line between all non-null points. When the
* time gap between two consecutive samples is larger than the configured
* spanGaps value, we inject a synthetic null at the midpoint so uPlot renders
* a visible break instead of a misleading straight line across the gap.
*
* Because uPlot's AlignedData shares a single x-axis across all series, a null
* is inserted for every series at each position where any series needs a break.
*
* Two-pass approach for performance:
* Pass 1 — count how many nulls will be inserted (no allocations).
* Pass 2 — fill pre-allocated output arrays by index (no push/reallocation).
*/
export function insertLargeGapNullsIntoAlignedData(
data: uPlot.AlignedData,
seriesOptions: SeriesSpanGapsOption[],
): uPlot.AlignedData {
const [xValues, ...seriesValues] = data;
if (
!Array.isArray(xValues) ||
xValues.length < 2 ||
seriesValues.length === 0
) {
return data;
}
const xs = xValues as number[];
const n = xs.length;
// Pass 1: count insertions needed so we know the exact output length.
// This lets us pre-allocate arrays rather than growing them dynamically.
let insertionCount = 0;
for (let i = 0; i < n - 1; i += 1) {
if (gapExceedsThreshold(xs[i + 1] - xs[i], seriesOptions)) {
insertionCount += 1;
}
}
// No gaps exceed any threshold — return the original data unchanged.
if (insertionCount === 0) {
return data;
}
// Pass 2: build output arrays of exact size and fill them.
// `out` is the write cursor into the output arrays.
const outputLen = n + insertionCount;
const newX = new Array<number>(outputLen);
const newSeries: SeriesArray[] = seriesValues.map(
() => new Array<number | null | undefined>(outputLen),
);
let out = 0;
for (let i = 0; i < n; i += 1) {
// Copy the real data point at position i
newX[out] = xs[i];
for (let s = 0; s < seriesValues.length; s += 1) {
newSeries[s][out] = (seriesValues[s] as SeriesArray)[i];
}
out += 1;
// If the gap to the next x timestamp exceeds the threshold, insert a
// synthetic null at the midpoint. The midpoint x is placed halfway
// between xs[i] and xs[i+1] (minimum 1 unit past xs[i] to stay unique).
if (i < n - 1 && gapExceedsThreshold(xs[i + 1] - xs[i], seriesOptions)) {
newX[out] = xs[i] + Math.max(1, Math.floor((xs[i + 1] - xs[i]) / 2));
for (let s = 0; s < seriesValues.length; s += 1) {
newSeries[s][out] = null; // null tells uPlot to break the line here
}
out += 1;
}
}
return [newX, ...newSeries] as uPlot.AlignedData;
}
/**
* Apply per-series spanGaps (boolean | number) handling to an aligned dataset.
*
* spanGaps controls how uPlot handles gaps in a series:
* - boolean true → convert null → undefined so uPlot spans over every gap
* (draws a continuous line, skipping missing samples)
* - boolean false → no change; nulls render as visible breaks (default)
* - number → insert a null break point between any two consecutive
* timestamps whose difference exceeds the threshold;
* gaps smaller than the threshold are left as-is
*
* The input data is expected to be of the form:
* [xValues, series1Values, series2Values, ...]
*/
export function applySpanGapsToAlignedData(
data: uPlot.AlignedData,
seriesOptions: SeriesSpanGapsOption[],
): uPlot.AlignedData {
const [xValues, ...seriesValues] = data;
if (!Array.isArray(xValues) || seriesValues.length === 0) {
return data;
}
// Numeric spanGaps: operates on the whole dataset at once because inserting
// null break points requires modifying the shared x-axis.
const hasNumericSpanGaps = seriesOptions.some(
({ spanGaps }) => typeof spanGaps === 'number',
);
const gapProcessed = hasNumericSpanGaps
? insertLargeGapNullsIntoAlignedData(data, seriesOptions)
: data;
// Boolean spanGaps === true: convert null → undefined per series so uPlot
// draws a continuous line across missing samples instead of breaking it.
// Skip this pass entirely if no series uses spanGaps: true.
const hasBooleanTrue = seriesOptions.some(({ spanGaps }) => spanGaps === true);
if (!hasBooleanTrue) {
return gapProcessed;
}
const [newX, ...newSeries] = gapProcessed;
const transformedSeries = newSeries.map((ys, idx) => {
const { spanGaps } = seriesOptions[idx] ?? {};
if (spanGaps !== true) {
// This series doesn't use spanGaps: true — leave it unchanged.
return ys;
}
// Replace null with undefined: uPlot skips undefined points without
// breaking the line, effectively spanning over the gap.
return (ys as SeriesArray).map((v) =>
v === null ? undefined : v,
) as uPlot.AlignedData[0];
});
return [newX, ...transformedSeries] as uPlot.AlignedData;
}

View File

@@ -1,8 +1,8 @@
import { RoletypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
import { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
const orgId = '019ba2bb-2fa1-7b24-8159-cfca08617ef9';
export const managedRoles: RoletypesRoleDTO[] = [
export const managedRoles: AuthtypesRoleDTO[] = [
{
id: '019c24aa-2248-756f-9833-984f1ab63819',
createdAt: new Date('2026-02-03T18:00:55.624356Z'),
@@ -35,7 +35,7 @@ export const managedRoles: RoletypesRoleDTO[] = [
},
];
export const customRoles: RoletypesRoleDTO[] = [
export const customRoles: AuthtypesRoleDTO[] = [
{
id: '019c24aa-3333-0001-aaaa-111111111111',
createdAt: new Date('2026-02-10T10:30:00.000Z'),
@@ -56,7 +56,7 @@ export const customRoles: RoletypesRoleDTO[] = [
},
];
export const allRoles: RoletypesRoleDTO[] = [...managedRoles, ...customRoles];
export const allRoles: AuthtypesRoleDTO[] = [...managedRoles, ...customRoles];
export const listRolesSuccessResponse = {
status: 'success',

View File

@@ -141,6 +141,7 @@ export interface IBaseWidget {
showPoints?: boolean;
lineStyle?: LineStyle;
fillMode?: FillMode;
spanGaps?: boolean | number;
}
export interface Widgets extends IBaseWidget {
query: Query;

View File

@@ -1,9 +1,8 @@
import { sentryVitePlugin } from '@sentry/vite-plugin';
import react from '@vitejs/plugin-react';
import { readFileSync } from 'fs';
import { resolve } from 'path';
import { visualizer } from 'rollup-plugin-visualizer';
import type { Plugin, UserConfig } from 'vite';
import type { Plugin, TransformResult, UserConfig } from 'vite';
import { defineConfig, loadEnv } from 'vite';
import vitePluginChecker from 'vite-plugin-checker';
import viteCompression from 'vite-plugin-compression';
@@ -14,15 +13,14 @@ import tsconfigPaths from 'vite-tsconfig-paths';
function rawMarkdownPlugin(): Plugin {
return {
name: 'raw-markdown',
transform(_, id): any {
if (id.endsWith('.md')) {
const content = readFileSync(id, 'utf-8');
return {
code: `export default ${JSON.stringify(content)};`,
map: null,
};
transform(code, id): TransformResult | undefined {
if (!id.endsWith('.md')) {
return undefined;
}
return undefined;
return {
code: `export default ${JSON.stringify(code)};`,
map: null,
};
},
};
}
@@ -71,7 +69,7 @@ export default defineConfig(
);
}
if (env.NODE_ENV === 'production') {
if (mode === 'production') {
plugins.push(
ViteImageOptimizer({
jpeg: { quality: 80 },
@@ -102,22 +100,25 @@ export default defineConfig(
},
define: {
// TODO: Remove this in favor of import.meta.env
'process.env': JSON.stringify({
NODE_ENV: mode,
FRONTEND_API_ENDPOINT: env.VITE_FRONTEND_API_ENDPOINT,
WEBSOCKET_API_ENDPOINT: env.VITE_WEBSOCKET_API_ENDPOINT,
PYLON_APP_ID: env.VITE_PYLON_APP_ID,
PYLON_IDENTITY_SECRET: env.VITE_PYLON_IDENTITY_SECRET,
APPCUES_APP_ID: env.VITE_APPCUES_APP_ID,
POSTHOG_KEY: env.VITE_POSTHOG_KEY,
SENTRY_AUTH_TOKEN: env.VITE_SENTRY_AUTH_TOKEN,
SENTRY_ORG: env.VITE_SENTRY_ORG,
SENTRY_PROJECT_ID: env.VITE_SENTRY_PROJECT_ID,
SENTRY_DSN: env.VITE_SENTRY_DSN,
TUNNEL_URL: env.VITE_TUNNEL_URL,
TUNNEL_DOMAIN: env.VITE_TUNNEL_DOMAIN,
DOCS_BASE_URL: env.VITE_DOCS_BASE_URL,
}),
'process.env.NODE_ENV': JSON.stringify(mode),
'process.env.FRONTEND_API_ENDPOINT': JSON.stringify(
env.VITE_FRONTEND_API_ENDPOINT,
),
'process.env.WEBSOCKET_API_ENDPOINT': JSON.stringify(
env.VITE_WEBSOCKET_API_ENDPOINT,
),
'process.env.PYLON_APP_ID': JSON.stringify(env.VITE_PYLON_APP_ID),
'process.env.PYLON_IDENTITY_SECRET': JSON.stringify(
env.VITE_PYLON_IDENTITY_SECRET,
),
'process.env.APPCUES_APP_ID': JSON.stringify(env.VITE_APPCUES_APP_ID),
'process.env.POSTHOG_KEY': JSON.stringify(env.VITE_POSTHOG_KEY),
'process.env.SENTRY_ORG': JSON.stringify(env.VITE_SENTRY_ORG),
'process.env.SENTRY_PROJECT_ID': JSON.stringify(env.VITE_SENTRY_PROJECT_ID),
'process.env.SENTRY_DSN': JSON.stringify(env.VITE_SENTRY_DSN),
'process.env.TUNNEL_URL': JSON.stringify(env.VITE_TUNNEL_URL),
'process.env.TUNNEL_DOMAIN': JSON.stringify(env.VITE_TUNNEL_DOMAIN),
'process.env.DOCS_BASE_URL': JSON.stringify(env.VITE_DOCS_BASE_URL),
},
build: {
sourcemap: true,

View File

@@ -4506,6 +4506,19 @@
"@radix-ui/react-use-callback-ref" "1.1.1"
"@radix-ui/react-use-escape-keydown" "1.1.1"
"@radix-ui/react-dropdown-menu@^2.1.16":
version "2.1.16"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz#5ee045c62bad8122347981c479d92b1ff24c7254"
integrity sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==
dependencies:
"@radix-ui/primitive" "1.1.3"
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-context" "1.1.2"
"@radix-ui/react-id" "1.1.1"
"@radix-ui/react-menu" "2.1.16"
"@radix-ui/react-primitive" "2.1.3"
"@radix-ui/react-use-controllable-state" "1.2.2"
"@radix-ui/react-focus-guards@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.0.tgz#339c1c69c41628c1a5e655f15f7020bf11aa01fa"
@@ -4565,6 +4578,30 @@
dependencies:
"@radix-ui/react-use-layout-effect" "1.1.1"
"@radix-ui/react-menu@2.1.16":
version "2.1.16"
resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.1.16.tgz#528a5a973c3a7413d3d49eb9ccd229aa52402911"
integrity sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==
dependencies:
"@radix-ui/primitive" "1.1.3"
"@radix-ui/react-collection" "1.1.7"
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-context" "1.1.2"
"@radix-ui/react-direction" "1.1.1"
"@radix-ui/react-dismissable-layer" "1.1.11"
"@radix-ui/react-focus-guards" "1.1.3"
"@radix-ui/react-focus-scope" "1.1.7"
"@radix-ui/react-id" "1.1.1"
"@radix-ui/react-popper" "1.2.8"
"@radix-ui/react-portal" "1.1.9"
"@radix-ui/react-presence" "1.1.5"
"@radix-ui/react-primitive" "2.1.3"
"@radix-ui/react-roving-focus" "1.1.11"
"@radix-ui/react-slot" "1.2.3"
"@radix-ui/react-use-callback-ref" "1.1.1"
aria-hidden "^1.2.4"
react-remove-scroll "^2.6.3"
"@radix-ui/react-popover@^1.1.15", "@radix-ui/react-popover@^1.1.2":
version "1.1.15"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.1.15.tgz#9c852f93990a687ebdc949b2c3de1f37cdc4c5d5"
@@ -4804,6 +4841,20 @@
"@radix-ui/react-roving-focus" "1.0.4"
"@radix-ui/react-use-controllable-state" "1.0.1"
"@radix-ui/react-tabs@^1.1.3":
version "1.1.13"
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz#3537ce379d7e7ff4eeb6b67a0973e139c2ac1f15"
integrity sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==
dependencies:
"@radix-ui/primitive" "1.1.3"
"@radix-ui/react-context" "1.1.2"
"@radix-ui/react-direction" "1.1.1"
"@radix-ui/react-id" "1.1.1"
"@radix-ui/react-presence" "1.1.5"
"@radix-ui/react-primitive" "2.1.3"
"@radix-ui/react-roving-focus" "1.1.11"
"@radix-ui/react-use-controllable-state" "1.2.2"
"@radix-ui/react-toggle-group@^1.1.7":
version "1.1.11"
resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz#e513d6ffdb07509b400ab5b26f2523747c0d51c1"
@@ -5675,6 +5726,42 @@
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@signozhq/ui@0.0.4":
version "0.0.4"
resolved "https://registry.yarnpkg.com/@signozhq/ui/-/ui-0.0.4.tgz#2a9c403900311298d881ca9feb6245d94ca0aa0e"
integrity sha512-ViiLsAciCzUgHCu3uDCOEMmjE6OkLpA2g8xvjjbbzi4XFosYBhEASx1Pf3a4f5wlh/JID7z12DOc5YnjzEcn4Q==
dependencies:
"@radix-ui/react-checkbox" "^1.2.3"
"@radix-ui/react-dialog" "^1.1.11"
"@radix-ui/react-dropdown-menu" "^2.1.16"
"@radix-ui/react-icons" "^1.3.0"
"@radix-ui/react-popover" "^1.1.15"
"@radix-ui/react-radio-group" "^1.3.4"
"@radix-ui/react-slot" "^1.2.3"
"@radix-ui/react-switch" "^1.1.4"
"@radix-ui/react-tabs" "^1.1.3"
"@radix-ui/react-toggle" "^1.1.6"
"@radix-ui/react-toggle-group" "^1.1.7"
"@radix-ui/react-tooltip" "^1.2.6"
"@tanstack/react-table" "^8.21.3"
"@tanstack/react-virtual" "^3.13.9"
"@types/lodash-es" "^4.17.12"
class-variance-authority "^0.7.0"
clsx "^2.1.1"
cmdk "^1.1.1"
date-fns "^4.1.0"
dayjs "^1.11.10"
lodash-es "^4.17.21"
lucide-react "^0.445.0"
lucide-solid "^0.510.0"
motion "^11.11.17"
next-themes "^0.4.6"
nuqs "^2.8.9"
react-day-picker "^9.8.1"
react-resizable-panels "^4.7.1"
sonner "^2.0.7"
tailwind-merge "^3.5.0"
"@sinclair/typebox@^0.25.16":
version "0.25.24"
resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz"
@@ -9573,6 +9660,11 @@ dayjs@^1.10.7, dayjs@^1.11.1:
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz"
integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==
dayjs@^1.11.10:
version "1.11.20"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.20.tgz#88d919fd639dc991415da5f4cb6f1b6650811938"
integrity sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==
debounce@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5"
@@ -11092,6 +11184,15 @@ fraction.js@^4.3.7:
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
framer-motion@^11.18.2:
version "11.18.2"
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-11.18.2.tgz#0c6bd05677f4cfd3b3bdead4eb5ecdd5ed245718"
integrity sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==
dependencies:
motion-dom "^11.18.1"
motion-utils "^11.18.1"
tslib "^2.4.0"
framer-motion@^12.4.13:
version "12.4.13"
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.4.13.tgz#1efd954f95e6a54685b660929c00f5a61e35256a"
@@ -15002,6 +15103,13 @@ moment@^2.29.4:
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
motion-dom@^11.18.1:
version "11.18.1"
resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-11.18.1.tgz#e7fed7b7dc6ae1223ef1cce29ee54bec826dc3f2"
integrity sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==
dependencies:
motion-utils "^11.18.1"
motion-dom@^12.4.11:
version "12.4.11"
resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.4.11.tgz#0419c8686cda4d523f08249deeb8fa6683a9b9d3"
@@ -15009,6 +15117,11 @@ motion-dom@^12.4.11:
dependencies:
motion-utils "^12.4.10"
motion-utils@^11.18.1:
version "11.18.1"
resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-11.18.1.tgz#671227669833e991c55813cf337899f41327db5b"
integrity sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==
motion-utils@^12.4.10:
version "12.4.10"
resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-12.4.10.tgz#3d93acea5454419eaaad8d5e5425cb71cbfa1e7f"
@@ -15022,6 +15135,14 @@ motion@12.4.13:
framer-motion "^12.4.13"
tslib "^2.4.0"
motion@^11.11.17:
version "11.18.2"
resolved "https://registry.yarnpkg.com/motion/-/motion-11.18.2.tgz#17fb372f3ed94fc9ee1384a25a9068e9da1951e7"
integrity sha512-JLjvFDuFr42NFtcVoMAyC2sEjnpA8xpy6qWPyzQvCloznAyQ8FIXioxWfHiLtgYhoVpfUqSWpn1h9++skj9+Wg==
dependencies:
framer-motion "^11.18.2"
tslib "^2.4.0"
mri@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
@@ -15292,6 +15413,13 @@ nuqs@2.8.8:
dependencies:
"@standard-schema/spec" "1.0.0"
nuqs@^2.8.9:
version "2.8.9"
resolved "https://registry.yarnpkg.com/nuqs/-/nuqs-2.8.9.tgz#e2c27d87c0dd0e3b4412fe867bcd0947cc4c998f"
integrity sha512-8ou6AEwsxMWSYo2qkfZtYFVzngwbKmg4c00HVxC1fF6CEJv3Fwm6eoZmfVPALB+vw8Udo7KL5uy96PFcYe1BIQ==
dependencies:
"@standard-schema/spec" "1.0.0"
nwsapi@^2.2.2:
version "2.2.23"
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.23.tgz#59712c3a88e6de2bb0b6ccc1070397267019cf6c"
@@ -16957,6 +17085,11 @@ react-resizable-panels@^3.0.5:
resolved "https://registry.yarnpkg.com/react-resizable-panels/-/react-resizable-panels-3.0.5.tgz#50a20645263eed02344de4a70d1319bbc0014bbd"
integrity sha512-3z1yN25DMTXLg2wfyFrW32r5k4WEcUa3F7cJ2EgtNK07lnOs4mpM8yWLGunCpkhcQRwJX4fqoLcIh/pHPxzlmQ==
react-resizable-panels@^4.7.1:
version "4.7.3"
resolved "https://registry.yarnpkg.com/react-resizable-panels/-/react-resizable-panels-4.7.3.tgz#4040aa0f5c5c4cc4bb685cb69973601ccda3b014"
integrity sha512-PYcYMLtvJD+Pr0TQNeMvddcnLOwUa/Yb4iNwU7ThNLlHaQYEEC9MIBWHaBGODzYuXIkPRZ/OWe5sbzG1Rzq5ew==
react-resizable@3.0.4:
version "3.0.4"
resolved "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.4.tgz"
@@ -18797,6 +18930,11 @@ tailwind-merge@^2.5.2:
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz#ac5fb7e227910c038d458f396b7400d93a3142d5"
integrity sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==
tailwind-merge@^3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-3.5.0.tgz#06502f4496ba15151445d97d916a26564d50d1ca"
integrity sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==
tailwindcss-animate@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz#318b692c4c42676cc9e67b19b78775742388bef4"

7
go.mod
View File

@@ -107,8 +107,6 @@ require (
github.com/aws/smithy-go v1.24.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/c-bata/go-prompt v0.2.6 // indirect
github.com/chdb-io/chdb-go v1.11.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-openapi/swag/cmdutils v0.25.4 // indirect
@@ -128,16 +126,11 @@ require (
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/hashicorp/go-metrics v0.5.4 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mattn/go-tty v0.0.5 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pkg/term v1.2.0-beta.2 // indirect
github.com/prometheus/client_golang/exp v0.0.0-20260108101519-fb0838f53562 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.15.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/swaggest/refl v1.4.0 // indirect
github.com/swaggest/usecase v1.3.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect

21
go.sum
View File

@@ -206,8 +206,6 @@ github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/c-bata/go-prompt v0.2.6 h1:POP+nrHE+DfLYx370bedwNhsqmpCUynWPxuHi0C5vZI=
github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdfpU9wwHfY=
github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c/go.mod h1:l/bIBLeOl9eX+wxJAzxS4TveKRtAqlyDpHjhkfO0MEI=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
@@ -218,8 +216,6 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chdb-io/chdb-go v1.11.0 h1:G6+Oy1onzNL3bSxncGfIdiB6beTpxwKztjfai7qLckE=
github.com/chdb-io/chdb-go v1.11.0/go.mod h1:RkT+xLXhdBKtUtJJPwhQQR4p6qiXHisJNS712QldDg8=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -764,7 +760,6 @@ github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxq
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
@@ -778,14 +773,6 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=
github.com/mattn/go-tty v0.0.5 h1:s09uXI7yDbXzzTTfw3zonKFzwGkyYlgU3OMjqA0ddz4=
github.com/mattn/go-tty v0.0.5/go.mod h1:u5GGXBtZU6RQoKV8gY5W6UhMudbR5vXnUe7j3pxse28=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
@@ -913,8 +900,6 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw=
github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -976,9 +961,6 @@ github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
@@ -1551,7 +1533,6 @@ golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1573,8 +1554,6 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@@ -6,7 +6,6 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/gorilla/mux"
)
@@ -16,7 +15,7 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
Tags: []string{"role"},
Summary: "Create role",
Description: "This endpoint creates a role",
Request: new(roletypes.PostableRole),
Request: new(authtypes.PostableRole),
RequestContentType: "",
Response: new(types.Identifiable),
ResponseContentType: "application/json",
@@ -35,7 +34,7 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
Description: "This endpoint lists all roles",
Request: nil,
RequestContentType: "",
Response: make([]*roletypes.Role, 0),
Response: make([]*authtypes.Role, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
@@ -52,7 +51,7 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
Description: "This endpoint gets a role",
Request: nil,
RequestContentType: "",
Response: new(roletypes.Role),
Response: new(authtypes.Role),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
@@ -84,7 +83,7 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
Tags: []string{"role"},
Summary: "Patch role",
Description: "This endpoint patches a role",
Request: new(roletypes.PatchableRole),
Request: new(authtypes.PatchableRole),
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",

View File

@@ -6,7 +6,6 @@ import (
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
)
@@ -30,10 +29,10 @@ type AuthZ interface {
ListObjects(context.Context, string, authtypes.Relation, authtypes.Typeable) ([]*authtypes.Object, error)
// Creates the role.
Create(context.Context, valuer.UUID, *roletypes.Role) error
Create(context.Context, valuer.UUID, *authtypes.Role) error
// Gets the role if it exists or creates one.
GetOrCreate(context.Context, valuer.UUID, *roletypes.Role) (*roletypes.Role, error)
GetOrCreate(context.Context, valuer.UUID, *authtypes.Role) (*authtypes.Role, error)
// Gets the objects associated with the given role and relation.
GetObjects(context.Context, valuer.UUID, valuer.UUID, authtypes.Relation) ([]*authtypes.Object, error)
@@ -42,7 +41,7 @@ type AuthZ interface {
GetResources(context.Context) []*authtypes.Resource
// Patches the role.
Patch(context.Context, valuer.UUID, *roletypes.Role) error
Patch(context.Context, valuer.UUID, *authtypes.Role) error
// Patches the objects in authorization server associated with the given role and relation
PatchObjects(context.Context, valuer.UUID, string, authtypes.Relation, []*authtypes.Object, []*authtypes.Object) error
@@ -51,19 +50,19 @@ type AuthZ interface {
Delete(context.Context, valuer.UUID, valuer.UUID) error
// Gets the role
Get(context.Context, valuer.UUID, valuer.UUID) (*roletypes.Role, error)
Get(context.Context, valuer.UUID, valuer.UUID) (*authtypes.Role, error)
// Gets the role by org_id and name
GetByOrgIDAndName(context.Context, valuer.UUID, string) (*roletypes.Role, error)
GetByOrgIDAndName(context.Context, valuer.UUID, string) (*authtypes.Role, error)
// Lists all the roles for the organization.
List(context.Context, valuer.UUID) ([]*roletypes.Role, error)
List(context.Context, valuer.UUID) ([]*authtypes.Role, error)
// Lists all the roles for the organization filtered by name
ListByOrgIDAndNames(context.Context, valuer.UUID, []string) ([]*roletypes.Role, error)
ListByOrgIDAndNames(context.Context, valuer.UUID, []string) ([]*authtypes.Role, error)
// Lists all the roles for the organization filtered by ids
ListByOrgIDAndIDs(context.Context, valuer.UUID, []valuer.UUID) ([]*roletypes.Role, error)
ListByOrgIDAndIDs(context.Context, valuer.UUID, []valuer.UUID) ([]*authtypes.Role, error)
// Grants a role to the subject based on role name.
Grant(context.Context, valuer.UUID, []string, string) error
@@ -75,7 +74,7 @@ type AuthZ interface {
ModifyGrant(context.Context, valuer.UUID, []string, []string, string) error
// Bootstrap the managed roles.
CreateManagedRoles(context.Context, valuer.UUID, []*roletypes.Role) error
CreateManagedRoles(context.Context, valuer.UUID, []*authtypes.Role) error
// Bootstrap managed roles transactions and user assignments
CreateManagedUserRoleTransactions(context.Context, valuer.UUID, valuer.UUID) error

View File

@@ -5,7 +5,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
@@ -14,11 +14,11 @@ type store struct {
sqlstore sqlstore.SQLStore
}
func NewSqlAuthzStore(sqlstore sqlstore.SQLStore) roletypes.Store {
func NewSqlAuthzStore(sqlstore sqlstore.SQLStore) authtypes.RoleStore {
return &store{sqlstore: sqlstore}
}
func (store *store) Create(ctx context.Context, role *roletypes.StorableRole) error {
func (store *store) Create(ctx context.Context, role *authtypes.StorableRole) error {
_, err := store.
sqlstore.
BunDBCtx(ctx).
@@ -32,8 +32,8 @@ func (store *store) Create(ctx context.Context, role *roletypes.StorableRole) er
return nil
}
func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*roletypes.StorableRole, error) {
role := new(roletypes.StorableRole)
func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.StorableRole, error) {
role := new(authtypes.StorableRole)
err := store.
sqlstore.
BunDBCtx(ctx).
@@ -43,14 +43,14 @@ func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID)
Where("id = ?", id).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, roletypes.ErrCodeRoleNotFound, "role with id: %s doesn't exist", id)
return nil, store.sqlstore.WrapNotFoundErrf(err, authtypes.ErrCodeRoleNotFound, "role with id: %s doesn't exist", id)
}
return role, nil
}
func (store *store) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*roletypes.StorableRole, error) {
role := new(roletypes.StorableRole)
func (store *store) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*authtypes.StorableRole, error) {
role := new(authtypes.StorableRole)
err := store.
sqlstore.
BunDBCtx(ctx).
@@ -60,14 +60,14 @@ func (store *store) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, na
Where("name = ?", name).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, roletypes.ErrCodeRoleNotFound, "role with name: %s doesn't exist", name)
return nil, store.sqlstore.WrapNotFoundErrf(err, authtypes.ErrCodeRoleNotFound, "role with name: %s doesn't exist", name)
}
return role, nil
}
func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes.StorableRole, error) {
roles := make([]*roletypes.StorableRole, 0)
func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*authtypes.StorableRole, error) {
roles := make([]*authtypes.StorableRole, 0)
err := store.
sqlstore.
BunDBCtx(ctx).
@@ -82,8 +82,8 @@ func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes.S
return roles, nil
}
func (store *store) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*roletypes.StorableRole, error) {
roles := make([]*roletypes.StorableRole, 0)
func (store *store) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*authtypes.StorableRole, error) {
roles := make([]*authtypes.StorableRole, 0)
err := store.
sqlstore.
BunDBCtx(ctx).
@@ -99,7 +99,7 @@ func (store *store) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID,
if len(roles) != len(names) {
return nil, store.sqlstore.WrapNotFoundErrf(
nil,
roletypes.ErrCodeRoleNotFound,
authtypes.ErrCodeRoleNotFound,
"not all roles found for the provided names: %v", names,
)
}
@@ -107,8 +107,8 @@ func (store *store) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID,
return roles, nil
}
func (store *store) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*roletypes.StorableRole, error) {
roles := make([]*roletypes.StorableRole, 0)
func (store *store) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*authtypes.StorableRole, error) {
roles := make([]*authtypes.StorableRole, 0)
err := store.
sqlstore.
BunDBCtx(ctx).
@@ -124,7 +124,7 @@ func (store *store) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, id
if len(roles) != len(ids) {
return nil, store.sqlstore.WrapNotFoundErrf(
nil,
roletypes.ErrCodeRoleNotFound,
authtypes.ErrCodeRoleNotFound,
"not all roles found for the provided ids: %v", ids,
)
}
@@ -132,7 +132,7 @@ func (store *store) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, id
return roles, nil
}
func (store *store) Update(ctx context.Context, orgID valuer.UUID, role *roletypes.StorableRole) error {
func (store *store) Update(ctx context.Context, orgID valuer.UUID, role *authtypes.StorableRole) error {
_, err := store.
sqlstore.
BunDBCtx(ctx).
@@ -153,12 +153,12 @@ func (store *store) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUI
sqlstore.
BunDBCtx(ctx).
NewDelete().
Model(new(roletypes.StorableRole)).
Model(new(authtypes.StorableRole)).
Where("org_id = ?", orgID).
Where("id = ?", id).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapNotFoundErrf(err, roletypes.ErrCodeRoleNotFound, "role with id %s doesn't exist", id)
return store.sqlstore.WrapNotFoundErrf(err, authtypes.ErrCodeRoleNotFound, "role with id %s doesn't exist", id)
}
return nil

View File

@@ -8,7 +8,6 @@ import (
"github.com/SigNoz/signoz/pkg/authz/openfgaserver"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/SigNoz/signoz/pkg/factory"
@@ -19,7 +18,7 @@ import (
type provider struct {
server *openfgaserver.Server
store roletypes.Store
store authtypes.RoleStore
}
func NewProviderFactory(sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile) factory.ProviderFactory[authz.AuthZ, authz.Config] {
@@ -68,61 +67,61 @@ func (provider *provider) ListObjects(ctx context.Context, subject string, relat
return provider.server.ListObjects(ctx, subject, relation, typeable)
}
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*roletypes.Role, error) {
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.Role, error) {
storableRole, err := provider.store.Get(ctx, orgID, id)
if err != nil {
return nil, err
}
return roletypes.NewRoleFromStorableRole(storableRole), nil
return authtypes.NewRoleFromStorableRole(storableRole), nil
}
func (provider *provider) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*roletypes.Role, error) {
func (provider *provider) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*authtypes.Role, error) {
storableRole, err := provider.store.GetByOrgIDAndName(ctx, orgID, name)
if err != nil {
return nil, err
}
return roletypes.NewRoleFromStorableRole(storableRole), nil
return authtypes.NewRoleFromStorableRole(storableRole), nil
}
func (provider *provider) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes.Role, error) {
func (provider *provider) List(ctx context.Context, orgID valuer.UUID) ([]*authtypes.Role, error) {
storableRoles, err := provider.store.List(ctx, orgID)
if err != nil {
return nil, err
}
roles := make([]*roletypes.Role, len(storableRoles))
roles := make([]*authtypes.Role, len(storableRoles))
for idx, storableRole := range storableRoles {
roles[idx] = roletypes.NewRoleFromStorableRole(storableRole)
roles[idx] = authtypes.NewRoleFromStorableRole(storableRole)
}
return roles, nil
}
func (provider *provider) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*roletypes.Role, error) {
func (provider *provider) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*authtypes.Role, error) {
storableRoles, err := provider.store.ListByOrgIDAndNames(ctx, orgID, names)
if err != nil {
return nil, err
}
roles := make([]*roletypes.Role, len(storableRoles))
roles := make([]*authtypes.Role, len(storableRoles))
for idx, storable := range storableRoles {
roles[idx] = roletypes.NewRoleFromStorableRole(storable)
roles[idx] = authtypes.NewRoleFromStorableRole(storable)
}
return roles, nil
}
func (provider *provider) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*roletypes.Role, error) {
func (provider *provider) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*authtypes.Role, error) {
storableRoles, err := provider.store.ListByOrgIDAndIDs(ctx, orgID, ids)
if err != nil {
return nil, err
}
roles := make([]*roletypes.Role, len(storableRoles))
roles := make([]*authtypes.Role, len(storableRoles))
for idx, storable := range storableRoles {
roles[idx] = roletypes.NewRoleFromStorableRole(storable)
roles[idx] = authtypes.NewRoleFromStorableRole(storable)
}
return roles, nil
@@ -179,10 +178,10 @@ func (provider *provider) Revoke(ctx context.Context, orgID valuer.UUID, names [
return provider.Write(ctx, nil, tuples)
}
func (provider *provider) CreateManagedRoles(ctx context.Context, _ valuer.UUID, managedRoles []*roletypes.Role) error {
func (provider *provider) CreateManagedRoles(ctx context.Context, _ valuer.UUID, managedRoles []*authtypes.Role) error {
err := provider.store.RunInTx(ctx, func(ctx context.Context) error {
for _, role := range managedRoles {
err := provider.store.Create(ctx, roletypes.NewStorableRoleFromRole(role))
err := provider.store.Create(ctx, authtypes.NewStorableRoleFromRole(role))
if err != nil {
return err
}
@@ -199,15 +198,15 @@ func (provider *provider) CreateManagedRoles(ctx context.Context, _ valuer.UUID,
}
func (provider *provider) CreateManagedUserRoleTransactions(ctx context.Context, orgID valuer.UUID, userID valuer.UUID) error {
return provider.Grant(ctx, orgID, []string{roletypes.SigNozAdminRoleName}, authtypes.MustNewSubject(authtypes.TypeableUser, userID.String(), orgID, nil))
return provider.Grant(ctx, orgID, []string{authtypes.SigNozAdminRoleName}, authtypes.MustNewSubject(authtypes.TypeableUser, userID.String(), orgID, nil))
}
func (setter *provider) Create(_ context.Context, _ valuer.UUID, _ *roletypes.Role) error {
return errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented")
func (setter *provider) Create(_ context.Context, _ valuer.UUID, _ *authtypes.Role) error {
return errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
}
func (provider *provider) GetOrCreate(_ context.Context, _ valuer.UUID, _ *roletypes.Role) (*roletypes.Role, error) {
return nil, errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented")
func (provider *provider) GetOrCreate(_ context.Context, _ valuer.UUID, _ *authtypes.Role) (*authtypes.Role, error) {
return nil, errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
}
func (provider *provider) GetResources(_ context.Context) []*authtypes.Resource {
@@ -215,19 +214,19 @@ func (provider *provider) GetResources(_ context.Context) []*authtypes.Resource
}
func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation) ([]*authtypes.Object, error) {
return nil, errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented")
return nil, errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
}
func (provider *provider) Patch(_ context.Context, _ valuer.UUID, _ *roletypes.Role) error {
return errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented")
func (provider *provider) Patch(_ context.Context, _ valuer.UUID, _ *authtypes.Role) error {
return errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
}
func (provider *provider) PatchObjects(_ context.Context, _ valuer.UUID, _ string, _ authtypes.Relation, _, _ []*authtypes.Object) error {
return errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented")
return errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
}
func (provider *provider) Delete(_ context.Context, _ valuer.UUID, _ valuer.UUID) error {
return errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented")
return errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
}
func (provider *provider) MustGetTypeables() []authtypes.Typeable {

View File

@@ -9,7 +9,6 @@ import (
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
@@ -30,13 +29,13 @@ func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
return
}
req := new(roletypes.PostableRole)
req := new(authtypes.PostableRole)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
role := roletypes.NewRole(req.Name, req.Description, roletypes.RoleTypeCustom, valuer.MustNewUUID(claims.OrgID))
role := authtypes.NewRole(req.Name, req.Description, authtypes.RoleTypeCustom, valuer.MustNewUUID(claims.OrgID))
err = handler.authz.Create(ctx, valuer.MustNewUUID(claims.OrgID), role)
if err != nil {
render.Error(rw, err)
@@ -56,7 +55,7 @@ func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
id, ok := mux.Vars(r)["id"]
if !ok {
render.Error(rw, errors.New(errors.TypeInvalidInput, roletypes.ErrCodeRoleInvalidInput, "id is missing from the request"))
render.Error(rw, errors.New(errors.TypeInvalidInput, authtypes.ErrCodeRoleInvalidInput, "id is missing from the request"))
return
}
roleID, err := valuer.NewUUID(id)
@@ -84,7 +83,7 @@ func (handler *handler) GetObjects(rw http.ResponseWriter, r *http.Request) {
id, ok := mux.Vars(r)["id"]
if !ok {
render.Error(rw, errors.New(errors.TypeInvalidInput, roletypes.ErrCodeRoleInvalidInput, "id is missing from the request"))
render.Error(rw, errors.New(errors.TypeInvalidInput, authtypes.ErrCodeRoleInvalidInput, "id is missing from the request"))
return
}
roleID, err := valuer.NewUUID(id)
@@ -95,7 +94,7 @@ func (handler *handler) GetObjects(rw http.ResponseWriter, r *http.Request) {
relationStr, ok := mux.Vars(r)["relation"]
if !ok {
render.Error(rw, errors.New(errors.TypeInvalidInput, roletypes.ErrCodeRoleInvalidInput, "relation is missing from the request"))
render.Error(rw, errors.New(errors.TypeInvalidInput, authtypes.ErrCodeRoleInvalidInput, "relation is missing from the request"))
return
}
relation, err := authtypes.NewRelation(relationStr)
@@ -150,7 +149,7 @@ func (handler *handler) Patch(rw http.ResponseWriter, r *http.Request) {
return
}
req := new(roletypes.PatchableRole)
req := new(authtypes.PatchableRole)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return

View File

@@ -10,7 +10,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
@@ -56,9 +55,9 @@ func (middleware *AuthZ) ViewAccess(next http.HandlerFunc) http.HandlerFunc {
}
selectors := []authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozAdminRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozEditorRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozViewerRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozEditorRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozViewerRoleName),
}
err = middleware.authzService.CheckWithTupleCreation(
@@ -108,8 +107,8 @@ func (middleware *AuthZ) EditAccess(next http.HandlerFunc) http.HandlerFunc {
}
selectors := []authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozAdminRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozEditorRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozEditorRoleName),
}
err = middleware.authzService.CheckWithTupleCreation(
@@ -159,7 +158,7 @@ func (middleware *AuthZ) AdminAccess(next http.HandlerFunc) http.HandlerFunc {
}
selectors := []authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozAdminRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
}
err = middleware.authzService.CheckWithTupleCreation(

View File

@@ -17,28 +17,34 @@ import (
// todo: will move this in types layer with service account integration
type apiKeyTokenKey struct{}
type resolver struct {
type provider struct {
store sqlstore.SQLStore
headers []string
config identn.Config
settings factory.ScopedProviderSettings
sfGroup *singleflight.Group
}
func New(providerSettings factory.ProviderSettings, store sqlstore.SQLStore, headers []string) identn.IdentN {
return &resolver{
store: store,
headers: headers,
settings: factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/identn/apikeyidentn"),
sfGroup: &singleflight.Group{},
}
func NewFactory(store sqlstore.SQLStore) factory.ProviderFactory[identn.IdentN, identn.Config] {
return factory.NewProviderFactory(factory.MustNewName(authtypes.IdentNProviderAPIkey.StringValue()), func(ctx context.Context, providerSettings factory.ProviderSettings, config identn.Config) (identn.IdentN, error) {
return New(providerSettings, store, config)
})
}
func (r *resolver) Name() authtypes.IdentNProvider {
func New(providerSettings factory.ProviderSettings, store sqlstore.SQLStore, config identn.Config) (identn.IdentN, error) {
return &provider{
store: store,
config: config,
settings: factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/identn/apikeyidentn"),
sfGroup: &singleflight.Group{},
}, nil
}
func (provider *provider) Name() authtypes.IdentNProvider {
return authtypes.IdentNProviderAPIkey
}
func (r *resolver) Test(req *http.Request) bool {
for _, header := range r.headers {
func (provider *provider) Test(req *http.Request) bool {
for _, header := range provider.config.APIKeyConfig.Headers {
if req.Header.Get(header) != "" {
return true
}
@@ -46,8 +52,12 @@ func (r *resolver) Test(req *http.Request) bool {
return false
}
func (r *resolver) Pre(req *http.Request) *http.Request {
token := r.extractToken(req)
func (provider *provider) Enabled() bool {
return provider.config.APIKeyConfig.Enabled
}
func (provider *provider) Pre(req *http.Request) *http.Request {
token := provider.extractToken(req)
if token == "" {
return req
}
@@ -56,16 +66,16 @@ func (r *resolver) Pre(req *http.Request) *http.Request {
return req.WithContext(ctx)
}
func (r *resolver) GetIdentity(req *http.Request) (*authtypes.Identity, error) {
func (provider *provider) GetIdentity(req *http.Request) (*authtypes.Identity, error) {
ctx := req.Context()
apiKeyToken, ok := ctx.Value(apiKeyTokenKey{}).(string)
if !ok || apiKeyToken == "" {
return nil, errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "missing api key")
}
var apiKey types.StorableAPIKey
err := r.store.
err := provider.
store.
BunDB().
NewSelect().
Model(&apiKey).
@@ -80,7 +90,8 @@ func (r *resolver) GetIdentity(req *http.Request) (*authtypes.Identity, error) {
}
var user types.User
err = r.store.
err = provider.
store.
BunDB().
NewSelect().
Model(&user).
@@ -99,14 +110,15 @@ func (r *resolver) GetIdentity(req *http.Request) (*authtypes.Identity, error) {
return &identity, nil
}
func (r *resolver) Post(ctx context.Context, _ *http.Request, _ authtypes.Claims) {
func (provider *provider) Post(ctx context.Context, _ *http.Request, _ authtypes.Claims) {
apiKeyToken, ok := ctx.Value(apiKeyTokenKey{}).(string)
if !ok || apiKeyToken == "" {
return
}
_, _, _ = r.sfGroup.Do(apiKeyToken, func() (any, error) {
_, err := r.store.
_, _, _ = provider.sfGroup.Do(apiKeyToken, func() (any, error) {
_, err := provider.
store.
BunDB().
NewUpdate().
Model(new(types.StorableAPIKey)).
@@ -115,14 +127,14 @@ func (r *resolver) Post(ctx context.Context, _ *http.Request, _ authtypes.Claims
Where("revoked = false").
Exec(ctx)
if err != nil {
r.settings.Logger().ErrorContext(ctx, "failed to update last used of api key", "error", err)
provider.settings.Logger().ErrorContext(ctx, "failed to update last used of api key", "error", err)
}
return true, nil
})
}
func (r *resolver) extractToken(req *http.Request) string {
for _, header := range r.headers {
func (provider *provider) extractToken(req *http.Request) string {
for _, header := range provider.config.APIKeyConfig.Headers {
if v := req.Header.Get(header); v != "" {
return v
}

48
pkg/identn/config.go Normal file
View File

@@ -0,0 +1,48 @@
package identn
import (
"github.com/SigNoz/signoz/pkg/factory"
)
type Config struct {
// Config for tokenizer identN resolver
Tokenizer TokenizerConfig `mapstructure:"tokenizer"`
// Config for apikey identN resolver
APIKeyConfig APIKeyConfig `mapstructure:"apikey"`
}
type TokenizerConfig struct {
// Toggles the identN resolver
Enabled bool `mapstructure:"enabled"`
// Headers to extract from incoming requests
Headers []string `mapstructure:"headers"`
}
type APIKeyConfig struct {
// Toggles the identN resolver
Enabled bool `mapstructure:"enabled"`
// Headers to extract from incoming requests
Headers []string `mapstructure:"headers"`
}
func NewConfigFactory() factory.ConfigFactory {
return factory.NewConfigFactory(factory.MustNewName("identn"), newConfig)
}
func newConfig() factory.Config {
return &Config{
Tokenizer: TokenizerConfig{
Enabled: true,
Headers: []string{"Authorization", "Sec-WebSocket-Protocol"},
},
APIKeyConfig: APIKeyConfig{
Enabled: true,
Headers: []string{"SIGNOZ-API-KEY"},
},
}
}
func (c Config) Validate() error {
return nil
}

View File

@@ -23,6 +23,8 @@ type IdentN interface {
GetIdentity(r *http.Request) (*authtypes.Identity, error)
Name() authtypes.IdentNProvider
Enabled() bool
}
// IdentNWithPreHook is optionally implemented by resolvers that need to

View File

@@ -12,8 +12,16 @@ type identNResolver struct {
}
func NewIdentNResolver(providerSettings factory.ProviderSettings, identNs ...IdentN) IdentNResolver {
enabledIdentNs := []IdentN{}
for _, identN := range identNs {
if identN.Enabled() {
enabledIdentNs = append(enabledIdentNs, identN)
}
}
return &identNResolver{
identNs: identNs,
identNs: enabledIdentNs,
settings: factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/identn"),
}
}

View File

@@ -13,28 +13,34 @@ import (
"golang.org/x/sync/singleflight"
)
type resolver struct {
type provider struct {
tokenizer tokenizer.Tokenizer
headers []string
config identn.Config
settings factory.ScopedProviderSettings
sfGroup *singleflight.Group
}
func New(providerSettings factory.ProviderSettings, tokenizer tokenizer.Tokenizer, headers []string) identn.IdentN {
return &resolver{
tokenizer: tokenizer,
headers: headers,
settings: factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/identn/tokenizeridentn"),
sfGroup: &singleflight.Group{},
}
func NewFactory(tokenizer tokenizer.Tokenizer) factory.ProviderFactory[identn.IdentN, identn.Config] {
return factory.NewProviderFactory(factory.MustNewName(authtypes.IdentNProviderTokenizer.StringValue()), func(ctx context.Context, providerSettings factory.ProviderSettings, config identn.Config) (identn.IdentN, error) {
return New(providerSettings, tokenizer, config)
})
}
func (r *resolver) Name() authtypes.IdentNProvider {
func New(providerSettings factory.ProviderSettings, tokenizer tokenizer.Tokenizer, config identn.Config) (identn.IdentN, error) {
return &provider{
tokenizer: tokenizer,
config: config,
settings: factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/identn/tokenizeridentn"),
sfGroup: &singleflight.Group{},
}, nil
}
func (provider *provider) Name() authtypes.IdentNProvider {
return authtypes.IdentNProviderTokenizer
}
func (r *resolver) Test(req *http.Request) bool {
for _, header := range r.headers {
func (provider *provider) Test(req *http.Request) bool {
for _, header := range provider.config.Tokenizer.Headers {
if req.Header.Get(header) != "" {
return true
}
@@ -42,8 +48,12 @@ func (r *resolver) Test(req *http.Request) bool {
return false
}
func (r *resolver) Pre(req *http.Request) *http.Request {
accessToken := r.extractToken(req)
func (provider *provider) Enabled() bool {
return provider.config.Tokenizer.Enabled
}
func (provider *provider) Pre(req *http.Request) *http.Request {
accessToken := provider.extractToken(req)
if accessToken == "" {
return req
}
@@ -52,7 +62,7 @@ func (r *resolver) Pre(req *http.Request) *http.Request {
return req.WithContext(ctx)
}
func (r *resolver) GetIdentity(req *http.Request) (*authtypes.Identity, error) {
func (provider *provider) GetIdentity(req *http.Request) (*authtypes.Identity, error) {
ctx := req.Context()
accessToken, err := authtypes.AccessTokenFromContext(ctx)
@@ -60,41 +70,45 @@ func (r *resolver) GetIdentity(req *http.Request) (*authtypes.Identity, error) {
return nil, err
}
return r.tokenizer.GetIdentity(ctx, accessToken)
return provider.tokenizer.GetIdentity(ctx, accessToken)
}
func (r *resolver) Post(ctx context.Context, _ *http.Request, _ authtypes.Claims) {
func (provider *provider) Post(ctx context.Context, _ *http.Request, _ authtypes.Claims) {
if !provider.config.Tokenizer.Enabled {
return
}
accessToken, err := authtypes.AccessTokenFromContext(ctx)
if err != nil {
return
}
_, _, _ = r.sfGroup.Do(accessToken, func() (any, error) {
if err := r.tokenizer.SetLastObservedAt(ctx, accessToken, time.Now()); err != nil {
r.settings.Logger().ErrorContext(ctx, "failed to set last observed at", "error", err)
_, _, _ = provider.sfGroup.Do(accessToken, func() (any, error) {
if err := provider.tokenizer.SetLastObservedAt(ctx, accessToken, time.Now()); err != nil {
provider.settings.Logger().ErrorContext(ctx, "failed to set last observed at", "error", err)
return false, err
}
return true, nil
})
}
func (r *resolver) extractToken(req *http.Request) string {
func (provider *provider) extractToken(req *http.Request) string {
var value string
for _, header := range r.headers {
for _, header := range provider.config.Tokenizer.Headers {
if v := req.Header.Get(header); v != "" {
value = v
break
}
}
accessToken, ok := r.parseBearerAuth(value)
accessToken, ok := provider.parseBearerAuth(value)
if !ok {
return value
}
return accessToken
}
func (r *resolver) parseBearerAuth(auth string) (string, bool) {
func (provider *provider) parseBearerAuth(auth string) (string, bool) {
const prefix = "Bearer "
if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) {
return "", false

View File

@@ -19,7 +19,6 @@ import (
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/emailtypes"
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/dustin/go-humanize"
)
@@ -263,7 +262,7 @@ func (module *Module) CreateUser(ctx context.Context, input *types.User, opts ..
createUserOpts := root.NewCreateUserOptions(opts...)
// since assign is idempotant multiple calls to assign won't cause issues in case of retries.
err := module.authz.Grant(ctx, input.OrgID, []string{roletypes.MustGetSigNozManagedRoleFromExistingRole(input.Role)}, authtypes.MustNewSubject(authtypes.TypeableUser, input.ID.StringValue(), input.OrgID, nil))
err := module.authz.Grant(ctx, input.OrgID, []string{authtypes.MustGetSigNozManagedRoleFromExistingRole(input.Role)}, authtypes.MustNewSubject(authtypes.TypeableUser, input.ID.StringValue(), input.OrgID, nil))
if err != nil {
return err
}
@@ -333,8 +332,8 @@ func (m *Module) UpdateUser(ctx context.Context, orgID valuer.UUID, id string, u
if user.Role != "" && user.Role != existingUser.Role {
err = m.authz.ModifyGrant(ctx,
orgID,
[]string{roletypes.MustGetSigNozManagedRoleFromExistingRole(existingUser.Role)},
[]string{roletypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)},
[]string{authtypes.MustGetSigNozManagedRoleFromExistingRole(existingUser.Role)},
[]string{authtypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)},
authtypes.MustNewSubject(authtypes.TypeableUser, id, orgID, nil),
)
if err != nil {
@@ -395,7 +394,7 @@ func (module *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id stri
}
// since revoke is idempotant multiple calls to revoke won't cause issues in case of retries
err = module.authz.Revoke(ctx, orgID, []string{roletypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)}, authtypes.MustNewSubject(authtypes.TypeableUser, id, orgID, nil))
err = module.authz.Revoke(ctx, orgID, []string{authtypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)}, authtypes.MustNewSubject(authtypes.TypeableUser, id, orgID, nil))
if err != nil {
return err
}
@@ -558,7 +557,7 @@ func (module *Module) UpdatePasswordByResetPasswordToken(ctx context.Context, to
if err = module.authz.Grant(
ctx,
user.OrgID,
[]string{roletypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)},
[]string{authtypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)},
authtypes.MustNewSubject(authtypes.TypeableUser, user.ID.StringValue(), user.OrgID, nil),
); err != nil {
return err
@@ -692,7 +691,7 @@ func (module *Module) CreateFirstUser(ctx context.Context, organization *types.O
return nil, err
}
managedRoles := roletypes.NewManagedRoles(organization.ID)
managedRoles := authtypes.NewManagedRoles(organization.ID)
err = module.authz.CreateManagedUserRoleTransactions(ctx, organization.ID, user.ID)
if err != nil {
return nil, err
@@ -793,7 +792,7 @@ func (module *Module) activatePendingUser(ctx context.Context, user *types.User)
err := module.authz.Grant(
ctx,
user.OrgID,
[]string{roletypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)},
[]string{authtypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)},
authtypes.MustNewSubject(authtypes.TypeableUser, user.ID.StringValue(), user.OrgID, nil),
)
if err != nil {

View File

@@ -11,7 +11,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -159,8 +158,8 @@ func (s *service) createOrPromoteRootUser(ctx context.Context, orgID valuer.UUID
if oldRole != types.RoleAdmin {
if err := s.authz.ModifyGrant(ctx,
orgID,
[]string{roletypes.MustGetSigNozManagedRoleFromExistingRole(oldRole)},
[]string{roletypes.MustGetSigNozManagedRoleFromExistingRole(types.RoleAdmin)},
[]string{authtypes.MustGetSigNozManagedRoleFromExistingRole(oldRole)},
[]string{authtypes.MustGetSigNozManagedRoleFromExistingRole(types.RoleAdmin)},
authtypes.MustNewSubject(authtypes.TypeableUser, existingUser.ID.StringValue(), orgID, nil),
); err != nil {
return err

View File

@@ -20,6 +20,7 @@ import (
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
"github.com/SigNoz/signoz/pkg/types/metrictypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/dustin/go-humanize"
"golang.org/x/exp/maps"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
@@ -158,7 +159,8 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
metricNames := make([]string, 0)
for idx, query := range req.CompositeQuery.Queries {
event.QueryType = query.Type.StringValue()
if query.Type == qbtypes.QueryTypeBuilder {
switch query.Type {
case qbtypes.QueryTypeBuilder:
if spec, ok := query.Spec.(qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]); ok {
for _, agg := range spec.Aggregations {
if agg.MetricName != "" {
@@ -236,7 +238,7 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
}
req.CompositeQuery.Queries[idx].Spec = spec
}
} else if query.Type == qbtypes.QueryTypePromQL {
case qbtypes.QueryTypePromQL:
event.MetricsUsed = true
switch spec := query.Spec.(type) {
case qbtypes.PromQuery:
@@ -247,7 +249,7 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
}
req.CompositeQuery.Queries[idx].Spec = spec
}
} else if query.Type == qbtypes.QueryTypeClickHouseSQL {
case qbtypes.QueryTypeClickHouseSQL:
switch spec := query.Spec.(type) {
case qbtypes.ClickHouseQuery:
if strings.TrimSpace(spec.Query) != "" {
@@ -256,7 +258,7 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
event.TracesUsed = strings.Contains(spec.Query, "signoz_traces")
}
}
} else if query.Type == qbtypes.QueryTypeTraceOperator {
case qbtypes.QueryTypeTraceOperator:
if spec, ok := query.Spec.(qbtypes.QueryBuilderTraceOperator); ok {
if spec.StepInterval.Seconds() == 0 {
spec.StepInterval = qbtypes.Step{
@@ -276,23 +278,9 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
}
}
// Fetch temporality for all metrics at once
var metricTemporality map[string]metrictypes.Temporality
var metricTypes map[string]metrictypes.Type
if len(metricNames) > 0 {
var err error
metricTemporality, metricTypes, err = q.metadataStore.FetchTemporalityAndTypeMulti(ctx, req.Start, req.End, metricNames...)
if err != nil {
q.logger.WarnContext(ctx, "failed to fetch metric temporality", "error", err, "metrics", metricNames)
// Continue without temporality - statement builder will handle unspecified
metricTemporality = make(map[string]metrictypes.Temporality)
metricTypes = make(map[string]metrictypes.Type)
}
q.logger.DebugContext(ctx, "fetched metric temporalities and types", "metric_temporality", metricTemporality, "metric_types", metricTypes)
}
queries := make(map[string]qbtypes.Query)
steps := make(map[string]qbtypes.Step)
missingMetrics := []string{}
for _, query := range req.CompositeQuery.Queries {
var queryName string
@@ -374,15 +362,26 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
queries[spec.Name] = bq
steps[spec.Name] = spec.StepInterval
case qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]:
var metricTemporality map[string]metrictypes.Temporality
var metricTypes map[string]metrictypes.Type
if len(metricNames) > 0 {
var err error
metricTemporality, metricTypes, err = q.metadataStore.FetchTemporalityAndTypeMulti(ctx, req.Start, req.End, metricNames...)
if err != nil {
q.logger.WarnContext(ctx, "failed to fetch metric temporality", "error", err, "metrics", metricNames)
return nil, errors.NewInternalf(errors.CodeInternal, "failed to fetch metrics temporality")
}
q.logger.DebugContext(ctx, "fetched metric temporalities and types", "metric_temporality", metricTemporality, "metric_types", metricTypes)
}
for i := range spec.Aggregations {
if spec.Aggregations[i].MetricName != "" && spec.Aggregations[i].Temporality == metrictypes.Unknown {
if temp, ok := metricTemporality[spec.Aggregations[i].MetricName]; ok && temp != metrictypes.Unknown {
spec.Aggregations[i].Temporality = temp
}
}
// TODO(srikanthccv): warn when the metric is missing
if spec.Aggregations[i].Temporality == metrictypes.Unknown {
spec.Aggregations[i].Temporality = metrictypes.Unspecified
missingMetrics = append(missingMetrics, spec.Aggregations[i].MetricName)
continue
}
if spec.Aggregations[i].MetricName != "" && spec.Aggregations[i].Type == metrictypes.UnspecifiedType {
@@ -409,6 +408,24 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
}
}
}
if len(missingMetrics) > 0 {
lastSeenInfo, _ := q.metadataStore.FetchLastSeenInfoMulti(ctx, missingMetrics...)
lastSeenStr := func(name string) string {
if ts, ok := lastSeenInfo[name]; ok && ts > 0 {
ago := humanize.RelTime(time.UnixMilli(ts), time.Now(), "ago", "from now")
return fmt.Sprintf("%s (last seen %s)", name, ago)
}
return name
}
if len(missingMetrics) == 1 {
return nil, errors.NewNotFoundf(errors.CodeNotFound, "no data found for the metric %s in the query time range", lastSeenStr(missingMetrics[0]))
}
parts := make([]string, len(missingMetrics))
for i, m := range missingMetrics {
parts[i] = lastSeenStr(m)
}
return nil, errors.NewNotFoundf(errors.CodeNotFound, "no data found for the following metrics in the query time range: %s", strings.Join(parts, ", "))
}
qbResp, qbErr := q.run(ctx, orgID, queries, req, steps, event)
if qbResp != nil {
qbResp.QBEvent = event
@@ -663,7 +680,7 @@ func (q *querier) run(
}
// executeWithCache executes a query using the bucket cache
func (q *querier) executeWithCache(ctx context.Context, orgID valuer.UUID, query qbtypes.Query, step qbtypes.Step, noCache bool) (*qbtypes.Result, error) {
func (q *querier) executeWithCache(ctx context.Context, orgID valuer.UUID, query qbtypes.Query, step qbtypes.Step, _ bool) (*qbtypes.Result, error) {
// Get cached data and missing ranges
cachedResult, missingRanges := q.bucketCache.GetMissRanges(ctx, orgID, query, step)

View File

@@ -76,6 +76,21 @@ func TestManager_TestNotification_SendUnmatched_ThresholdRule(t *testing.T) {
alertDataRows := cmock.NewRows(cols, tc.Values)
mock := mockStore.Mock()
// Mock metadata queries for FetchTemporalityAndTypeMulti
// First query: fetchMetricsTemporalityAndType (from signoz_metrics time series table)
metadataCols := []cmock.ColumnType{
{Name: "metric_name", Type: "String"},
{Name: "temporality", Type: "String"},
{Name: "type", Type: "String"},
{Name: "is_monotonic", Type: "Bool"},
}
metadataRows := cmock.NewRows(metadataCols, [][]any{
{"probe_success", metrictypes.Unspecified, metrictypes.GaugeType, false},
})
mock.ExpectQuery("*distributed_time_series_v4*").WithArgs(nil, nil, nil).WillReturnRows(metadataRows)
// Second query: fetchMeterSourceMetricsTemporalityAndType (from signoz_meter table)
emptyMetadataRows := cmock.NewRows(metadataCols, [][]any{})
mock.ExpectQuery("*meter*").WithArgs(nil).WillReturnRows(emptyMetadataRows)
// Generate query arguments for the metric query
evalTime := time.Now().UTC()

View File

@@ -20,6 +20,7 @@ import (
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/gateway"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/identn"
"github.com/SigNoz/signoz/pkg/instrumentation"
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
"github.com/SigNoz/signoz/pkg/modules/user"
@@ -113,6 +114,9 @@ type Config struct {
// User config
User user.Config `mapstructure:"user"`
// IdentN config
IdentN identn.Config `mapstructure:"identn"`
}
// DeprecatedFlags are the flags that are deprecated and scheduled for removal.
@@ -176,6 +180,7 @@ func NewConfig(ctx context.Context, logger *slog.Logger, resolverConfig config.R
metricsexplorer.NewConfigFactory(),
flagger.NewConfigFactory(),
user.NewConfigFactory(),
identn.NewConfigFactory(),
}
conf, err := config.New(ctx, resolverConfig, configFactories)

View File

@@ -22,6 +22,9 @@ import (
"github.com/SigNoz/signoz/pkg/flagger/configflagger"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/global/signozglobal"
"github.com/SigNoz/signoz/pkg/identn"
"github.com/SigNoz/signoz/pkg/identn/apikeyidentn"
"github.com/SigNoz/signoz/pkg/identn/tokenizeridentn"
"github.com/SigNoz/signoz/pkg/modules/authdomain/implauthdomain"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
@@ -271,6 +274,13 @@ func NewTokenizerProviderFactories(cache cache.Cache, sqlstore sqlstore.SQLStore
)
}
func NewIdentNProviderFactories(sqlstore sqlstore.SQLStore, tokenizer tokenizer.Tokenizer) factory.NamedMap[factory.ProviderFactory[identn.IdentN, identn.Config]] {
return factory.MustNewNamedMap(
tokenizeridentn.NewFactory(tokenizer),
apikeyidentn.NewFactory(sqlstore),
)
}
func NewGlobalProviderFactories() factory.NamedMap[factory.ProviderFactory[global.Global, global.Config]] {
return factory.MustNewNamedMap(
signozglobal.NewFactory(),

View File

@@ -17,8 +17,6 @@ import (
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/gateway"
"github.com/SigNoz/signoz/pkg/identn"
"github.com/SigNoz/signoz/pkg/identn/apikeyidentn"
"github.com/SigNoz/signoz/pkg/identn/tokenizeridentn"
"github.com/SigNoz/signoz/pkg/instrumentation"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
@@ -395,9 +393,16 @@ func New(
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, userGetter)
// Initialize identN resolver
tokenizeridentN := tokenizeridentn.New(providerSettings, tokenizer, []string{"Authorization", "Sec-WebSocket-Protocol"})
apikeyIdentN := apikeyidentn.New(providerSettings, sqlstore, []string{"SIGNOZ-API-KEY"})
identNResolver := identn.NewIdentNResolver(providerSettings, tokenizeridentN, apikeyIdentN)
identNFactories := NewIdentNProviderFactories(sqlstore, tokenizer)
identNs := []identn.IdentN{}
for _, identNFactory := range identNFactories.GetInOrder() {
identN, err := identNFactory.New(ctx, providerSettings, config.IdentN)
if err != nil {
return nil, err
}
identNs = append(identNs, identN)
}
identNResolver := identn.NewIdentNResolver(providerSettings, identNs...)
userService := impluser.NewService(providerSettings, impluser.NewStore(sqlstore, providerSettings), modules.User, orgGetter, authz, config.User.Root)

View File

@@ -7,7 +7,7 @@ import (
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
@@ -54,7 +54,7 @@ func (migration *addManagedRoles) Up(ctx context.Context, db *bun.DB) error {
return err
}
managedRoles := []*roletypes.StorableRole{}
managedRoles := []*authtypes.StorableRole{}
for _, orgIDStr := range orgIDs {
orgID, err := valuer.NewUUID(orgIDStr)
if err != nil {
@@ -62,20 +62,20 @@ func (migration *addManagedRoles) Up(ctx context.Context, db *bun.DB) error {
}
// signoz admin
signozAdminRole := roletypes.NewRole(roletypes.SigNozAdminRoleName, roletypes.SigNozAdminRoleDescription, roletypes.RoleTypeManaged, orgID)
managedRoles = append(managedRoles, roletypes.NewStorableRoleFromRole(signozAdminRole))
signozAdminRole := authtypes.NewRole(authtypes.SigNozAdminRoleName, authtypes.SigNozAdminRoleDescription, authtypes.RoleTypeManaged, orgID)
managedRoles = append(managedRoles, authtypes.NewStorableRoleFromRole(signozAdminRole))
// signoz editor
signozEditorRole := roletypes.NewRole(roletypes.SigNozEditorRoleName, roletypes.SigNozEditorRoleDescription, roletypes.RoleTypeManaged, orgID)
managedRoles = append(managedRoles, roletypes.NewStorableRoleFromRole(signozEditorRole))
signozEditorRole := authtypes.NewRole(authtypes.SigNozEditorRoleName, authtypes.SigNozEditorRoleDescription, authtypes.RoleTypeManaged, orgID)
managedRoles = append(managedRoles, authtypes.NewStorableRoleFromRole(signozEditorRole))
// signoz viewer
signozViewerRole := roletypes.NewRole(roletypes.SigNozViewerRoleName, roletypes.SigNozViewerRoleDescription, roletypes.RoleTypeManaged, orgID)
managedRoles = append(managedRoles, roletypes.NewStorableRoleFromRole(signozViewerRole))
signozViewerRole := authtypes.NewRole(authtypes.SigNozViewerRoleName, authtypes.SigNozViewerRoleDescription, authtypes.RoleTypeManaged, orgID)
managedRoles = append(managedRoles, authtypes.NewStorableRoleFromRole(signozViewerRole))
// signoz anonymous
signozAnonymousRole := roletypes.NewRole(roletypes.SigNozAnonymousRoleName, roletypes.SigNozAnonymousRoleDescription, roletypes.RoleTypeManaged, orgID)
managedRoles = append(managedRoles, roletypes.NewStorableRoleFromRole(signozAnonymousRole))
signozAnonymousRole := authtypes.NewRole(authtypes.SigNozAnonymousRoleName, authtypes.SigNozAnonymousRoleDescription, authtypes.RoleTypeManaged, orgID)
managedRoles = append(managedRoles, authtypes.NewStorableRoleFromRole(signozAnonymousRole))
}
if len(managedRoles) > 0 {

View File

@@ -6,7 +6,7 @@ import (
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/oklog/ulid/v2"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
@@ -83,7 +83,7 @@ func (migration *addAnonymousPublicDashboardTransaction) Up(ctx context.Context,
INSERT INTO tuple (store, object_type, object_id, relation, _user, user_type, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, object_type, object_id, relation, _user) DO NOTHING`,
storeID, "metaresource", "organization/"+orgID+"/public-dashboard/*", "read", "role:organization/"+orgID+"/role/"+roletypes.SigNozAnonymousRoleName+"#assignee", "userset", tupleID, now,
storeID, "metaresource", "organization/"+orgID+"/public-dashboard/*", "read", "role:organization/"+orgID+"/role/"+authtypes.SigNozAnonymousRoleName+"#assignee", "userset", tupleID, now,
)
if err != nil {
return err
@@ -102,7 +102,7 @@ func (migration *addAnonymousPublicDashboardTransaction) Up(ctx context.Context,
INSERT INTO changelog (store, object_type, object_id, relation, _user, operation, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
storeID, "metaresource", "organization/"+orgID+"/public-dashboard/*", "read", "role:organization/"+orgID+"/role/"+roletypes.SigNozAnonymousRoleName+"#assignee", "TUPLE_OPERATION_WRITE", tupleID, now,
storeID, "metaresource", "organization/"+orgID+"/public-dashboard/*", "read", "role:organization/"+orgID+"/role/"+authtypes.SigNozAnonymousRoleName+"#assignee", "TUPLE_OPERATION_WRITE", tupleID, now,
)
if err != nil {
return err
@@ -113,7 +113,7 @@ func (migration *addAnonymousPublicDashboardTransaction) Up(ctx context.Context,
INSERT INTO tuple (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, user_type, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation) DO NOTHING`,
storeID, "metaresource", "organization/"+orgID+"/public-dashboard/*", "read", "role", "organization/"+orgID+"/role/"+roletypes.SigNozAnonymousRoleName, "assignee", "userset", tupleID, now,
storeID, "metaresource", "organization/"+orgID+"/public-dashboard/*", "read", "role", "organization/"+orgID+"/role/"+authtypes.SigNozAnonymousRoleName, "assignee", "userset", tupleID, now,
)
if err != nil {
return err
@@ -132,7 +132,7 @@ func (migration *addAnonymousPublicDashboardTransaction) Up(ctx context.Context,
INSERT INTO changelog (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, operation, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
storeID, "metaresource", "organization/"+orgID+"/public-dashboard/*", "read", "role", "organization/"+orgID+"/role/"+roletypes.SigNozAnonymousRoleName, "assignee", 0, tupleID, now,
storeID, "metaresource", "organization/"+orgID+"/public-dashboard/*", "read", "role", "organization/"+orgID+"/role/"+authtypes.SigNozAnonymousRoleName, "assignee", 0, tupleID, now,
)
if err != nil {
return err

View File

@@ -8,8 +8,9 @@ import (
)
var (
IndexTypeUnique = IndexType{s: valuer.NewString("uq")}
IndexTypeIndex = IndexType{s: valuer.NewString("ix")}
IndexTypeUnique = IndexType{s: valuer.NewString("uq")}
IndexTypeIndex = IndexType{s: valuer.NewString("ix")}
IndexTypePartialUnique = IndexType{s: valuer.NewString("puq")}
)
type IndexType struct{ s valuer.String }
@@ -22,6 +23,7 @@ type Index interface {
// The name of the index.
// - Indexes are named as `ix_<table_name>_<column_names>`. The column names are separated by underscores.
// - Unique constraints are named as `uq_<table_name>_<column_names>`. The column names are separated by underscores.
// - Partial unique indexes are named as `puq_<table_name>_<column_names>_<predicate_hash>`.
// The name is autogenerated and should not be set by the user.
Name() string
@@ -133,3 +135,101 @@ func (index *UniqueIndex) ToDropSQL(fmter SQLFormatter) []byte {
return sql
}
type PartialUniqueIndex struct {
TableName TableName
ColumnNames []ColumnName
Where string
name string
}
func (index *PartialUniqueIndex) Name() string {
if index.name != "" {
return index.name
}
var b strings.Builder
b.WriteString(IndexTypePartialUnique.String())
b.WriteString("_")
b.WriteString(string(index.TableName))
b.WriteString("_")
for i, column := range index.ColumnNames {
if i > 0 {
b.WriteString("_")
}
b.WriteString(string(column))
}
b.WriteString("_")
b.WriteString((&whereNormalizer{input: index.Where}).hash())
return b.String()
}
func (index *PartialUniqueIndex) Named(name string) Index {
copyOfColumnNames := make([]ColumnName, len(index.ColumnNames))
copy(copyOfColumnNames, index.ColumnNames)
return &PartialUniqueIndex{
TableName: index.TableName,
ColumnNames: copyOfColumnNames,
Where: index.Where,
name: name,
}
}
func (index *PartialUniqueIndex) IsNamed() bool {
return index.name != ""
}
func (*PartialUniqueIndex) Type() IndexType {
return IndexTypePartialUnique
}
func (index *PartialUniqueIndex) Columns() []ColumnName {
return index.ColumnNames
}
func (index *PartialUniqueIndex) Equals(other Index) bool {
if other.Type() != IndexTypePartialUnique {
return false
}
otherPartial, ok := other.(*PartialUniqueIndex)
if !ok {
return false
}
return index.Name() == other.Name() && slices.Equal(index.Columns(), other.Columns()) && (&whereNormalizer{input: index.Where}).normalize() == (&whereNormalizer{input: otherPartial.Where}).normalize()
}
func (index *PartialUniqueIndex) 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, column := range index.ColumnNames {
if i > 0 {
sql = append(sql, ", "...)
}
sql = fmter.AppendIdent(sql, string(column))
}
sql = append(sql, ") WHERE "...)
sql = append(sql, index.Where...)
return sql
}
func (index *PartialUniqueIndex) ToDropSQL(fmter SQLFormatter) []byte {
sql := []byte{}
sql = append(sql, "DROP INDEX IF EXISTS "...)
sql = fmter.AppendIdent(sql, index.Name())
return sql
}

View File

@@ -38,6 +38,110 @@ func TestIndexToCreateSQL(t *testing.T) {
},
sql: `CREATE UNIQUE INDEX IF NOT EXISTS "my_index" ON "users" ("id", "name", "email")`,
},
{
name: "PartialUnique_1Column",
index: &PartialUniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Where: `"deleted_at" IS NULL`,
},
sql: `CREATE UNIQUE INDEX IF NOT EXISTS "puq_users_email_94610c77" ON "users" ("email") WHERE "deleted_at" IS NULL`,
},
{
name: "PartialUnique_2Columns",
index: &PartialUniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"org_id", "email"},
Where: `"deleted_at" IS NULL`,
},
sql: `CREATE UNIQUE INDEX IF NOT EXISTS "puq_users_org_id_email_94610c77" ON "users" ("org_id", "email") WHERE "deleted_at" IS NULL`,
},
{
name: "PartialUnique_Named",
index: &PartialUniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Where: `"deleted_at" IS NULL`,
name: "my_partial_index",
},
sql: `CREATE UNIQUE INDEX IF NOT EXISTS "my_partial_index" ON "users" ("email") WHERE "deleted_at" IS NULL`,
},
{
name: "PartialUnique_WhereWithParentheses",
index: &PartialUniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Where: `("deleted_at" IS NULL)`,
},
sql: `CREATE UNIQUE INDEX IF NOT EXISTS "puq_users_email_94610c77" ON "users" ("email") WHERE ("deleted_at" IS NULL)`,
},
{
name: "PartialUnique_WhereWithQuotedIdentifier",
index: &PartialUniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Where: `"order" IS NULL`,
},
sql: `CREATE UNIQUE INDEX IF NOT EXISTS "puq_users_email_14c5f5f2" ON "users" ("email") WHERE "order" IS NULL`,
},
{
name: "PartialUnique_WhereWithQuotedLiteral",
index: &PartialUniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Where: `status = 'somewhere'`,
},
sql: `CREATE UNIQUE INDEX IF NOT EXISTS "puq_users_email_9817c709" ON "users" ("email") WHERE status = 'somewhere'`,
},
{
name: "PartialUnique_WhereWith2Columns",
index: &PartialUniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email", "status"},
Where: `email = 'test@example.com' AND status = 'active'`,
},
sql: `CREATE UNIQUE INDEX IF NOT EXISTS "puq_users_email_status_e70e78c3" ON "users" ("email", "status") WHERE email = 'test@example.com' AND status = 'active'`,
},
// postgres docs example
{
name: "PartialUnique_WhereWithPostgresDocsExample_1",
index: &PartialUniqueIndex{
TableName: "access_log",
ColumnNames: []ColumnName{"client_ip"},
Where: `NOT (client_ip > inet '192.168.100.0' AND client_ip < inet '192.168.100.255')`,
},
sql: `CREATE UNIQUE INDEX IF NOT EXISTS "puq_access_log_client_ip_5a596410" ON "access_log" ("client_ip") WHERE NOT (client_ip > inet '192.168.100.0' AND client_ip < inet '192.168.100.255')`,
},
// postgres docs example
{
name: "PartialUnique_WhereWithPostgresDocsExample_2",
index: &PartialUniqueIndex{
TableName: "orders",
ColumnNames: []ColumnName{"order_nr"},
Where: `billed is not true`,
},
sql: `CREATE UNIQUE INDEX IF NOT EXISTS "puq_orders_order_nr_6d31bb0e" ON "orders" ("order_nr") WHERE billed is not true`,
},
// sqlite docs example
{
name: "PartialUnique_WhereWithSqliteDocsExample_1",
index: &PartialUniqueIndex{
TableName: "person",
ColumnNames: []ColumnName{"team_id"},
Where: `is_team_leader`,
},
sql: `CREATE UNIQUE INDEX IF NOT EXISTS "puq_person_team_id_c8604a29" ON "person" ("team_id") WHERE is_team_leader`,
},
// sqlite docs example
{
name: "PartialUnique_WhereWithSqliteDocsExample_2",
index: &PartialUniqueIndex{
TableName: "purchaseorder",
ColumnNames: []ColumnName{"parent_po"},
Where: `parent_po IS NOT NULL`,
},
sql: `CREATE UNIQUE INDEX IF NOT EXISTS "puq_purchaseorder_parent_po_dbe2929d" ON "purchaseorder" ("parent_po") WHERE parent_po IS NOT NULL`,
},
}
for _, testCase := range testCases {
@@ -49,3 +153,109 @@ func TestIndexToCreateSQL(t *testing.T) {
})
}
}
func TestIndexEquals(t *testing.T) {
testCases := []struct {
name string
a Index
b Index
equals bool
}{
{
name: "PartialUnique_Same",
a: &PartialUniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Where: `"deleted_at" IS NULL`,
},
b: &PartialUniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Where: `"deleted_at" IS NULL`,
},
equals: true,
},
{
name: "PartialUnique_NormalizedPostgresWhere",
a: &PartialUniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Where: `"deleted_at" IS NULL`,
},
b: &PartialUniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Where: `(deleted_at IS NULL)`,
},
equals: true,
},
{
name: "PartialUnique_DifferentWhere",
a: &PartialUniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Where: `"deleted_at" IS NULL`,
},
b: &PartialUniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Where: `"active" = true`,
},
equals: false,
},
{
name: "PartialUnique_NotEqual_Unique",
a: &PartialUniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Where: `"deleted_at" IS NULL`,
},
b: &UniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
},
equals: false,
},
{
name: "Unique_NotEqual_PartialUnique",
a: &UniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
},
b: &PartialUniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Where: `"deleted_at" IS NULL`,
},
equals: false,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
assert.Equal(t, testCase.equals, testCase.a.Equals(testCase.b))
})
}
}
func TestPartialUniqueIndexName(t *testing.T) {
a := &PartialUniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Where: `"deleted_at" IS NULL`,
}
b := &PartialUniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Where: `(deleted_at IS NULL)`,
}
c := &PartialUniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Where: `"active" = true`,
}
assert.Equal(t, "puq_users_email_94610c77", a.Name())
assert.Equal(t, a.Name(), b.Name())
assert.NotEqual(t, a.Name(), c.Name())
}

162
pkg/sqlschema/normalizer.go Normal file
View File

@@ -0,0 +1,162 @@
package sqlschema
import (
"fmt"
"hash/fnv"
"strings"
)
type whereNormalizer struct {
input string
}
func (n *whereNormalizer) hash() string {
hasher := fnv.New32a()
_, _ = hasher.Write([]byte(n.normalize()))
return fmt.Sprintf("%08x", hasher.Sum32())
}
func (n *whereNormalizer) normalize() string {
where := strings.TrimSpace(n.input)
where = n.stripOuterParentheses(where)
var output strings.Builder
output.Grow(len(where))
for i := 0; i < len(where); i++ {
switch where[i] {
case ' ', '\t', '\n', '\r':
if output.Len() > 0 {
last := output.String()[output.Len()-1]
if last != ' ' {
output.WriteByte(' ')
}
}
case '\'':
end := n.consumeSingleQuotedLiteral(where, i, &output)
i = end
case '"':
token, end := n.consumeDoubleQuotedToken(where, i)
output.WriteString(token)
i = end
default:
output.WriteByte(where[i])
}
}
return strings.TrimSpace(output.String())
}
func (n *whereNormalizer) stripOuterParentheses(s string) string {
for {
s = strings.TrimSpace(s)
if len(s) < 2 || s[0] != '(' || s[len(s)-1] != ')' || !n.hasWrappingParentheses(s) {
return s
}
s = s[1 : len(s)-1]
}
}
func (n *whereNormalizer) hasWrappingParentheses(s string) bool {
depth := 0
inSingleQuotedLiteral := false
inDoubleQuotedToken := false
for i := 0; i < len(s); i++ {
switch s[i] {
case '\'':
if inDoubleQuotedToken {
continue
}
if inSingleQuotedLiteral && i+1 < len(s) && s[i+1] == '\'' {
i++
continue
}
inSingleQuotedLiteral = !inSingleQuotedLiteral
case '"':
if inSingleQuotedLiteral {
continue
}
if inDoubleQuotedToken && i+1 < len(s) && s[i+1] == '"' {
i++
continue
}
inDoubleQuotedToken = !inDoubleQuotedToken
case '(':
if inSingleQuotedLiteral || inDoubleQuotedToken {
continue
}
depth++
case ')':
if inSingleQuotedLiteral || inDoubleQuotedToken {
continue
}
depth--
if depth == 0 && i != len(s)-1 {
return false
}
}
}
return depth == 0
}
func (n *whereNormalizer) consumeSingleQuotedLiteral(s string, start int, output *strings.Builder) int {
output.WriteByte(s[start])
for i := start + 1; i < len(s); i++ {
output.WriteByte(s[i])
if s[i] == '\'' {
if i+1 < len(s) && s[i+1] == '\'' {
i++
output.WriteByte(s[i])
continue
}
return i
}
}
return len(s) - 1
}
func (n *whereNormalizer) consumeDoubleQuotedToken(s string, start int) (string, int) {
var ident strings.Builder
for i := start + 1; i < len(s); i++ {
if s[i] == '"' {
if i+1 < len(s) && s[i+1] == '"' {
ident.WriteByte('"')
i++
continue
}
if n.isSimpleUnquotedIdentifier(ident.String()) {
return ident.String(), i
}
return s[start : i+1], i
}
ident.WriteByte(s[i])
}
return s[start:], len(s) - 1
}
func (n *whereNormalizer) isSimpleUnquotedIdentifier(s string) bool {
if s == "" || strings.ToLower(s) != s {
return false
}
for i := 0; i < len(s); i++ {
ch := s[i]
if (ch >= 'a' && ch <= 'z') || ch == '_' {
continue
}
if i > 0 && ch >= '0' && ch <= '9' {
continue
}
return false
}
return true
}

View File

@@ -0,0 +1,57 @@
package sqlschema
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestWhereNormalizerNormalize(t *testing.T) {
testCases := []struct {
name string
input string
output string
}{
{
name: "BooleanComparison",
input: `"active" = true`,
output: `active = true`,
},
{
name: "QuotedStringLiteralPreserved",
input: `status = 'somewhere'`,
output: `status = 'somewhere'`,
},
{
name: "EscapedStringLiteralPreserved",
input: `status = 'it''s active'`,
output: `status = 'it''s active'`,
},
{
name: "OuterParenthesesRemoved",
input: `(("deleted_at" IS NULL))`,
output: `deleted_at IS NULL`,
},
{
name: "InnerParenthesesPreserved",
input: `("deleted_at" IS NULL OR ("active" = true AND "status" = 'open'))`,
output: `deleted_at IS NULL OR (active = true AND status = 'open')`,
},
{
name: "MultipleClausesWhitespaceCollapsed",
input: " ( \"deleted_at\" IS NULL \n AND\t\"active\" = true AND status = 'open' ) ",
output: `deleted_at IS NULL AND active = true AND status = 'open'`,
},
{
name: "ComplexBooleanClauses",
input: `NOT ("deleted_at" IS NOT NULL AND ("active" = false OR "status" = 'archived'))`,
output: `NOT (deleted_at IS NOT NULL AND (active = false OR status = 'archived'))`,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
assert.Equal(t, testCase.output, (&whereNormalizer{input: testCase.input}).normalize())
})
}
}

View File

@@ -1146,3 +1146,100 @@ func TestOperatorAlterTable(t *testing.T) {
})
}
}
func TestOperatorDiffIndices(t *testing.T) {
testCases := []struct {
name string
oldIndices []Index
newIndices []Index
expectedSQLs [][]byte
}{
{
name: "UniqueToPartialUnique_DropAndCreate",
oldIndices: []Index{
&UniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
},
},
newIndices: []Index{
&PartialUniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Where: `"deleted_at" IS NULL`,
},
},
expectedSQLs: [][]byte{
[]byte(`DROP INDEX IF EXISTS "uq_users_email"`),
[]byte(`CREATE UNIQUE INDEX IF NOT EXISTS "puq_users_email_94610c77" ON "users" ("email") WHERE "deleted_at" IS NULL`),
},
},
{
name: "PartialUnique_SameWhere_NoOp",
oldIndices: []Index{
&PartialUniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Where: `"deleted_at" IS NULL`,
},
},
newIndices: []Index{
&PartialUniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Where: `"deleted_at" IS NULL`,
},
},
expectedSQLs: [][]byte{},
},
{
name: "PartialUnique_NormalizedWhere_NoOp",
oldIndices: []Index{
&PartialUniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Where: `(deleted_at IS NULL)`,
},
},
newIndices: []Index{
&PartialUniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Where: `"deleted_at" IS NULL`,
},
},
expectedSQLs: [][]byte{},
},
{
name: "PartialUnique_DifferentWhere_DropAndCreate",
oldIndices: []Index{
&PartialUniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Where: `"deleted_at" IS NULL`,
},
},
newIndices: []Index{
&PartialUniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Where: `"active" = true`,
},
},
expectedSQLs: [][]byte{
[]byte(`DROP INDEX IF EXISTS "puq_users_email_94610c77"`),
[]byte(`CREATE UNIQUE INDEX IF NOT EXISTS "puq_users_email_202121f8" ON "users" ("email") WHERE "active" = true`),
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
fmter := NewFormatter(schema.NewNopFormatter().Dialect())
operator := NewOperator(fmter, OperatorSupport{})
actuals := operator.DiffIndices(testCase.oldIndices, testCase.newIndices)
assert.Equal(t, testCase.expectedSQLs, actuals)
})
}
}

View File

@@ -3,6 +3,7 @@ package sqlitesqlschema
import (
"context"
"strconv"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
@@ -114,7 +115,29 @@ func (provider *provider) GetIndices(ctx context.Context, tableName sqlschema.Ta
return nil, err
}
if unique {
if unique && partial {
var indexSQL string
if err := provider.
sqlstore.
BunDB().
NewRaw("SELECT sql FROM sqlite_master WHERE type = 'index' AND name = ?", name).
Scan(ctx, &indexSQL); err != nil {
return nil, err
}
where := extractWhereClause(indexSQL)
index := &sqlschema.PartialUniqueIndex{
TableName: tableName,
ColumnNames: columns,
Where: where,
}
if index.Name() == name {
indices = append(indices, index)
} else {
indices = append(indices, index.Named(name))
}
} else if unique {
index := &sqlschema.UniqueIndex{
TableName: tableName,
ColumnNames: columns,
@@ -148,3 +171,73 @@ func (provider *provider) ToggleFKEnforcement(ctx context.Context, db bun.IDB, o
return errors.NewInternalf(errors.CodeInternal, "foreign_keys(actual: %s, expected: %s), maybe a transaction is in progress?", strconv.FormatBool(val), strconv.FormatBool(on))
}
func extractWhereClause(sql string) string {
lastWhere := -1
inSingleQuotedLiteral := false
inDoubleQuotedIdentifier := false
inBacktickQuotedIdentifier := false
inBracketQuotedIdentifier := false
for i := 0; i < len(sql); i++ {
switch sql[i] {
case '\'':
if inDoubleQuotedIdentifier || inBacktickQuotedIdentifier || inBracketQuotedIdentifier {
continue
}
if inSingleQuotedLiteral && i+1 < len(sql) && sql[i+1] == '\'' {
i++
continue
}
inSingleQuotedLiteral = !inSingleQuotedLiteral
case '"':
if inSingleQuotedLiteral || inBacktickQuotedIdentifier || inBracketQuotedIdentifier {
continue
}
if inDoubleQuotedIdentifier && i+1 < len(sql) && sql[i+1] == '"' {
i++
continue
}
inDoubleQuotedIdentifier = !inDoubleQuotedIdentifier
case '`':
if inSingleQuotedLiteral || inDoubleQuotedIdentifier || inBracketQuotedIdentifier {
continue
}
inBacktickQuotedIdentifier = !inBacktickQuotedIdentifier
case '[':
if inSingleQuotedLiteral || inDoubleQuotedIdentifier || inBacktickQuotedIdentifier || inBracketQuotedIdentifier {
continue
}
inBracketQuotedIdentifier = true
case ']':
if inBracketQuotedIdentifier {
inBracketQuotedIdentifier = false
}
}
if inSingleQuotedLiteral || inDoubleQuotedIdentifier || inBacktickQuotedIdentifier || inBracketQuotedIdentifier {
continue
}
if strings.EqualFold(sql[i:min(i+5, len(sql))], "WHERE") &&
(i == 0 || !isSQLiteIdentifierChar(sql[i-1])) &&
(i+5 == len(sql) || !isSQLiteIdentifierChar(sql[i+5])) {
lastWhere = i
i += 4
}
}
if lastWhere == -1 {
return ""
}
return strings.TrimSpace(sql[lastWhere+len("WHERE"):])
}
func isSQLiteIdentifierChar(ch byte) bool {
return (ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') ||
ch == '_'
}

View File

@@ -0,0 +1,52 @@
package sqlitesqlschema
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestExtractWhereClause(t *testing.T) {
testCases := []struct {
name string
sql string
where string
}{
{
name: "UppercaseWhere",
sql: `CREATE UNIQUE INDEX "idx" ON "users" ("email") WHERE "deleted_at" IS NULL`,
where: `"deleted_at" IS NULL`,
},
{
name: "LowercaseWhere",
sql: `CREATE UNIQUE INDEX "idx" ON "users" ("email") where "deleted_at" IS NULL`,
where: `"deleted_at" IS NULL`,
},
{
name: "NewlineBeforeWhere",
sql: "CREATE UNIQUE INDEX \"idx\" ON \"users\" (\"email\")\nWHERE \"deleted_at\" IS NULL",
where: `"deleted_at" IS NULL`,
},
{
name: "ExtraWhitespace",
sql: "CREATE UNIQUE INDEX \"idx\" ON \"users\" (\"email\") \n \t where \"deleted_at\" IS NULL ",
where: `"deleted_at" IS NULL`,
},
{
name: "WhereInStringLiteral",
sql: `CREATE UNIQUE INDEX "idx" ON "users" ("email") WHERE status = 'somewhere'`,
where: `status = 'somewhere'`,
},
{
name: "BooleanLiteral",
sql: `CREATE UNIQUE INDEX "idx" ON "users" ("email") WHERE active = true`,
where: `active = true`,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
assert.Equal(t, testCase.where, extractWhereClause(testCase.sql))
})
}
}

View File

@@ -2,15 +2,17 @@ package telemetrylogs
import (
"context"
"slices"
"strings"
"testing"
"time"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/querybuilder/resourcefilter"
"github.com/SigNoz/signoz/pkg/telemetrystore/chdbtelemetrystore"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes/telemetrytypestest"
"github.com/stretchr/testify/require"
)
@@ -807,30 +809,44 @@ func TestStatementBuilderListQueryBodyMessage(t *testing.T) {
}
}
func enableBodyJSONQuery(_ *testing.T) {
querybuilder.BodyJSONQueryEnabled = true
}
func buildTestTelemetryMetadataStore(t *testing.T, promotedPaths ...string) *telemetrytypestest.MockMetadataStore {
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
func disableBodyJSONQuery(_ *testing.T) {
querybuilder.BodyJSONQueryEnabled = false
types, _ := telemetrytypes.TestJSONTypeSet()
for path, jsonTypes := range types {
promoted := false
split := strings.Split(path, telemetrytypes.ArraySep)
if path == "message" {
promoted = true
} else if slices.Contains(promotedPaths, split[0]) {
promoted = true
}
// Create a TelemetryFieldKey for each JSONDataType for this path
// Since a path can have multiple types, we create one key per type
for _, jsonType := range jsonTypes {
key := &telemetrytypes.TelemetryFieldKey{
Name: path,
Signal: telemetrytypes.SignalLogs,
FieldContext: telemetrytypes.FieldContextBody,
FieldDataType: telemetrytypes.MappingJSONDataTypeToFieldDataType[jsonType],
JSONDataType: &jsonType,
Materialized: promoted,
}
err := key.SetJSONAccessPlan(telemetrytypes.JSONColumnMetadata{
BaseColumn: LogsV2BodyJSONColumn,
PromotedColumn: LogsV2BodyPromotedColumn,
}, types)
require.NoError(t, err)
mockMetadataStore.SetKey(key)
}
}
return mockMetadataStore
}
func buildJSONTestStatementBuilder(t *testing.T, promotedPaths ...string) *logQueryStatementBuilder {
t.Helper()
provider, cleanup, err := chdbtelemetrystore.New()
require.NoError(t, err)
t.Cleanup(cleanup)
ctx := context.Background()
types, _ := telemetrytypes.TestJSONTypeSet()
require.NoError(t, provider.SeedBodyJSONPaths(ctx, types))
// "message" is always promoted in these tests.
allPromoted := append([]string{"message"}, promotedPaths...)
require.NoError(t, provider.SeedPromotedPaths(ctx, allPromoted...))
metadataStore := chdbtelemetrystore.NewChdbMetadataStore(provider)
mockMetadataStore := buildTestTelemetryMetadataStore(t, promotedPaths...)
fm := NewFieldMapper()
cb := NewConditionBuilder(fm)
@@ -839,14 +855,14 @@ func buildJSONTestStatementBuilder(t *testing.T, promotedPaths ...string) *logQu
instrumentationtest.New().ToProviderSettings(),
fm,
cb,
metadataStore,
mockMetadataStore,
DefaultFullTextColumn,
GetBodyJSONKey,
)
return NewLogQueryStatementBuilder(
statementBuilder := NewLogQueryStatementBuilder(
instrumentationtest.New().ToProviderSettings(),
metadataStore,
mockMetadataStore,
fm,
cb,
resourceFilterStmtBuilder,
@@ -854,4 +870,27 @@ func buildJSONTestStatementBuilder(t *testing.T, promotedPaths ...string) *logQu
DefaultFullTextColumn,
GetBodyJSONKey,
)
return statementBuilder
}
func testAddIndexedPaths(t *testing.T, statementBuilder *logQueryStatementBuilder, telemetryFieldKeys ...*telemetrytypes.TelemetryFieldKey) {
mockMetadataStore := statementBuilder.metadataStore.(*telemetrytypestest.MockMetadataStore)
for _, key := range telemetryFieldKeys {
if strings.Contains(key.Name, telemetrytypes.ArraySep) || strings.Contains(key.Name, telemetrytypes.ArrayAnyIndex) {
t.Fatalf("array paths are not supported: %s", key.Name)
}
for _, storedKey := range mockMetadataStore.KeysMap[key.Name] {
storedKey.Indexes = append(storedKey.Indexes, key.Indexes...)
}
}
}
func enableBodyJSONQuery(_ *testing.T) {
querybuilder.BodyJSONQueryEnabled = true
}
func disableBodyJSONQuery(_ *testing.T) {
querybuilder.BodyJSONQueryEnabled = false
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/SigNoz/signoz-otel-collector/constants"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/telemetrylogs"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
@@ -251,13 +252,13 @@ func (t *telemetryMetaStore) getJSONPathIndexes(ctx context.Context, paths ...st
return cleanIndexes, nil
}
func buildListLogsJSONIndexesQuery(cluster, logsDBName, logsV2LocalTblName string, filters ...string) (string, []any) {
func buildListLogsJSONIndexesQuery(cluster string, filters ...string) (string, []any) {
sb := sqlbuilder.Select(
"name", "type_full", "expr", "granularity",
).From(fmt.Sprintf("clusterAllReplicas('%s', %s)", cluster, SkipIndexTableName))
sb.Where(sb.Equal("database", logsDBName))
sb.Where(sb.Equal("table", logsV2LocalTblName))
sb.Where(sb.Equal("database", telemetrylogs.DBName))
sb.Where(sb.Equal("table", telemetrylogs.LogsV2LocalTableName))
sb.Where(sb.Or(
sb.ILike("expr", fmt.Sprintf("%%%s%%", querybuilder.FormatValueForContains(constants.BodyV2ColumnPrefix))),
sb.ILike("expr", fmt.Sprintf("%%%s%%", querybuilder.FormatValueForContains(constants.BodyPromotedColumnPrefix))),
@@ -274,7 +275,7 @@ func buildListLogsJSONIndexesQuery(cluster, logsDBName, logsV2LocalTblName strin
func (t *telemetryMetaStore) ListLogsJSONIndexes(ctx context.Context, filters ...string) (map[string][]schemamigrator.Index, error) {
ctx = withTelemetryContext(ctx, "ListLogsJSONIndexes")
query, args := buildListLogsJSONIndexesQuery(t.telemetrystore.Cluster(), t.logsDBName, logsV2LocalTableName, filters...)
query, args := buildListLogsJSONIndexesQuery(t.telemetrystore.Cluster(), filters...)
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
if err != nil {
return nil, errors.WrapInternalf(err, CodeFailLoadLogsJSONIndexes, "failed to load string indexed columns")
@@ -316,12 +317,12 @@ func (t *telemetryMetaStore) ListJSONValues(ctx context.Context, path string, li
}
if promoted {
path = constants.BodyPromotedColumnPrefix + path
path = telemetrylogs.BodyPromotedColumnPrefix + path
} else {
path = constants.BodyV2ColumnPrefix + path
path = telemetrylogs.BodyJSONColumnPrefix + path
}
from := fmt.Sprintf("%s.%s", t.logsDBName, t.logsV2TblName)
from := fmt.Sprintf("%s.%s", telemetrylogs.DBName, telemetrylogs.LogsV2TableName)
colExpr := func(typ telemetrytypes.JSONDataType) string {
return fmt.Sprintf("dynamicElement(%s, '%s')", path, typ.StringValue())
}
@@ -470,7 +471,7 @@ func (t *telemetryMetaStore) IsPathPromoted(ctx context.Context, path string) (b
split := strings.Split(path, telemetrytypes.ArraySep)
pathSegment := split[0]
query := fmt.Sprintf("SELECT 1 FROM %s.%s WHERE signal = ? AND column_name = ? AND field_context = ? AND field_name = ? LIMIT 1", DBName, PromotedPathsTableName)
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, telemetrytypes.SignalLogs, constants.BodyPromotedColumn, telemetrytypes.FieldContextBody, pathSegment)
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, telemetrytypes.SignalLogs, telemetrylogs.LogsV2BodyPromotedColumn, telemetrytypes.FieldContextBody, pathSegment)
if err != nil {
return false, errors.WrapInternalf(err, CodeFailCheckPathPromoted, "failed to check if path %s is promoted", path)
}
@@ -485,7 +486,7 @@ func (t *telemetryMetaStore) GetPromotedPaths(ctx context.Context, paths ...stri
sb := sqlbuilder.Select("field_name").From(fmt.Sprintf("%s.%s", DBName, PromotedPathsTableName))
conditions := []string{
sb.Equal("signal", telemetrytypes.SignalLogs),
sb.Equal("column_name", constants.BodyPromotedColumn),
sb.Equal("column_name", telemetrylogs.LogsV2BodyPromotedColumn),
sb.Equal("field_context", telemetrytypes.FieldContextBody),
sb.NotEqual("field_name", "__all__"),
}
@@ -521,8 +522,8 @@ func (t *telemetryMetaStore) GetPromotedPaths(ctx context.Context, paths ...stri
// TODO(Piyush): Remove this function
func CleanPathPrefixes(path string) string {
path = strings.TrimPrefix(path, telemetrytypes.BodyJSONStringSearchPrefix)
path = strings.TrimPrefix(path, constants.BodyV2ColumnPrefix)
path = strings.TrimPrefix(path, constants.BodyPromotedColumnPrefix)
path = strings.TrimPrefix(path, telemetrylogs.BodyJSONColumnPrefix)
path = strings.TrimPrefix(path, telemetrylogs.BodyPromotedColumnPrefix)
return path
}
@@ -542,7 +543,7 @@ func (t *telemetryMetaStore) PromotePaths(ctx context.Context, paths ...string)
if trimmed == "" {
continue
}
if err := batch.Append(telemetrytypes.SignalLogs, constants.BodyPromotedColumn, "JSON()", telemetrytypes.FieldContextBody, trimmed, 0, releaseTime); err != nil {
if err := batch.Append(telemetrytypes.SignalLogs, telemetrylogs.LogsV2BodyPromotedColumn, "JSON()", telemetrytypes.FieldContextBody, trimmed, 0, releaseTime); err != nil {
_ = batch.Abort()
return errors.WrapInternalf(err, CodeFailedToAppendPath, "failed to append path")
}

View File

@@ -140,7 +140,7 @@ func TestBuildListLogsJSONIndexesQuery(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
query, args := buildListLogsJSONIndexesQuery(tc.cluster, telemetrylogs.DBName, telemetrylogs.LogsV2LocalTableName, tc.filters...)
query, args := buildListLogsJSONIndexesQuery(tc.cluster, tc.filters...)
require.Equal(t, tc.expectedSQL, query)
require.Equal(t, tc.expectedArgs, args)

View File

@@ -1,6 +0,0 @@
package telemetrymetadata
// logsV2LocalTableName is the local (non-distributed) ClickHouse table for logs v2.
// Defined here instead of importing telemetrylogs to avoid an import cycle:
// telemetrylogs tests → chdbtelemetrystoretest → telemetrymetadata → telemetrylogs.
const logsV2LocalTableName = "logs_v2"

View File

@@ -1928,3 +1928,37 @@ func (t *telemetryMetaStore) GetFirstSeenFromMetricMetadata(ctx context.Context,
return result, nil
}
func (t *telemetryMetaStore) FetchLastSeenInfoMulti(ctx context.Context, metricNames ...string) (map[string]int64, error) {
sb := sqlbuilder.Select(
"metric_name",
"max(unix_milli)",
).
From(t.metricsDBName + "." + telemetrymetrics.TimeseriesV4TableName)
sb.Where(sb.In("metric_name", metricNames))
sb.GroupBy("metric_name")
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
t.logger.DebugContext(ctx, "fetching metric last seen timestamp", "query", query, "args", args)
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to fetch metric last seen info")
}
defer rows.Close()
lastSeenInfo := make(map[string]int64)
for rows.Next() {
var metricName string
var unix_milli int64
if err := rows.Scan(&metricName, &unix_milli); err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to scan last seen info result")
}
lastSeenInfo[metricName] = unix_milli
}
if err := rows.Err(); err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "error iterating over metrics temporality rows")
}
return lastSeenInfo, nil
}

View File

@@ -1,120 +0,0 @@
//go:build chdb
// Package chdbtelemetrystoretest provides central test builder functions backed by
// an in-process chdb session. These builders are used across multiple signal packages
// to avoid import cycles: telemetrymetadata previously imported telemetrylogs, which
// would create a cycle if telemetrylogs tests tried to use telemetrymetadata.
// With that dependency removed, this package can safely import both.
package chdbtelemetrystoretest
import (
"context"
"fmt"
"strings"
"testing"
"time"
otelcollectorconstants "github.com/SigNoz/signoz-otel-collector/constants"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/telemetrymetadata"
"github.com/SigNoz/signoz/pkg/telemetrystore/chdbtelemetrystore"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/stretchr/testify/require"
)
// Logs table name constants mirroring telemetrylogs — kept here to avoid importing
// that package (which would create a cycle when telemetrylogs tests import this package).
const (
logsDBName = "signoz_logs"
logsV2TblName = "distributed_logs_v2"
logsTagAttrTblName = "distributed_tag_attributes_v2"
logAttrKeysTblName = "distributed_logs_attribute_keys"
logResKeysTblName = "distributed_logs_resource_keys"
)
// NewLogsMetadataStore creates a chdb-backed MetadataStore seeded from the provided
// TelemetryFieldKeys. Body-context keys are inserted into distributed_json_path_types;
// keys with Materialized=true have their root path inserted into the column-evolution
// metadata table so the store treats them as promoted.
// The returned cleanup function must be called (typically via t.Cleanup).
func NewLogsMetadataStore(t *testing.T, keys ...*telemetrytypes.TelemetryFieldKey) (telemetrytypes.MetadataStore, func()) {
t.Helper()
provider, cleanup, err := chdbtelemetrystore.New()
require.NoError(t, err)
ctx := context.Background()
require.NoError(t, seedFromFieldKeys(ctx, provider, keys))
store := telemetrymetadata.NewTelemetryMetaStore(
instrumentationtest.New().ToProviderSettings(),
provider,
"", "", "", "", // traces (unused in logs tests)
"", "", // metrics (unused in logs tests)
"", "", // meter (unused in logs tests)
logsDBName,
logsV2TblName,
logsTagAttrTblName,
logAttrKeysTblName,
logResKeysTblName,
telemetrymetadata.DBName,
telemetrymetadata.AttributesMetadataLocalTableName,
)
return store, cleanup
}
// seedFromFieldKeys inserts body-JSON path/type rows and promoted-path rows derived
// from the given keys into the chdb session backing provider.
func seedFromFieldKeys(ctx context.Context, provider *chdbtelemetrystore.Provider, keys []*telemetrytypes.TelemetryFieldKey) error {
lastSeen := uint64(time.Now().UnixNano())
releaseTime := time.Now().UnixNano()
conn := provider.ClickhouseDB()
promotedPaths := map[string]bool{}
for _, key := range keys {
if key.FieldContext != telemetrytypes.FieldContextBody || key.JSONDataType == nil {
continue
}
// Insert into distributed_json_path_types
query := fmt.Sprintf(
"INSERT INTO %s.%s (%s, %s, %s) VALUES (?, ?, ?)",
otelcollectorconstants.SignozMetadataDB,
otelcollectorconstants.DistributedPathTypesTable,
otelcollectorconstants.PathTypesTablePathColumn,
otelcollectorconstants.PathTypesTableTypeColumn,
otelcollectorconstants.PathTypesTableLastSeenColumn,
)
if err := conn.Exec(ctx, query, key.Name, key.JSONDataType.StringValue(), lastSeen); err != nil {
return fmt.Errorf("seedFromFieldKeys: insert path %s/%s: %w", key.Name, key.JSONDataType.StringValue(), err)
}
if key.Materialized {
rootPath := strings.Split(key.Name, telemetrytypes.ArraySep)[0]
promotedPaths[rootPath] = true
}
}
for path := range promotedPaths {
query := fmt.Sprintf(
"INSERT INTO %s.%s (signal, column_name, column_type, field_context, field_name, version, release_time) VALUES (?, ?, ?, ?, ?, ?, ?)",
telemetrymetadata.DBName,
telemetrymetadata.PromotedPathsTableName,
)
if err := conn.Exec(ctx, query,
telemetrytypes.SignalLogs,
otelcollectorconstants.BodyPromotedColumn,
"JSON()",
telemetrytypes.FieldContextBody,
path,
0,
releaseTime,
); err != nil {
return fmt.Errorf("seedFromFieldKeys: insert promoted path %s: %w", path, err)
}
}
return nil
}

View File

@@ -1,51 +0,0 @@
//go:build !chdb
package chdbtelemetrystoretest
import (
"strings"
"testing"
otelcollectorconstants "github.com/SigNoz/signoz-otel-collector/constants"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes/telemetrytypestest"
)
// NewLogsMetadataStore returns a MockMetadataStore populated from the provided
// TelemetryFieldKeys. A type-cache is built from the keys so that SetJSONAccessPlan
// can be resolved for each body-context key before it is stored.
// The returned cleanup function is a no-op (nothing to tear down for an in-memory store).
func NewLogsMetadataStore(t *testing.T, keys ...*telemetrytypes.TelemetryFieldKey) (telemetrytypes.MetadataStore, func()) {
t.Helper()
mockStore := telemetrytypestest.NewMockMetadataStore()
// Build type-cache from the incoming keys so SetJSONAccessPlan can resolve
// parent-path array types (used by nested / array paths).
typeCache := make(map[string][]telemetrytypes.JSONDataType)
for _, key := range keys {
if key.JSONDataType != nil {
typeCache[key.Name] = append(typeCache[key.Name], *key.JSONDataType)
}
}
for _, key := range keys {
if key.FieldContext == telemetrytypes.FieldContextBody && key.JSONDataType != nil {
if err := key.SetJSONAccessPlan(telemetrytypes.JSONColumnMetadata{
BaseColumn: otelcollectorconstants.BodyV2Column,
PromotedColumn: otelcollectorconstants.BodyPromotedColumn,
}, typeCache); err != nil {
t.Fatalf("NewLogsMetadataStore: SetJSONAccessPlan for %q: %v", key.Name, err)
}
}
if key.Materialized {
rootPath := strings.Split(key.Name, telemetrytypes.ArraySep)[0]
mockStore.PromotedPathsMap[rootPath] = true
}
mockStore.SetKey(key)
}
return mockStore, func() {}
}

View File

@@ -1,140 +0,0 @@
//go:build chdb
package chdbtelemetrystore
import (
"context"
"fmt"
"regexp"
"strings"
"github.com/ClickHouse/clickhouse-go/v2"
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
chdb "github.com/chdb-io/chdb-go/chdb"
"github.com/huandu/go-sqlbuilder"
)
// clusterAllReplicasRe matches clusterAllReplicas('<cluster>', <table>) and captures
// the table expression so we can rewrite it for chdb's single-node context.
var clusterAllReplicasRe = regexp.MustCompile(`(?i)clusterAllReplicas\('[^']*',\s*([^)]+)\)`)
// rewriteClusterAllReplicas strips the clusterAllReplicas wrapper from a query,
// replacing it with a direct table reference. This lets single-node chdb sessions
// execute queries originally written for a multi-node ClickHouse cluster.
func rewriteClusterAllReplicas(query string) string {
return clusterAllReplicasRe.ReplaceAllStringFunc(query, func(match string) string {
sub := clusterAllReplicasRe.FindStringSubmatch(match)
if len(sub) < 2 {
return match
}
return strings.TrimSpace(sub[1])
})
}
// interpolateArgs substitutes ? placeholders in query using the ClickHouse SQL flavor
// from go-sqlbuilder — the same mechanism used by chdb's own database/sql driver.
func interpolateArgs(query string, args []any) (string, error) {
if len(args) == 0 {
return query, nil
}
return sqlbuilder.ClickHouse.Interpolate(query, args)
}
// chdbConn wraps a chdb Session and exposes it as a clickhouse.Conn.
// Exec, Select, Query, and QueryRow execute queries for real via chdb.
// The remaining interface methods are lightweight stubs sufficient for testing.
type chdbConn struct {
session *chdb.Session
}
var _ clickhouse.Conn = (*chdbConn)(nil)
func (c *chdbConn) Contributors() []string { return nil }
func (c *chdbConn) ServerVersion() (*driver.ServerVersion, error) {
return &driver.ServerVersion{DisplayName: "chdb"}, nil
}
func (c *chdbConn) Ping(_ context.Context) error { return nil }
func (c *chdbConn) Stats() driver.Stats { return driver.Stats{} }
func (c *chdbConn) Close() error {
c.session.Close()
return nil
}
func (c *chdbConn) AsyncInsert(ctx context.Context, query string, _ bool, args ...any) error {
return c.Exec(ctx, query, args...)
}
func (c *chdbConn) PrepareBatch(_ context.Context, _ string, _ ...driver.PrepareBatchOption) (driver.Batch, error) {
return nil, fmt.Errorf("chdbConn: PrepareBatch not implemented")
}
// Exec executes a DDL or DML statement (CREATE TABLE, INSERT, DROP, …) via chdb.
// Any result set is discarded; only errors are surfaced.
func (c *chdbConn) Exec(_ context.Context, query string, args ...any) error {
query = rewriteClusterAllReplicas(query)
compiled, err := interpolateArgs(query, args)
if err != nil {
return fmt.Errorf("chdbConn: Exec: interpolate args: %w", err)
}
result, err := c.session.Query(compiled, "CSV")
if err != nil {
return fmt.Errorf("chdbConn: Exec: %w", err)
}
defer result.Free()
return result.Error()
}
// Select executes query and scans all result rows into dest.
// dest must be a pointer to a slice of structs or maps.
//
// Struct fields are matched to ClickHouse columns using the following priority:
// 1. `ch:"<column>"` struct tag
// 2. `json:"<column>"` struct tag
// 3. Lowercased field name
func (c *chdbConn) Select(_ context.Context, dest any, query string, args ...any) error {
query = rewriteClusterAllReplicas(query)
compiled, err := interpolateArgs(query, args)
if err != nil {
return fmt.Errorf("chdbConn: Select: interpolate args: %w", err)
}
result, err := c.session.Query(compiled, "JSONCompact")
if err != nil {
return fmt.Errorf("chdbConn: Select: %w", err)
}
defer result.Free()
if err := result.Error(); err != nil {
return err
}
return scanJSONCompactIntoSlice(result.String(), dest)
}
// Query executes query and returns a Rows iterator.
func (c *chdbConn) Query(_ context.Context, query string, args ...any) (driver.Rows, error) {
query = rewriteClusterAllReplicas(query)
compiled, err := interpolateArgs(query, args)
if err != nil {
return nil, fmt.Errorf("chdbConn: Query: interpolate args: %w", err)
}
result, err := c.session.Query(compiled, "JSONCompact")
if err != nil {
return nil, fmt.Errorf("chdbConn: Query: %w", err)
}
if err := result.Error(); err != nil {
result.Free()
return nil, err
}
return newChdbRows(result)
}
// QueryRow executes query and returns a single Row.
func (c *chdbConn) QueryRow(ctx context.Context, query string, args ...any) driver.Row {
rows, err := c.Query(ctx, query, args...)
if err != nil {
return &chdbRow{err: err}
}
return &chdbRow{rows: rows.(*chdbRows)}
}

View File

@@ -1,177 +0,0 @@
//go:build chdb
package chdbtelemetrystore
import (
"encoding/json"
"fmt"
"reflect"
"strings"
)
// jsonCompactResult is the top-level structure of ClickHouse's JSONCompact output format.
type jsonCompactResult struct {
Meta []jsonMeta `json:"meta"`
Data [][]json.RawMessage `json:"data"`
}
type jsonMeta struct {
Name string `json:"name"`
Type string `json:"type"`
}
// scanJSONCompactIntoSlice parses a JSONCompact response and appends rows into dest
// (must be a pointer to a slice of structs or maps).
func scanJSONCompactIntoSlice(jsonStr string, dest any) error {
if strings.TrimSpace(jsonStr) == "" {
return nil
}
var jr jsonCompactResult
if err := json.Unmarshal([]byte(jsonStr), &jr); err != nil {
return fmt.Errorf("chdbConn: Select: parse response: %w", err)
}
destVal := reflect.ValueOf(dest)
if destVal.Kind() != reflect.Ptr || destVal.Elem().Kind() != reflect.Slice {
return fmt.Errorf("chdbConn: Select: dest must be a pointer to a slice, got %T", dest)
}
sliceVal := destVal.Elem()
elemType := sliceVal.Type().Elem()
for _, row := range jr.Data {
elem := reflect.New(elemType).Elem()
if err := scanRowIntoValue(jr.Meta, row, elem); err != nil {
return err
}
sliceVal.Set(reflect.Append(sliceVal, elem))
}
return nil
}
// scanRowIntoValue fills a struct or map Value from a single JSONCompact data row.
func scanRowIntoValue(meta []jsonMeta, row []json.RawMessage, elem reflect.Value) error {
switch elem.Kind() {
case reflect.Struct:
for i, m := range meta {
if i >= len(row) {
break
}
field := findStructField(elem, m.Name)
if !field.IsValid() {
continue
}
if err := unmarshalIntoField(row[i], field); err != nil {
return fmt.Errorf("column %q: %w", m.Name, err)
}
}
case reflect.Map:
if elem.IsNil() {
elem.Set(reflect.MakeMap(elem.Type()))
}
for i, m := range meta {
if i >= len(row) {
break
}
var v any
if err := json.Unmarshal(row[i], &v); err != nil {
return err
}
elem.SetMapIndex(reflect.ValueOf(m.Name), reflect.ValueOf(v))
}
default:
return fmt.Errorf("chdbConn: Select: unsupported element kind %s", elem.Kind())
}
return nil
}
// findStructField returns the reflect.Value of the struct field corresponding to colName.
// Priority: `ch` tag → `json` tag → lowercased field name.
func findStructField(structVal reflect.Value, colName string) reflect.Value {
t := structVal.Type()
colLower := strings.ToLower(colName)
for i := range t.NumField() {
f := t.Field(i)
if tag, _, _ := strings.Cut(f.Tag.Get("ch"), ","); tag == colName {
return structVal.Field(i)
}
if tag, _, _ := strings.Cut(f.Tag.Get("json"), ","); tag == colName {
return structVal.Field(i)
}
if strings.ToLower(f.Name) == colLower {
return structVal.Field(i)
}
}
return reflect.Value{}
}
// unmarshalIntoField deserializes raw JSON into field, performing numeric conversions
// needed for ClickHouse integer types (UInt64, Int64, …).
func unmarshalIntoField(raw json.RawMessage, field reflect.Value) error {
dec := json.NewDecoder(strings.NewReader(string(raw)))
dec.UseNumber()
var v any
if err := dec.Decode(&v); err != nil {
return err
}
return assignToField(field, v)
}
// assignToField converts src (from json.Decoder with UseNumber) and assigns it to field.
func assignToField(field reflect.Value, src any) error {
if src == nil {
field.Set(reflect.Zero(field.Type()))
return nil
}
if num, ok := src.(json.Number); ok {
switch field.Kind() {
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
n, err := num.Int64()
if err != nil {
return err
}
field.SetUint(uint64(n))
return nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
n, err := num.Int64()
if err != nil {
return err
}
field.SetInt(n)
return nil
case reflect.Float32, reflect.Float64:
n, err := num.Float64()
if err != nil {
return err
}
field.SetFloat(n)
return nil
case reflect.String:
field.SetString(num.String())
return nil
}
}
// Handle []interface{} → []T conversions (ClickHouse arrays decoded from JSON).
if srcSlice, ok := src.([]interface{}); ok && field.Kind() == reflect.Slice {
result := reflect.MakeSlice(field.Type(), len(srcSlice), len(srcSlice))
for i, item := range srcSlice {
if err := assignToField(result.Index(i), item); err != nil {
return fmt.Errorf("slice element %d: %w", i, err)
}
}
field.Set(result)
return nil
}
srcVal := reflect.ValueOf(src)
if srcVal.Type().AssignableTo(field.Type()) {
field.Set(srcVal)
return nil
}
if srcVal.Type().ConvertibleTo(field.Type()) {
field.Set(srcVal.Convert(field.Type()))
return nil
}
return fmt.Errorf("cannot assign %T to %s", src, field.Type())
}

View File

@@ -1,109 +0,0 @@
//go:build chdb
package chdbtelemetrystore
import (
"fmt"
schemamigrator "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
chdb "github.com/chdb-io/chdb-go/chdb"
)
// runMigrations applies the full signoz-otel-collector logs schema against the given
// chdb session. It mirrors the same migration set that the collector runs on a real
// ClickHouse cluster (CustomRetentionLogsMigrations + LogsMigrationsV2), with the
// following chdb-specific adaptations:
//
// - CREATE DATABASE statements are prepended so the tables have a home.
// - Distributed engine tables are replaced with MergeTree ORDER BY tuple() so
// every "distributed_*" table is a real, writable table in single-node chdb.
// - Operations that don't make sense without a cluster (TTL materialisation,
// MATERIALIZE COLUMN, MODIFY SETTINGS with serialisation keys) are skipped.
func runMigrations(session *chdb.Session) error {
// Ensure databases exist before any table DDL.
for _, db := range []string{"signoz_logs", "signoz_metadata"} {
if err := execSQL(session, fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", db)); err != nil {
return fmt.Errorf("create database %s: %w", db, err)
}
}
migrationSets := [][]schemamigrator.SchemaMigrationRecord{
schemamigrator.CustomRetentionLogsMigrations,
schemamigrator.MetadataMigrations,
schemamigrator.LogsMigrationsV2,
}
for _, set := range migrationSets {
for _, record := range set {
for _, op := range record.UpItems {
sql, skip := toChdbSQL(op)
if skip {
continue
}
if err := execSQL(session, sql); err != nil {
return fmt.Errorf("migration %d: %w", record.MigrationID, err)
}
}
}
}
return nil
}
// toChdbSQL converts a schemamigrator.Operation to a chdb-compatible SQL string.
// Returns (sql, skip=true) for operations that should be omitted in a single-node
// chdb context.
func toChdbSQL(op schemamigrator.Operation) (sql string, skip bool) {
switch o := op.(type) {
case schemamigrator.CreateTableOperation:
return adaptCreateTable(o), false
case schemamigrator.DropTableOperation:
// Idempotent; safe to run even if the table never existed.
return o.ToSQL(), false
case schemamigrator.AlterTableAddColumn,
schemamigrator.AlterTableAddIndex,
schemamigrator.AlterTableDropColumn,
schemamigrator.AlterTableDropIndex:
return o.ToSQL(), false
// TTL is a production data-retention concern; irrelevant for test sessions.
case schemamigrator.AlterTableModifyTTL,
schemamigrator.AlterTableDropTTL,
// Background mutation; not needed in ephemeral test tables.
schemamigrator.AlterTableMaterializeColumn,
// Includes serialisation settings (object_serialization_version, …) that
// may not be recognised by the embedded chdb build.
schemamigrator.AlterTableModifySettings,
// Materialized views are not required for query-generation tests.
schemamigrator.CreateMaterializedViewOperation:
return "", true
default:
// Unknown operation type — skip conservatively.
return "", true
}
}
// adaptCreateTable rewrites a CreateTableOperation for chdb:
// - If the engine is Distributed, it is replaced with a plain MergeTree so the
// "distributed_*" table is a real, directly-writable table on the single chdb
// node. This preserves the exact column list while dropping distribution.
// - All other engines (MergeTree, ReplacingMergeTree, …) are used as-is.
func adaptCreateTable(op schemamigrator.CreateTableOperation) string {
if op.Engine.EngineType() == "Distributed" {
op.Engine = schemamigrator.MergeTree{OrderBy: "tuple()"}
}
return op.ToSQL()
}
// execSQL runs a single SQL statement against the session and returns any error.
func execSQL(session *chdb.Session, sql string) error {
result, err := session.Query(sql, "CSV")
if err != nil {
return err
}
defer result.Free()
return result.Error()
}

View File

@@ -1,63 +0,0 @@
//go:build chdb
package chdbtelemetrystore
import (
"fmt"
"github.com/ClickHouse/clickhouse-go/v2"
"github.com/SigNoz/signoz/pkg/telemetrystore"
chdb "github.com/chdb-io/chdb-go/chdb"
)
// Provider implements TelemetryStore using chdb-go for in-process ClickHouse execution.
//
// Unlike the mock-based provider (which uses go-sqlmock and requires pre-registered
// expectations), this provider actually executes SQL against an embedded ClickHouse engine.
// This makes it suitable for integration-style tests that need real query execution
// without an external ClickHouse server.
//
// # Session lifecycle
//
// chdb-go maintains a package-level singleton session. Creating multiple Provider
// instances in the same process shares the same underlying session, meaning DDL
// (CREATE TABLE, DROP TABLE, INSERT) issued by one consumer is visible to others.
// To maintain test isolation, use unique database or table names and call the cleanup
// function returned by New via t.Cleanup.
type Provider struct {
conn *chdbConn
cluster string
}
var _ telemetrystore.TelemetryStore = (*Provider)(nil)
// New creates a Provider backed by an in-process chdb session and runs the full
// signoz-otel-collector logs schema migrations so the tables are ready for use.
// The returned cleanup function closes the session and should be wired in via t.Cleanup.
func New() (*Provider, func(), error) {
session, err := chdb.NewSession()
if err != nil {
return nil, nil, fmt.Errorf("chdbtelemetrystore: failed to create session: %w", err)
}
if err := runMigrations(session); err != nil {
session.Close()
return nil, nil, fmt.Errorf("chdbtelemetrystore: schema migration failed: %w", err)
}
cleanup := func() { session.Close() }
return &Provider{
conn: &chdbConn{session: session},
cluster: "local",
}, cleanup, nil
}
// ClickhouseDB returns the chdb-backed clickhouse.Conn.
func (p *Provider) ClickhouseDB() clickhouse.Conn {
return p.conn
}
// Cluster returns the cluster name for this provider.
func (p *Provider) Cluster() string {
return p.cluster
}

View File

@@ -1,144 +0,0 @@
//go:build chdb
package chdbtelemetrystore
import (
"encoding/json"
"fmt"
"reflect"
"strings"
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
chdbpurego "github.com/chdb-io/chdb-go/chdb-purego"
)
// chdbRows implements clickhouse/v2/lib/driver.Rows over a parsed JSONCompact response.
type chdbRows struct {
meta []jsonMeta
data [][]json.RawMessage
cursor int
result chdbpurego.ChdbResult // held so we can Free() on Close
}
func newChdbRows(result chdbpurego.ChdbResult) (*chdbRows, error) {
str := result.String()
if strings.TrimSpace(str) == "" {
return &chdbRows{result: result, cursor: -1}, nil
}
var jr jsonCompactResult
if err := json.Unmarshal([]byte(str), &jr); err != nil {
return nil, fmt.Errorf("chdbRows: parse response: %w", err)
}
return &chdbRows{
meta: jr.Meta,
data: jr.Data,
cursor: -1,
result: result,
}, nil
}
func (r *chdbRows) Next() bool {
r.cursor++
return r.cursor < len(r.data)
}
// Scan copies the current row's columns into dest (positional pointer arguments).
func (r *chdbRows) Scan(dest ...any) error {
if r.cursor < 0 || r.cursor >= len(r.data) {
return fmt.Errorf("chdbRows: Scan called outside a valid row")
}
row := r.data[r.cursor]
for i, d := range dest {
if i >= len(row) {
break
}
dv := reflect.ValueOf(d)
if dv.Kind() != reflect.Ptr {
return fmt.Errorf("chdbRows: Scan dest[%d] must be a pointer", i)
}
if err := unmarshalIntoField(row[i], dv.Elem()); err != nil {
return fmt.Errorf("chdbRows: Scan col %d: %w", i, err)
}
}
return nil
}
// ScanStruct fills a struct from the current row using the same tag-based field
// matching as Select.
func (r *chdbRows) ScanStruct(dest any) error {
if r.cursor < 0 || r.cursor >= len(r.data) {
return fmt.Errorf("chdbRows: ScanStruct called outside a valid row")
}
elem := reflect.ValueOf(dest)
if elem.Kind() == reflect.Ptr {
elem = elem.Elem()
}
return scanRowIntoValue(r.meta, r.data[r.cursor], elem)
}
func (r *chdbRows) ColumnTypes() []driver.ColumnType {
types := make([]driver.ColumnType, len(r.meta))
for i, m := range r.meta {
types[i] = &chdbColumnType{name: m.Name, dbType: m.Type}
}
return types
}
func (r *chdbRows) Totals(_ ...any) error { return nil }
func (r *chdbRows) Columns() []string {
cols := make([]string, len(r.meta))
for i, m := range r.meta {
cols[i] = m.Name
}
return cols
}
func (r *chdbRows) Close() error {
if r.result != nil {
r.result.Free()
r.result = nil
}
return nil
}
func (r *chdbRows) Err() error { return nil }
// chdbRow wraps chdbRows and exposes the first row as clickhouse/v2/lib/driver.Row.
type chdbRow struct {
err error
rows *chdbRows
}
func (r *chdbRow) Err() error { return r.err }
func (r *chdbRow) Scan(dest ...any) error {
if r.err != nil {
return r.err
}
if !r.rows.Next() {
return fmt.Errorf("chdb: no rows in result set")
}
return r.rows.Scan(dest...)
}
func (r *chdbRow) ScanStruct(dest any) error {
if r.err != nil {
return r.err
}
if !r.rows.Next() {
return fmt.Errorf("chdb: no rows in result set")
}
return r.rows.ScanStruct(dest)
}
// chdbColumnType implements driver.ColumnType for chdb result metadata.
type chdbColumnType struct {
name string
dbType string
}
func (c *chdbColumnType) Name() string { return c.name }
func (c *chdbColumnType) Nullable() bool { return strings.HasPrefix(c.dbType, "Nullable") }
func (c *chdbColumnType) ScanType() reflect.Type { return reflect.TypeOf("") }
func (c *chdbColumnType) DatabaseTypeName() string { return c.dbType }

View File

@@ -1,13 +1,13 @@
package roletypes
package authtypes
import (
"context"
"encoding/json"
"regexp"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
"github.com/uptrace/bun"
@@ -51,7 +51,7 @@ var (
)
var (
TypeableResourcesRoles = authtypes.MustNewTypeableMetaResources(authtypes.MustNewName("roles"))
TypeableResourcesRoles = MustNewTypeableMetaResources(MustNewName("roles"))
)
type StorableRole struct {
@@ -194,20 +194,20 @@ func (role *PatchableRole) UnmarshalJSON(data []byte) error {
return nil
}
func GetAdditionTuples(name string, orgID valuer.UUID, relation authtypes.Relation, additions []*authtypes.Object) ([]*openfgav1.TupleKey, error) {
func GetAdditionTuples(name string, orgID valuer.UUID, relation Relation, additions []*Object) ([]*openfgav1.TupleKey, error) {
tuples := make([]*openfgav1.TupleKey, 0)
for _, object := range additions {
typeable := authtypes.MustNewTypeableFromType(object.Resource.Type, object.Resource.Name)
typeable := MustNewTypeableFromType(object.Resource.Type, object.Resource.Name)
transactionTuples, err := typeable.Tuples(
authtypes.MustNewSubject(
authtypes.TypeableRole,
MustNewSubject(
TypeableRole,
name,
orgID,
&authtypes.RelationAssignee,
&RelationAssignee,
),
relation,
[]authtypes.Selector{object.Selector},
[]Selector{object.Selector},
orgID,
)
if err != nil {
@@ -220,20 +220,20 @@ func GetAdditionTuples(name string, orgID valuer.UUID, relation authtypes.Relati
return tuples, nil
}
func GetDeletionTuples(name string, orgID valuer.UUID, relation authtypes.Relation, deletions []*authtypes.Object) ([]*openfgav1.TupleKey, error) {
func GetDeletionTuples(name string, orgID valuer.UUID, relation Relation, deletions []*Object) ([]*openfgav1.TupleKey, error) {
tuples := make([]*openfgav1.TupleKey, 0)
for _, object := range deletions {
typeable := authtypes.MustNewTypeableFromType(object.Resource.Type, object.Resource.Name)
typeable := MustNewTypeableFromType(object.Resource.Type, object.Resource.Name)
transactionTuples, err := typeable.Tuples(
authtypes.MustNewSubject(
authtypes.TypeableRole,
MustNewSubject(
TypeableRole,
name,
orgID,
&authtypes.RelationAssignee,
&RelationAssignee,
),
relation,
[]authtypes.Selector{object.Selector},
[]Selector{object.Selector},
orgID,
)
if err != nil {
@@ -254,3 +254,15 @@ func MustGetSigNozManagedRoleFromExistingRole(role types.Role) string {
return managedRole
}
type RoleStore interface {
Create(context.Context, *StorableRole) error
Get(context.Context, valuer.UUID, valuer.UUID) (*StorableRole, error)
GetByOrgIDAndName(context.Context, valuer.UUID, string) (*StorableRole, error)
List(context.Context, valuer.UUID) ([]*StorableRole, error)
ListByOrgIDAndNames(context.Context, valuer.UUID, []string) ([]*StorableRole, error)
ListByOrgIDAndIDs(context.Context, valuer.UUID, []valuer.UUID) ([]*StorableRole, error)
Update(context.Context, valuer.UUID, *StorableRole) error
Delete(context.Context, valuer.UUID, valuer.UUID) error
RunInTx(context.Context, func(ctx context.Context) error) error
}

View File

@@ -1,19 +0,0 @@
package roletypes
import (
"context"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Store interface {
Create(context.Context, *StorableRole) error
Get(context.Context, valuer.UUID, valuer.UUID) (*StorableRole, error)
GetByOrgIDAndName(context.Context, valuer.UUID, string) (*StorableRole, error)
List(context.Context, valuer.UUID) ([]*StorableRole, error)
ListByOrgIDAndNames(context.Context, valuer.UUID, []string) ([]*StorableRole, error)
ListByOrgIDAndIDs(context.Context, valuer.UUID, []valuer.UUID) ([]*StorableRole, error)
Update(context.Context, valuer.UUID, *StorableRole) error
Delete(context.Context, valuer.UUID, valuer.UUID) error
RunInTx(context.Context, func(ctx context.Context) error) error
}

View File

@@ -9,7 +9,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
@@ -102,10 +102,10 @@ func NewServiceAccountFromStorables(storableServiceAccount *StorableServiceAccou
}
}
func NewServiceAccountsFromRoles(storableServiceAccounts []*StorableServiceAccount, roles []*roletypes.Role, serviceAccountIDToRoleIDsMap map[string][]valuer.UUID) []*ServiceAccount {
func NewServiceAccountsFromRoles(storableServiceAccounts []*StorableServiceAccount, roles []*authtypes.Role, serviceAccountIDToRoleIDsMap map[string][]valuer.UUID) []*ServiceAccount {
serviceAccounts := make([]*ServiceAccount, 0, len(storableServiceAccounts))
roleIDToRole := make(map[string]*roletypes.Role, len(roles))
roleIDToRole := make(map[string]*authtypes.Role, len(roles))
for _, role := range roles {
roleIDToRole[role.ID.String()] = role
}

View File

@@ -5,7 +5,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
@@ -19,7 +19,7 @@ type StorableServiceAccountRole struct {
RoleID string `bun:"role_id"`
}
func NewStorableServiceAccountRoles(serviceAccountID valuer.UUID, roles []*roletypes.Role) []*StorableServiceAccountRole {
func NewStorableServiceAccountRoles(serviceAccountID valuer.UUID, roles []*authtypes.Role) []*StorableServiceAccountRole {
storableServiceAccountRoles := make([]*StorableServiceAccountRole, len(roles))
for idx, role := range roles {
storableServiceAccountRoles[idx] = &StorableServiceAccountRole{
@@ -38,7 +38,7 @@ func NewStorableServiceAccountRoles(serviceAccountID valuer.UUID, roles []*rolet
return storableServiceAccountRoles
}
func NewRolesFromStorableServiceAccountRoles(storable []*StorableServiceAccountRole, roles []*roletypes.Role) ([]string, error) {
func NewRolesFromStorableServiceAccountRoles(storable []*StorableServiceAccountRole, roles []*authtypes.Role) ([]string, error) {
roleIDToName := make(map[string]string, len(roles))
for _, role := range roles {
roleIDToName[role.ID.String()] = role.Name

View File

@@ -45,6 +45,8 @@ type MetadataStore interface {
// GetFirstSeenFromMetricMetadata gets the first seen timestamp for a metric metadata lookup key.
GetFirstSeenFromMetricMetadata(ctx context.Context, lookupKeys []MetricMetadataLookupKey) (map[MetricMetadataLookupKey]int64, error)
FetchLastSeenInfoMulti(ctx context.Context, metricNames ...string) (map[string]int64, error)
}
type MetricMetadataLookupKey struct {

View File

@@ -342,3 +342,7 @@ func (m *MockMetadataStore) SetFirstSeenFromMetricMetadata(firstSeenMap map[tele
m.LookupKeysMap[key] = value
}
}
func (m *MockMetadataStore) FetchLastSeenInfoMulti(ctx context.Context, metricNames ...string) (map[string]int64, error) {
return make(map[string]int64), nil
}

View File

@@ -1,3 +1,4 @@
from datetime import datetime, timedelta, timezone
from http import HTTPStatus
from typing import Callable, List
@@ -7,6 +8,7 @@ from sqlalchemy import sql
from wiremock.resources.mappings import Mapping
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD, add_license
from fixtures.metrics import Metrics
from fixtures.types import Operation, SigNoz, TestContainerDocker
@@ -74,9 +76,37 @@ def test_public_dashboard_widget_query_range(
signoz: SigNoz,
create_user_admin: Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_metrics: Callable[[List[Metrics]], None],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# Insert metric data so the widget query returns results instead of 404
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
metrics: List[Metrics] = [
Metrics(
metric_name="container.cpu.time",
labels={"service": "test-service"},
timestamp=now - timedelta(minutes=5),
value=100.0,
temporality="Cumulative",
),
Metrics(
metric_name="container.cpu.time",
labels={"service": "test-service"},
timestamp=now - timedelta(minutes=3),
value=200.0,
temporality="Cumulative",
),
Metrics(
metric_name="container.cpu.time",
labels={"service": "test-service"},
timestamp=now - timedelta(minutes=1),
value=300.0,
temporality="Cumulative",
),
]
insert_metrics(metrics)
dashboard_req = {
"title": "Test Widget Query Range Dashboard",
"description": "For testing widget query range",

View File

@@ -10,12 +10,16 @@ from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.metrics import Metrics
from fixtures.querier import (
assert_minutely_bucket_values,
build_builder_query,
find_named_result,
index_series_by_label,
make_query_request,
)
from fixtures.utils import get_testdata_file_path
FILL_GAPS = "fillGaps"
FILL_ZERO = "fillZero"
HISTOGRAM_FILE = get_testdata_file_path("histogram_data_1h.jsonl")
def _build_format_options(fill_mode: str) -> Dict[str, Any]:
@@ -580,3 +584,39 @@ def test_metrics_fill_formula_with_group_by(
expected_by_ts=expectations[group],
context=f"metrics/{fill_mode}/F1/{group}",
)
def test_histogram_p90_returns_404_outside_data_window(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_metrics: Callable[[List[Metrics]], None],
) -> None:
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
metric_name = "test_p90_last_seen_bucket"
metrics = Metrics.load_from_file(
HISTOGRAM_FILE,
base_time=now - timedelta(minutes=90),
metric_name_override=metric_name,
)
insert_metrics(metrics)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
query = build_builder_query(
"A",
metric_name,
"doesnotreallymatter",
"p90",
)
end_ms = int(now.timestamp() * 1000)
start_2h = int((now - timedelta(hours=2)).timestamp() * 1000)
response = make_query_request(signoz, token, start_2h, end_ms, [query])
assert response.status_code == HTTPStatus.OK
assert response.json()["status"] == "success"
start_15m = int((now - timedelta(minutes=15)).timestamp() * 1000)
response = make_query_request(signoz, token, start_15m, end_ms, [query])
assert response.status_code == HTTPStatus.NOT_FOUND