mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-14 02:20:32 +01:00
Compare commits
5 Commits
worktree-h
...
hotfix/spa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1361c070e0 | ||
|
|
ebe7a264d5 | ||
|
|
dbacd34ec2 | ||
|
|
0cbf3e8ee2 | ||
|
|
1f057f041b |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -189,9 +189,7 @@ go.mod @therealpandey
|
||||
## Infrastructure Monitoring
|
||||
/frontend/src/pages/InfrastructureMonitoring/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/InfraMonitoringHosts/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/InfraMonitoringHostsV2/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/InfraMonitoringK8s/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/InfraMonitoringK8sV2/ @SigNoz/pulse-frontend
|
||||
|
||||
## Alerts
|
||||
/frontend/src/pages/AlertList/ @SigNoz/pulse-frontend
|
||||
|
||||
@@ -543,31 +543,6 @@ components:
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
AuthtypesGettableRole:
|
||||
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
|
||||
AuthtypesGettableToken:
|
||||
properties:
|
||||
accessToken:
|
||||
@@ -719,6 +694,43 @@ components:
|
||||
- detach
|
||||
type: string
|
||||
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:
|
||||
type: string
|
||||
groupMappings:
|
||||
additionalProperties:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
useRoleAttribute:
|
||||
type: boolean
|
||||
type: object
|
||||
AuthtypesRoleWithTransactionGroups:
|
||||
properties:
|
||||
createdAt:
|
||||
format: date-time
|
||||
@@ -746,18 +758,6 @@ components:
|
||||
- orgId
|
||||
- transactionGroups
|
||||
type: object
|
||||
AuthtypesRoleMapping:
|
||||
properties:
|
||||
defaultRole:
|
||||
type: string
|
||||
groupMappings:
|
||||
additionalProperties:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
useRoleAttribute:
|
||||
type: boolean
|
||||
type: object
|
||||
AuthtypesSamlConfig:
|
||||
properties:
|
||||
attributeMapping:
|
||||
@@ -7998,6 +7998,15 @@ components:
|
||||
required:
|
||||
- items
|
||||
type: object
|
||||
SpantypesGettableSpanMappers:
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapper'
|
||||
type: array
|
||||
required:
|
||||
- items
|
||||
type: object
|
||||
SpantypesGettableTraceAggregations:
|
||||
properties:
|
||||
aggregations:
|
||||
@@ -8150,7 +8159,7 @@ components:
|
||||
type: boolean
|
||||
fieldContext:
|
||||
$ref: '#/components/schemas/SpantypesFieldContext'
|
||||
group_id:
|
||||
groupId:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
@@ -8163,7 +8172,7 @@ components:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- group_id
|
||||
- groupId
|
||||
- name
|
||||
- fieldContext
|
||||
- config
|
||||
@@ -12022,7 +12031,7 @@ paths:
|
||||
properties:
|
||||
data:
|
||||
items:
|
||||
$ref: '#/components/schemas/AuthtypesGettableRole'
|
||||
$ref: '#/components/schemas/AuthtypesRole'
|
||||
type: array
|
||||
status:
|
||||
type: string
|
||||
@@ -12206,7 +12215,7 @@ paths:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/AuthtypesRole'
|
||||
$ref: '#/components/schemas/AuthtypesRoleWithTransactionGroups'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
@@ -13742,7 +13751,7 @@ paths:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SpantypesGettableSpanMapperGroups'
|
||||
$ref: '#/components/schemas/SpantypesGettableSpanMappers'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
|
||||
@@ -119,6 +119,10 @@ func (provider *provider) CheckTransactions(ctx context.Context, subject string,
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (provider *provider) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType coretypes.Type) ([]*coretypes.Object, error) {
|
||||
return provider.openfgaServer.ListObjects(ctx, subject, relation, objectType)
|
||||
}
|
||||
|
||||
func (provider *provider) Write(ctx context.Context, additions []*openfgav1.TupleKey, deletions []*openfgav1.TupleKey) error {
|
||||
return provider.openfgaServer.Write(ctx, additions, deletions)
|
||||
}
|
||||
@@ -127,6 +131,10 @@ func (provider *provider) ReadTuples(ctx context.Context, tupleKey *openfgav1.Re
|
||||
return provider.openfgaServer.ReadTuples(ctx, tupleKey)
|
||||
}
|
||||
|
||||
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) (*authtypes.Role, error) {
|
||||
return provider.pkgAuthzService.GetByOrgIDAndName(ctx, orgID, name)
|
||||
}
|
||||
@@ -175,7 +183,7 @@ func (provider *provider) CreateManagedUserRoleTransactions(ctx context.Context,
|
||||
return provider.Write(ctx, tuples, nil)
|
||||
}
|
||||
|
||||
func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) error {
|
||||
func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *authtypes.RoleWithTransactionGroups) 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())
|
||||
@@ -200,31 +208,70 @@ func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *a
|
||||
return err
|
||||
}
|
||||
|
||||
return provider.store.Create(ctx, role)
|
||||
if err := provider.store.Create(ctx, role.Role); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.Role, error) {
|
||||
return provider.store.Get(ctx, orgID, id)
|
||||
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())
|
||||
}
|
||||
|
||||
existingRole, err := provider.store.GetByOrgIDAndName(ctx, role.OrgID, role.Name)
|
||||
if err != nil {
|
||||
if !errors.Ast(err, errors.TypeNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if existingRole != nil {
|
||||
return existingRole, nil
|
||||
}
|
||||
|
||||
err = provider.store.Create(ctx, role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return role, nil
|
||||
}
|
||||
|
||||
func (provider *provider) Update(ctx context.Context, orgID valuer.UUID, updatedRole *authtypes.Role) error {
|
||||
func (provider *provider) GetWithTransactionGroups(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.RoleWithTransactionGroups, 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())
|
||||
}
|
||||
|
||||
role, err := provider.store.Get(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tuples, err := provider.readAllTuplesForRole(ctx, role.Name, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
transactionGroups := authtypes.MustNewTransactionGroupsFromTuples(tuples)
|
||||
return authtypes.MakeRoleWithTransactionGroups(role, transactionGroups), nil
|
||||
}
|
||||
|
||||
func (provider *provider) Update(ctx context.Context, orgID valuer.UUID, updatedRole *authtypes.RoleWithTransactionGroups) 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())
|
||||
}
|
||||
|
||||
existingRole, err := provider.Get(ctx, orgID, updatedRole.ID)
|
||||
existingRole, err := provider.GetWithTransactionGroups(ctx, orgID, updatedRole.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
existingTuples, err := provider.readAllTuplesForRole(ctx, existingRole.Name, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
existingGroups := authtypes.MustNewTransactionGroupsFromTuples(existingTuples)
|
||||
additions, deletions := existingGroups.Diff(updatedRole.TransactionGroups)
|
||||
additions, deletions := existingRole.TransactionGroups.Diff(updatedRole.TransactionGroups)
|
||||
additionTuples, err := authtypes.NewTuplesFromTransactionGroups(existingRole.Name, orgID, additions)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -240,7 +287,7 @@ func (provider *provider) Update(ctx context.Context, orgID valuer.UUID, updated
|
||||
return err
|
||||
}
|
||||
|
||||
return provider.store.Update(ctx, orgID, updatedRole)
|
||||
return provider.store.Update(ctx, orgID, updatedRole.Role)
|
||||
}
|
||||
|
||||
func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
|
||||
@@ -249,7 +296,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
role, err := provider.Get(ctx, orgID, id)
|
||||
role, err := provider.GetWithTransactionGroups(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -265,7 +312,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
|
||||
}
|
||||
}
|
||||
|
||||
tuples, err := provider.readAllTuplesForRole(ctx, role.Name, orgID)
|
||||
tuples, err := authtypes.NewTuplesFromTransactionGroups(role.Name, orgID, role.TransactionGroups)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -277,24 +324,6 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
|
||||
return provider.store.Delete(ctx, orgID, id)
|
||||
}
|
||||
|
||||
func (provider *provider) readAllTuplesForRole(ctx context.Context, roleName string, orgID valuer.UUID) ([]*openfgav1.TupleKey, error) {
|
||||
subject := authtypes.MustNewSubject(coretypes.NewResourceRole(), roleName, orgID, &coretypes.VerbAssignee)
|
||||
|
||||
tuples := make([]*openfgav1.TupleKey, 0)
|
||||
for _, objectType := range provider.registry.Types() {
|
||||
typeTuples, err := provider.openfgaServer.ReadTuples(ctx, &openfgav1.ReadRequestTupleKey{
|
||||
User: subject,
|
||||
Object: objectType.StringValue() + ":",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tuples = append(tuples, typeTuples...)
|
||||
}
|
||||
|
||||
return tuples, nil
|
||||
}
|
||||
|
||||
func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID valuer.UUID) []*openfgav1.TupleKey {
|
||||
tuples := []*openfgav1.TupleKey{}
|
||||
|
||||
@@ -346,3 +375,21 @@ func (provider *provider) getManagedRoleTransactionTuples(orgID valuer.UUID) []*
|
||||
|
||||
return tuples
|
||||
}
|
||||
|
||||
func (provider *provider) readAllTuplesForRole(ctx context.Context, roleName string, orgID valuer.UUID) ([]*openfgav1.TupleKey, error) {
|
||||
subject := authtypes.MustNewSubject(coretypes.NewResourceRole(), roleName, orgID, &coretypes.VerbAssignee)
|
||||
|
||||
tuples := make([]*openfgav1.TupleKey, 0)
|
||||
for _, objectType := range provider.registry.Types() {
|
||||
typeTuples, err := provider.ReadTuples(ctx, &openfgav1.ReadRequestTupleKey{
|
||||
User: subject,
|
||||
Object: objectType.StringValue() + ":",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tuples = append(tuples, typeTuples...)
|
||||
}
|
||||
|
||||
return tuples, nil
|
||||
}
|
||||
|
||||
@@ -105,6 +105,10 @@ func (server *Server) BatchCheck(ctx context.Context, tupleReq map[string]*openf
|
||||
return server.pkgAuthzService.BatchCheck(ctx, tupleReq)
|
||||
}
|
||||
|
||||
func (server *Server) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType coretypes.Type) ([]*coretypes.Object, error) {
|
||||
return server.pkgAuthzService.ListObjects(ctx, subject, relation, objectType)
|
||||
}
|
||||
|
||||
func (server *Server) Write(ctx context.Context, additions []*openfgav1.TupleKey, deletions []*openfgav1.TupleKey) error {
|
||||
return server.pkgAuthzService.Write(ctx, additions, deletions)
|
||||
}
|
||||
|
||||
29
frontend/__mocks__/resizableMock.tsx
Normal file
29
frontend/__mocks__/resizableMock.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
type CommonProps = PropsWithChildren<{
|
||||
className?: string;
|
||||
minSize?: number;
|
||||
maxSize?: number;
|
||||
defaultSize?: number;
|
||||
direction?: 'horizontal' | 'vertical';
|
||||
autoSaveId?: string;
|
||||
withHandle?: boolean;
|
||||
}>;
|
||||
|
||||
export function ResizablePanelGroup({
|
||||
children,
|
||||
className,
|
||||
}: CommonProps): JSX.Element {
|
||||
return <div className={className}>{children}</div>;
|
||||
}
|
||||
|
||||
export function ResizablePanel({
|
||||
children,
|
||||
className,
|
||||
}: CommonProps): JSX.Element {
|
||||
return <div className={className}>{children}</div>;
|
||||
}
|
||||
|
||||
export function ResizableHandle({ className }: CommonProps): JSX.Element {
|
||||
return <div className={className} />;
|
||||
}
|
||||
@@ -21,6 +21,7 @@ const config: Config.InitialOptions = {
|
||||
'\\.md$': '<rootDir>/__mocks__/cssMock.ts',
|
||||
'^uplot$': '<rootDir>/__mocks__/uplotMock.ts',
|
||||
'^motion/react$': '<rootDir>/__mocks__/motionMock.tsx',
|
||||
'^@signozhq/resizable$': '<rootDir>/__mocks__/resizableMock.tsx',
|
||||
'^hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
|
||||
'^src/hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
|
||||
'^.*/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"@sentry/vite-plugin": "5.3.0",
|
||||
"@signozhq/design-tokens": "2.1.4",
|
||||
"@signozhq/icons": "0.4.0",
|
||||
"@signozhq/resizable": "0.0.2",
|
||||
"@signozhq/ui": "0.0.23",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@tanstack/react-virtual": "3.13.22",
|
||||
@@ -127,7 +128,7 @@
|
||||
"timestamp-nano": "^1.0.0",
|
||||
"typescript": "5.9.3",
|
||||
"uplot": "1.6.31",
|
||||
"uuid": "14.0.1",
|
||||
"uuid": "^8.3.2",
|
||||
"vite": "npm:rolldown-vite@7.3.1",
|
||||
"vite-plugin-html": "3.2.2",
|
||||
"zod": "4.3.6",
|
||||
@@ -221,5 +222,34 @@
|
||||
"*.(scss|css)": [
|
||||
"stylelint"
|
||||
]
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "18.0.26",
|
||||
"@types/react-dom": "18.0.10",
|
||||
"debug": "4.3.4",
|
||||
"semver": "7.5.4",
|
||||
"xml2js": "0.5.0",
|
||||
"phin": "^3.7.1",
|
||||
"body-parser": "1.20.3",
|
||||
"http-proxy-middleware": "4.1.1",
|
||||
"cross-spawn": "7.0.5",
|
||||
"cookie": "^0.7.1",
|
||||
"serialize-javascript": "6.0.2",
|
||||
"prismjs": "1.30.0",
|
||||
"got": "11.8.5",
|
||||
"form-data": "4.0.6",
|
||||
"brace-expansion": "^2.0.3",
|
||||
"on-headers": "^1.1.0",
|
||||
"js-cookie": "^3.0.7",
|
||||
"tmp": "0.2.7",
|
||||
"vite": "npm:rolldown-vite@7.3.1",
|
||||
"dompurify": "3.4.11",
|
||||
"js-yaml@3": "3.15.0",
|
||||
"js-yaml@4": "4.2.0",
|
||||
"yaml@1": "1.10.3",
|
||||
"react-router@6": "6.30.4",
|
||||
"markdown-it": "14.2.0",
|
||||
"mdast-util-to-hast@13": "13.2.1",
|
||||
"protocol-buffers-schema": "3.6.1"
|
||||
}
|
||||
}
|
||||
1864
frontend/pnpm-lock.yaml
generated
1864
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,53 +1,6 @@
|
||||
blockExoticSubdeps: true
|
||||
trustPolicy: no-downgrade
|
||||
minimumReleaseAge: 2880 # 2d
|
||||
minimumReleaseAgeStrict: true
|
||||
minimumReleaseAgeExclude:
|
||||
- '@signozhq/*'
|
||||
minimumReleaseAgeStrict: true
|
||||
# Security floors for vulnerable transitive deps. Where possible, targets are
|
||||
# capped to avoid crossing breaking versions (major; and minor for 0.x).
|
||||
# Some entries may still force a breaking bump when no safe release exists
|
||||
# within the consumer’s constraint—see per-entry notes.
|
||||
overrides:
|
||||
# via: direct devDep @babel/core ^7.22.11 (+ babel plugin peers)
|
||||
# remove: bump @babel/core in package.json to ^7.29.6
|
||||
'@babel/core@<=7.29.0': '>=7.29.6 <8'
|
||||
# via: jest > babel-plugin-istanbul > @istanbuljs/load-nyc-config@1.1.0 (js-yaml ^3.13.1)
|
||||
# remove: blocked — 1.1.0 is latest and still depends on js-yaml 3.x
|
||||
'@istanbuljs/load-nyc-config>js-yaml': '>=4.2.0 <5'
|
||||
# via: msw@1.3.2 (devDep) > cookie ^0.4.2
|
||||
# remove: upgrade msw to >=2 (ships cookie ^1). Do NOT open the cap: cookie >=1 is
|
||||
# ESM-only and breaks msw under jest's CJS sandbox (kills every test suite)
|
||||
cookie@<0.7.0: '>=0.7.1 <1'
|
||||
# via: direct dep dompurify 3.4.0; @grafana/data@11.6.15 (3.4.0/3.2.4 exact);
|
||||
# @monaco-editor/react > monaco-editor@0.55.1 (3.2.7 exact)
|
||||
# remove: bump direct dep to 3.4.11; @grafana/data (latest 13.1.0) and
|
||||
# monaco-editor (latest 0.55.1) still pin vulnerable versions — blocked
|
||||
dompurify@<=3.4.10: '>=3.4.11 <4'
|
||||
# via: rolldown-vite@7.3.1 (esbuild ^0.27.0); orval@8.9.1 (^0.27.4); ts-jest@29.4.9 (~0.27.4)
|
||||
# remove: blocked on rolldown-vite (7.3.1 is latest, still ^0.27.0);
|
||||
# orval >=8.20.0 and ts-jest >=29.4.11 already fixed on their side
|
||||
esbuild@>=0.27.3 <0.28.1: '>=0.28.1 <0.29.0'
|
||||
# via: react-use@17.5.1 (direct, js-cookie ^2.2.1); @grafana/data > react-use@17.6.0
|
||||
# remove: bump react-use to >=17.6.1 (js-cookie ^3); @grafana/data side blocked
|
||||
js-cookie@<=3.0.5: '>=3.0.7 <4'
|
||||
# via: @orval/core@8.9.1 (devDep, js-yaml 4.1.1 EXACT pin — not deletable);
|
||||
# json-schema-to-typescript@15 > @apidevtools/json-schema-ref-parser (^4.1.0)
|
||||
# remove: upgrade orval to >=8.20.0 (drops js-yaml dependency entirely)
|
||||
js-yaml@>=4.0.0 <=4.1.1: '>=4.2.0 <5'
|
||||
# via: react-syntax-highlighter@15.5.0 (prismjs ^1.27.0 + refractor@3 ~1.27.0 tilde-pinned)
|
||||
# remove: bump react-syntax-highlighter to >=16.1.1 (prismjs ^1.30.0, refractor@5)
|
||||
prismjs@<1.30.0: '>=1.30.0 <2'
|
||||
# via: direct dep react-router-dom-v5-compat@6.30.3 (react-router 6.30.3 exact)
|
||||
# remove: bump react-router-dom-v5-compat to 6.30.4. Do NOT open the cap:
|
||||
# react-router >=7 requires React 19 and breaks the app-wide CompatRouter
|
||||
react-router@>=6.7.0 <6.30.4: '>=6.30.4 <7'
|
||||
# via: msw@1.3.2 (devDep) > inquirer@8 > external-editor@3.1.0 (tmp ^0.0.33)
|
||||
# remove: upgrade msw to >=2 (drops the inquirer/external-editor chain)
|
||||
tmp@<0.2.6: '>=0.2.6 <0.3.0'
|
||||
# via: jest > babel-plugin-macros > cosmiconfig@7 (yaml ^1.10.0);
|
||||
# typescript-plugin-css-modules > postcss-load-config@3 (^1.10.2)
|
||||
# remove: blocked — babel-plugin-macros 3.1.0 (latest) still uses cosmiconfig@7
|
||||
yaml@>=1.0.0 <1.10.3: '>=1.10.3 <2'
|
||||
trustPolicy: no-downgrade
|
||||
trustPolicyExclude:
|
||||
- 'semver@6.3.1 || 5.7.2'
|
||||
blockExoticSubdeps: true
|
||||
|
||||
@@ -2082,39 +2082,6 @@ export interface AuthtypesGettableAuthDomainDTO {
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface AuthtypesGettableRoleDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
/**
|
||||
* @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?: string;
|
||||
}
|
||||
|
||||
export interface AuthtypesGettableTokenDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -2358,6 +2325,39 @@ export interface AuthtypesPostableUserRoleDTO {
|
||||
}
|
||||
|
||||
export interface AuthtypesRoleDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
/**
|
||||
* @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?: string;
|
||||
}
|
||||
|
||||
export interface AuthtypesRoleWithTransactionGroupsDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
@@ -9194,6 +9194,76 @@ export interface SpantypesGettableSpanMapperGroupsDTO {
|
||||
items: SpantypesSpanMapperGroupDTO[];
|
||||
}
|
||||
|
||||
export enum SpantypesSpanMapperOperationDTO {
|
||||
move = 'move',
|
||||
copy = 'copy',
|
||||
}
|
||||
export interface SpantypesSpanMapperSourceDTO {
|
||||
context: SpantypesFieldContextDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
key: string;
|
||||
operation: SpantypesSpanMapperOperationDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
priority: number;
|
||||
}
|
||||
|
||||
export interface SpantypesSpanMapperConfigDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
sources: SpantypesSpanMapperSourceDTO[] | null;
|
||||
}
|
||||
|
||||
export interface SpantypesSpanMapperDTO {
|
||||
config: SpantypesSpanMapperConfigDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
createdBy?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled: boolean;
|
||||
fieldContext: SpantypesFieldContextDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
groupId: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
updatedBy?: string;
|
||||
}
|
||||
|
||||
export interface SpantypesGettableSpanMappersDTO {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
items: SpantypesSpanMapperDTO[];
|
||||
}
|
||||
|
||||
export enum SpantypesSpanAggregationTypeDTO {
|
||||
span_count = 'span_count',
|
||||
execution_time_percentage = 'execution_time_percentage',
|
||||
@@ -9440,30 +9510,6 @@ export interface SpantypesPostableFlamegraphDTO {
|
||||
selectedSpanId?: string;
|
||||
}
|
||||
|
||||
export enum SpantypesSpanMapperOperationDTO {
|
||||
move = 'move',
|
||||
copy = 'copy',
|
||||
}
|
||||
export interface SpantypesSpanMapperSourceDTO {
|
||||
context: SpantypesFieldContextDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
key: string;
|
||||
operation: SpantypesSpanMapperOperationDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
priority: number;
|
||||
}
|
||||
|
||||
export interface SpantypesSpanMapperConfigDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
sources: SpantypesSpanMapperSourceDTO[] | null;
|
||||
}
|
||||
|
||||
export interface SpantypesPostableSpanMapperDTO {
|
||||
config: SpantypesSpanMapperConfigDTO;
|
||||
/**
|
||||
@@ -9512,45 +9558,6 @@ export interface SpantypesPostableWaterfallDTO {
|
||||
uncollapsedSpans?: string[] | null;
|
||||
}
|
||||
|
||||
export interface SpantypesSpanMapperDTO {
|
||||
config: SpantypesSpanMapperConfigDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
createdBy?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled: boolean;
|
||||
fieldContext: SpantypesFieldContextDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
group_id: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
updatedBy?: string;
|
||||
}
|
||||
|
||||
export interface SpantypesUpdatableSpanMapperDTO {
|
||||
config?: SpantypesSpanMapperConfigDTO;
|
||||
/**
|
||||
@@ -10614,7 +10621,7 @@ export type ListRoles200 = {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
data: AuthtypesGettableRoleDTO[];
|
||||
data: AuthtypesRoleDTO[];
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -10636,7 +10643,7 @@ export type GetRolePathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetRole200 = {
|
||||
data: AuthtypesRoleDTO;
|
||||
data: AuthtypesRoleWithTransactionGroupsDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -10852,7 +10859,7 @@ export type ListSpanMappersPathParameters = {
|
||||
groupId: string;
|
||||
};
|
||||
export type ListSpanMappers200 = {
|
||||
data: SpantypesGettableSpanMapperGroupsDTO;
|
||||
data: SpantypesGettableSpanMappersDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
|
||||
1
frontend/src/auto-import-registry.d.ts
vendored
1
frontend/src/auto-import-registry.d.ts
vendored
@@ -12,4 +12,5 @@
|
||||
|
||||
import '@signozhq/design-tokens';
|
||||
import '@signozhq/icons';
|
||||
import '@signozhq/resizable';
|
||||
import '@signozhq/ui';
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Select } from 'antd';
|
||||
import { Checkbox } from '@signozhq/ui/checkbox';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { useListRoles } from 'api/generated/services/role';
|
||||
import type { AuthtypesGettableRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import cx from 'classnames';
|
||||
import APIError from 'types/api/error';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
@@ -16,7 +16,7 @@ export interface RoleOption {
|
||||
}
|
||||
|
||||
export function useRoles(): {
|
||||
roles: AuthtypesGettableRoleDTO[];
|
||||
roles: AuthtypesRoleDTO[];
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
error: APIError | undefined;
|
||||
@@ -33,7 +33,7 @@ export function useRoles(): {
|
||||
}
|
||||
|
||||
export function getRoleOptions(
|
||||
roles: AuthtypesGettableRoleDTO[],
|
||||
roles: AuthtypesRoleDTO[],
|
||||
valueField: 'id' | 'name',
|
||||
): RoleOption[] {
|
||||
return roles.map((role) => ({
|
||||
@@ -79,7 +79,7 @@ interface BaseProps {
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
getPopupContainer?: (trigger: HTMLElement) => HTMLElement;
|
||||
roles?: AuthtypesGettableRoleDTO[];
|
||||
roles?: AuthtypesRoleDTO[];
|
||||
loading?: boolean;
|
||||
isError?: boolean;
|
||||
error?: APIError;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Badge } from '@signozhq/ui/badge';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import type { AuthtypesGettableRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
import { withAuthZContent } from 'lib/authz/components/withAuthZ/withAuthZContent';
|
||||
import RolesSelect from 'components/RolesSelect';
|
||||
@@ -28,7 +28,7 @@ interface OverviewTabProps {
|
||||
localRoles: string[];
|
||||
onRolesChange: (v: string[]) => void;
|
||||
isDisabled: boolean;
|
||||
availableRoles: AuthtypesGettableRoleDTO[];
|
||||
availableRoles: AuthtypesRoleDTO[];
|
||||
rolesLoading?: boolean;
|
||||
rolesError?: boolean;
|
||||
rolesErrorObj?: APIError | undefined;
|
||||
|
||||
@@ -14,28 +14,11 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.entityMetricsTitleContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.entityMetricsTitle {
|
||||
font-size: var(--periscope-font-size-base);
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.metricsExplorerLink {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--l2-foreground);
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.metricsHeader {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Compass } from '@signozhq/icons';
|
||||
import { Skeleton, Tooltip } from 'antd';
|
||||
import { Skeleton } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import TimeSeries from 'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries';
|
||||
@@ -21,7 +19,6 @@ import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { getMetricsExplorerUrl } from 'utils/explorerUtils';
|
||||
|
||||
import { buildEntityMetricsChartConfig } from './configBuilder';
|
||||
|
||||
@@ -207,31 +204,9 @@ function EntityMetrics<T>({
|
||||
key={entityWidgetInfo[idx].title}
|
||||
className={styles.entityMetricsCol}
|
||||
>
|
||||
<div className={styles.entityMetricsTitleContainer}>
|
||||
<span className={styles.entityMetricsTitle}>
|
||||
{entityWidgetInfo[idx].title}
|
||||
</span>
|
||||
{queryPayloads[idx] &&
|
||||
queryPayloads[idx].graphType !== PANEL_TYPES.TABLE && (
|
||||
<Tooltip title="Open in Metrics Explorer">
|
||||
<Link
|
||||
to={getMetricsExplorerUrl({
|
||||
query: queryPayloads[idx].query,
|
||||
...(selectedInterval && selectedInterval !== 'custom'
|
||||
? { relativeTime: selectedInterval }
|
||||
: {
|
||||
startTimeMs: timeRange.startTime * 1000,
|
||||
endTimeMs: timeRange.endTime * 1000,
|
||||
}),
|
||||
})}
|
||||
className={styles.metricsExplorerLink}
|
||||
data-testid={`open-metrics-explorer-${idx}`}
|
||||
>
|
||||
<Compass size={14} />
|
||||
</Link>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<span className={styles.entityMetricsTitle}>
|
||||
{entityWidgetInfo[idx].title}
|
||||
</span>
|
||||
<div className={styles.entityMetricsCard} ref={graphRef}>
|
||||
{renderCardContent(query, idx)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import * as appContextHooks from 'providers/App/App';
|
||||
@@ -296,19 +295,17 @@ const renderEntityMetrics = (overrides = {}): any => {
|
||||
};
|
||||
|
||||
return render(
|
||||
<MemoryRouter>
|
||||
<EntityMetrics
|
||||
timeRange={defaultProps.timeRange}
|
||||
isModalTimeSelection={defaultProps.isModalTimeSelection}
|
||||
handleTimeChange={defaultProps.handleTimeChange}
|
||||
selectedInterval={defaultProps.selectedInterval}
|
||||
entity={defaultProps.entity}
|
||||
entityWidgetInfo={defaultProps.entityWidgetInfo}
|
||||
getEntityQueryPayload={defaultProps.getEntityQueryPayload}
|
||||
queryKey={defaultProps.queryKey}
|
||||
category={defaultProps.category}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
<EntityMetrics
|
||||
timeRange={defaultProps.timeRange}
|
||||
isModalTimeSelection={defaultProps.isModalTimeSelection}
|
||||
handleTimeChange={defaultProps.handleTimeChange}
|
||||
selectedInterval={defaultProps.selectedInterval}
|
||||
entity={defaultProps.entity}
|
||||
entityWidgetInfo={defaultProps.entityWidgetInfo}
|
||||
getEntityQueryPayload={defaultProps.getEntityQueryPayload}
|
||||
queryKey={defaultProps.queryKey}
|
||||
category={defaultProps.category}
|
||||
/>,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -337,8 +334,8 @@ const mockTableData: (import('../utils').MetricsTableData[] | null)[] = [
|
||||
];
|
||||
|
||||
const mockQueryPayloads = [
|
||||
{ graphType: 'graph', query: { queryType: 'builder' } }, // time_series
|
||||
{ graphType: 'table', query: { queryType: 'builder' } }, // table
|
||||
{ graphType: 'graph' }, // time_series
|
||||
{ graphType: 'table' }, // table
|
||||
];
|
||||
|
||||
describe('EntityMetrics', () => {
|
||||
@@ -445,34 +442,6 @@ describe('EntityMetrics', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('renders metrics explorer link only for non-table panels', () => {
|
||||
renderEntityMetrics();
|
||||
expect(screen.getByTestId('open-metrics-explorer-0')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('open-metrics-explorer-1'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('builds metrics explorer link with relativeTime when a relative interval is selected', () => {
|
||||
renderEntityMetrics({ selectedInterval: '5m' as Time });
|
||||
const href = screen
|
||||
.getByTestId('open-metrics-explorer-0')
|
||||
.getAttribute('href');
|
||||
expect(href).toContain('relativeTime=5m');
|
||||
expect(href).not.toContain('startTime=');
|
||||
expect(href).not.toContain('endTime=');
|
||||
});
|
||||
|
||||
it('builds metrics explorer link with absolute time range in milliseconds for custom interval', () => {
|
||||
renderEntityMetrics({ selectedInterval: 'custom' as Time });
|
||||
const href = screen
|
||||
.getByTestId('open-metrics-explorer-0')
|
||||
.getAttribute('href');
|
||||
expect(href).toContain(`startTime=${mockTimeRange.startTime * 1000}`);
|
||||
expect(href).toContain(`endTime=${mockTimeRange.endTime * 1000}`);
|
||||
expect(href).not.toContain('relativeTime=');
|
||||
});
|
||||
|
||||
it('passes correct parameters to useEntityMetrics hook', () => {
|
||||
renderEntityMetrics();
|
||||
expect(mockUseEntityMetrics).toHaveBeenCalledWith(
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useHistory } from 'react-router-dom';
|
||||
import cx from 'classnames';
|
||||
import { Pagination, Skeleton } from 'antd';
|
||||
import { useListRoles } from 'api/generated/services/role';
|
||||
import { AuthtypesGettableRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useRolesFeatureGate } from 'hooks/useRolesFeatureGate';
|
||||
@@ -21,7 +21,7 @@ const PAGE_SIZE = 20;
|
||||
|
||||
type DisplayItem =
|
||||
| { type: 'section'; label: string; count?: number }
|
||||
| { type: 'role'; role: AuthtypesGettableRoleDTO };
|
||||
| { type: 'role'; role: AuthtypesRoleDTO };
|
||||
|
||||
interface RolesListContentProps {
|
||||
searchQuery: string;
|
||||
@@ -176,7 +176,7 @@ function RolesListContent({ searchQuery }: RolesListContentProps): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
const renderRow = (role: AuthtypesGettableRoleDTO): JSX.Element => (
|
||||
const renderRow = (role: AuthtypesRoleDTO): JSX.Element => (
|
||||
<div
|
||||
key={role.id}
|
||||
className={cx(styles.tableRow, {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from 'react-query';
|
||||
import { ErrorType } from 'api/generatedAPIInstance';
|
||||
import type {
|
||||
AuthtypesPostableRoleDTO,
|
||||
AuthtypesRoleDTO,
|
||||
AuthtypesRoleWithTransactionGroupsDTO,
|
||||
AuthtypesTransactionGroupDTO,
|
||||
AuthtypesUpdatableRoleDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
@@ -133,7 +133,7 @@ export function transformTransactionGroupsToResourcePermissions(
|
||||
}
|
||||
|
||||
export function transformApiToRolePermissions(
|
||||
role: AuthtypesRoleDTO,
|
||||
role: AuthtypesRoleWithTransactionGroupsDTO,
|
||||
): RolePermissionsData {
|
||||
return {
|
||||
roleId: role.id,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import type { AuthtypesGettableRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
getGetRolesByUserIDQueryKey,
|
||||
useGetRolesByUserID,
|
||||
@@ -21,11 +21,11 @@ export interface MemberRoleUpdateFailure {
|
||||
}
|
||||
|
||||
interface UseMemberRoleManagerResult {
|
||||
currentRoles: AuthtypesGettableRoleDTO[];
|
||||
currentRoles: AuthtypesRoleDTO[];
|
||||
isLoading: boolean;
|
||||
applyDiff: (
|
||||
localRoleIds: string[],
|
||||
availableRoles: AuthtypesGettableRoleDTO[],
|
||||
availableRoles: AuthtypesRoleDTO[],
|
||||
) => Promise<MemberRoleUpdateFailure[]>;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export function useMemberRoleManager(
|
||||
{ query: { enabled: !!userId && enabled } },
|
||||
);
|
||||
|
||||
const currentRoles = useMemo<AuthtypesGettableRoleDTO[]>(
|
||||
const currentRoles = useMemo<AuthtypesRoleDTO[]>(
|
||||
() => data?.data ?? [],
|
||||
[data?.data],
|
||||
);
|
||||
@@ -61,7 +61,7 @@ export function useMemberRoleManager(
|
||||
const applyDiff = useCallback(
|
||||
async (
|
||||
localRoleIds: string[],
|
||||
availableRoles: AuthtypesGettableRoleDTO[],
|
||||
availableRoles: AuthtypesRoleDTO[],
|
||||
): Promise<MemberRoleUpdateFailure[]> => {
|
||||
const currentRoleIds = new Set(
|
||||
currentRoles.map((r) => r.id).filter(Boolean),
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
useDeleteServiceAccountRoleDeprecated,
|
||||
useGetServiceAccountRoles,
|
||||
} from 'api/generated/services/serviceaccount';
|
||||
import type { AuthtypesGettableRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { retryOn429 } from 'utils/errorUtils';
|
||||
|
||||
const enum PromiseStatus {
|
||||
@@ -21,11 +21,11 @@ export interface RoleUpdateFailure {
|
||||
}
|
||||
|
||||
interface UseServiceAccountRoleManagerResult {
|
||||
currentRoles: AuthtypesGettableRoleDTO[];
|
||||
currentRoles: AuthtypesRoleDTO[];
|
||||
isLoading: boolean;
|
||||
applyDiff: (
|
||||
localRoleIds: string[],
|
||||
availableRoles: AuthtypesGettableRoleDTO[],
|
||||
availableRoles: AuthtypesRoleDTO[],
|
||||
) => Promise<RoleUpdateFailure[]>;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export function useServiceAccountRoleManager(
|
||||
{ query: { enabled: options?.enabled ?? true } },
|
||||
);
|
||||
|
||||
const currentRoles = useMemo<AuthtypesGettableRoleDTO[]>(
|
||||
const currentRoles = useMemo<AuthtypesRoleDTO[]>(
|
||||
() => data?.data ?? [],
|
||||
[data?.data],
|
||||
);
|
||||
@@ -64,7 +64,7 @@ export function useServiceAccountRoleManager(
|
||||
const applyDiff = useCallback(
|
||||
async (
|
||||
localRoleIds: string[],
|
||||
availableRoles: AuthtypesGettableRoleDTO[],
|
||||
availableRoles: AuthtypesRoleDTO[],
|
||||
): Promise<RoleUpdateFailure[]> => {
|
||||
const currentRoleIds = new Set(
|
||||
currentRoles.map((r) => r.id).filter(Boolean),
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import {
|
||||
AuthtypesGettableRoleDTO,
|
||||
AuthtypesRoleDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
const orgId = '019ba2bb-2fa1-7b24-8159-cfca08617ef9';
|
||||
|
||||
export const managedRoles: AuthtypesGettableRoleDTO[] = [
|
||||
export const managedRoles: AuthtypesRoleDTO[] = [
|
||||
{
|
||||
id: '019c24aa-2248-756f-9833-984f1ab63819',
|
||||
createdAt: '2026-02-03T18:00:55.624356Z',
|
||||
@@ -38,7 +35,7 @@ export const managedRoles: AuthtypesGettableRoleDTO[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const customRoles: AuthtypesGettableRoleDTO[] = [
|
||||
export const customRoles: AuthtypesRoleDTO[] = [
|
||||
{
|
||||
id: '019c24aa-3333-0001-aaaa-111111111111',
|
||||
createdAt: '2026-02-10T10:30:00.000Z',
|
||||
@@ -59,24 +56,12 @@ export const customRoles: AuthtypesGettableRoleDTO[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const allRoles: AuthtypesGettableRoleDTO[] = [
|
||||
...managedRoles,
|
||||
...customRoles,
|
||||
];
|
||||
export const allRoles: AuthtypesRoleDTO[] = [...managedRoles, ...customRoles];
|
||||
|
||||
export const listRolesSuccessResponse = {
|
||||
status: 'success',
|
||||
data: allRoles,
|
||||
};
|
||||
|
||||
const customRole: AuthtypesRoleDTO = {
|
||||
...customRoles[0],
|
||||
transactionGroups: [],
|
||||
};
|
||||
const managedRole: AuthtypesRoleDTO = {
|
||||
...managedRoles[0],
|
||||
transactionGroups: [],
|
||||
};
|
||||
|
||||
export const customRoleResponse = { status: 'success', data: customRole };
|
||||
export const managedRoleResponse = { status: 'success', data: managedRole };
|
||||
export const customRoleResponse = { status: 'success', data: customRoles[0] };
|
||||
export const managedRoleResponse = { status: 'success', data: managedRoles[0] };
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
// Mapping between panel types and explorer views
|
||||
export const panelTypeToExplorerView: Record<PANEL_TYPES, ExplorerViews> = {
|
||||
@@ -52,36 +50,3 @@ export const getExplorerViewFromUrl = (
|
||||
export const getExplorerViewForPanelType = (
|
||||
panelType: PANEL_TYPES,
|
||||
): ExplorerViews => panelTypeToExplorerView[panelType];
|
||||
|
||||
export interface MetricsExplorerUrlParams {
|
||||
query: Query;
|
||||
relativeTime?: string;
|
||||
startTimeMs?: number;
|
||||
endTimeMs?: number;
|
||||
}
|
||||
|
||||
export const getMetricsExplorerUrl = ({
|
||||
query,
|
||||
relativeTime,
|
||||
startTimeMs,
|
||||
endTimeMs,
|
||||
}: MetricsExplorerUrlParams): string => {
|
||||
const params = new URLSearchParams();
|
||||
params.set(
|
||||
QueryParams.compositeQuery,
|
||||
encodeURIComponent(JSON.stringify(query)),
|
||||
);
|
||||
|
||||
if (relativeTime) {
|
||||
params.set(QueryParams.relativeTime, relativeTime);
|
||||
} else {
|
||||
if (startTimeMs !== undefined) {
|
||||
params.set(QueryParams.startTime, String(startTimeMs));
|
||||
}
|
||||
if (endTimeMs !== undefined) {
|
||||
params.set(QueryParams.endTime, String(endTimeMs));
|
||||
}
|
||||
}
|
||||
|
||||
return `${ROUTES.METRICS_EXPLORER_EXPLORER}?${params.toString()}`;
|
||||
};
|
||||
|
||||
@@ -47,7 +47,7 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
|
||||
Description: "This endpoint lists all roles",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: make([]*authtypes.GettableRole, 0),
|
||||
Response: make([]*authtypes.Role, 0),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
@@ -73,7 +73,7 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
|
||||
Description: "This endpoint gets a role",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(authtypes.Role),
|
||||
Response: new(authtypes.RoleWithTransactionGroups),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
|
||||
@@ -98,7 +98,7 @@ func (provider *provider) addSpanMapperRoutes(router *mux.Router) error {
|
||||
Description: "Returns all mappers belonging to a mapping group.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(spantypes.GettableSpanMapperGroups),
|
||||
Response: new(spantypes.GettableSpanMappers),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
|
||||
@@ -30,14 +30,17 @@ type AuthZ interface {
|
||||
// Write accepts the insertion tuples and the deletion tuples.
|
||||
Write(context.Context, []*openfgav1.TupleKey, []*openfgav1.TupleKey) error
|
||||
|
||||
// ReadTuples reads tuples from the authorization server matching the given tuple key filter.
|
||||
ReadTuples(context.Context, *openfgav1.ReadRequestTupleKey) ([]*openfgav1.TupleKey, error)
|
||||
// Lists the selectors for objects assigned to subject (s) with relation (r) on resource (s)
|
||||
ListObjects(context.Context, string, authtypes.Relation, coretypes.Type) ([]*coretypes.Object, error)
|
||||
|
||||
// Creates the role with its transaction groups.
|
||||
Create(context.Context, valuer.UUID, *authtypes.Role) error
|
||||
Create(context.Context, valuer.UUID, *authtypes.RoleWithTransactionGroups) error
|
||||
|
||||
// Gets the role if it exists or creates one.
|
||||
GetOrCreate(context.Context, valuer.UUID, *authtypes.Role) (*authtypes.Role, error)
|
||||
|
||||
// Updates the role's metadata and reconciles its transaction groups.
|
||||
Update(context.Context, valuer.UUID, *authtypes.Role) error
|
||||
Update(context.Context, valuer.UUID, *authtypes.RoleWithTransactionGroups) error
|
||||
|
||||
// Deletes the role and tuples in authorization server.
|
||||
Delete(context.Context, valuer.UUID, valuer.UUID) error
|
||||
@@ -45,6 +48,9 @@ type AuthZ interface {
|
||||
// Gets the role
|
||||
Get(context.Context, valuer.UUID, valuer.UUID) (*authtypes.Role, error)
|
||||
|
||||
// Gets the role with transaction groups
|
||||
GetWithTransactionGroups(context.Context, valuer.UUID, valuer.UUID) (*authtypes.RoleWithTransactionGroups, error)
|
||||
|
||||
// Gets the role by org_id and name
|
||||
GetByOrgIDAndName(context.Context, valuer.UUID, string) (*authtypes.Role, error)
|
||||
|
||||
@@ -74,6 +80,9 @@ type AuthZ interface {
|
||||
|
||||
// Bootstrap managed roles transactions and user assignments
|
||||
CreateManagedUserRoleTransactions(context.Context, valuer.UUID, valuer.UUID) error
|
||||
|
||||
// ReadTuples reads tuples from the authorization server matching the given tuple key filter.
|
||||
ReadTuples(context.Context, *openfgav1.ReadRequestTupleKey) ([]*openfgav1.TupleKey, error)
|
||||
}
|
||||
|
||||
// OnBeforeRoleDelete is a callback invoked before a role is deleted.
|
||||
|
||||
@@ -83,6 +83,10 @@ func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.
|
||||
return provider.store.Get(ctx, orgID, id)
|
||||
}
|
||||
|
||||
func (provider *provider) GetWithTransactionGroups(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.RoleWithTransactionGroups, error) {
|
||||
return nil, errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
|
||||
}
|
||||
|
||||
func (provider *provider) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*authtypes.Role, error) {
|
||||
return provider.store.GetByOrgIDAndName(ctx, orgID, name)
|
||||
}
|
||||
@@ -177,11 +181,15 @@ func (provider *provider) CreateManagedUserRoleTransactions(ctx context.Context,
|
||||
return provider.Grant(ctx, orgID, []string{authtypes.SigNozAdminRoleName}, authtypes.MustNewSubject(coretypes.NewResourceUser(), userID.String(), orgID, nil))
|
||||
}
|
||||
|
||||
func (setter *provider) Create(_ context.Context, _ valuer.UUID, _ *authtypes.Role) error {
|
||||
func (setter *provider) Create(_ context.Context, _ valuer.UUID, _ *authtypes.RoleWithTransactionGroups) error {
|
||||
return errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
|
||||
}
|
||||
|
||||
func (provider *provider) Update(_ context.Context, _ valuer.UUID, _ *authtypes.Role) error {
|
||||
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) Update(_ context.Context, _ valuer.UUID, _ *authtypes.RoleWithTransactionGroups) error {
|
||||
return errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
|
||||
}
|
||||
|
||||
|
||||
@@ -36,14 +36,14 @@ func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
role := authtypes.NewRole(req.Name, req.Description, authtypes.RoleTypeCustom, valuer.MustNewUUID(claims.OrgID), req.TransactionGroups)
|
||||
err = handler.authz.Create(ctx, valuer.MustNewUUID(claims.OrgID), role)
|
||||
roleWithTransactionGroups := authtypes.NewRoleWithTransactionGroups(req.Name, req.Description, authtypes.RoleTypeCustom, valuer.MustNewUUID(claims.OrgID), req.TransactionGroups)
|
||||
err = handler.authz.Create(ctx, valuer.MustNewUUID(claims.OrgID), roleWithTransactionGroups)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusCreated, types.Identifiable{ID: role.ID})
|
||||
render.Success(rw, http.StatusCreated, types.Identifiable{ID: roleWithTransactionGroups.ID})
|
||||
}
|
||||
|
||||
func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
|
||||
@@ -65,13 +65,13 @@ func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
role, err := handler.authz.Get(ctx, valuer.MustNewUUID(claims.OrgID), roleID)
|
||||
roleWithTransactionGroups, err := handler.authz.GetWithTransactionGroups(ctx, valuer.MustNewUUID(claims.OrgID), roleID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, role)
|
||||
render.Success(rw, http.StatusOK, roleWithTransactionGroups)
|
||||
}
|
||||
|
||||
func (handler *handler) List(rw http.ResponseWriter, r *http.Request) {
|
||||
@@ -88,7 +88,7 @@ func (handler *handler) List(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, authtypes.NewGettableRolesFromRoles(roles))
|
||||
render.Success(rw, http.StatusOK, roles)
|
||||
}
|
||||
|
||||
func (handler *handler) Update(rw http.ResponseWriter, r *http.Request) {
|
||||
@@ -117,13 +117,14 @@ func (handler *handler) Update(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
err = role.Update(req.Description, req.TransactionGroups)
|
||||
roleWithTransactionGroups := authtypes.MakeRoleWithTransactionGroups(role, nil)
|
||||
err = roleWithTransactionGroups.Update(req.Description, req.TransactionGroups)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.authz.Update(ctx, valuer.MustNewUUID(claims.OrgID), role)
|
||||
err = handler.authz.Update(ctx, valuer.MustNewUUID(claims.OrgID), roleWithTransactionGroups)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
|
||||
@@ -729,11 +729,7 @@ func (v *filterExpressionVisitor) VisitFunctionCall(ctx *grammar.FunctionCallCon
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
|
||||
value, err := normalizeFunctionValue(operator, functionName, params[1:])
|
||||
if err != nil {
|
||||
v.errors = append(v.errors, err.Error())
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
value := params[1:]
|
||||
|
||||
conds, ok := v.buildConditions(key, matchingFieldKeys(key, v.fieldKeys), operator, value)
|
||||
if !ok {
|
||||
@@ -749,44 +745,6 @@ func (v *filterExpressionVisitor) VisitFunctionCall(ctx *grammar.FunctionCallCon
|
||||
return v.builder.Or(conds...)
|
||||
}
|
||||
|
||||
// normalizeFunctionValue validates and normalizes the value argument(s) of a has-family
|
||||
// function call, returning them in the wrapper slice the condition builder unwraps.
|
||||
//
|
||||
// - has/hasToken take exactly one scalar value. More than one argument, or an array
|
||||
// argument, is rejected rather than silently dropping the extras.
|
||||
// - hasAny/hasAll take a set of values, supplied either as a single array literal
|
||||
// (hasAny(k, ['a','b'])) or as several scalar arguments (hasAny(k, 'a', 'b')); the
|
||||
// latter are folded into one list so no argument is silently ignored.
|
||||
func normalizeFunctionValue(operator qbtypes.FilterOperator, functionName string, valueParams []any) (any, error) {
|
||||
switch operator {
|
||||
case qbtypes.FilterOperatorHas, qbtypes.FilterOperatorHasToken:
|
||||
if len(valueParams) != 1 {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "function `%s` expects exactly one value argument", functionName)
|
||||
}
|
||||
if _, isArray := valueParams[0].([]any); isArray {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "function `%s` expects a single scalar value, not an array", functionName)
|
||||
}
|
||||
return valueParams, nil
|
||||
case qbtypes.FilterOperatorHasAny, qbtypes.FilterOperatorHasAll:
|
||||
// A single array literal is already the value set.
|
||||
if len(valueParams) == 1 {
|
||||
if _, isArray := valueParams[0].([]any); isArray {
|
||||
return valueParams, nil
|
||||
}
|
||||
}
|
||||
// Otherwise fold the positional scalar arguments into one list.
|
||||
values := make([]any, 0, len(valueParams))
|
||||
for _, p := range valueParams {
|
||||
if _, isArray := p.([]any); isArray {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "function `%s` expects either a single array literal or scalar values, not a mix of the two", functionName)
|
||||
}
|
||||
values = append(values, p)
|
||||
}
|
||||
return []any{values}, nil
|
||||
}
|
||||
return valueParams, nil
|
||||
}
|
||||
|
||||
// VisitFunctionParamList handles the parameter list for function calls.
|
||||
func (v *filterExpressionVisitor) VisitFunctionParamList(ctx *grammar.FunctionParamListContext) any {
|
||||
params := ctx.AllFunctionParam()
|
||||
|
||||
@@ -218,7 +218,6 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewMigrateSSORoleMappingNamesFactory(sqlstore),
|
||||
sqlmigration.NewAddMetricReductionRulesFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewRemoveOrganizationTuplesFactory(sqlstore),
|
||||
sqlmigration.NewAddRoleTransactionGroupsFactory(sqlstore, sqlschema),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package sqlmigration
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
@@ -19,30 +18,6 @@ type addManagedRoles struct {
|
||||
sqlschema sqlschema.SQLSchema
|
||||
}
|
||||
|
||||
type role struct {
|
||||
bun.BaseModel `bun:"table:role"`
|
||||
|
||||
ID valuer.UUID `bun:"id,pk,type:text"`
|
||||
CreatedAt time.Time `bun:"created_at"`
|
||||
UpdatedAt time.Time `bun:"updated_at"`
|
||||
Name string `bun:"name,type:string"`
|
||||
Description string `bun:"description,type:string"`
|
||||
Type string `bun:"type,type:string"`
|
||||
OrgID valuer.UUID `bun:"org_id,type:string"`
|
||||
}
|
||||
|
||||
func newManagedRole(name, description string, orgID valuer.UUID) *role {
|
||||
return &role{
|
||||
ID: valuer.GenerateUUID(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
Name: name,
|
||||
Description: description,
|
||||
Type: authtypes.RoleTypeManaged.StringValue(),
|
||||
OrgID: orgID,
|
||||
}
|
||||
}
|
||||
|
||||
func NewAddManagedRolesFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_managed_roles"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return newAddManagedRoles(ctx, ps, c, sqlstore, sqlschema)
|
||||
@@ -79,19 +54,28 @@ func (migration *addManagedRoles) Up(ctx context.Context, db *bun.DB) error {
|
||||
return err
|
||||
}
|
||||
|
||||
managedRoles := []*role{}
|
||||
managedRoles := []*authtypes.Role{}
|
||||
for _, orgIDStr := range orgIDs {
|
||||
orgID, err := valuer.NewUUID(orgIDStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
managedRoles = append(managedRoles,
|
||||
newManagedRole(authtypes.SigNozAdminRoleName, authtypes.SigNozAdminRoleDescription, orgID),
|
||||
newManagedRole(authtypes.SigNozEditorRoleName, authtypes.SigNozEditorRoleDescription, orgID),
|
||||
newManagedRole(authtypes.SigNozViewerRoleName, authtypes.SigNozViewerRoleDescription, orgID),
|
||||
newManagedRole(authtypes.SigNozAnonymousRoleName, authtypes.SigNozAnonymousRoleDescription, orgID),
|
||||
)
|
||||
// signoz admin
|
||||
signozAdminRole := authtypes.NewRole(authtypes.SigNozAdminRoleName, authtypes.SigNozAdminRoleDescription, authtypes.RoleTypeManaged, orgID)
|
||||
managedRoles = append(managedRoles, signozAdminRole)
|
||||
|
||||
// signoz editor
|
||||
signozEditorRole := authtypes.NewRole(authtypes.SigNozEditorRoleName, authtypes.SigNozEditorRoleDescription, authtypes.RoleTypeManaged, orgID)
|
||||
managedRoles = append(managedRoles, signozEditorRole)
|
||||
|
||||
// signoz viewer
|
||||
signozViewerRole := authtypes.NewRole(authtypes.SigNozViewerRoleName, authtypes.SigNozViewerRoleDescription, authtypes.RoleTypeManaged, orgID)
|
||||
managedRoles = append(managedRoles, signozViewerRole)
|
||||
|
||||
// signoz anonymous
|
||||
signozAnonymousRole := authtypes.NewRole(authtypes.SigNozAnonymousRoleName, authtypes.SigNozAnonymousRoleDescription, authtypes.RoleTypeManaged, orgID)
|
||||
managedRoles = append(managedRoles, signozAnonymousRole)
|
||||
}
|
||||
|
||||
if len(managedRoles) > 0 {
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type addRoleTransactionGroups struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
sqlschema sqlschema.SQLSchema
|
||||
}
|
||||
|
||||
type roles struct {
|
||||
bun.BaseModel `bun:"table:role"`
|
||||
|
||||
ID string `bun:"id,pk"`
|
||||
Name string `bun:"name"`
|
||||
OrgID string `bun:"org_id"`
|
||||
}
|
||||
|
||||
func NewAddRoleTransactionGroupsFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(
|
||||
factory.MustNewName("add_role_transaction_groups"),
|
||||
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &addRoleTransactionGroups{
|
||||
sqlstore: sqlstore,
|
||||
sqlschema: sqlschema,
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (migration *addRoleTransactionGroups) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
func (migration *addRoleTransactionGroups) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
table, uniqueConstraints, err := migration.sqlschema.GetTable(ctx, "role")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
column := &sqlschema.Column{
|
||||
Name: sqlschema.ColumnName("transaction_groups"),
|
||||
DataType: sqlschema.DataTypeText,
|
||||
Nullable: true,
|
||||
}
|
||||
|
||||
sqls := migration.sqlschema.Operator().AddColumn(table, uniqueConstraints, column, nil)
|
||||
for _, sql := range sqls {
|
||||
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var storeID string
|
||||
err = tx.QueryRowContext(ctx, `SELECT id FROM store WHERE name = ? LIMIT 1`, "signoz").Scan(&storeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var orgIDs []string
|
||||
err = tx.NewSelect().
|
||||
Table("organizations").
|
||||
Column("id").
|
||||
Scan(ctx, &orgIDs)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
|
||||
managedRoleGroups := make(map[string]string, len(coretypes.ManagedRoleToTransactions))
|
||||
for roleName, transactions := range coretypes.ManagedRoleToTransactions {
|
||||
data, err := json.Marshal(authtypes.NewTransactionGroupsFromTransactions(transactions))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
managedRoleGroups[roleName] = string(data)
|
||||
}
|
||||
|
||||
isPG := migration.sqlstore.BunDB().Dialect().Name() == dialect.PG
|
||||
for _, orgID := range orgIDs {
|
||||
customRoles := make([]*roles, 0)
|
||||
err = tx.NewSelect().
|
||||
Model(&customRoles).
|
||||
Column("id", "name").
|
||||
Where("org_id = ?", orgID).
|
||||
Where("type = ?", authtypes.RoleTypeCustom.StringValue()).
|
||||
Where("transaction_groups IS NULL").
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, role := range customRoles {
|
||||
tuples, err := migration.readRoleTuples(ctx, tx, isPG, storeID, orgID, role.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
groups := authtypes.MustNewTransactionGroupsFromTuples(tuples)
|
||||
data, err := json.Marshal(groups)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.NewUpdate().
|
||||
Model(new(roles)).
|
||||
Set("transaction_groups = ?", string(data)).
|
||||
Where("id = ?", role.ID).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for roleName, data := range managedRoleGroups {
|
||||
if _, err := tx.NewUpdate().
|
||||
Model(new(roles)).
|
||||
Set("transaction_groups = ?", data).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("type = ?", authtypes.RoleTypeManaged.StringValue()).
|
||||
Where("name = ?", roleName).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *addRoleTransactionGroups) readRoleTuples(ctx context.Context, tx bun.Tx, isPG bool, storeID, orgID, roleName string) ([]*openfgav1.TupleKey, error) {
|
||||
roleSubject := "organization/" + orgID + "/role/" + roleName
|
||||
|
||||
var tupleRows *sql.Rows
|
||||
var err error
|
||||
if isPG {
|
||||
tupleRows, err = tx.QueryContext(ctx, `
|
||||
SELECT object_type, object_id, relation FROM tuple
|
||||
WHERE store = ? AND _user = ?`,
|
||||
storeID, "role:"+roleSubject+"#assignee",
|
||||
)
|
||||
} else {
|
||||
tupleRows, err = tx.QueryContext(ctx, `
|
||||
SELECT object_type, object_id, relation FROM tuple
|
||||
WHERE store = ? AND user_object_type = ? AND user_object_id = ? AND user_relation = ?`,
|
||||
storeID, "role", roleSubject, "assignee",
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tupleRows.Close()
|
||||
|
||||
tuples := make([]*openfgav1.TupleKey, 0)
|
||||
for tupleRows.Next() {
|
||||
var objectType, objectID, relation string
|
||||
if err := tupleRows.Scan(&objectType, &objectID, &relation); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tuples = append(tuples, &openfgav1.TupleKey{Object: objectType + ":" + objectID, Relation: relation})
|
||||
}
|
||||
if err := tupleRows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tuples, nil
|
||||
}
|
||||
|
||||
func (migration *addRoleTransactionGroups) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -40,10 +40,12 @@ func isBodyJSONSearch(key *telemetrytypes.TelemetryFieldKey, columns []*schema.C
|
||||
return false
|
||||
}
|
||||
|
||||
// conditionForArrayFunction builds has/hasAny/hasAll over a body JSON path — via the JSON
|
||||
// access plan (flag on) or legacy typed extraction (flag off).
|
||||
// conditionForArrayFunction builds `has/hasAny/hasAll(<arrayFieldExpr>, value)` over a
|
||||
// body JSON array field. The field expression uses the JSON accessor (flag on) or
|
||||
// legacy string extraction (flag off); value[0] is the needle.
|
||||
func (c *conditionBuilder) conditionForArrayFunction(
|
||||
ctx context.Context,
|
||||
startNs, endNs uint64,
|
||||
key *telemetrytypes.TelemetryFieldKey,
|
||||
operator qbtypes.FilterOperator,
|
||||
value any,
|
||||
@@ -60,48 +62,24 @@ func (c *conditionBuilder) conditionForArrayFunction(
|
||||
needle = args[0]
|
||||
}
|
||||
|
||||
var fieldExpr string
|
||||
if c.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{})) {
|
||||
// JSON access plan: data-type collision handling, nested array paths.
|
||||
valueType, needle := InferDataType(needle, operator, key)
|
||||
return NewJSONConditionBuilder(key, valueType).buildArrayFunctionCondition(operator, needle, sb)
|
||||
fe, err := c.fm.FieldFor(ctx, startNs, endNs, key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fieldExpr = fe
|
||||
} else {
|
||||
// legacy string-body path; value drives array-type inference (e.g. `[*]` paths)
|
||||
fieldExpr, _ = GetBodyJSONKey(ctx, key, qbtypes.FilterOperatorUnknown, value)
|
||||
}
|
||||
|
||||
// legacy string-body path: type-matched array extraction, OR-ed with a scalar comparison
|
||||
// for a scalar body value (coalesced to false so NOT has() matches missing-key rows).
|
||||
elemType := legacyElemType(needle)
|
||||
arrayExpr := getBodyJSONArrayKey(key, elemType)
|
||||
scalarExpr, scalarGuard, hasScalar := getBodyJSONScalarKey(key, elemType)
|
||||
if list, ok := needle.([]any); ok {
|
||||
vals := make([]any, len(list))
|
||||
for i, v := range list {
|
||||
vals[i] = legacyCoerceNeedle(v, elemType)
|
||||
}
|
||||
arrayCond := fmt.Sprintf("%s(%s, %s)", operator.FunctionName(), arrayExpr, sb.Var(vals))
|
||||
if !hasScalar {
|
||||
return arrayCond, nil
|
||||
}
|
||||
var membership string
|
||||
if operator == qbtypes.FilterOperatorHasAll {
|
||||
eqs := make([]string, len(vals))
|
||||
for i, v := range vals {
|
||||
eqs[i] = sb.E(scalarExpr, v)
|
||||
}
|
||||
membership = sb.And(eqs...)
|
||||
} else {
|
||||
membership = sb.In(scalarExpr, vals...)
|
||||
}
|
||||
return fmt.Sprintf("(%s OR ifNull(%s, false))", arrayCond, sb.And(membership, scalarGuard)), nil
|
||||
}
|
||||
typedNeedle := legacyCoerceNeedle(needle, elemType)
|
||||
arrayCond := fmt.Sprintf("%s(%s, %s)", operator.FunctionName(), arrayExpr, sb.Var(typedNeedle))
|
||||
if !hasScalar {
|
||||
return arrayCond, nil
|
||||
}
|
||||
return fmt.Sprintf("(%s OR ifNull(%s, false))", arrayCond, sb.And(sb.E(scalarExpr, typedNeedle), scalarGuard)), nil
|
||||
return fmt.Sprintf("%s(%s, %s)", operator.FunctionName(), fieldExpr, sb.Var(needle)), nil
|
||||
}
|
||||
|
||||
// conditionForHasToken builds a hasToken full-text search over the body column, resolving the
|
||||
// column from the key name + use_json_body flag.
|
||||
// conditionForHasToken builds `hasToken(LOWER(<bodyColumn>), LOWER(<needle>))`, a
|
||||
// full-text token search over the body column. It resolves the column from the key
|
||||
// name + use_json_body flag, validates the field/value, and tags errors with the doc URL.
|
||||
func (c *conditionBuilder) conditionForHasToken(
|
||||
ctx context.Context,
|
||||
key *telemetrytypes.TelemetryFieldKey,
|
||||
@@ -114,37 +92,28 @@ func (c *conditionBuilder) conditionForHasToken(
|
||||
needle = args[0]
|
||||
}
|
||||
|
||||
// TODO(Tushar): thread orgID here to evaluate correctly
|
||||
bodyJSONEnabled := c.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{}))
|
||||
|
||||
columnName := LogsV2BodyColumn
|
||||
if bodyJSONEnabled {
|
||||
if key.Name != LogsV2BodyColumn && key.Name != bodyMessageField {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||
"function `hasToken` only supports body/body.message field as first parameter").WithUrl(hasTokenFunctionDocURL)
|
||||
}
|
||||
columnName = bodyMessageField
|
||||
} else if key.Name != LogsV2BodyColumn {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||
"function `hasToken` only supports body field as first parameter").WithUrl(hasTokenFunctionDocURL)
|
||||
}
|
||||
|
||||
// hasToken matches string tokens only.
|
||||
if _, ok := needle.(string); !ok {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||
"function `hasToken` expects value parameter to be a string").WithUrl(hasTokenFunctionDocURL)
|
||||
}
|
||||
|
||||
// TODO(Tushar): thread orgID here to evaluate correctly
|
||||
bodyJSONEnabled := c.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{}))
|
||||
|
||||
if !bodyJSONEnabled {
|
||||
// legacy: token search over the plain body string column only.
|
||||
if key.Name != LogsV2BodyColumn {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||
"function `hasToken` only supports body field as first parameter").WithUrl(hasTokenFunctionDocURL)
|
||||
}
|
||||
return fmt.Sprintf("hasToken(LOWER(%s), LOWER(%s))", LogsV2BodyColumn, sb.Var(needle)), nil
|
||||
}
|
||||
|
||||
// JSON mode: a bare body/body.message key searches the body.message column; any other body
|
||||
// field is a token search over its JSON string field, incl. strings nested in arrays.
|
||||
// `body.message` resolves to a body-context key named `message`, so match that too — else it
|
||||
// falls through and emits dynamicElement over the already-typed String column, which errors.
|
||||
if key.Name == LogsV2BodyColumn || key.Name == bodyMessageField ||
|
||||
(key.FieldContext == telemetrytypes.FieldContextBody && key.Name == messageSubField) {
|
||||
return fmt.Sprintf("hasToken(LOWER(%s), LOWER(%s))", bodyMessageField, sb.Var(needle)), nil
|
||||
}
|
||||
if key.FieldContext == telemetrytypes.FieldContextBody {
|
||||
return NewJSONConditionBuilder(key, telemetrytypes.FieldDataTypeString).buildTokenFunctionCondition(needle, sb)
|
||||
}
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||
"function `hasToken` only supports the body field or a body JSON string field as first parameter").WithUrl(hasTokenFunctionDocURL)
|
||||
return fmt.Sprintf("hasToken(LOWER(%s), LOWER(%s))", columnName, sb.Var(needle)), nil
|
||||
}
|
||||
|
||||
func (c *conditionBuilder) conditionFor(
|
||||
@@ -155,7 +124,8 @@ func (c *conditionBuilder) conditionFor(
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) (string, error) {
|
||||
// hasToken resolves from the key name + flag alone (no column resolution), so handle it first.
|
||||
// hasToken is a token search over the body column resolved purely from the key
|
||||
// name + flag, independent of column resolution, so handle it before anything else.
|
||||
if operator == qbtypes.FilterOperatorHasToken {
|
||||
return c.conditionForHasToken(ctx, key, value, sb)
|
||||
}
|
||||
@@ -165,9 +135,10 @@ func (c *conditionBuilder) conditionFor(
|
||||
return "", err
|
||||
}
|
||||
|
||||
// has/hasAny/hasAll take the body-JSON path, not the normal operator paths.
|
||||
// has/hasAny/hasAll build `has(<arrayFieldExpr>, value)` over body JSON arrays
|
||||
// rather than going through the normal operator paths, so handle them up front.
|
||||
if operator.IsArrayFunctionOperator() {
|
||||
return c.conditionForArrayFunction(ctx, key, operator, value, columns, sb)
|
||||
return c.conditionForArrayFunction(ctx, startNs, endNs, key, operator, value, columns, sb)
|
||||
}
|
||||
|
||||
// TODO(Piyush): Update this to support multiple JSON columns based on evolutions
|
||||
@@ -431,6 +402,23 @@ func (c *conditionBuilder) ConditionFor(
|
||||
}
|
||||
}
|
||||
|
||||
// has/hasAny/hasAll need an array field: in JSON-body mode drop non-array matches so a
|
||||
// scalar errors clearly instead of failing at ClickHouse runtime (legacy mode skips this).
|
||||
if operator.IsArrayFunctionOperator() &&
|
||||
c.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{})) {
|
||||
arrayKeys := make([]*telemetrytypes.TelemetryFieldKey, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
if k.FieldDataType.IsArray() {
|
||||
arrayKeys = append(arrayKeys, k)
|
||||
}
|
||||
}
|
||||
if len(arrayKeys) == 0 {
|
||||
return nil, warnings, errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||
"function `%s` expects key parameter to be an array field; no array fields found", operator.FunctionName())
|
||||
}
|
||||
keys = arrayKeys
|
||||
}
|
||||
|
||||
conds := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
cond, err := c.conditionForKey(ctx, startNs, endNs, k, operator, value, sb)
|
||||
|
||||
@@ -44,66 +44,34 @@ func TestFilterExprLogsBodyJSON(t *testing.T) {
|
||||
category: "json",
|
||||
query: "has(body.requestor_list[*], 'index_service')",
|
||||
shouldPass: true,
|
||||
expectedQuery: `WHERE (has(JSONExtract(JSON_QUERY(body, '$."requestor_list"[*]'), 'Array(Nullable(String))'), ?) OR ifNull((JSON_VALUE(body, '$."requestor_list"') = ? AND JSONType(body, 'requestor_list') NOT IN ('Array', 'Object', 'Null')), false))`,
|
||||
expectedArgs: []any{"index_service", "index_service"},
|
||||
expectedQuery: `WHERE has(JSONExtract(JSON_QUERY(body, '$."requestor_list"[*]'), 'Array(String)'), ?)`,
|
||||
expectedArgs: []any{"index_service"},
|
||||
expectedErrorContains: "",
|
||||
},
|
||||
{
|
||||
category: "json",
|
||||
query: "has(body.int_numbers[*], 2)",
|
||||
shouldPass: true,
|
||||
expectedQuery: `WHERE (has(JSONExtract(JSON_QUERY(body, '$."int_numbers"[*]'), 'Array(Nullable(Float64))'), ?) OR ifNull((JSONExtract(JSON_VALUE(body, '$."int_numbers"'), 'Nullable(Float64)') = ? AND JSONType(body, 'int_numbers') NOT IN ('Array', 'Object', 'Null')), false))`,
|
||||
expectedArgs: []any{float64(2), float64(2)},
|
||||
expectedQuery: `WHERE has(JSONExtract(JSON_QUERY(body, '$."int_numbers"[*]'), 'Array(Float64)'), ?)`,
|
||||
expectedArgs: []any{float64(2)},
|
||||
expectedErrorContains: "",
|
||||
},
|
||||
{
|
||||
category: "json",
|
||||
query: "has(body.bool[*], true)",
|
||||
shouldPass: true,
|
||||
expectedQuery: `WHERE (has(JSONExtract(JSON_QUERY(body, '$."bool"[*]'), 'Array(Nullable(String))'), ?) OR ifNull((JSON_VALUE(body, '$."bool"') = ? AND JSONType(body, 'bool') NOT IN ('Array', 'Object', 'Null')), false))`,
|
||||
expectedArgs: []any{"true", "true"},
|
||||
expectedQuery: `WHERE has(JSONExtract(JSON_QUERY(body, '$."bool"[*]'), 'Array(Bool)'), ?)`,
|
||||
expectedArgs: []any{true},
|
||||
expectedErrorContains: "",
|
||||
},
|
||||
{
|
||||
category: "json",
|
||||
query: "NOT has(body.nested_num[*].float_nums[*], 2.2)",
|
||||
shouldPass: true,
|
||||
expectedQuery: `WHERE NOT (has(JSONExtract(JSON_QUERY(body, '$."nested_num"[*]."float_nums"[*]'), 'Array(Nullable(Float64))'), ?))`,
|
||||
expectedQuery: `WHERE NOT (has(JSONExtract(JSON_QUERY(body, '$."nested_num"[*]."float_nums"[*]'), 'Array(Float64)'), ?))`,
|
||||
expectedArgs: []any{float64(2.2)},
|
||||
expectedErrorContains: "",
|
||||
},
|
||||
{
|
||||
category: "json",
|
||||
query: "has(body.tags, 'production')",
|
||||
shouldPass: true,
|
||||
expectedQuery: `WHERE (has(JSONExtract(JSON_QUERY(body, '$."tags"[*]'), 'Array(Nullable(String))'), ?) OR ifNull((JSON_VALUE(body, '$."tags"') = ? AND JSONType(body, 'tags') NOT IN ('Array', 'Object', 'Null')), false))`,
|
||||
expectedArgs: []any{"production", "production"},
|
||||
expectedErrorContains: "",
|
||||
},
|
||||
{
|
||||
category: "json",
|
||||
query: "hasAny(body.tags, ['critical', 'test'])",
|
||||
shouldPass: true,
|
||||
expectedQuery: `WHERE (hasAny(JSONExtract(JSON_QUERY(body, '$."tags"[*]'), 'Array(Nullable(String))'), ?) OR ifNull((JSON_VALUE(body, '$."tags"') IN (?, ?) AND JSONType(body, 'tags') NOT IN ('Array', 'Object', 'Null')), false))`,
|
||||
expectedArgs: []any{[]any{"critical", "test"}, "critical", "test"},
|
||||
expectedErrorContains: "",
|
||||
},
|
||||
{
|
||||
category: "json",
|
||||
query: "hasAll(body.tags, ['production', 'web'])",
|
||||
shouldPass: true,
|
||||
expectedQuery: `WHERE (hasAll(JSONExtract(JSON_QUERY(body, '$."tags"[*]'), 'Array(Nullable(String))'), ?) OR ifNull(((JSON_VALUE(body, '$."tags"') = ? AND JSON_VALUE(body, '$."tags"') = ?) AND JSONType(body, 'tags') NOT IN ('Array', 'Object', 'Null')), false))`,
|
||||
expectedArgs: []any{[]any{"production", "web"}, "production", "web"},
|
||||
expectedErrorContains: "",
|
||||
},
|
||||
{
|
||||
category: "json",
|
||||
query: "has(body.ids, \"200\")",
|
||||
shouldPass: true,
|
||||
expectedQuery: `WHERE (has(JSONExtract(JSON_QUERY(body, '$."ids"[*]'), 'Array(Nullable(Int64))'), ?) OR ifNull((JSONExtract(JSON_VALUE(body, '$."ids"'), 'Nullable(Int64)') = ? AND JSONType(body, 'ids') NOT IN ('Array', 'Object', 'Null')), false))`,
|
||||
expectedArgs: []any{int64(200), int64(200)},
|
||||
expectedErrorContains: "",
|
||||
},
|
||||
{
|
||||
category: "json",
|
||||
query: "body.message = hello",
|
||||
|
||||
@@ -1561,25 +1561,6 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
expectedArgs: []any{"download"},
|
||||
expectedErrorContains: "function `hasToken` expects value parameter to be a string",
|
||||
},
|
||||
// extra / mis-shaped value arguments are rejected, not silently dropped.
|
||||
{
|
||||
category: "hasExtraArgs",
|
||||
query: "has(body.tags[*], \"a\", \"b\")",
|
||||
shouldPass: false,
|
||||
expectedErrorContains: "function `has` expects exactly one value argument",
|
||||
},
|
||||
{
|
||||
category: "hasArrayArg",
|
||||
query: "has(body.tags[*], [\"a\", \"b\"])",
|
||||
shouldPass: false,
|
||||
expectedErrorContains: "function `has` expects a single scalar value, not an array",
|
||||
},
|
||||
{
|
||||
category: "hasTokenExtraArgs",
|
||||
query: "hasToken(body, \"a\", \"b\")",
|
||||
shouldPass: false,
|
||||
expectedErrorContains: "function `hasToken` expects exactly one value argument",
|
||||
},
|
||||
|
||||
// Basic materialized key
|
||||
{
|
||||
|
||||
@@ -96,18 +96,6 @@ func applyNotCondition(operator qbtypes.FilterOperator) (bool, qbtypes.FilterOpe
|
||||
return false, operator
|
||||
}
|
||||
|
||||
// branchArrayExpr returns the ClickHouse array expression for a given array-type branch
|
||||
// at this hop. The JSON branch reads Array(JSON(...)) directly; the Dynamic branch filters
|
||||
// the Array(Dynamic) down to its JSON elements and maps them to JSON.
|
||||
func (c *jsonConditionBuilder) branchArrayExpr(node *telemetrytypes.JSONAccessNode, branch telemetrytypes.JSONAccessBranchType) string {
|
||||
fieldPath := node.FieldPath()
|
||||
if branch == telemetrytypes.BranchDynamic {
|
||||
dynBaseExpr := fmt.Sprintf("dynamicElement(%s, 'Array(Dynamic)')", fieldPath)
|
||||
return fmt.Sprintf("arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), %s))", dynBaseExpr)
|
||||
}
|
||||
return fmt.Sprintf("dynamicElement(%s, 'Array(JSON(max_dynamic_types=%d, max_dynamic_paths=%d))')", fieldPath, node.MaxDynamicTypes, node.MaxDynamicPaths)
|
||||
}
|
||||
|
||||
// buildAccessNodeBranches builds conditions for each branch of the access node.
|
||||
func (c *jsonConditionBuilder) buildAccessNodeBranches(current *telemetrytypes.JSONAccessNode, operator qbtypes.FilterOperator, value any, sb *sqlbuilder.SelectBuilder) (string, error) {
|
||||
if current == nil {
|
||||
@@ -115,15 +103,31 @@ func (c *jsonConditionBuilder) buildAccessNodeBranches(current *telemetrytypes.J
|
||||
}
|
||||
|
||||
currAlias := current.Alias()
|
||||
// At this hop, compute the child condition per array branch (JSON before Dynamic) and
|
||||
// wrap each in arrayExists over the corresponding array expression.
|
||||
fieldPath := current.FieldPath()
|
||||
// Determine availability of Array(JSON) and Array(Dynamic) at this hop
|
||||
hasArrayJSON := current.Branches[telemetrytypes.BranchJSON] != nil
|
||||
hasArrayDynamic := current.Branches[telemetrytypes.BranchDynamic] != nil
|
||||
|
||||
// Then, at this hop, compute child per branch and wrap
|
||||
branches := make([]string, 0, 2)
|
||||
for _, branch := range current.BranchesInOrder() {
|
||||
childGroup, err := c.recurseArrayHops(current.Branches[branch], operator, value, sb)
|
||||
if hasArrayJSON {
|
||||
jsonArrayExpr := fmt.Sprintf("dynamicElement(%s, 'Array(JSON(max_dynamic_types=%d, max_dynamic_paths=%d))')", fieldPath, current.MaxDynamicTypes, current.MaxDynamicPaths)
|
||||
childGroupJSON, err := c.recurseArrayHops(current.Branches[telemetrytypes.BranchJSON], operator, value, sb)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
branches = append(branches, fmt.Sprintf("arrayExists(%s-> %s, %s)", currAlias, childGroup, c.branchArrayExpr(current, branch)))
|
||||
branches = append(branches, fmt.Sprintf("arrayExists(%s-> %s, %s)", currAlias, childGroupJSON, jsonArrayExpr))
|
||||
}
|
||||
if hasArrayDynamic {
|
||||
dynBaseExpr := fmt.Sprintf("dynamicElement(%s, 'Array(Dynamic)')", fieldPath)
|
||||
dynFilteredExpr := fmt.Sprintf("arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), %s))", dynBaseExpr)
|
||||
|
||||
// Create the Query for Dynamic array
|
||||
childGroupDyn, err := c.recurseArrayHops(current.Branches[telemetrytypes.BranchDynamic], operator, value, sb)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
branches = append(branches, fmt.Sprintf("arrayExists(%s-> %s, %s)", currAlias, childGroupDyn, dynFilteredExpr))
|
||||
}
|
||||
|
||||
if len(branches) == 1 {
|
||||
@@ -305,174 +309,6 @@ func (c *jsonConditionBuilder) buildArrayMembershipCondition(node *telemetrytype
|
||||
return fmt.Sprintf("arrayExists(%s -> %s, %s)", key, op, arrayExpr), nil
|
||||
}
|
||||
|
||||
// buildArrayFunctionCondition builds a has/hasAny/hasAll condition over a body JSON path,
|
||||
// with contains-all semantics uniform across every leaf shape:
|
||||
// - has(v) = the path HAS v
|
||||
// - hasAny([v...]) = the path has ANY listed value (OR of has)
|
||||
// - hasAll([v...]) = the path has ALL listed values (AND of has)
|
||||
//
|
||||
// "the path has v" is an existential match resolved per leaf shape: for an array-typed leaf
|
||||
// (top-level `body.tags`, or nested `body.education[].scores`) it is native membership; for a
|
||||
// scalar leaf — whether reached through an array hop (`body.items[].sku`) or a plain scalar
|
||||
// path (`body.level`) — it is `<elem> = v`, wrapped in arrayExists over any array hops. So
|
||||
// `hasAll(body.education[].name, ['a','b'])` = "some element is a AND some element is b", and
|
||||
// for a plain scalar hasAll collapses to has (a one-element set can hold at most one value).
|
||||
//
|
||||
// Element comparisons reuse DataTypeCollisionHandledFieldName so a numeric literal against an
|
||||
// Int64 array (or a numeric literal against a String array) no longer silently misses.
|
||||
func (c *jsonConditionBuilder) buildArrayFunctionCondition(operator qbtypes.FilterOperator, value any, sb *sqlbuilder.SelectBuilder) (string, error) {
|
||||
if len(c.key.JSONPlan) == 0 {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "function `%s` could not resolve a JSON access plan for field `%s`", operator.FunctionName(), c.key.Name)
|
||||
}
|
||||
|
||||
switch operator {
|
||||
case qbtypes.FilterOperatorHas, qbtypes.FilterOperatorHasAny:
|
||||
return c.buildOredRootChains(func(node *telemetrytypes.JSONAccessNode) (string, error) {
|
||||
return c.arrayFunctionLeaf(node, operator, value, sb)
|
||||
}, sb)
|
||||
case qbtypes.FilterOperatorHasAll:
|
||||
// contains-all: AND of a per-value "has" so the AND sits outside the array hops.
|
||||
values := toAnyList(value)
|
||||
conditions := make([]string, 0, len(values))
|
||||
for _, v := range values {
|
||||
v := v
|
||||
cond, err := c.buildOredRootChains(func(node *telemetrytypes.JSONAccessNode) (string, error) {
|
||||
return c.arrayFunctionLeaf(node, qbtypes.FilterOperatorHas, v, sb)
|
||||
}, sb)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
conditions = append(conditions, cond)
|
||||
}
|
||||
if len(conditions) == 1 {
|
||||
return conditions[0], nil
|
||||
}
|
||||
return sb.And(conditions...), nil
|
||||
}
|
||||
return "", qbtypes.ErrUnsupportedOperator
|
||||
}
|
||||
|
||||
// buildOredRootChains applies leafFn down every JSONPlan root (base + promoted), wrapping each
|
||||
// in its arrayExists chain, and ORs the per-root results.
|
||||
func (c *jsonConditionBuilder) buildOredRootChains(leafFn func(*telemetrytypes.JSONAccessNode) (string, error), sb *sqlbuilder.SelectBuilder) (string, error) {
|
||||
conditions := make([]string, 0, len(c.key.JSONPlan))
|
||||
for _, root := range c.key.JSONPlan {
|
||||
cond, err := c.buildArrayExistsChain(root, leafFn, sb)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
conditions = append(conditions, cond)
|
||||
}
|
||||
if len(conditions) == 1 {
|
||||
return conditions[0], nil
|
||||
}
|
||||
return sb.Or(conditions...), nil
|
||||
}
|
||||
|
||||
// buildArrayExistsChain wraps the terminal condition (produced by leafFn) in an arrayExists
|
||||
// over every array hop between the root and the terminal. For a terminal root (a top-level
|
||||
// array leaf) it simply returns leafFn(root).
|
||||
func (c *jsonConditionBuilder) buildArrayExistsChain(node *telemetrytypes.JSONAccessNode, leafFn func(*telemetrytypes.JSONAccessNode) (string, error), sb *sqlbuilder.SelectBuilder) (string, error) {
|
||||
if node == nil {
|
||||
return "", errors.NewInternalf(CodeArrayNavigationFailed, "navigation failed, current node is nil")
|
||||
}
|
||||
if node.IsTerminal {
|
||||
return leafFn(node)
|
||||
}
|
||||
|
||||
branches := make([]string, 0, 2)
|
||||
for _, branch := range node.BranchesInOrder() {
|
||||
childCond, err := c.buildArrayExistsChain(node.Branches[branch], leafFn, sb)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
branches = append(branches, fmt.Sprintf("arrayExists(%s-> %s, %s)", node.Alias(), childCond, c.branchArrayExpr(node, branch)))
|
||||
}
|
||||
if len(branches) == 1 {
|
||||
return branches[0], nil
|
||||
}
|
||||
return sb.Or(branches...), nil
|
||||
}
|
||||
|
||||
// arrayFunctionLeaf builds the existential comparison for has/hasAny at a terminal node (hasAll
|
||||
// composes from has in buildArrayFunctionCondition). For an array leaf it delegates to native
|
||||
// membership; for a scalar leaf it compares the element directly.
|
||||
func (c *jsonConditionBuilder) arrayFunctionLeaf(node *telemetrytypes.JSONAccessNode, operator qbtypes.FilterOperator, value any, sb *sqlbuilder.SelectBuilder) (string, error) {
|
||||
if node.TerminalConfig.ElemType.IsArray {
|
||||
return c.arrayLeafMembership(node, operator, value, sb)
|
||||
}
|
||||
|
||||
switch operator {
|
||||
case qbtypes.FilterOperatorHas:
|
||||
return c.arrayFuncScalarLeaf(node, qbtypes.FilterOperatorEqual, value, sb)
|
||||
case qbtypes.FilterOperatorHasAny:
|
||||
return c.arrayFuncScalarLeaf(node, qbtypes.FilterOperatorIn, toAnyList(value), sb)
|
||||
}
|
||||
return "", qbtypes.ErrUnsupportedOperator
|
||||
}
|
||||
|
||||
// arrayLeafMembership builds native membership for an array-typed leaf, reusing
|
||||
// buildArrayMembershipCondition (which handles data-type collisions on each element).
|
||||
func (c *jsonConditionBuilder) arrayLeafMembership(node *telemetrytypes.JSONAccessNode, operator qbtypes.FilterOperator, value any, sb *sqlbuilder.SelectBuilder) (string, error) {
|
||||
switch operator {
|
||||
case qbtypes.FilterOperatorHas:
|
||||
return c.buildArrayMembershipCondition(node, qbtypes.FilterOperatorEqual, value, sb)
|
||||
case qbtypes.FilterOperatorHasAny:
|
||||
return c.buildArrayMembershipCondition(node, qbtypes.FilterOperatorIn, toAnyList(value), sb)
|
||||
}
|
||||
return "", qbtypes.ErrUnsupportedOperator
|
||||
}
|
||||
|
||||
// arrayFuncScalarLeaf builds `<elemExpr> <op> value` for a scalar leaf reached through an
|
||||
// array hop, applying data-type collision handling like the standard primitive path.
|
||||
// Coalesced to false so a missing key is a non-match, not NULL (NOT has() must match it).
|
||||
func (c *jsonConditionBuilder) arrayFuncScalarLeaf(node *telemetrytypes.JSONAccessNode, operator qbtypes.FilterOperator, value any, sb *sqlbuilder.SelectBuilder) (string, error) {
|
||||
fieldExpr := fmt.Sprintf("dynamicElement(%s, '%s')", node.FieldPath(), node.TerminalConfig.ElemType.StringValue())
|
||||
fieldExpr, value = querybuilder.DataTypeCollisionHandledFieldName(node.TerminalConfig.Key, value, fieldExpr, operator)
|
||||
cond, err := c.applyOperator(sb, fieldExpr, operator, value)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("ifNull(%s, false)", cond), nil
|
||||
}
|
||||
|
||||
// buildTokenFunctionCondition builds a hasToken search over a body JSON string field:
|
||||
// hasToken(LOWER(<elem>), LOWER(?)) wrapped in arrayExists over any array hops between the
|
||||
// root and the terminal. The field must resolve to a String leaf or a String array.
|
||||
func (c *jsonConditionBuilder) buildTokenFunctionCondition(needle any, sb *sqlbuilder.SelectBuilder) (string, error) {
|
||||
if len(c.key.JSONPlan) == 0 {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "function `hasToken` could not resolve a JSON access plan for field `%s`", c.key.Name)
|
||||
}
|
||||
|
||||
return c.buildOredRootChains(func(node *telemetrytypes.JSONAccessNode) (string, error) {
|
||||
return c.tokenLeaf(node, needle, sb)
|
||||
}, sb)
|
||||
}
|
||||
|
||||
// tokenLeaf builds the hasToken match at a terminal node: a direct match for a String leaf
|
||||
// (coalesced to false, as in arrayFuncScalarLeaf), or an arrayExists over the elements for a
|
||||
// String array leaf. hasToken is string-only, so any other element type is rejected.
|
||||
func (c *jsonConditionBuilder) tokenLeaf(node *telemetrytypes.JSONAccessNode, needle any, sb *sqlbuilder.SelectBuilder) (string, error) {
|
||||
switch node.TerminalConfig.ElemType {
|
||||
case telemetrytypes.String:
|
||||
fieldExpr := fmt.Sprintf("dynamicElement(%s, 'String')", node.FieldPath())
|
||||
return fmt.Sprintf("ifNull(hasToken(LOWER(%s), LOWER(%s)), false)", fieldExpr, sb.Var(needle)), nil
|
||||
case telemetrytypes.ArrayString:
|
||||
arrayExpr := fmt.Sprintf("dynamicElement(%s, '%s')", node.FieldPath(), node.TerminalConfig.ElemType.StringValue())
|
||||
return fmt.Sprintf("arrayExists(x -> hasToken(LOWER(x), LOWER(%s)), %s)", sb.Var(needle), arrayExpr), nil
|
||||
default:
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "function `hasToken` only supports string fields; field `%s` is `%s`", c.key.Name, node.TerminalConfig.Key.FieldDataType.StringValue())
|
||||
}
|
||||
}
|
||||
|
||||
// toAnyList normalizes a has-family value into a slice; a scalar becomes a one-element list.
|
||||
func toAnyList(value any) []any {
|
||||
if list, ok := value.([]any); ok {
|
||||
return list
|
||||
}
|
||||
return []any{value}
|
||||
}
|
||||
|
||||
func (c *jsonConditionBuilder) applyOperator(sb *sqlbuilder.SelectBuilder, fieldExpr string, operator qbtypes.FilterOperator, value any) (string, error) {
|
||||
switch operator {
|
||||
case qbtypes.FilterOperatorEqual:
|
||||
|
||||
@@ -602,7 +602,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
|
||||
name: "Simple has filter",
|
||||
filter: "has(body.education[].parameters, 1.65)",
|
||||
expected: TestExpected{
|
||||
WhereClause: "(arrayExists(`body_v2.education`-> arrayExists(x -> toFloat64(x) = ?, dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')) OR arrayExists(`body_v2.education`-> arrayExists(x -> accurateCastOrNull(x, 'Float64') = ?, arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))",
|
||||
WhereClause: "(has(arrayFlatten(arrayConcat(arrayMap(`body_v2.education`->dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))'), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))), ?) OR has(arrayFlatten(arrayConcat(arrayMap(`body_v2.education`->dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))), ?))",
|
||||
Args: []any{1.65, 1.65, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
Warnings: []string{
|
||||
"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64 name=education[].parameters,context=body,datatype=[]dynamic].",
|
||||
@@ -613,8 +613,8 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
|
||||
name: "Flat path hasAll filter",
|
||||
filter: "hasAll(body.user.permissions, ['read', 'write'])",
|
||||
expected: TestExpected{
|
||||
WhereClause: "(arrayExists(x -> x = ?, dynamicElement(body_v2.`user.permissions`, 'Array(Nullable(String))')) AND arrayExists(x -> x = ?, dynamicElement(body_v2.`user.permissions`, 'Array(Nullable(String))')))",
|
||||
Args: []any{"read", "write", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
WhereClause: "hasAll(dynamicElement(body_v2.`user.permissions`, 'Array(Nullable(String))'), ?)",
|
||||
Args: []any{[]any{"read", "write"}, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -739,8 +739,8 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
|
||||
name: "Nested path hasAny filter",
|
||||
filter: "hasAny(education[].awards[].participated[].members, ['Piyush', 'Tushar'])",
|
||||
expected: TestExpected{
|
||||
WhereClause: "arrayExists(`body_v2.education`-> (arrayExists(`body_v2.education[].awards`-> (arrayExists(`body_v2.education[].awards[].participated`-> arrayExists(x -> x IN (?, ?), dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))')), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')) OR arrayExists(`body_v2.education[].awards[].participated`-> arrayExists(x -> x IN (?, ?), dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))')), arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(Dynamic)'))))), dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_v2.education[].awards`-> (arrayExists(`body_v2.education[].awards[].participated`-> arrayExists(x -> x IN (?, ?), dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))')), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')) OR arrayExists(`body_v2.education[].awards[].participated`-> arrayExists(x -> x IN (?, ?), dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))')), arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(Dynamic)'))))), arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))",
|
||||
Args: []any{"Piyush", "Tushar", "Piyush", "Tushar", "Piyush", "Tushar", "Piyush", "Tushar", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
WhereClause: "hasAny(arrayFlatten(arrayConcat(arrayMap(`body_v2.education`->arrayConcat(arrayMap(`body_v2.education[].awards`->arrayConcat(arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), arrayMap(x->assumeNotNull(dynamicElement(x, 'JSON')), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(Dynamic)'))))), dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), arrayMap(`body_v2.education[].awards`->arrayConcat(arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')), arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), arrayMap(x->assumeNotNull(dynamicElement(x, 'JSON')), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(Dynamic)'))))), arrayMap(x->assumeNotNull(dynamicElement(x, 'JSON')), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))), ?)",
|
||||
Args: []any{[]any{"Piyush", "Tushar"}, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -755,8 +755,8 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
|
||||
name: "dynamic_array_element_compare_HAS_STRING",
|
||||
filter: "has(interests[].entities[].product_codes, '2002')",
|
||||
expected: TestExpected{
|
||||
WhereClause: "arrayExists(`body_v2.interests`-> arrayExists(`body_v2.interests[].entities`-> arrayExists(x -> accurateCastOrNull(x, 'Float64') = ?, arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.interests[].entities`.`product_codes`, 'Array(Dynamic)'))), dynamicElement(`body_v2.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_v2.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))",
|
||||
Args: []any{int64(2002), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
WhereClause: "has(arrayFlatten(arrayConcat(arrayMap(`body_v2.interests`->arrayMap(`body_v2.interests[].entities`->dynamicElement(`body_v2.interests[].entities`.`product_codes`, 'Array(Dynamic)'), dynamicElement(`body_v2.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_v2.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))), ?)",
|
||||
Args: []any{"2002", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -771,146 +771,10 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
|
||||
name: "dynamic_array_element_compare_HAS_INT",
|
||||
filter: "has(interests[].entities[].product_codes, 1001)",
|
||||
expected: TestExpected{
|
||||
WhereClause: "arrayExists(`body_v2.interests`-> arrayExists(`body_v2.interests[].entities`-> arrayExists(x -> accurateCastOrNull(x, 'Float64') = ?, arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.interests[].entities`.`product_codes`, 'Array(Dynamic)'))), dynamicElement(`body_v2.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_v2.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))",
|
||||
WhereClause: "has(arrayFlatten(arrayConcat(arrayMap(`body_v2.interests`->arrayMap(`body_v2.interests[].entities`->dynamicElement(`body_v2.interests[].entities`.`product_codes`, 'Array(Dynamic)'), dynamicElement(`body_v2.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_v2.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))), ?)",
|
||||
Args: []any{float64(1001), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
},
|
||||
|
||||
// ── scalar leaf reached through an array ───
|
||||
{
|
||||
name: "Nested primitive leaf has",
|
||||
filter: "has(body.education[].name, 'IIT')",
|
||||
expected: TestExpected{
|
||||
WhereClause: "arrayExists(`body_v2.education`-> ifNull(dynamicElement(`body_v2.education`.`name`, 'String') = ?, false), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))",
|
||||
Args: []any{"IIT", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Nested primitive leaf hasAny",
|
||||
filter: "hasAny(body.education[].name, ['IIT', 'MIT'])",
|
||||
expected: TestExpected{
|
||||
WhereClause: "arrayExists(`body_v2.education`-> ifNull(dynamicElement(`body_v2.education`.`name`, 'String') IN (?, ?), false), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))",
|
||||
Args: []any{"IIT", "MIT", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
},
|
||||
{
|
||||
// contains-all: single-value hasAll over a nested leaf collapses to has.
|
||||
name: "Nested primitive leaf hasAll single collapses to has",
|
||||
filter: "hasAll(body.education[].name, 'IIT')",
|
||||
expected: TestExpected{
|
||||
WhereClause: "arrayExists(`body_v2.education`-> ifNull(dynamicElement(`body_v2.education`.`name`, 'String') = ?, false), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))",
|
||||
Args: []any{"IIT", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
},
|
||||
{
|
||||
// contains-all: "some element is IIT AND some element is MIT" (AND of per-value has).
|
||||
name: "Nested primitive leaf hasAll multi (contains-all)",
|
||||
filter: "hasAll(body.education[].name, ['IIT', 'MIT'])",
|
||||
expected: TestExpected{
|
||||
WhereClause: "(arrayExists(`body_v2.education`-> ifNull(dynamicElement(`body_v2.education`.`name`, 'String') = ?, false), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')) AND arrayExists(`body_v2.education`-> ifNull(dynamicElement(`body_v2.education`.`name`, 'String') = ?, false), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))",
|
||||
Args: []any{"IIT", "MIT", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
},
|
||||
// ── numeric literal against an Int64 array is collision-handled ───
|
||||
{
|
||||
name: "Nested Int64 array has collision",
|
||||
filter: "has(body.education[].scores, 90)",
|
||||
expected: TestExpected{
|
||||
WhereClause: "arrayExists(`body_v2.education`-> arrayExists(x -> toFloat64(x) = ?, dynamicElement(`body_v2.education`.`scores`, 'Array(Nullable(Int64))')), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))",
|
||||
Args: []any{float64(90), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
},
|
||||
// ── hasAny folds multiple scalar arguments into one value set ─────
|
||||
{
|
||||
name: "hasAny folds multiple scalar args",
|
||||
filter: "hasAny(body.user.permissions, 'read', 'write')",
|
||||
expected: TestExpected{
|
||||
WhereClause: "arrayExists(x -> x IN (?, ?), dynamicElement(body_v2.`user.permissions`, 'Array(Nullable(String))'))",
|
||||
Args: []any{"read", "write", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
},
|
||||
// ── hasToken over JSON string fields (nested leaf, top-level array, nested array) ──
|
||||
{
|
||||
name: "hasToken nested string leaf",
|
||||
filter: "hasToken(body.education[].name, 'harvard')",
|
||||
expected: TestExpected{
|
||||
WhereClause: "arrayExists(`body_v2.education`-> ifNull(hasToken(LOWER(dynamicElement(`body_v2.education`.`name`, 'String')), LOWER(?)), false), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))",
|
||||
Args: []any{"harvard", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hasToken top-level string array",
|
||||
filter: "hasToken(body.user.permissions, 'admin')",
|
||||
expected: TestExpected{
|
||||
WhereClause: "arrayExists(x -> hasToken(LOWER(x), LOWER(?)), dynamicElement(body_v2.`user.permissions`, 'Array(Nullable(String))'))",
|
||||
Args: []any{"admin", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hasToken nested string array",
|
||||
filter: "hasToken(body.education[].awards[].participated[].members, 'piyush')",
|
||||
expected: TestExpected{
|
||||
WhereClause: "arrayExists(`body_v2.education`-> (arrayExists(`body_v2.education[].awards`-> (arrayExists(`body_v2.education[].awards[].participated`-> arrayExists(x -> hasToken(LOWER(x), LOWER(?)), dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))')), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')) OR arrayExists(`body_v2.education[].awards[].participated`-> arrayExists(x -> hasToken(LOWER(x), LOWER(?)), dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))')), arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(Dynamic)'))))), dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_v2.education[].awards`-> (arrayExists(`body_v2.education[].awards[].participated`-> arrayExists(x -> hasToken(LOWER(x), LOWER(?)), dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))')), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')) OR arrayExists(`body_v2.education[].awards[].participated`-> arrayExists(x -> hasToken(LOWER(x), LOWER(?)), dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))')), arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(Dynamic)'))))), arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))",
|
||||
Args: []any{"piyush", "piyush", "piyush", "piyush", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
},
|
||||
|
||||
// ── hasToken over the message field: bare body and explicit body.message are
|
||||
// equivalent, both target the body.message column directly (no dynamicElement) ──
|
||||
{
|
||||
name: "hasToken bare body",
|
||||
filter: "hasToken(body, 'production')",
|
||||
expected: TestExpected{
|
||||
WhereClause: "hasToken(LOWER(body.message), LOWER(?))",
|
||||
Args: []any{"production", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
Warnings: []string{bodySearchDefaultWarning},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hasToken explicit body.message",
|
||||
filter: "hasToken(body.message, 'production')",
|
||||
expected: TestExpected{
|
||||
WhereClause: "hasToken(LOWER(body.message), LOWER(?))",
|
||||
Args: []any{"production", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
},
|
||||
|
||||
// ── scalar (non-array) leaf: treated as a single-element set ─────────────
|
||||
{
|
||||
name: "Scalar leaf has",
|
||||
filter: "has(body.user.name, 'alice')",
|
||||
expected: TestExpected{
|
||||
WhereClause: "ifNull(dynamicElement(body_v2.`user.name`, 'String') = ?, false)",
|
||||
Args: []any{"alice", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Scalar leaf hasAny",
|
||||
filter: "hasAny(body.user.name, ['alice', 'bob'])",
|
||||
expected: TestExpected{
|
||||
WhereClause: "ifNull(dynamicElement(body_v2.`user.name`, 'String') IN (?, ?), false)",
|
||||
Args: []any{"alice", "bob", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
},
|
||||
{
|
||||
// contains-all: single-value hasAll over a plain scalar collapses to has.
|
||||
name: "Scalar leaf hasAll single collapses to has",
|
||||
filter: "hasAll(body.user.name, 'alice')",
|
||||
expected: TestExpected{
|
||||
WhereClause: "ifNull(dynamicElement(body_v2.`user.name`, 'String') = ?, false)",
|
||||
Args: []any{"alice", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
},
|
||||
{
|
||||
// contains-all over a one-element set: the scalar must equal every value, so
|
||||
// distinct values never match.
|
||||
name: "Scalar leaf hasAll multi (contains-all)",
|
||||
filter: "hasAll(body.user.name, ['alice', 'bob'])",
|
||||
expected: TestExpected{
|
||||
WhereClause: "(ifNull(dynamicElement(body_v2.`user.name`, 'String') = ?, false) AND ifNull(dynamicElement(body_v2.`user.name`, 'String') = ?, false))",
|
||||
Args: []any{"alice", "bob", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
|
||||
@@ -130,121 +130,3 @@ func GetBodyJSONKey(_ context.Context, key *telemetrytypes.TelemetryFieldKey, op
|
||||
func GetBodyJSONKeyForExists(_ context.Context, key *telemetrytypes.TelemetryFieldKey, _ qbtypes.FilterOperator, _ any) string {
|
||||
return fmt.Sprintf("JSON_EXISTS(body, '$.%s')", getBodyJSONPath(key))
|
||||
}
|
||||
|
||||
// legacyElemType infers the has-family element type from the needle (legacy has no schema). It
|
||||
// scans EVERY value so the chosen array type and all coerced needles agree — else ClickHouse
|
||||
// raises "no supertype ... String" (code 386). Int64 stays distinct from Float64 so a quoted
|
||||
// integer is exact past 2^53 (unquoted literals already arrive as float64, parsed upstream).
|
||||
func legacyElemType(needle any) telemetrytypes.FieldDataType {
|
||||
list, ok := needle.([]any)
|
||||
if !ok {
|
||||
list = []any{needle}
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return telemetrytypes.FieldDataTypeString
|
||||
}
|
||||
allInt, allNumeric := true, true
|
||||
for _, v := range list {
|
||||
switch t := v.(type) {
|
||||
case float32, float64:
|
||||
allInt = false
|
||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
|
||||
// integer Go types stay int-exact
|
||||
case string:
|
||||
if _, err := strconv.ParseInt(t, 10, 64); err != nil {
|
||||
allInt = false
|
||||
}
|
||||
if _, err := strconv.ParseFloat(t, 64); err != nil {
|
||||
allNumeric = false
|
||||
}
|
||||
default:
|
||||
// booleans (and anything else) -> String; a bool renders to 'true'/'false', so a
|
||||
// bool needle only matches genuine JSON booleans, not truthy numbers/strings.
|
||||
allInt, allNumeric = false, false
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case allInt:
|
||||
return telemetrytypes.FieldDataTypeInt64
|
||||
case allNumeric:
|
||||
return telemetrytypes.FieldDataTypeFloat64
|
||||
default:
|
||||
return telemetrytypes.FieldDataTypeString
|
||||
}
|
||||
}
|
||||
|
||||
// legacyCoerceNeedle coerces a needle to elem type dt so its bound-arg type matches the
|
||||
// extracted column (legacyElemType guarantees it's coercible).
|
||||
func legacyCoerceNeedle(v any, dt telemetrytypes.FieldDataType) any {
|
||||
switch dt {
|
||||
case telemetrytypes.FieldDataTypeInt64:
|
||||
if s, ok := v.(string); ok {
|
||||
if i, err := strconv.ParseInt(s, 10, 64); err == nil {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return v
|
||||
case telemetrytypes.FieldDataTypeFloat64:
|
||||
if s, ok := v.(string); ok {
|
||||
f, _ := strconv.ParseFloat(s, 64)
|
||||
return f
|
||||
}
|
||||
return v
|
||||
default:
|
||||
return bodyArrayNeedleString(v)
|
||||
}
|
||||
}
|
||||
|
||||
// getBodyJSONArrayKey extracts the leaf as Array(Nullable(<dt>)) — Nullable so a value of a
|
||||
// different JSON type maps to NULL instead of corrupting (e.g. a non-numeric string → 0).
|
||||
func getBodyJSONArrayKey(key *telemetrytypes.TelemetryFieldKey, dt telemetrytypes.FieldDataType) string {
|
||||
arrKey := *key
|
||||
if !strings.HasSuffix(arrKey.Name, "[*]") && !strings.HasSuffix(arrKey.Name, "[]") {
|
||||
arrKey.Name += "[*]"
|
||||
}
|
||||
return fmt.Sprintf("JSONExtract(JSON_QUERY(body, '$.%s'), 'Array(Nullable(%s))')", getBodyJSONPath(&arrKey), dt.CHDataType())
|
||||
}
|
||||
|
||||
// getBodyJSONScalarKey builds the single-element-set fallback for a scalar body value: the leaf
|
||||
// extracted as a scalar of type dt, plus a guard restricting it to a genuinely scalar body. The
|
||||
// guard is required because JSON_VALUE returns '' for an array/object/missing value, which would
|
||||
// otherwise zero-value match (has(x,0) / has(x,false) / has(x,'') on any array). ok=false when
|
||||
// the path still traverses an array ([*]/[]).
|
||||
func getBodyJSONScalarKey(key *telemetrytypes.TelemetryFieldKey, dt telemetrytypes.FieldDataType) (expr string, guard string, ok bool) {
|
||||
name := strings.TrimSuffix(strings.TrimSuffix(key.Name, "[*]"), "[]")
|
||||
if strings.Contains(name, "[") {
|
||||
return "", "", false
|
||||
}
|
||||
scalarKey := *key
|
||||
scalarKey.Name = name
|
||||
path := getBodyJSONPath(&scalarKey)
|
||||
if dt == telemetrytypes.FieldDataTypeString {
|
||||
expr = fmt.Sprintf("JSON_VALUE(body, '$.%s')", path)
|
||||
} else {
|
||||
// Nullable so a scalar of a different type (e.g. a bool/string where a number is
|
||||
// searched) extracts to NULL rather than the type's default (0/false), which would
|
||||
// otherwise zero-value match has(x, 0).
|
||||
expr = fmt.Sprintf("JSONExtract(JSON_VALUE(body, '$.%s'), 'Nullable(%s)')", path, dt.CHDataType())
|
||||
}
|
||||
keys := strings.Split(name, ".")
|
||||
for i, k := range keys {
|
||||
keys[i] = "'" + k + "'"
|
||||
}
|
||||
guard = fmt.Sprintf("JSONType(body, %s) NOT IN ('Array', 'Object', 'Null')", strings.Join(keys, ", "))
|
||||
return expr, guard, true
|
||||
}
|
||||
|
||||
func bodyArrayNeedleString(v any) string {
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
return t
|
||||
case bool:
|
||||
return strconv.FormatBool(t)
|
||||
case float64:
|
||||
return strconv.FormatFloat(t, 'f', -1, 64)
|
||||
case float32:
|
||||
return strconv.FormatFloat(float64(t), 'f', -1, 64)
|
||||
default:
|
||||
return fmt.Sprintf("%v", t)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,8 +495,8 @@ func TestStatementBuilderListQueryResourceTests(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE (has(JSONExtract(JSON_QUERY(body, '$.\"user_names\"[*]'), 'Array(Nullable(String))'), ?) OR ifNull((JSON_VALUE(body, '$.\"user_names\"') = ? AND JSONType(body, 'user_names') NOT IN ('Array', 'Object', 'Null')), false)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"john_doe", "john_doe", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
Query: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE has(JSONExtract(JSON_QUERY(body, '$.\"user_names\"[*]'), 'Array(String)'), ?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"john_doe", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
|
||||
@@ -66,20 +66,15 @@ type Role struct {
|
||||
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
Name string `bun:"name,type:string" json:"name" required:"true"`
|
||||
Description string `bun:"description,type:string" json:"description" required:"true"`
|
||||
Type valuer.String `bun:"type,type:string" json:"type" required:"true"`
|
||||
OrgID valuer.UUID `bun:"org_id,type:string" json:"orgId" required:"true"`
|
||||
TransactionGroups TransactionGroups `bun:"transaction_groups,nullzero,type:text" json:"transactionGroups" required:"true" nullable:"false"`
|
||||
Name string `bun:"name,type:string" json:"name" required:"true"`
|
||||
Description string `bun:"description,type:string" json:"description" required:"true"`
|
||||
Type valuer.String `bun:"type,type:string" json:"type" required:"true"`
|
||||
OrgID valuer.UUID `bun:"org_id,type:string" json:"orgId" required:"true"`
|
||||
}
|
||||
|
||||
type GettableRole struct {
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
Name string `json:"name" required:"true"`
|
||||
Description string `json:"description" required:"true"`
|
||||
Type valuer.String `json:"type" required:"true"`
|
||||
OrgID valuer.UUID `json:"orgId" required:"true"`
|
||||
type RoleWithTransactionGroups struct {
|
||||
*Role
|
||||
TransactionGroups TransactionGroups `json:"transactionGroups" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
type PostableRole struct {
|
||||
@@ -93,7 +88,7 @@ type UpdatableRole struct {
|
||||
TransactionGroups TransactionGroups `json:"transactionGroups" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
func NewRole(name, description string, roleType valuer.String, orgID valuer.UUID, transactionGroups TransactionGroups) *Role {
|
||||
func NewRole(name, description string, roleType valuer.String, orgID valuer.UUID) *Role {
|
||||
return &Role{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: valuer.GenerateUUID(),
|
||||
@@ -102,37 +97,37 @@ func NewRole(name, description string, roleType valuer.String, orgID valuer.UUID
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
Name: name,
|
||||
Description: description,
|
||||
Type: roleType,
|
||||
OrgID: orgID,
|
||||
Name: name,
|
||||
Description: description,
|
||||
Type: roleType,
|
||||
OrgID: orgID,
|
||||
}
|
||||
}
|
||||
|
||||
func NewRoleWithTransactionGroups(name, description string, roleType valuer.String, orgID valuer.UUID, transactionGroups TransactionGroups) *RoleWithTransactionGroups {
|
||||
role := NewRole(name, description, roleType, orgID)
|
||||
|
||||
return &RoleWithTransactionGroups{
|
||||
Role: role,
|
||||
TransactionGroups: transactionGroups,
|
||||
}
|
||||
}
|
||||
|
||||
func NewGettableRolesFromRoles(roles []*Role) []*GettableRole {
|
||||
gettableRoles := make([]*GettableRole, len(roles))
|
||||
for index, role := range roles {
|
||||
gettableRoles[index] = &GettableRole{
|
||||
Identifiable: role.Identifiable,
|
||||
TimeAuditable: role.TimeAuditable,
|
||||
Name: role.Name,
|
||||
Description: role.Description,
|
||||
Type: role.Type,
|
||||
OrgID: role.OrgID,
|
||||
}
|
||||
func MakeRoleWithTransactionGroups(role *Role, transactionGroups TransactionGroups) *RoleWithTransactionGroups {
|
||||
return &RoleWithTransactionGroups{
|
||||
Role: role,
|
||||
TransactionGroups: transactionGroups,
|
||||
}
|
||||
|
||||
return gettableRoles
|
||||
}
|
||||
|
||||
func NewManagedRoles(orgID valuer.UUID) []*Role {
|
||||
return []*Role{
|
||||
NewRole(SigNozAdminRoleName, SigNozAdminRoleDescription, RoleTypeManaged, orgID, NewTransactionGroupsFromTransactions(coretypes.ManagedRoleToTransactions[SigNozAdminRoleName])),
|
||||
NewRole(SigNozEditorRoleName, SigNozEditorRoleDescription, RoleTypeManaged, orgID, NewTransactionGroupsFromTransactions(coretypes.ManagedRoleToTransactions[SigNozEditorRoleName])),
|
||||
NewRole(SigNozViewerRoleName, SigNozViewerRoleDescription, RoleTypeManaged, orgID, NewTransactionGroupsFromTransactions(coretypes.ManagedRoleToTransactions[SigNozViewerRoleName])),
|
||||
NewRole(SigNozAnonymousRoleName, SigNozAnonymousRoleDescription, RoleTypeManaged, orgID, NewTransactionGroupsFromTransactions(coretypes.ManagedRoleToTransactions[SigNozAnonymousRoleName])),
|
||||
NewRole(SigNozAdminRoleName, SigNozAdminRoleDescription, RoleTypeManaged, orgID),
|
||||
NewRole(SigNozEditorRoleName, SigNozEditorRoleDescription, RoleTypeManaged, orgID),
|
||||
NewRole(SigNozViewerRoleName, SigNozViewerRoleDescription, RoleTypeManaged, orgID),
|
||||
NewRole(SigNozAnonymousRoleName, SigNozAnonymousRoleDescription, RoleTypeManaged, orgID),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func NewStatsFromRoles(roles []*Role) map[string]any {
|
||||
@@ -149,7 +144,7 @@ func NewStatsFromRoles(roles []*Role) map[string]any {
|
||||
return stats
|
||||
}
|
||||
|
||||
func (role *Role) Update(description string, transactionGroups TransactionGroups) error {
|
||||
func (role *RoleWithTransactionGroups) Update(description string, transactionGroups TransactionGroups) error {
|
||||
err := role.ErrIfManaged()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package authtypes
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
@@ -71,28 +70,6 @@ func NewTransactionGroups(data []byte) (TransactionGroups, error) {
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func NewTransactionGroupsFromTransactions(transactions []coretypes.Transaction) TransactionGroups {
|
||||
objectsByVerb := make(map[string][]*coretypes.Object)
|
||||
for _, transaction := range transactions {
|
||||
object := transaction.Object
|
||||
objectsByVerb[transaction.Verb.StringValue()] = append(objectsByVerb[transaction.Verb.StringValue()], &object)
|
||||
}
|
||||
|
||||
groups := make(TransactionGroups, 0)
|
||||
for _, verb := range coretypes.Verbs {
|
||||
objects := objectsByVerb[verb.StringValue()]
|
||||
if len(objects) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, objectGroup := range coretypes.NewObjectGroupsFromObjects(objects) {
|
||||
groups = append(groups, &TransactionGroup{Relation: Relation{Verb: verb}, ObjectGroup: *objectGroup})
|
||||
}
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
func NewGettableTransaction(results []*TransactionWithAuthorization) []*GettableTransaction {
|
||||
gettableTransactions := make([]*GettableTransaction, len(results))
|
||||
for i, result := range results {
|
||||
@@ -110,40 +87,6 @@ func (groups TransactionGroups) Diff(desired TransactionGroups) (additions, dele
|
||||
return desired.subtract(groups), groups.subtract(desired)
|
||||
}
|
||||
|
||||
func (groups TransactionGroups) Value() (driver.Value, error) {
|
||||
data, err := json.Marshal(groups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func (groups *TransactionGroups) Scan(value any) error {
|
||||
if value == nil {
|
||||
*groups = make(TransactionGroups, 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
var data []byte
|
||||
switch typed := value.(type) {
|
||||
case string:
|
||||
data = []byte(typed)
|
||||
case []byte:
|
||||
data = typed
|
||||
default:
|
||||
return errors.Newf(errors.TypeInternal, errors.CodeInternal, "unsupported type %T for transaction groups", value)
|
||||
}
|
||||
|
||||
parsed, err := NewTransactionGroups(data)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, errors.TypeInternal, errors.CodeInternal, "failed to scan transactionGroups")
|
||||
}
|
||||
|
||||
*groups = parsed
|
||||
return nil
|
||||
}
|
||||
|
||||
func (transaction *Transaction) UnmarshalJSON(data []byte) error {
|
||||
var shadow = struct {
|
||||
Relation Relation
|
||||
|
||||
@@ -68,7 +68,7 @@ func NewTuplesFromTransactionGroups(name string, orgID valuer.UUID, transactionG
|
||||
return tuples, nil
|
||||
}
|
||||
|
||||
func MustNewTransactionGroupsFromTuples(tuples []*openfgav1.TupleKey) TransactionGroups {
|
||||
func MustNewTransactionGroupsFromTuples(tuples []*openfgav1.TupleKey) []*TransactionGroup {
|
||||
objectsByRelation := make(map[string][]*coretypes.Object)
|
||||
|
||||
for _, tuple := range tuples {
|
||||
@@ -81,7 +81,7 @@ func MustNewTransactionGroupsFromTuples(tuples []*openfgav1.TupleKey) Transactio
|
||||
objectsByRelation[verb.StringValue()] = append(objectsByRelation[verb.StringValue()], object)
|
||||
}
|
||||
|
||||
transactionGroups := make(TransactionGroups, 0)
|
||||
transactionGroups := make([]*TransactionGroup, 0)
|
||||
for _, verb := range coretypes.Verbs {
|
||||
objects := objectsByRelation[verb.StringValue()]
|
||||
if len(objects) == 0 {
|
||||
|
||||
@@ -54,7 +54,7 @@ type SpanMapper struct {
|
||||
types.UserAuditable
|
||||
|
||||
ID valuer.UUID `json:"id" required:"true"`
|
||||
GroupID valuer.UUID `json:"group_id" required:"true"`
|
||||
GroupID valuer.UUID `json:"groupId" required:"true"`
|
||||
Name string `json:"name" required:"true"`
|
||||
FieldContext FieldContext `json:"fieldContext" required:"true"`
|
||||
Config SpanMapperConfig `json:"config" required:"true"`
|
||||
@@ -63,7 +63,7 @@ type SpanMapper struct {
|
||||
|
||||
type PostableSpanMapper struct {
|
||||
Name string `json:"name" required:"true"`
|
||||
FieldContext FieldContext `json:"fieldContext" required:"true"`
|
||||
FieldContext FieldContext `json:"fieldContext" required:"true"`
|
||||
Config SpanMapperConfig `json:"config" required:"true"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -563,6 +563,214 @@ def test_logs_json_body_nested_keys(
|
||||
assert all(code == 200 for code in status_codes)
|
||||
|
||||
|
||||
def test_logs_json_body_array_membership(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[list[Logs]], None],
|
||||
) -> None:
|
||||
"""
|
||||
Setup:
|
||||
Insert logs with JSON bodies containing arrays
|
||||
|
||||
Tests:
|
||||
1. Search by has(body.tags[*], "value") - string array
|
||||
2. Search by has(body.ids[*], 123) - numeric array
|
||||
3. Search by has(body.flags[*], true) - boolean array
|
||||
"""
|
||||
now = datetime.now(tz=UTC)
|
||||
|
||||
log1_body = json.dumps(
|
||||
{
|
||||
"tags": ["production", "api", "critical"],
|
||||
"ids": [100, 200, 300],
|
||||
"flags": [True, False, True],
|
||||
"users": [
|
||||
{"name": "alice", "role": "admin"},
|
||||
{"name": "bob", "role": "user"},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
log2_body = json.dumps(
|
||||
{
|
||||
"tags": ["staging", "api", "test"],
|
||||
"ids": [200, 400, 500],
|
||||
"flags": [False, False, True],
|
||||
"users": [
|
||||
{"name": "charlie", "role": "user"},
|
||||
{"name": "david", "role": "admin"},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
log3_body = json.dumps(
|
||||
{
|
||||
"tags": ["production", "web", "important"],
|
||||
"ids": [100, 600, 700],
|
||||
"flags": [True, True, False],
|
||||
"users": [
|
||||
{"name": "alice", "role": "admin"},
|
||||
{"name": "eve", "role": "user"},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
insert_logs(
|
||||
[
|
||||
Logs(
|
||||
timestamp=now - timedelta(seconds=3),
|
||||
resources={"service.name": "app-service"},
|
||||
attributes={},
|
||||
body=log1_body,
|
||||
severity_text="INFO",
|
||||
),
|
||||
Logs(
|
||||
timestamp=now - timedelta(seconds=2),
|
||||
resources={"service.name": "app-service"},
|
||||
attributes={},
|
||||
body=log2_body,
|
||||
severity_text="INFO",
|
||||
),
|
||||
Logs(
|
||||
timestamp=now - timedelta(seconds=1),
|
||||
resources={"service.name": "app-service"},
|
||||
attributes={},
|
||||
body=log3_body,
|
||||
severity_text="INFO",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Test 1: Search by has(body.tags[*], "production")
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v5/query_range"),
|
||||
timeout=2,
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"schemaVersion": "v1",
|
||||
"start": int((now - timedelta(seconds=10)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"requestType": "raw",
|
||||
"compositeQuery": {
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "logs",
|
||||
"disabled": False,
|
||||
"limit": 100,
|
||||
"offset": 0,
|
||||
"filter": {"expression": 'has(body.tags[*], "production")'},
|
||||
"order": [
|
||||
{"key": {"name": "timestamp"}, "direction": "desc"},
|
||||
],
|
||||
"aggregations": [{"expression": "count()"}],
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
"formatOptions": {"formatTableResultForUI": False, "fillGaps": False},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
results = response.json()["data"]["data"]["results"]
|
||||
assert len(results) == 1
|
||||
rows = results[0]["rows"]
|
||||
assert len(rows) == 2 # log1 and log3 have "production" in tags
|
||||
tags_list = [json.loads(row["data"]["body"])["tags"] for row in rows]
|
||||
assert all("production" in tags for tags in tags_list)
|
||||
|
||||
# Test 2: Search by has(body.ids[*], 200)
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v5/query_range"),
|
||||
timeout=2,
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"schemaVersion": "v1",
|
||||
"start": int((now - timedelta(seconds=10)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"requestType": "raw",
|
||||
"compositeQuery": {
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "logs",
|
||||
"disabled": False,
|
||||
"limit": 100,
|
||||
"offset": 0,
|
||||
"filter": {"expression": "has(body.ids[*], 200)"},
|
||||
"order": [
|
||||
{"key": {"name": "timestamp"}, "direction": "desc"},
|
||||
],
|
||||
"aggregations": [{"expression": "count()"}],
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
"formatOptions": {"formatTableResultForUI": False, "fillGaps": False},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
results = response.json()["data"]["data"]["results"]
|
||||
assert len(results) == 1
|
||||
rows = results[0]["rows"]
|
||||
assert len(rows) == 2 # log1 and log2 have 200 in ids
|
||||
ids_list = [json.loads(row["data"]["body"])["ids"] for row in rows]
|
||||
assert all(200 in ids for ids in ids_list)
|
||||
|
||||
# Test 3: Search by has(body.flags[*], true)
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v5/query_range"),
|
||||
timeout=2,
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"schemaVersion": "v1",
|
||||
"start": int((now - timedelta(seconds=10)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"requestType": "raw",
|
||||
"compositeQuery": {
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "logs",
|
||||
"disabled": False,
|
||||
"limit": 100,
|
||||
"offset": 0,
|
||||
"filter": {"expression": "has(body.flags[*], true)"},
|
||||
"order": [
|
||||
{"key": {"name": "timestamp"}, "direction": "desc"},
|
||||
],
|
||||
"aggregations": [{"expression": "count()"}],
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
"formatOptions": {"formatTableResultForUI": False, "fillGaps": False},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
results = response.json()["data"]["data"]["results"]
|
||||
assert len(results) == 1
|
||||
rows = results[0]["rows"]
|
||||
assert len(rows) == 3 # All logs have true in flags
|
||||
flags_list = [json.loads(row["data"]["body"])["flags"] for row in rows]
|
||||
assert all(True in flags for flags in flags_list)
|
||||
|
||||
|
||||
def test_logs_json_body_listing(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user