Compare commits

..

69 Commits

Author SHA1 Message Date
Ashwin Bhatkal
67fb4cf8a5 put back json 2026-03-31 03:51:17 +05:30
Ashwin Bhatkal
11f9daf2a7 queries review done 2026-03-31 03:39:52 +05:30
Ashwin Bhatkal
01cc509c1b panels review done 2026-03-31 03:32:33 +05:30
Ashwin Bhatkal
c17021aade variables review done 2026-03-31 02:25:35 +05:30
Ashwin Bhatkal
d9961fe527 local commit refactor 2026-03-31 01:56:00 +05:30
Ashwin Bhatkal
5035390236 local commit 2026-03-31 00:41:21 +05:30
Naman Verma
7d3612c10a feat: textbox variable 2026-03-25 13:21:46 +05:30
Naman Verma
c82cd32f61 Merge branch 'main' into nv/4172 2026-03-25 13:11:49 +05:30
Naman Verma
a3980e084c Merge branch 'main' into nv/4172 2026-03-24 16:44:50 +05:30
Naman Verma
4319dd9cef chore: doc for how to add a panel spec 2026-03-24 16:44:34 +05:30
Naman Verma
92a5e9b9c9 chore: common threshold type 2026-03-24 16:23:11 +05:30
Naman Verma
408a914129 chore: change attr name to name 2026-03-24 16:13:09 +05:30
Naman Verma
0e304b1d40 fix: normalise enum defs 2026-03-24 16:12:09 +05:30
Naman Verma
58a9be24d3 chore: single version for all schemas 2026-03-24 14:07:24 +05:30
Naman Verma
adf439fcf1 fix: proper type for selectFields 2026-03-24 13:56:14 +05:30
Naman Verma
a1a54c4bb2 fix: promql step duration schema 2026-03-24 13:53:19 +05:30
Naman Verma
3c1961d3fc fix: datasource in perses.json 2026-03-24 13:44:58 +05:30
Naman Verma
c3efa0660b fix: only allow one of metric or expr aggregation in builder query 2026-03-24 13:40:42 +05:30
Naman Verma
183dd09082 fix: functions in formula 2026-03-24 13:34:59 +05:30
Naman Verma
a351373c49 chore: common package for variables' repeated definitions 2026-03-24 13:23:53 +05:30
Naman Verma
8e7653b90d chore: common package for queries' repeated definitions 2026-03-24 13:22:49 +05:30
Naman Verma
5c40d6b68b chore: common package for panels' repeated definitions 2026-03-24 13:19:12 +05:30
Naman Verma
31115df41c chore: actually name every panel as a panel 2026-03-24 13:05:55 +05:30
Naman Verma
869c3dccb2 fix: less verbose field names in dynamic var 2026-03-24 12:57:34 +05:30
Naman Verma
c5d7a7ef8c fix: no nesting in context links 2026-03-24 12:55:40 +05:30
Naman Verma
544b87b254 chore: remove unimplemented join query schema 2026-03-24 12:48:36 +05:30
Naman Verma
e885fb98e5 chore: no need for threshold prefix inside threshold obj 2026-03-24 12:44:50 +05:30
Naman Verma
be227eec43 chore: replace yAxisUnit by unit 2026-03-24 12:41:16 +05:30
Naman Verma
13263c1f25 Merge branch 'main' into nv/4172 2026-03-23 20:01:04 +05:30
Naman Verma
ccbf410d15 fix: no more online validation 2026-03-23 16:23:51 +05:30
Naman Verma
03b98ff824 chore: remaining fields file 2026-03-23 16:10:01 +05:30
Naman Verma
2cdba0d11c chore: examples for panel types 2026-03-23 16:06:29 +05:30
Naman Verma
84d2885530 chore: a more complex example 2026-03-23 16:03:18 +05:30
Naman Verma
b82dcc6138 docs: list panel schema without upstream ref 2026-03-23 15:31:53 +05:30
Naman Verma
a14d5847b9 docs: histogram chart panel schema without upstream ref 2026-03-23 15:21:27 +05:30
Naman Verma
d184746142 docs: table chart panel schema without upstream ref 2026-03-23 15:17:21 +05:30
Naman Verma
c335e17e1d docs: pie chart panel schema without upstream ref 2026-03-23 14:33:05 +05:30
Naman Verma
433dd0b2d0 docs: number panel schema without upstream ref 2026-03-23 14:27:46 +05:30
Naman Verma
05e97e246a docs: number panel schema without upstream ref 2026-03-23 14:27:37 +05:30
Naman Verma
bddfe30f6c docs: bar chart panel schema without upstream ref 2026-03-23 14:22:00 +05:30
Naman Verma
7a01a5250d chore: object for visualization section 2026-03-23 14:16:07 +05:30
Naman Verma
09c98c830d docs: time series panel schema without upstream ref 2026-03-23 14:11:02 +05:30
Naman Verma
0fbb90cc91 fix: promql fix 2026-03-23 14:09:48 +05:30
Naman Verma
15f0787610 chore: remove upstream import 2026-03-23 13:07:48 +05:30
Naman Verma
22ebc7732c feat: promql example 2026-03-22 19:39:57 +05:30
Naman Verma
cff18edf6e chore: comment explaining when to use composite query and when not 2026-03-22 19:22:45 +05:30
Naman Verma
cb49c0bf3b feat: custom time series schema 2026-03-22 19:18:15 +05:30
Naman Verma
1cb6f94d21 fix: proper composite query schema 2026-03-19 16:18:31 +05:30
Naman Verma
68155f374b chore: folders in schemas for arranging 2026-03-19 15:15:06 +05:30
Naman Verma
696524509f chore: folders in schemas for arranging 2026-03-19 15:14:43 +05:30
Naman Verma
705cdab38c chore: rename 2026-03-19 14:24:10 +05:30
Naman Verma
ae9b881413 chore: py script not needed 2026-03-19 01:39:17 +05:30
Naman Verma
05f4e15d07 chore: rearrange specs in package.json 2026-03-19 01:39:08 +05:30
Naman Verma
1653c6d725 chore: checkpoint for half correct setup 2026-03-19 01:34:22 +05:30
Naman Verma
070b4b7061 chore: test file with way more examples 2026-03-18 22:01:16 +05:30
Naman Verma
7f4c06edd6 chore: test file with way more examples 2026-03-18 22:01:10 +05:30
Naman Verma
6bed20b5b9 fix: remove fields from variable specs that are there in ListVariable 2026-03-18 19:19:00 +05:30
Naman Verma
033bd3c9b8 feat: validation script 2026-03-18 15:52:05 +05:30
Naman Verma
d4c9a923fd chore: no commons (for now) 2026-03-18 15:28:35 +05:30
Naman Verma
387dcb529f chore: no config folder 2026-03-18 15:04:16 +05:30
Naman Verma
7a4da7bcc5 chore: no config folder 2026-03-18 15:04:02 +05:30
Naman Verma
b152fae3fa chore: remove validate file 2026-03-18 15:03:19 +05:30
Naman Verma
2ed766726c chore: remove manually written manifest and package 2026-03-18 15:02:23 +05:30
Naman Verma
8767f6a57d chore: remove stub for time series chart 2026-03-18 15:01:58 +05:30
Naman Verma
22d8c7599b chore: rm comment 2026-03-18 13:10:19 +05:30
Naman Verma
1019264272 chore: no need for PageSize type in commons, only used once 2026-03-18 13:07:31 +05:30
Naman Verma
c950d7e784 chore: no need for Signal type in commons, only used once 2026-03-18 13:05:32 +05:30
Naman Verma
1e279e6193 Merge branch 'main' into nv/4172 2026-03-18 13:03:09 +05:30
Naman Verma
d3a278c43e docs: perses schema for dashboards 2026-03-17 14:56:07 +05:30
99 changed files with 4688 additions and 3009 deletions

3
.prettierrc Normal file
View File

@@ -0,0 +1,3 @@
{
"trailingComma": "none"
}

44
.vscode/settings.json vendored
View File

@@ -1,23 +1,25 @@
{
"eslint.workingDirectories": [
"./frontend"
],
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"prettier.requireConfig": true,
"[go]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "golang.go"
},
"[sql]": {
"editor.defaultFormatter": "adpyke.vscode-sql-formatter"
},
"[html]": {
"editor.defaultFormatter": "vscode.html-language-features"
},
"python-envs.defaultEnvManager": "ms-python.python:system",
"python-envs.pythonProjects": []
"eslint.workingDirectories": ["./frontend"],
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"prettier.requireConfig": true,
"[go]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "golang.go"
},
"[sql]": {
"editor.defaultFormatter": "adpyke.vscode-sql-formatter"
},
"[html]": {
"editor.defaultFormatter": "vscode.html-language-features"
},
"python-envs.defaultEnvManager": "ms-python.python:system",
"python-envs.pythonProjects": [],
"editor.tokenColorCustomizations": {
"comments": "",
"textMateRules": []
}
}

View File

@@ -190,7 +190,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.117.1
image: signoz/signoz:v0.117.0
ports:
- "8080:8080" # signoz port
# - "6060:6060" # pprof port

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.117.1
image: signoz/signoz:v0.117.0
ports:
- "8080:8080" # signoz port
volumes:

View File

@@ -181,7 +181,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.117.1}
image: signoz/signoz:${VERSION:-v0.117.0}
container_name: signoz
ports:
- "8080:8080" # signoz port

View File

@@ -109,7 +109,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.117.1}
image: signoz/signoz:${VERSION:-v0.117.0}
container_name: signoz
ports:
- "8080:8080" # signoz port

View File

@@ -327,27 +327,6 @@ components:
nullable: true
type: array
type: object
AuthtypesStorableRole:
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
type: object
AuthtypesTransaction:
properties:
object:
@@ -363,53 +342,6 @@ components:
config:
$ref: '#/components/schemas/AuthtypesAuthDomainConfig'
type: object
AuthtypesUserRole:
properties:
createdAt:
format: date-time
type: string
id:
type: string
role:
$ref: '#/components/schemas/AuthtypesStorableRole'
roleId:
type: string
updatedAt:
format: date-time
type: string
userId:
type: string
required:
- id
type: object
AuthtypesUserWithRoles:
properties:
createdAt:
format: date-time
type: string
displayName:
type: string
email:
type: string
id:
type: string
isRoot:
type: boolean
orgId:
type: string
status:
type: string
updatedAt:
format: date-time
type: string
userRoles:
items:
$ref: '#/components/schemas/AuthtypesUserRole'
nullable: true
type: array
required:
- id
type: object
CloudintegrationtypesAWSAccountConfig:
properties:
regions:
@@ -2674,13 +2606,6 @@ components:
token:
type: string
type: object
TypesPostableRole:
properties:
name:
type: string
required:
- name
type: object
TypesResetPasswordToken:
properties:
expiresAt:
@@ -2722,13 +2647,6 @@ components:
required:
- id
type: object
TypesUpdatableUser:
properties:
displayName:
type: string
required:
- displayName
type: object
TypesUser:
properties:
createdAt:
@@ -6195,7 +6113,7 @@ paths:
get:
deprecated: false
description: This endpoint lists all users
operationId: ListUsersDeprecated
operationId: ListUsers
responses:
"200":
content:
@@ -6288,7 +6206,7 @@ paths:
get:
deprecated: false
description: This endpoint returns the user by id
operationId: GetUserDeprecated
operationId: GetUser
parameters:
- in: path
name: id
@@ -6345,7 +6263,7 @@ paths:
put:
deprecated: false
description: This endpoint updates the user by id
operationId: UpdateUserDeprecated
operationId: UpdateUser
parameters:
- in: path
name: id
@@ -6414,7 +6332,7 @@ paths:
get:
deprecated: false
description: This endpoint returns the user I belong to
operationId: GetMyUserDeprecated
operationId: GetMyUser
responses:
"200":
content:
@@ -7863,66 +7781,6 @@ paths:
summary: Readiness check
tags:
- health
/api/v2/roles/{id}/users:
get:
deprecated: false
description: This endpoint returns the users having the role by role id
operationId: GetUsersByRoleID
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
items:
$ref: '#/components/schemas/TypesUser'
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Get users by role id
tags:
- users
/api/v2/sessions:
delete:
deprecated: false
@@ -8081,408 +7939,6 @@ paths:
summary: Rotate session
tags:
- sessions
/api/v2/users:
get:
deprecated: false
description: This endpoint lists all users for the organization
operationId: ListUsers
responses:
"200":
content:
application/json:
schema:
properties:
data:
items:
$ref: '#/components/schemas/TypesUser'
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: List users v2
tags:
- users
/api/v2/users/{id}:
get:
deprecated: false
description: This endpoint returns the user by id
operationId: GetUser
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/AuthtypesUserWithRoles'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Get user by user id
tags:
- users
put:
deprecated: false
description: This endpoint updates the user by id
operationId: UpdateUser
parameters:
- in: path
name: id
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TypesUpdatableUser'
responses:
"204":
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Update user v2
tags:
- users
/api/v2/users/{id}/roles:
get:
deprecated: false
description: This endpoint returns the user roles by user id
operationId: GetRolesByUserID
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
items:
$ref: '#/components/schemas/AuthtypesRole'
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Get user roles
tags:
- users
post:
deprecated: false
description: This endpoint assigns the role to the user roles by user id
operationId: SetRoleByUserID
parameters:
- in: path
name: id
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TypesPostableRole'
responses:
"200":
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Set user roles
tags:
- users
/api/v2/users/{id}/roles/{roleId}:
delete:
deprecated: false
description: This endpoint removes a role from the user by user id and role
id
operationId: RemoveUserRoleByUserIDAndRoleID
parameters:
- in: path
name: id
required: true
schema:
type: string
- in: path
name: roleId
required: true
schema:
type: string
responses:
"204":
description: No Content
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Remove a role from user
tags:
- users
/api/v2/users/me:
get:
deprecated: false
description: This endpoint returns the user I belong to
operationId: GetMyUser
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/AuthtypesUserWithRoles'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- tokenizer: []
summary: Get my user v2
tags:
- users
put:
deprecated: false
description: This endpoint updates the user I belong to
operationId: UpdateMyUserV2
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TypesUpdatableUser'
responses:
"204":
description: No Content
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- tokenizer: []
summary: Update my user v2
tags:
- users
/api/v2/zeus/hosts:
get:
deprecated: false

View File

@@ -425,39 +425,6 @@ export interface AuthtypesSessionContextDTO {
orgs?: AuthtypesOrgSessionContextDTO[] | null;
}
export interface AuthtypesStorableRoleDTO {
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
description?: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name?: string;
/**
* @type string
*/
orgId?: string;
/**
* @type string
*/
type?: string;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
}
export interface AuthtypesTransactionDTO {
object: AuthtypesObjectDTO;
/**
@@ -470,74 +437,6 @@ export interface AuthtypesUpdateableAuthDomainDTO {
config?: AuthtypesAuthDomainConfigDTO;
}
export interface AuthtypesUserRoleDTO {
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
id: string;
role?: AuthtypesStorableRoleDTO;
/**
* @type string
*/
roleId?: string;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
/**
* @type string
*/
userId?: string;
}
export interface AuthtypesUserWithRolesDTO {
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
displayName?: string;
/**
* @type string
*/
email?: string;
/**
* @type string
*/
id: string;
/**
* @type boolean
*/
isRoot?: boolean;
/**
* @type string
*/
orgId?: string;
/**
* @type string
*/
status?: string;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
/**
* @type array
* @nullable true
*/
userRoles?: AuthtypesUserRoleDTO[] | null;
}
export interface CloudintegrationtypesAWSAccountConfigDTO {
/**
* @type array
@@ -3180,13 +3079,6 @@ export interface TypesPostableResetPasswordDTO {
token?: string;
}
export interface TypesPostableRoleDTO {
/**
* @type string
*/
name: string;
}
export interface TypesResetPasswordTokenDTO {
/**
* @type string
@@ -3252,13 +3144,6 @@ export interface TypesStorableAPIKeyDTO {
userId?: string;
}
export interface TypesUpdatableUserDTO {
/**
* @type string
*/
displayName: string;
}
export interface TypesUserDTO {
/**
* @type string
@@ -3965,7 +3850,7 @@ export type UpdateServiceAccountKeyPathParameters = {
export type UpdateServiceAccountStatusPathParameters = {
id: string;
};
export type ListUsersDeprecated200 = {
export type ListUsers200 = {
/**
* @type array
*/
@@ -3979,10 +3864,10 @@ export type ListUsersDeprecated200 = {
export type DeleteUserPathParameters = {
id: string;
};
export type GetUserDeprecatedPathParameters = {
export type GetUserPathParameters = {
id: string;
};
export type GetUserDeprecated200 = {
export type GetUser200 = {
data: TypesDeprecatedUserDTO;
/**
* @type string
@@ -3990,10 +3875,10 @@ export type GetUserDeprecated200 = {
status: string;
};
export type UpdateUserDeprecatedPathParameters = {
export type UpdateUserPathParameters = {
id: string;
};
export type UpdateUserDeprecated200 = {
export type UpdateUser200 = {
data: TypesDeprecatedUserDTO;
/**
* @type string
@@ -4001,7 +3886,7 @@ export type UpdateUserDeprecated200 = {
status: string;
};
export type GetMyUserDeprecated200 = {
export type GetMyUser200 = {
data: TypesDeprecatedUserDTO;
/**
* @type string
@@ -4298,20 +4183,6 @@ export type Readyz503 = {
status: string;
};
export type GetUsersByRoleIDPathParameters = {
id: string;
};
export type GetUsersByRoleID200 = {
/**
* @type array
*/
data: TypesUserDTO[];
/**
* @type string
*/
status: string;
};
export type GetSessionContext200 = {
data: AuthtypesSessionContextDTO;
/**
@@ -4336,60 +4207,6 @@ export type RotateSession200 = {
status: string;
};
export type ListUsers200 = {
/**
* @type array
*/
data: TypesUserDTO[];
/**
* @type string
*/
status: string;
};
export type GetUserPathParameters = {
id: string;
};
export type GetUser200 = {
data: AuthtypesUserWithRolesDTO;
/**
* @type string
*/
status: string;
};
export type UpdateUserPathParameters = {
id: string;
};
export type GetRolesByUserIDPathParameters = {
id: string;
};
export type GetRolesByUserID200 = {
/**
* @type array
*/
data: AuthtypesRoleDTO[];
/**
* @type string
*/
status: string;
};
export type SetRoleByUserIDPathParameters = {
id: string;
};
export type RemoveUserRoleByUserIDAndRoleIDPathParameters = {
id: string;
roleId: string;
};
export type GetMyUser200 = {
data: AuthtypesUserWithRolesDTO;
/**
* @type string
*/
status: string;
};
export type GetHosts200 = {
data: ZeustypesGettableHostDTO;
/**

File diff suppressed because it is too large Load Diff

View File

@@ -109,6 +109,7 @@ export function useNavigateToExplorer(): (
widgetConfig: {
query: preparedQuery,
panelTypes: PANEL_TYPES.TIME_SERIES,
// does this change at any time? How does it change if so?
timePreferance: 'GLOBAL_TIME',
},
selectedDashboard,

View File

@@ -116,12 +116,7 @@ describe.each([
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(screen.getByText('FORMAT')).toBeInTheDocument();
expect(screen.getByText('Number of Rows')).toBeInTheDocument();
if (dataSource === DataSource.TRACES) {
expect(screen.queryByText('Columns')).not.toBeInTheDocument();
} else {
expect(screen.getByText('Columns')).toBeInTheDocument();
}
expect(screen.getByText('Columns')).toBeInTheDocument();
});
it('allows changing export format', () => {
@@ -151,17 +146,6 @@ describe.each([
});
it('allows changing columns scope', () => {
if (dataSource === DataSource.TRACES) {
renderWithStore(dataSource);
fireEvent.click(screen.getByTestId(testId));
expect(screen.queryByRole('radio', { name: 'All' })).not.toBeInTheDocument();
expect(
screen.queryByRole('radio', { name: 'Selected' }),
).not.toBeInTheDocument();
return;
}
renderWithStore(dataSource);
fireEvent.click(screen.getByTestId(testId));
@@ -226,12 +210,7 @@ describe.each([
mockUseQueryBuilder.mockReturnValue({ stagedQuery: mockQuery });
renderWithStore(dataSource);
fireEvent.click(screen.getByTestId(testId));
// For traces, column scope is always Selected and the radio is hidden
if (dataSource !== DataSource.TRACES) {
fireEvent.click(screen.getByRole('radio', { name: 'Selected' }));
}
fireEvent.click(screen.getByRole('radio', { name: 'Selected' }));
fireEvent.click(screen.getByText('Export'));
await waitFor(() => {
@@ -248,11 +227,6 @@ describe.each([
});
it('sends no selectFields when column scope is All', async () => {
// For traces, column scope is always Selected — this test only applies to other sources
if (dataSource === DataSource.TRACES) {
return;
}
renderWithStore(dataSource);
fireEvent.click(screen.getByTestId(testId));
fireEvent.click(screen.getByRole('radio', { name: 'All' }));

View File

@@ -1,6 +1,5 @@
import { useCallback, useMemo, useState } from 'react';
import { Button, Popover, Radio, Tooltip, Typography } from 'antd';
import { TelemetryFieldKey } from 'api/v5/v5';
import { useExportRawData } from 'hooks/useDownloadOptionsMenu/useDownloadOptionsMenu';
import { Download, DownloadIcon, Loader2 } from 'lucide-react';
import { DataSource } from 'types/common/queryBuilder';
@@ -15,12 +14,10 @@ import './DownloadOptionsMenu.styles.scss';
interface DownloadOptionsMenuProps {
dataSource: DataSource;
selectedColumns?: TelemetryFieldKey[];
}
export default function DownloadOptionsMenu({
dataSource,
selectedColumns,
}: DownloadOptionsMenuProps): JSX.Element {
const [exportFormat, setExportFormat] = useState<string>(DownloadFormats.CSV);
const [rowLimit, setRowLimit] = useState<number>(DownloadRowCounts.TEN_K);
@@ -38,19 +35,9 @@ export default function DownloadOptionsMenu({
await handleExportRawData({
format: exportFormat,
rowLimit,
clearSelectColumns:
dataSource !== DataSource.TRACES &&
columnsScope === DownloadColumnsScopes.ALL,
selectedColumns,
clearSelectColumns: columnsScope === DownloadColumnsScopes.ALL,
});
}, [
exportFormat,
rowLimit,
columnsScope,
selectedColumns,
handleExportRawData,
dataSource,
]);
}, [exportFormat, rowLimit, columnsScope, handleExportRawData]);
const popoverContent = useMemo(
() => (
@@ -85,22 +72,18 @@ export default function DownloadOptionsMenu({
</Radio.Group>
</div>
{dataSource !== DataSource.TRACES && (
<>
<div className="horizontal-line" />
<div className="horizontal-line" />
<div className="columns-scope">
<Typography.Text className="title">Columns</Typography.Text>
<Radio.Group
value={columnsScope}
onChange={(e): void => setColumnsScope(e.target.value)}
>
<Radio value={DownloadColumnsScopes.ALL}>All</Radio>
<Radio value={DownloadColumnsScopes.SELECTED}>Selected</Radio>
</Radio.Group>
</div>
</>
)}
<div className="columns-scope">
<Typography.Text className="title">Columns</Typography.Text>
<Radio.Group
value={columnsScope}
onChange={(e): void => setColumnsScope(e.target.value)}
>
<Radio value={DownloadColumnsScopes.ALL}>All</Radio>
<Radio value={DownloadColumnsScopes.SELECTED}>Selected</Radio>
</Radio.Group>
</div>
<Button
type="primary"
@@ -114,14 +97,7 @@ export default function DownloadOptionsMenu({
</Button>
</div>
),
[
exportFormat,
rowLimit,
columnsScope,
isDownloading,
handleExport,
dataSource,
],
[exportFormat, rowLimit, columnsScope, isDownloading, handleExport],
);
return (

View File

@@ -20,7 +20,7 @@ import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import {
getResetPasswordToken,
useDeleteUser,
useUpdateUserDeprecated,
useUpdateUser,
} from 'api/generated/services/users';
import { AxiosError } from 'axios';
import { MemberRow } from 'components/MembersTable/MembersTable';
@@ -60,7 +60,7 @@ function EditMemberDrawer({
const isInvited = member?.status === MemberStatus.Invited;
const { mutate: updateUser, isLoading: isSaving } = useUpdateUserDeprecated({
const { mutate: updateUser, isLoading: isSaving } = useUpdateUser({
mutation: {
onSuccess: (): void => {
toast.success('Member details updated successfully', { richColors: true });

View File

@@ -4,7 +4,7 @@ import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
getResetPasswordToken,
useDeleteUser,
useUpdateUserDeprecated,
useUpdateUser,
} from 'api/generated/services/users';
import { MemberStatus } from 'container/MembersSettings/utils';
import {
@@ -50,7 +50,7 @@ jest.mock('@signozhq/dialog', () => ({
jest.mock('api/generated/services/users', () => ({
useDeleteUser: jest.fn(),
useUpdateUserDeprecated: jest.fn(),
useUpdateUser: jest.fn(),
getResetPasswordToken: jest.fn(),
}));
@@ -105,7 +105,7 @@ function renderDrawer(
describe('EditMemberDrawer', () => {
beforeEach(() => {
jest.clearAllMocks();
(useUpdateUserDeprecated as jest.Mock).mockReturnValue({
(useUpdateUser as jest.Mock).mockReturnValue({
mutate: mockUpdateMutate,
isLoading: false,
});
@@ -130,7 +130,7 @@ describe('EditMemberDrawer', () => {
const onComplete = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
(useUpdateUserDeprecated as jest.Mock).mockImplementation((options) => ({
(useUpdateUser as jest.Mock).mockImplementation((options) => ({
mutate: mockUpdateMutate.mockImplementation(() => {
options?.mutation?.onSuccess?.();
}),
@@ -239,7 +239,7 @@ describe('EditMemberDrawer', () => {
const onComplete = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
(useUpdateUserDeprecated as jest.Mock).mockImplementation((options) => ({
(useUpdateUser as jest.Mock).mockImplementation((options) => ({
mutate: mockUpdateMutate.mockImplementation(() => {
options?.mutation?.onSuccess?.();
}),
@@ -280,7 +280,7 @@ describe('EditMemberDrawer', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const mockToast = jest.mocked(toast);
(useUpdateUserDeprecated as jest.Mock).mockImplementation((options) => ({
(useUpdateUser as jest.Mock).mockImplementation((options) => ({
mutate: mockUpdateMutate.mockImplementation(() => {
options?.mutation?.onError?.({});
}),

View File

@@ -16,9 +16,9 @@ function AverageResolutionCard({
}: TotalTriggeredCardProps): JSX.Element {
return (
<StatsCard
displayValue={formatTime(+currentAvgResolutionTime)}
totalCurrentCount={+currentAvgResolutionTime}
totalPastCount={+pastAvgResolutionTime}
displayValue={formatTime(currentAvgResolutionTime)}
totalCurrentCount={currentAvgResolutionTime}
totalPastCount={pastAvgResolutionTime}
title="Avg. Resolution Time"
timeSeries={timeSeries}
/>

View File

@@ -577,6 +577,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
open={isPanelNameModalOpen}
title="New Section"
rootClassName="section-naming"
// This is not required on the specification of a custom footer
onOk={(): void => handleAddRow()}
onCancel={(): void => {
setIsPanelNameModalOpen(false);

View File

@@ -48,7 +48,6 @@ import DashboardEmptyState from './DashboardEmptyState/DashboardEmptyState';
import GridCard from './GridCard';
import { Card, CardContainer, ReactGridLayout } from './styles';
import {
applyRowCollapse,
hasColumnWidthsChanged,
removeUndefinedValuesFromLayout,
} from './utils';
@@ -269,10 +268,13 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
return;
}
const updatedWidgets = selectedDashboard?.data?.widgets?.map((e) =>
e.id === currentSelectRowId ? { ...e, title: newTitle } : e,
currentWidget.title = newTitle;
const updatedWidgets = selectedDashboard?.data?.widgets?.filter(
(e) => e.id !== currentSelectRowId,
);
updatedWidgets?.push(currentWidget);
const updatedSelectedDashboard: Props = {
id: selectedDashboard.id,
data: {
@@ -314,13 +316,88 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
if (!selectedDashboard) {
return;
}
const { updatedLayout, updatedPanelMap } = applyRowCollapse(
id,
dashboardLayout,
currentPanelMap,
);
setCurrentPanelMap((prev) => ({ ...prev, ...updatedPanelMap }));
setDashboardLayout(sortLayout(updatedLayout));
const rowProperties = { ...currentPanelMap[id] };
const updatedPanelMap = { ...currentPanelMap };
let updatedDashboardLayout = [...dashboardLayout];
if (rowProperties.collapsed === true) {
rowProperties.collapsed = false;
const widgetsInsideTheRow = rowProperties.widgets;
let maxY = 0;
widgetsInsideTheRow.forEach((w) => {
maxY = Math.max(maxY, w.y + w.h);
});
const currentRowWidget = dashboardLayout.find((w) => w.i === id);
if (currentRowWidget && widgetsInsideTheRow.length) {
maxY -= currentRowWidget.h + currentRowWidget.y;
}
const idxCurrentRow = dashboardLayout.findIndex((w) => w.i === id);
for (let j = idxCurrentRow + 1; j < dashboardLayout.length; j++) {
updatedDashboardLayout[j].y += maxY;
if (updatedPanelMap[updatedDashboardLayout[j].i]) {
updatedPanelMap[updatedDashboardLayout[j].i].widgets = updatedPanelMap[
updatedDashboardLayout[j].i
].widgets.map((w) => ({
...w,
y: w.y + maxY,
}));
}
}
updatedDashboardLayout = [...updatedDashboardLayout, ...widgetsInsideTheRow];
} else {
rowProperties.collapsed = true;
const currentIdx = dashboardLayout.findIndex((w) => w.i === id);
let widgetsInsideTheRow: Layout[] = [];
let isPanelMapUpdated = false;
for (let j = currentIdx + 1; j < dashboardLayout.length; j++) {
if (currentPanelMap[dashboardLayout[j].i]) {
rowProperties.widgets = widgetsInsideTheRow;
widgetsInsideTheRow = [];
isPanelMapUpdated = true;
break;
} else {
widgetsInsideTheRow.push(dashboardLayout[j]);
}
}
if (!isPanelMapUpdated) {
rowProperties.widgets = widgetsInsideTheRow;
}
let maxY = 0;
widgetsInsideTheRow.forEach((w) => {
maxY = Math.max(maxY, w.y + w.h);
});
const currentRowWidget = dashboardLayout[currentIdx];
if (currentRowWidget && widgetsInsideTheRow.length) {
maxY -= currentRowWidget.h + currentRowWidget.y;
}
for (let j = currentIdx + 1; j < updatedDashboardLayout.length; j++) {
updatedDashboardLayout[j].y += maxY;
if (updatedPanelMap[updatedDashboardLayout[j].i]) {
updatedPanelMap[updatedDashboardLayout[j].i].widgets = updatedPanelMap[
updatedDashboardLayout[j].i
].widgets.map((w) => ({
...w,
y: w.y + maxY,
}));
}
}
updatedDashboardLayout = updatedDashboardLayout.filter(
(widget) => !rowProperties.widgets.some((w: Layout) => w.i === widget.i),
);
}
setCurrentPanelMap((prev) => ({
...prev,
...updatedPanelMap,
[id]: {
...rowProperties,
},
}));
setDashboardLayout(sortLayout(updatedDashboardLayout));
};
const handleDragStop: ItemCallback = (_, oldItem, newItem): void => {

View File

@@ -1,181 +0,0 @@
import { Layout } from 'react-grid-layout';
import { applyRowCollapse, PanelMap } from '../utils';
// Helper to produce deeply-frozen objects that mimic what zustand/immer returns.
function freeze<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj), (_, v) =>
typeof v === 'object' && v !== null ? Object.freeze(v) : v,
) as T;
}
// ─── fixtures ────────────────────────────────────────────────────────────────
const ROW_ID = 'row1';
/** A layout with one row followed by two widgets. */
function makeLayout(): Layout[] {
return [
{ i: ROW_ID, x: 0, y: 0, w: 12, h: 1 },
{ i: 'w1', x: 0, y: 1, w: 6, h: 4 },
{ i: 'w2', x: 6, y: 1, w: 6, h: 4 },
];
}
/** panelMap where the row is expanded (collapsed = false, widgets = []). */
function makeExpandedPanelMap(): PanelMap {
return {
[ROW_ID]: { collapsed: false, widgets: [] },
};
}
/** panelMap where the row is collapsed (widgets stored inside). */
function makeCollapsedPanelMap(): PanelMap {
return {
[ROW_ID]: {
collapsed: true,
widgets: [
{ i: 'w1', x: 0, y: 1, w: 6, h: 4 },
{ i: 'w2', x: 6, y: 1, w: 6, h: 4 },
],
},
};
}
// ─── frozen-input guard (regression for zustand/immer read-only bug) ──────────
describe('applyRowCollapse does not mutate frozen inputs', () => {
it('does not throw when collapsing a row with frozen layout + panelMap', () => {
expect(() =>
applyRowCollapse(
ROW_ID,
freeze(makeLayout()),
freeze(makeExpandedPanelMap()),
),
).not.toThrow();
});
it('does not throw when expanding a row with frozen layout + panelMap', () => {
// Collapsed layout only has the row item; widgets live in panelMap.
const collapsedLayout = freeze([{ i: ROW_ID, x: 0, y: 0, w: 12, h: 1 }]);
expect(() =>
applyRowCollapse(ROW_ID, collapsedLayout, freeze(makeCollapsedPanelMap())),
).not.toThrow();
});
it('leaves the original layout array untouched after collapse', () => {
const layout = makeLayout();
const originalY = layout[1].y; // w1.y before collapse
applyRowCollapse(ROW_ID, layout, makeExpandedPanelMap());
expect(layout[1].y).toBe(originalY);
});
it('leaves the original panelMap untouched after collapse', () => {
const panelMap = makeExpandedPanelMap();
applyRowCollapse(ROW_ID, makeLayout(), panelMap);
expect(panelMap[ROW_ID].collapsed).toBe(false);
});
});
// ─── collapse behaviour ───────────────────────────────────────────────────────
describe('applyRowCollapse collapsing a row', () => {
it('sets collapsed = true on the row entry', () => {
const { updatedPanelMap } = applyRowCollapse(
ROW_ID,
makeLayout(),
makeExpandedPanelMap(),
);
expect(updatedPanelMap[ROW_ID].collapsed).toBe(true);
});
it('stores the child widgets inside the panelMap entry', () => {
const { updatedPanelMap } = applyRowCollapse(
ROW_ID,
makeLayout(),
makeExpandedPanelMap(),
);
const ids = updatedPanelMap[ROW_ID].widgets.map((w) => w.i);
expect(ids).toContain('w1');
expect(ids).toContain('w2');
});
it('removes child widgets from the returned layout', () => {
const { updatedLayout } = applyRowCollapse(
ROW_ID,
makeLayout(),
makeExpandedPanelMap(),
);
const ids = updatedLayout.map((l) => l.i);
expect(ids).not.toContain('w1');
expect(ids).not.toContain('w2');
expect(ids).toContain(ROW_ID);
});
});
// ─── expand behaviour ─────────────────────────────────────────────────────────
describe('applyRowCollapse expanding a row', () => {
it('sets collapsed = false on the row entry', () => {
const collapsedLayout: Layout[] = [{ i: ROW_ID, x: 0, y: 0, w: 12, h: 1 }];
const { updatedPanelMap } = applyRowCollapse(
ROW_ID,
collapsedLayout,
makeCollapsedPanelMap(),
);
expect(updatedPanelMap[ROW_ID].collapsed).toBe(false);
});
it('restores child widgets to the returned layout', () => {
const collapsedLayout: Layout[] = [{ i: ROW_ID, x: 0, y: 0, w: 12, h: 1 }];
const { updatedLayout } = applyRowCollapse(
ROW_ID,
collapsedLayout,
makeCollapsedPanelMap(),
);
const ids = updatedLayout.map((l) => l.i);
expect(ids).toContain('w1');
expect(ids).toContain('w2');
});
it('restored child widgets appear in both the layout and the panelMap entry', () => {
const collapsedLayout: Layout[] = [{ i: ROW_ID, x: 0, y: 0, w: 12, h: 1 }];
const { updatedLayout, updatedPanelMap } = applyRowCollapse(
ROW_ID,
collapsedLayout,
makeCollapsedPanelMap(),
);
// The previously-stored widgets should now be back in the live layout.
expect(updatedLayout.map((l) => l.i)).toContain('w1');
// The panelMap entry still holds a reference to them (stale until next collapse).
expect(updatedPanelMap[ROW_ID].widgets.map((w) => w.i)).toContain('w1');
});
});
// ─── y-offset adjustment ──────────────────────────────────────────────────────
describe('applyRowCollapse y-offset adjustments for rows below', () => {
it('shifts items below a second row down when the first row expands', () => {
const ROW2 = 'row2';
// Layout: row1 (y=0,h=1) | w1 (y=1,h=4) | row2 (y=5,h=1) | w3 (y=6,h=2)
const layout: Layout[] = [
{ i: ROW_ID, x: 0, y: 0, w: 12, h: 1 },
{ i: 'w1', x: 0, y: 1, w: 12, h: 4 },
{ i: ROW2, x: 0, y: 5, w: 12, h: 1 },
{ i: 'w3', x: 0, y: 6, w: 12, h: 2 },
];
const panelMap: PanelMap = {
[ROW_ID]: {
collapsed: true,
widgets: [{ i: 'w1', x: 0, y: 1, w: 12, h: 4 }],
},
[ROW2]: { collapsed: false, widgets: [] },
};
// Expanding row1 should push row2 and w3 down by the height of w1 (4).
const collapsedLayout = layout.filter((l) => l.i !== 'w1');
const { updatedLayout } = applyRowCollapse(ROW_ID, collapsedLayout, panelMap);
const row2Item = updatedLayout.find((l) => l.i === ROW2);
expect(row2Item?.y).toBe(5 + 4); // shifted by maxY = 4
});
});

View File

@@ -4,122 +4,6 @@ import { isEmpty, isEqual } from 'lodash-es';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
export type PanelMap = Record<
string,
{ widgets: Layout[]; collapsed: boolean }
>;
export interface RowCollapseResult {
updatedLayout: Layout[];
updatedPanelMap: PanelMap;
}
/**
* Pure function that computes the new layout and panelMap after toggling a
* row's collapsed state. All inputs are treated as immutable — no input object
* is mutated, so it is safe to pass frozen objects from the zustand store.
*/
// eslint-disable-next-line sonarjs/cognitive-complexity
export function applyRowCollapse(
id: string,
dashboardLayout: Layout[],
currentPanelMap: PanelMap,
): RowCollapseResult {
// Deep-copy the row's own properties so we can mutate our local copy.
const rowProperties = {
...currentPanelMap[id],
widgets: [...(currentPanelMap[id]?.widgets ?? [])],
};
// Shallow-copy each entry's widgets array so inner .map() calls are safe.
const updatedPanelMap: PanelMap = Object.fromEntries(
Object.entries(currentPanelMap).map(([k, v]) => [
k,
{ ...v, widgets: [...v.widgets] },
]),
);
let updatedDashboardLayout = [...dashboardLayout];
if (rowProperties.collapsed === true) {
// ── EXPAND ──────────────────────────────────────────────────────────────
rowProperties.collapsed = false;
const widgetsInsideTheRow = rowProperties.widgets;
let maxY = 0;
widgetsInsideTheRow.forEach((w) => {
maxY = Math.max(maxY, w.y + w.h);
});
const currentRowWidget = dashboardLayout.find((w) => w.i === id);
if (currentRowWidget && widgetsInsideTheRow.length) {
maxY -= currentRowWidget.h + currentRowWidget.y;
}
const idxCurrentRow = dashboardLayout.findIndex((w) => w.i === id);
for (let j = idxCurrentRow + 1; j < dashboardLayout.length; j++) {
updatedDashboardLayout[j] = {
...updatedDashboardLayout[j],
y: updatedDashboardLayout[j].y + maxY,
};
if (updatedPanelMap[updatedDashboardLayout[j].i]) {
updatedPanelMap[updatedDashboardLayout[j].i].widgets = updatedPanelMap[
updatedDashboardLayout[j].i
].widgets.map((w) => ({ ...w, y: w.y + maxY }));
}
}
updatedDashboardLayout = [...updatedDashboardLayout, ...widgetsInsideTheRow];
} else {
// ── COLLAPSE ─────────────────────────────────────────────────────────────
rowProperties.collapsed = true;
const currentIdx = dashboardLayout.findIndex((w) => w.i === id);
let widgetsInsideTheRow: Layout[] = [];
let isPanelMapUpdated = false;
for (let j = currentIdx + 1; j < dashboardLayout.length; j++) {
if (currentPanelMap[dashboardLayout[j].i]) {
rowProperties.widgets = widgetsInsideTheRow;
widgetsInsideTheRow = [];
isPanelMapUpdated = true;
break;
} else {
widgetsInsideTheRow.push(dashboardLayout[j]);
}
}
if (!isPanelMapUpdated) {
rowProperties.widgets = widgetsInsideTheRow;
}
let maxY = 0;
widgetsInsideTheRow.forEach((w) => {
maxY = Math.max(maxY, w.y + w.h);
});
const currentRowWidget = dashboardLayout[currentIdx];
if (currentRowWidget && widgetsInsideTheRow.length) {
maxY -= currentRowWidget.h + currentRowWidget.y;
}
for (let j = currentIdx + 1; j < updatedDashboardLayout.length; j++) {
updatedDashboardLayout[j] = {
...updatedDashboardLayout[j],
y: updatedDashboardLayout[j].y + maxY,
};
if (updatedPanelMap[updatedDashboardLayout[j].i]) {
updatedPanelMap[updatedDashboardLayout[j].i].widgets = updatedPanelMap[
updatedDashboardLayout[j].i
].widgets.map((w) => ({ ...w, y: w.y + maxY }));
}
}
updatedDashboardLayout = updatedDashboardLayout.filter(
(widget) => !rowProperties.widgets.some((w: Layout) => w.i === widget.i),
);
}
updatedPanelMap[id] = { ...rowProperties };
return { updatedLayout: updatedDashboardLayout, updatedPanelMap };
}
export const removeUndefinedValuesFromLayout = (layout: Layout[]): Layout[] =>
layout.map((obj) =>
Object.fromEntries(

View File

@@ -92,10 +92,7 @@ function LogsActionsContainer({
/>
</div>
<div className="download-options-container">
<DownloadOptionsMenu
dataSource={DataSource.LOGS}
selectedColumns={options?.selectColumns}
/>
<DownloadOptionsMenu dataSource={DataSource.LOGS} />
</div>
<div className="format-options-container">
<LogsFormatOptionsMenu

View File

@@ -42,15 +42,8 @@ function LogsPanelComponent({
setPageSize(value);
setOffset(0);
setRequestData((prev) => {
const newQueryData = {
...prev.query,
builder: {
...prev.query.builder,
queryData: prev.query.builder.queryData.map((qd, i) =>
i === 0 ? { ...qd, pageSize: value } : qd,
),
},
};
const newQueryData = { ...prev.query };
newQueryData.builder.queryData[0].pageSize = value;
return {
...prev,
query: newQueryData,

View File

@@ -16,8 +16,10 @@ export interface NewWidgetProps {
}
export interface WidgetGraphProps {
// this should be inside the querier plugin definition
selectedLogFields: Widgets['selectedLogFields'];
setSelectedLogFields?: Dispatch<SetStateAction<Widgets['selectedLogFields']>>;
// this should be inside the querier plugin definition
selectedTracesFields: Widgets['selectedTracesFields'];
setSelectedTracesFields?: Dispatch<
SetStateAction<Widgets['selectedTracesFields']>

View File

@@ -42,19 +42,11 @@ function Panel({
};
}
updatedQuery.builder.queryData[0].pageSize = 10;
const initialDataSource = updatedQuery.builder.queryData[0].dataSource;
const updatedQueryForList = {
...updatedQuery,
builder: {
...updatedQuery.builder,
queryData: updatedQuery.builder.queryData.map((qd, i) =>
i === 0 ? { ...qd, pageSize: 10 } : qd,
),
},
};
return {
query: updatedQueryForList,
query: updatedQuery,
graphType: PANEL_TYPES.LIST,
selectedTime: widget.timePreferance || 'GLOBAL_TIME',
tableParams: {

View File

@@ -239,10 +239,7 @@ function ListView({
/>
</div>
<DownloadOptionsMenu
dataSource={DataSource.TRACES}
selectedColumns={options?.selectColumns}
/>
<DownloadOptionsMenu dataSource={DataSource.TRACES} />
<TraceExplorerControls
isLoading={isFetching}

View File

@@ -52,44 +52,37 @@ export const useGetQueryRange: UseGetQueryRange = (
!firstQueryData?.filters?.items.some((filter) => filter.key?.key === 'id') &&
firstQueryData?.orderBy[0].columnName === 'timestamp';
if (
isListWithSingleTimestampOrder &&
firstQueryData?.dataSource === DataSource.LOGS
) {
return {
...requestData,
graphType:
requestData.graphType === PANEL_TYPES.BAR
? PANEL_TYPES.TIME_SERIES
: requestData.graphType,
query: {
...requestData.query,
builder: {
...requestData.query.builder,
queryData: [
{
...firstQueryData,
orderBy: [
...(firstQueryData?.orderBy || []),
{
columnName: 'id',
order: firstQueryData?.orderBy[0]?.order,
},
],
},
],
},
},
};
}
return {
const modifiedRequestData = {
...requestData,
graphType:
requestData.graphType === PANEL_TYPES.BAR
? PANEL_TYPES.TIME_SERIES
: requestData.graphType,
};
// If the query is a list with a single timestamp order, we need to add the id column to the order by clause
if (
isListWithSingleTimestampOrder &&
firstQueryData?.dataSource === DataSource.LOGS
) {
modifiedRequestData.query.builder = {
...requestData.query.builder,
queryData: [
{
...firstQueryData,
orderBy: [
...(firstQueryData?.orderBy || []),
{
columnName: 'id',
order: firstQueryData?.orderBy[0]?.order,
},
],
},
],
};
}
return modifiedRequestData;
}, [requestData]);
const queryKey = useMemo(() => {

View File

@@ -3,7 +3,7 @@ import { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { message } from 'antd';
import { downloadExportData } from 'api/v1/download/downloadExportData';
import { prepareQueryRangePayloadV5, TelemetryFieldKey } from 'api/v5/v5';
import { prepareQueryRangePayloadV5 } from 'api/v5/v5';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { AppState } from 'store/reducers';
@@ -14,7 +14,6 @@ interface ExportOptions {
format: string;
rowLimit: number;
clearSelectColumns: boolean;
selectedColumns?: TelemetryFieldKey[];
}
interface UseExportRawDataProps {
@@ -43,7 +42,6 @@ export function useExportRawData({
format,
rowLimit,
clearSelectColumns,
selectedColumns,
}: ExportOptions): Promise<void> => {
if (!stagedQuery) {
return;
@@ -52,12 +50,6 @@ export function useExportRawData({
try {
setIsDownloading(true);
const selectColumnsOverride = clearSelectColumns
? {}
: selectedColumns?.length
? { selectColumns: selectedColumns }
: {};
const exportQuery = {
...stagedQuery,
builder: {
@@ -67,7 +59,7 @@ export function useExportRawData({
groupBy: [],
having: { expression: '' },
limit: rowLimit,
...selectColumnsOverride,
...(clearSelectColumns && { selectColumns: [] }),
})),
queryTraceOperator: (stagedQuery.builder.queryTraceOperator || []).map(
(traceOp) => ({
@@ -75,7 +67,7 @@ export function useExportRawData({
groupBy: [],
having: { expression: '' },
limit: rowLimit,
...selectColumnsOverride,
...(clearSelectColumns && { selectColumns: [] }),
}),
),
},

View File

@@ -56,8 +56,8 @@ export interface AlertRuleStats {
totalPastTriggers: number;
currentTriggersSeries: CurrentTriggersSeries;
pastTriggersSeries: CurrentTriggersSeries | null;
currentAvgResolutionTime: string;
pastAvgResolutionTime: string;
currentAvgResolutionTime: number;
pastAvgResolutionTime: number;
currentAvgResolutionTimeSeries: CurrentTriggersSeries;
pastAvgResolutionTimeSeries: any | null;
}

View File

@@ -118,7 +118,6 @@ export interface IBaseWidget {
opacity: string;
nullZeroValues: string;
timePreferance: timePreferenceType;
stepSize?: number;
yAxisUnit?: string;
decimalPrecision?: PrecisionOption; // number of decimals or 'full precision'
stackedBarChart?: boolean;

View File

@@ -112,12 +112,6 @@ export function formatEpochTimestamp(epoch: number): string {
*/
export function formatTime(seconds: number): string {
seconds = +seconds;
if (Number.isNaN(seconds)) {
return '-';
}
const days = seconds / 86400;
if (days >= 1) {

131
perses/common/common.cue Normal file
View File

@@ -0,0 +1,131 @@
package common
// ──────────────────────────────────────────────
// Shared types
// ──────────────────────────────────────────────
// ! What is used for - Got answer
// Which one of these will be there if this exists
#TelemetryFieldKey: {
name: string
key?: string
description?: string
unit?: string
signal?: string
fieldContext?: string
fieldDataType?: string
materialized?: bool
isIndexed?: bool
}
// ──────────────────────────────────────────────
// Panel types
// ──────────────────────────────────────────────
#ContextLinkProps: {
url: string
label: string
}
// is this used on the frontend or just for query building?
#TimePreference: *"globalTime" | "last5Min" | "last15Min" | "last30Min" | "last1Hr" | "last6Hr" | "last1Day" | "last3Days" | "last1Week" | "last1Month"
// is the default option the one in the front
#PrecisionOption: *2 | 0 | 1 | 3 | 4 | "full"
// how is this undefined and null? Would like to avoid undefined if possible
// also bools can required always?
#Axes: {
softMin?: number | *null
softMax?: number | *null
isLogScale?: bool | *false
}
#LegendPosition: *"bottom" | "right"
#ThresholdWithLabel: {
value: number
unit?: string
// is this required? I think it is auto on the frontend and it can be overridden when set
color: string
// What is this?
format: "Text" | "Background"
// What would be label if undefined here? There would be some default noe
label?: string
}
// where is this used
#ComparisonThreshold: {
value: number
operator: ">" | "<" | ">=" | "<=" | "="
unit?: string
color: string
format: "Text" | "Background"
}
// ──────────────────────────────────────────────
// Query types
// ──────────────────────────────────────────────
#QueryName: =~"^[A-Za-z][A-Za-z0-9_]*$"
// where used?
#Limit: int & >=0 & <=10000
#Offset: int & >=0
#ReduceTo: "sum" | "count" | "avg" | "min" | "max" | "last" | "median"
// what is close?
// where is this used?
#MetricAggregation: close({
metricName: string & !=""
timeAggregation: "latest" | "sum" | "avg" | "min" | "max" | "count" | "rate" | "increase"
spaceAggregation: "sum" | "avg" | "min" | "max" | "count" | "p50" | "p75" | "p90" | "p95" | "p99"
reduceTo?: #ReduceTo
// should unspecified be the default?
temporality?: "delta" | "cumulative" | "unspecified"
})
#ExpressionAggregation: close({
expression: string & !=""
alias?: string
})
#Aggregation: #MetricAggregation | #ExpressionAggregation
// Can this be extended with common struct? this also has - HavingExpression
#FilterExpression: close({
expression: string
})
#GroupByItem: close({
name: string & !=""
fieldDataType?: string
fieldContext?: string
})
#OrderByItem: close({
columnName: string & !=""
order: "asc" | "desc"
})
#HavingExpression: close({
expression: string
})
// Is the definition used for above?
#Function: close({
name: "cutOffMin" | "cutOffMax" | "clampMin" | "clampMax" |
"absolute" | "runningDiff" | "log2" | "log10" |
"cumulativeSum" | "ewma3" | "ewma5" | "ewma7" |
"median3" | "median5" | "median7" | "timeShift" |
"anomaly" | "fillZero"
args?: [...close({value: number | string | bool})]
})
// ──────────────────────────────────────────────
// Variable types
// ──────────────────────────────────────────────
#VariableSortOrder: *"disabled" | "asc" | "desc"

View File

@@ -0,0 +1,4 @@
module: "github.com/signoz"
language: {
version: "v0.12.0"
}

2124
perses/examples/current.json Normal file

File diff suppressed because it is too large Load Diff

926
perses/examples/perses.json Normal file
View File

@@ -0,0 +1,926 @@
{
"kind": "Dashboard",
"metadata": {
"name": "the-everything-dashboard",
"project": "signoz" // should this be an id? What is this project exactly?
// where would createdAt, updatedAt, createdBy, updatedBy be used?
// where would dashboardID or organizationID go
// where is locked going to come in
// can we have public path here as well? default time range and
// where would version go?
// where would image value go?
// where would tags go?
// we have uploadedGrafana. What is uploadedGrafana used for?
// ! all above looks like we need a plugin for the root dashboard as well
// how is panelMap and widgets translated to layout? Is layout the same as before?
// where is timePreferance set up
// collapsed?
// ? https://github.com/react-grid-layout/react-grid-layout?tab=readme-ov-file#layout-item
},
"spec": {
"display": {
"name": "The everything dashboard", // this is duplicate?
"description": "Trying to cover as many concepts here as possible"
},
"duration": "1h", // is this required?
"datasources": {
"SigNozDatasource": {
"default": true,
"plugin": {
"kind": "SigNozDatasource",
"spec": {}
}
}
},
"variables": [
{
// what is a ListVariable
"kind": "ListVariable",
"spec": {
"name": "serviceName",
"display": {
"name": "serviceName"
},
"allowAllValue": true,
"allowMultiple": false,
"plugin": {
"kind": "SigNozDynamicVariable",
"spec": {
"name": "service.name",
"source": "Metrics",
"sort": "disabled"
}
}
}
},
{
"kind": "ListVariable",
"spec": {
"name": "statusCodesFromQuery",
"display": {
"name": "statusCodesFromQuery"
},
"allowAllValue": true,
"allowMultiple": true,
"plugin": {
"kind": "SigNozQueryVariable",
"spec": {
// missing defaultValue for these list variables
"queryValue": "SELECT JSONExtractString(labels, 'http.status_code') AS status_code FROM signoz_metrics.distributed_time_series_v4_1day WHERE status_code != '' GROUP BY status_code",
"sort": "asc"
}
}
}
},
{
"kind": "ListVariable",
"spec": {
"name": "limit",
"display": {
"name": "limit"
},
"allowAllValue": false,
"allowMultiple": false,
"plugin": {
"kind": "SigNozCustomVariable",
"spec": {
// defaultValue missing
"customValue": "1,10,20,40,80,160,200",
"sort": "disabled"
}
}
}
},
{
// do we need uuid for id here? Do we need id at all?
// migration will require cleanup from localstorage as it is all saved with ids right now
"kind": "TextVariable",
"spec": {
"name": "textboxvar",
"display": {
"name": "textboxvar"
},
"value": "defaultvaluegoeshere",
"plugin": {
// just to confirm, this would be the type right?
"kind": "SigNozTextboxVariable",
"spec": {
// How should selectedValue be handled on share?
// we should have an option for selectedValue here
// should go in for all variables
// should be auto filled with the default or with the queried value. All for multi, first for single
// if selectedValue goes in, then we will need allSelected as well
}
}
}
}
],
"panels": {
// need metadata inside this for section creation
// maybe another key just for sections. much easier
"24e2697b": {
"kind": "Panel",
"spec": {
"display": {
"name": "total resp size",
"description": ""
},
"plugin": {
"kind": "SigNozTimeSeriesPanel",
"spec": {
"visualization": {
"fillSpans": true
},
"formatting": {
"unit": "By",
"decimalPrecision": 3
},
"axes": {
"softMax": 800,
"isLogScale": true
},
"legend": {
"position": "right",
"customColors": {
"{service.name=\"sampleapp-gateway\"}": "#9ea5f7"
}
},
"contextLinks": [
{
"label": "View service details",
"url": "http://localhost:8080/{{_service.name}}?dfddf=%7B%7Blimit%7D%7D"
}
],
"thresholds": [
{
"value": 1024,
"unit": "By",
"color": "Red",
"format": "Text",
"label": "upper limit"
},
{
"value": 100,
"unit": "By",
"color": "Orange",
"format": "Text",
"label": "kinda bad"
}
]
}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "SigNozBuilderQuery",
"spec": {
"name": "A",
"signal": "metrics",
"expression": "A",
"aggregations": [
{
"metricName": "http.server.response.body.size.sum",
"reduceTo": "sum",
"spaceAggregation": "sum",
"timeAggregation": "rate"
}
],
"filter": {
"expression": "http.response.status_code IN $statusCodesFromQuery"
},
"groupBy": [
{
"name": "service.name",
"fieldDataType": "string",
"fieldContext": "tag"
}
]
}
}
}
}
]
}
},
"ff2f72f1": {
"kind": "Panel",
"spec": {
"display": {
"name": "fraction of calls",
"description": ""
},
"plugin": {
"kind": "SigNozTimeSeriesPanel",
"spec": {
"visualization": {
"fillSpans": true
},
"formatting": {
"decimalPrecision": 1
},
"thresholds": [
{
"value": 1,
"color": "Blue",
"format": "Background",
"label": "max possible"
}
]
}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "SigNozCompositeQuery",
"spec": {
"queries": [
{
"type": "builder_query",
"spec": {
"name": "A",
"signal": "metrics",
"expression": "A",
"disabled": true,
"aggregations": [
{
"metricName": "signoz_calls_total",
"reduceTo": "sum",
"spaceAggregation": "sum",
"timeAggregation": "rate"
}
],
"filter": {
"expression": "service.name IN $serviceName AND http.status_code IN $statusCodesFromQuery"
}
}
},
{
"type": "builder_query",
"spec": {
"name": "B",
"signal": "metrics",
"expression": "B",
"disabled": true,
"aggregations": [
{
"metricName": "signoz_calls_total",
"reduceTo": "sum",
"spaceAggregation": "sum",
"timeAggregation": "rate"
}
],
"filter": {
"expression": "service.name in $serviceName"
}
}
},
{
"type": "builder_formula",
"spec": {
"name": "F1",
"expression": "A / B"
}
}
]
}
}
}
}
]
}
},
"011605e7": {
"kind": "Panel",
"spec": {
"display": {
"name": "total resp size"
},
"plugin": {
"kind": "SigNozBarChartPanel",
"spec": {
"visualization": {
"stackedBarChart": false
},
"formatting": {
"unit": "By"
}
}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "SigNozBuilderQuery",
"spec": {
"name": "A",
"signal": "metrics",
"expression": "A",
"aggregations": [
{
"metricName": "http.server.response.body.size.sum",
"reduceTo": "sum",
"spaceAggregation": "sum",
"timeAggregation": "rate"
}
],
"filter": {
"expression": "http.response.status_code IN $statusCodesFromQuery"
},
"groupBy": [
{
"name": "service.name",
"fieldDataType": "string",
"fieldContext": "tag"
}
]
}
}
}
}
]
}
},
"e23516fc": {
"kind": "Panel",
"spec": {
"display": {
"name": "num traces for service"
},
"plugin": {
"kind": "SigNozNumberPanel",
"spec": {
"formatting": {
"unit": "none",
"decimalPrecision": 1
},
"thresholds": [
{
"value": 1200000,
"operator": ">",
"unit": "none",
"color": "Red",
"format": "Text"
},
{
"value": 1200000,
"operator": "<=",
"unit": "none",
"color": "Green",
"format": "Text"
}
]
}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "SigNozBuilderQuery",
"spec": {
"name": "A",
"signal": "traces",
"expression": "A",
"aggregations": [
{
"expression": "count() "
}
],
"filter": {
"expression": "service.name = $serviceName "
}
}
}
}
}
]
}
},
"130c8d6b": {
"kind": "Panel",
"spec": {
"display": {
"name": "num logs for service"
},
"plugin": {
"kind": "SigNozNumberPanel",
"spec": {
"formatting": {
"unit": "none",
"decimalPrecision": 1
}
}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "SigNozBuilderQuery",
"spec": {
"name": "A",
"signal": "logs",
"expression": "A",
"aggregations": [
{
"expression": "count() "
}
],
"filter": {
"expression": "service.name = $serviceName "
}
}
}
}
}
]
}
},
"246f7c6d": {
"kind": "Panel",
"spec": {
"display": {
"name": "num traces for service per resp code"
},
"plugin": {
"kind": "SigNozPieChartPanel",
"spec": {
"formatting": {
"decimalPrecision": 1
},
"legend": {
"customColors": {
"\"201\"": "#2bc051",
"\"400\"": "#cc462e",
"\"500\"": "#ff0000"
}
}
}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "SigNozBuilderQuery",
"spec": {
"name": "A",
"signal": "traces",
"expression": "A",
"aggregations": [
{
"expression": "count() "
}
],
"filter": {
"expression": "service.name = $serviceName isEntryPoint = 'true'"
},
"groupBy": [
{
"name": "http.response.status_code",
"fieldDataType": "float64",
"fieldContext": "tag"
}
],
"legend": "\"{{http.response.status_code}}\""
}
}
}
}
]
}
},
"21f7d4d0": {
"kind": "Panel",
"spec": {
"display": {
"name": "average latency per service"
},
"plugin": {
"kind": "SigNozTablePanel",
"spec": {
"formatting": {
"columnUnits": {
"A": "s"
}
},
"thresholds": [
{
"value": 1,
"operator": ">",
"unit": "min",
"color": "Red",
"format": "Text",
"tableOptions": "A"
}
]
}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "SigNozClickHouseSQL",
"spec": {
"name": "A",
"query": "WITH\n __spatial_aggregation_cte AS\n (\n SELECT\n toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(60)) AS ts,\n `service.name`,\n le,\n sum(value) / 60 AS value\n FROM signoz_metrics.distributed_samples_v4 AS points\n INNER JOIN\n (\n SELECT\n fingerprint,\n JSONExtractString(labels, 'service.name') AS `service.name`,\n JSONExtractString(labels, 'le') AS le\n FROM signoz_metrics.time_series_v4\n WHERE (metric_name IN ('signoz_latency.bucket')) AND (LOWER(temporality) LIKE LOWER('delta')) AND (__normalized = 0)\n GROUP BY\n fingerprint,\n `service.name`,\n le\n ) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint\n WHERE metric_name IN ('signoz_latency.bucket')\n GROUP BY\n ts,\n `service.name`,\n le\n ),\n __histogramCTE AS\n (\n SELECT\n ts,\n `service.name`,\n histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.9) AS value\n FROM __spatial_aggregation_cte\n GROUP BY\n `service.name`,\n ts\n ORDER BY\n `service.name` ASC,\n ts ASC\n )\nSELECT\n `service.name` AS service,\n avg(value) AS A\nFROM __histogramCTE\nGROUP BY `service.name`"
}
}
}
}
]
}
},
"ad5fd556": {
"kind": "Panel",
"spec": {
"display": {
"name": "logs from service"
},
"plugin": {
"kind": "SigNozListPanel",
"spec": {
"selectedLogFields": [
{
"name": "timestamp",
"type": "log",
"dataType": ""
},
{
"name": "body",
"type": "log",
"dataType": ""
},
{
"name": "error",
"type": "",
"dataType": "string"
}
]
}
},
"queries": [
{
"kind": "LogQuery",
"spec": {
"plugin": {
"kind": "SigNozBuilderQuery",
"spec": {
"name": "A",
"signal": "logs",
"expression": "A",
"aggregations": [
{
"expression": "count() "
}
],
"filter": {
"expression": "service.name = $serviceName"
},
"groupBy": [],
"order": [
{
"columnName": "timestamp",
"order": "desc"
},
{
"columnName": "id",
"order": "desc"
}
]
}
}
}
}
]
}
},
"f07b59ee": {
"kind": "Panel",
"spec": {
"display": {
"name": "response size buckets"
},
"plugin": {
"kind": "SigNozHistogramPanel",
"spec": {
"histogramBuckets": {
"bucketCount": 60,
"mergeAllActiveQueries": true
}
}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "SigNozBuilderQuery",
"spec": {
"name": "A",
"signal": "metrics",
"expression": "A",
"aggregations": [
{
"metricName": "http.server.response.body.size.bucket",
"reduceTo": "avg",
"spaceAggregation": "p90",
"timeAggregation": "rate"
}
]
}
}
}
}
]
}
},
"e1a41831": {
"kind": "Panel",
"spec": {
"display": {
"name": "trace operator",
"description": ""
},
"plugin": {
"kind": "SigNozTimeSeriesPanel",
"spec": {
"legend": {
"position": "right"
}
}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "SigNozCompositeQuery",
"spec": {
"queries": [
{
"type": "builder_query",
"spec": {
"name": "A",
"signal": "traces",
"expression": "A",
"aggregations": [
{
"expression": "count() "
}
],
"filter": {
"expression": "service.name = 'sampleapp-gateway' "
},
"legend": "Gateway"
}
},
{
"type": "builder_query",
"spec": {
"name": "B",
"signal": "traces",
"expression": "B",
"aggregations": [
{
"expression": "count() "
}
],
"filter": {
"expression": "http.response.status_code = 200"
},
"legend": "$serviceName"
}
},
{
"type": "builder_trace_operator",
"spec": {
"name": "T1",
"expression": "A -> B ",
"aggregations": [
{
"expression": "count()",
"alias": "request_count"
}
]
}
}
]
}
}
}
}
]
}
},
"f0d70491": {
"kind": "Panel",
"spec": {
"display": {
"name": "no results in this promql",
"description": ""
},
"plugin": {
"kind": "SigNozTimeSeriesPanel",
"spec": {}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "SigNozCompositeQuery",
"spec": {
"queries": [
{
"type": "promql",
"spec": {
"name": "A",
"query": "sum(rate(flask_exporter_info[5m]))"
}
},
{
"type": "promql",
"spec": {
"name": "B",
"query": "sum(increase(flask_exporter_info[5m]))"
}
}
]
}
}
}
}
]
}
},
"0e6eb4ca": {
"kind": "Panel",
"spec": {
"display": {
"name": "no results in this promql",
"description": ""
},
"plugin": {
"kind": "SigNozTimeSeriesPanel",
"spec": {}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "SigNozPromQLQuery",
"spec": {
"name": "A",
"query": "sum(rate(flask_exporter_info[5m]))"
}
}
}
}
]
}
}
},
"layouts": [
{
"kind": "Grid",
"spec": {
// ! where is row header?
// w: 12,
// minW: 12,
// minH: 1,
// maxH: 1,
// x: 0,
// h: 1,
// y: 0,
// These above properties are required for the headers only
// need collapsed state for a section
"items": [
{
// we need id here. could be injected from the frontend
"x": 0,
"y": 0,
"width": 6,
"height": 6,
// how easy would it be to introduce more here? like maxW, minW, static, etc. Do we want to introduce those from the start?
// "moved" might need experimenting
"content": {
"$ref": "#/spec/panels/24e2697b"
}
},
{
"x": 6,
"y": 0,
"width": 6,
"height": 6,
"content": {
"$ref": "#/spec/panels/ff2f72f1"
}
},
{
"x": 0,
"y": 6,
"width": 6,
"height": 6,
"content": {
"$ref": "#/spec/panels/011605e7"
}
},
{
"x": 6,
"y": 6,
"width": 6,
"height": 3,
"content": {
"$ref": "#/spec/panels/e23516fc"
}
},
{
"x": 6,
"y": 9,
"width": 6,
"height": 3,
"content": {
"$ref": "#/spec/panels/130c8d6b"
}
},
{
"x": 0,
"y": 12,
"width": 6,
"height": 6,
"content": {
"$ref": "#/spec/panels/246f7c6d"
}
},
{
"x": 6,
"y": 12,
"width": 6,
"height": 6,
"content": {
"$ref": "#/spec/panels/21f7d4d0"
}
},
{
"x": 0,
"y": 18,
"width": 6,
"height": 6,
"content": {
"$ref": "#/spec/panels/ad5fd556"
}
},
{
"x": 6,
"y": 18,
"width": 6,
"height": 6,
"content": {
"$ref": "#/spec/panels/f07b59ee"
}
},
{
"x": 0,
"y": 24,
"width": 12,
"height": 6,
"content": {
"$ref": "#/spec/panels/e1a41831"
}
},
{
"x": 0,
"y": 30,
"width": 6,
"height": 6,
"content": {
"$ref": "#/spec/panels/f0d70491"
}
},
{
"x": 6,
"y": 30,
"width": 6,
"height": 6,
"content": {
"$ref": "#/spec/panels/0e6eb4ca"
}
}
]
}
}
]
}
}

140
perses/howToDefineAPanel.md Normal file
View File

@@ -0,0 +1,140 @@
# Adding a new panel plugin
This guide is for developers adding a new panel kind to the Perses schema. It covers the file structure you need to create, the shared types available in `common.cue`, and how to compose them into your panel's spec.
---
## 1. File structure
Create a new directory under `schemas-wrapper/schemas/` named after your panel:
```
schemas-wrapper/schemas/your-panel-name/
├── your-panel-name.cue # Schema definition
└── example.json # Example JSON that validates against the schema
```
The CUE file must use `package model` and declare a `kind` and `spec`:
```cue
package model
import "github.com/signoz/common"
kind: "YourPanelName"
spec: close({
// your fields here
})
```
Register the panel in `schemas-wrapper/schemas/package.json` by adding an entry to the `plugins` array:
```json
{
"kind": "Panel",
"spec": {
"name": "YourPanelName"
}
}
```
---
## 2. Available shared types from `common.cue`
These types are defined in `common/common.cue` and can be used via `import "github.com/signoz/common"`. Use them instead of redefining the same structures in your panel.
## 3. Defining your spec
Your spec is a `close({})` block containing the fields relevant to your panel. Pick from the shared types above and add panel-specific types as needed. Every field should be optional (suffixed with `?`) since a panel should be valid with just defaults.
### Typical patterns
**Panel with graphs** (has axes, legend, thresholds as reference lines):
```cue
spec: close({
visualization?: #Visualization
formatting?: #Formatting
axes?: common.#Axes
legend?: #Legend
contextLinks?: [...common.#ContextLinkProps]
thresholds?: [...common.#ThresholdWithLabel]
})
```
**Panel with a single value** (no axes/legend, thresholds as conditional formatting):
```cue
spec: close({
visualization?: #Visualization
formatting?: #Formatting
contextLinks?: [...common.#ContextLinkProps]
thresholds?: [...common.#ComparisonThreshold]
})
```
**Panel with custom data structure** (panel-specific fields only):
```cue
spec: close({
yourCustomConfig?: #YourCustomConfig
contextLinks?: [...common.#ContextLinkProps]
})
```
### Defining panel-local types
Types that are specific to your panel (not reusable) should be defined in your CUE file, not in common. For example, `#Visualization` varies per panel because each panel has different rendering options:
```cue
#Visualization: {
timePreference?: common.#TimePreference
yourCustomFlag?: bool | *false
}
```
If your panel needs a threshold type that extends a common one, embed it:
```cue
#YourThreshold: {
common.#ComparisonThreshold
extraField: string
}
```
---
## 4. Writing the example JSON
Create an `example.json` that exercises all fields in your spec. This file is used for validation — `./validate.sh` will check it against your schema.
```json
{
"kind": "YourPanelName",
"spec": {
...
}
}
```
Also add at least one panel using your new kind to `examples/perses.json` so it gets validated as part of a full dashboard.
---
## 5. Validation
Run `./validate.sh` from the `perses/` directory. It lints `examples/perses.json` against all registered schemas. If your schema or example has issues, the error will point to the specific field.
---
## Quick reference: existing panels and what they use
| Field | Time-series | Bar-chart | Number | Pie | Table | Histogram | List |
|-------|:-----------:|:---------:|:------:|:---:|:-----:|:---------:|:----:|
| `visualization` | yes | yes | yes | yes | yes | — | — |
| `formatting` | yes | yes | yes | yes | yes | — | — |
| `axes` | yes | yes | — | — | — | — | — |
| `legend` | yes | yes | — | yes | — | yes | — |
| `contextLinks` | yes | yes | yes | yes | yes | yes | — |
| `thresholds` | yes | yes | yes | — | yes | — | — |

View File

@@ -0,0 +1,9 @@
{
"id": "signoz",
"name": "signoz",
"metaData": {
"buildInfo": {
"buildVersion": "0.0.1"
}
}
}

View File

@@ -0,0 +1,11 @@
package model
kind: "SigNozDatasource"
// SigNoz has a single built-in backend — the frontend already knows
// the API endpoint, so there is no connection config to validate.
// Add fields here if SigNoz ever supports multiple backends or
// configurable API versions.
spec: close({})
// this is required to override the default that always requires a spec

View File

@@ -0,0 +1,4 @@
{
"kind": "SigNozDatasource",
"spec": {}
}

View File

@@ -0,0 +1,26 @@
package model
// Fields from IBaseWidget that are NOT in any dedicated panel CUE file.
// This file exists as a reference only and will be removed in the end.
// Needs to be removed from package.json as well.
kind: "SigNozIgnoredFieldsChart"
spec: close({
opacity?: string // not wired to chart rendering
nullZeroValues?: string // not wired to chart rendering
stepSize?: number
columnWidths?: [string]: number // not a config choice — persisted user-resized column widths
// "Chart Appearance" section in UI — could not find this section in the app.
// This is behind a boolean flag right now. Can help reproduce locally
// These 4 fields are gated behind panelTypeVs* constants (TIME_SERIES only).
lineInterpolation?: #LineInterpolation
showPoints?: bool
lineStyle?: #LineStyle
fillMode?: #FillMode
})
#LineInterpolation: "linear" | "spline" | "stepAfter" | "stepBefore"
#LineStyle: "solid" | "dashed"
#FillMode: "solid" | "gradient" | "none"

View File

@@ -0,0 +1,17 @@
package model
import "github.com/signoz/common"
// Source: pkg/types/querybuildertypes/querybuildertypesv5/formula.go — QueryBuilderFormula
kind: "SigNozFormula"
spec: close({
name: common.#QueryName
expression: string
disabled?: bool | *false
legend?: string
limit?: common.#Limit
having?: common.#HavingExpression
stepInterval?: number
order?: [...common.#OrderByItem]
functions?: [...common.#Function]
})

View File

@@ -0,0 +1,8 @@
{
"kind": "SigNozFormula",
"spec": {
"name": "F1",
"expression": "A / B * 100",
"legend": "Hit rate %"
}
}

View File

@@ -0,0 +1,31 @@
package model
import "github.com/signoz/common"
// Source: pkg/types/querybuildertypes/querybuildertypesv5/trace_operator.go — QueryBuilderTraceOperator
// SigNozTraceOperator composes multiple trace BuilderQueries using
// relational operators (=>, ->, &&, ||, NOT) to query trace relationships.
// Signal is implicitly "traces" — all referenced queries must be trace queries.
kind: "SigNozTraceOperator"
spec: close({
name: common.#QueryName
// Operator expression composing trace queries, e.g. "A => B && C".
expression: string & !=""
disabled?: bool | *false
// Which query's spans to return (must be a query referenced in expression).
returnSpansFrom?: common.#QueryName
aggregations?: [...common.#ExpressionAggregation]
filter?: common.#FilterExpression
groupBy?: [...common.#GroupByItem]
order?: [...common.#OrderByItem]
limit?: common.#Limit
offset?: common.#Offset
cursor?: string
functions?: [...common.#Function]
stepInterval?: number
having?: common.#HavingExpression
legend?: string
selectFields?: [...common.#TelemetryFieldKey]
})

View File

@@ -0,0 +1,19 @@
{
"kind": "SigNozTraceOperator",
"spec": {
"name": "T1",
"expression": "A => B",
"returnSpansFrom": "A",
"aggregations": [
{
"expression": "count()",
"alias": "request_count"
}
],
"filter": {
"expression": "service.name = 'frontend'"
},
"groupBy": [],
"order": []
}
}

View File

@@ -0,0 +1,136 @@
{
"name": "@signoz/signoz-schemas",
"version": "0.0.1",
"description": "SigNoz schema plugins",
"perses": {
"schemasPath": ".",
"plugins": [
{
"kind": "Datasource",
"spec": {
"name": "SigNozDatasource"
}
},
{
"kind": "Panel",
"spec": {
"name": "SigNozTimeSeriesPanel"
}
},
{
"kind": "Panel",
"spec": {
"name": "SigNozBarChartPanel"
}
},
{
"kind": "Panel",
"spec": {
"name": "SigNozNumberPanel"
}
},
{
"kind": "Panel",
"spec": {
"name": "SigNozPieChartPanel"
}
},
{
"kind": "Panel",
"spec": {
"name": "SigNozTablePanel"
}
},
{
"kind": "Panel",
"spec": {
"name": "SigNozHistogramPanel"
}
},
{
"kind": "Panel",
"spec": {
"name": "SigNozListPanel"
}
},
{
"kind": "Panel",
"spec": {
"name": "SigNozIgnoredFieldsChart"
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"name": "SigNozBuilderQuery"
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"name": "SigNozClickHouseSQL"
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"name": "SigNozPromQLQuery"
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"name": "SigNozCompositeQuery"
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"name": "SigNozTraceOperator"
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"name": "SigNozFormula"
}
},
{
"kind": "LogQuery",
"spec": {
"name": "SigNozBuilderQuery"
}
},
{
"kind": "TraceQuery",
"spec": {
"name": "SigNozBuilderQuery"
}
},
{
"kind": "Variable",
"spec": {
"name": "SigNozCustomVariable"
}
},
{
"kind": "Variable",
"spec": {
"name": "SigNozDynamicVariable"
}
},
{
"kind": "Variable",
"spec": {
"name": "SigNozQueryVariable"
}
},
{
"kind": "Variable",
"spec": {
"name": "SigNozTextboxVariable"
}
}
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

View File

@@ -0,0 +1,20 @@
which panel type only caters to specific signals?
metrics has no option for list panel type. also, no other querying types
should the panel definition be linked to the query type?
| type | QB | P | CH |
| ----------- | --- | --- | --- |
| Time Series | ✅ | ✅ | ✅ |
| Number | ✅ | ✅ | ✅ |
| Table | ✅ | ❌ | ✅ |
| List | ✅ | ❌ | ❌ |
| Bar | ✅ | ✅ | ✅ |
| Pie | ✅ | ❌ | ✅ |
| Histogram | ✅ | ✅ | ✅ |
- unsure where this is used
stepSize - can be removed
renderColumnCell - used though - only table
customColumTitles - used though - only table
hiddenColumns - used though - only table

View File

@@ -0,0 +1,40 @@
{
"kind": "SigNozBarChartPanel",
"spec": {
"visualization": {
"timePreference": "globalTime",
"fillSpans": false,
"stackedBarChart": false
},
"formatting": {
"unit": "By",
"decimalPrecision": 2
},
"axes": {
"softMin": 0,
"softMax": 1000,
"isLogScale": false
},
"legend": {
"position": "bottom",
"customColors": {
"series-A": "#9ea5f7"
}
},
"contextLinks": [
{
"label": "View details",
"url": "http://localhost:8080/{{_service.name}}"
}
],
"thresholds": [
{
"value": 500,
"unit": "By",
"color": "Red",
"format": "Text",
"label": "upper limit"
}
]
}
}

View File

@@ -0,0 +1,31 @@
package model
import "github.com/signoz/common"
kind: "SigNozBarChartPanel"
spec: close({
// need to put more thought into this being optional?
visualization?: #Visualization
formatting?: #Formatting
axes?: common.#Axes
legend?: #Legend
contextLinks?: [...common.#ContextLinkProps]
thresholds?: [...common.#ThresholdWithLabel]
})
#Visualization: {
timePreference?: common.#TimePreference
fillSpans?: bool | *false
stackedBarChart?: bool | *true
}
#Formatting: {
unit?: string | *""
decimalPrecision?: common.#PrecisionOption
}
// can this be more composable?
#Legend: {
position?: common.#LegendPosition
customColors?: [string]: string
}

View File

@@ -0,0 +1,21 @@
{
"kind": "SigNozHistogramPanel",
"spec": {
"histogramBuckets": {
"bucketCount": 60,
"bucketWidth": 100,
"mergeAllActiveQueries": true
},
"legend": {
"customColors": {
"series-A": "#9ea5f7"
}
},
"contextLinks": [
{
"label": "View histogram details",
"url": "http://localhost:8080/histogram"
}
]
}
}

View File

@@ -0,0 +1,20 @@
package model
import "github.com/signoz/common"
kind: "SigNozHistogramPanel"
spec: close({
histogramBuckets?: #HistogramBuckets
legend?: #Legend
contextLinks?: [...common.#ContextLinkProps]
})
#HistogramBuckets: {
bucketCount?: number | *30
bucketWidth?: number | *0
mergeAllActiveQueries?: bool | *false
}
#Legend: {
customColors?: [string]: string
}

View File

@@ -0,0 +1,54 @@
{
"kind": "SigNozListPanel",
"spec": {
"selectedLogFields": [
{
"name": "timestamp",
"type": "log",
"dataType": ""
},
{
"name": "body",
"type": "log",
"dataType": ""
},
{
"name": "error",
"type": "",
"dataType": "string"
}
],
"selectedTracesFields": [
{
"name": "service.name",
"signal": "traces",
"fieldContext": "resource",
"fieldDataType": "string"
},
{
"name": "name",
"signal": "traces",
"fieldContext": "span",
"fieldDataType": "string"
},
{
"name": "duration_nano",
"signal": "traces",
"fieldContext": "span",
"fieldDataType": ""
},
{
"name": "http_method",
"signal": "traces",
"fieldContext": "span",
"fieldDataType": ""
},
{
"name": "response_status_code",
"signal": "traces",
"fieldContext": "span",
"fieldDataType": ""
}
]
}
}

View File

@@ -0,0 +1,15 @@
package model
import "github.com/signoz/common"
kind: "SigNozListPanel"
spec: close({
selectedLogFields?: [...#LogField]
selectedTracesFields?: [...common.#TelemetryFieldKey]
})
#LogField: {
name: string
type: string
dataType: string
}

View File

@@ -0,0 +1,34 @@
{
"kind": "SigNozNumberPanel",
"spec": {
"visualization": {
"timePreference": "globalTime"
},
"formatting": {
"unit": "none",
"decimalPrecision": 1
},
"contextLinks": [
{
"label": "View details",
"url": "http://localhost:8080/details"
}
],
"thresholds": [
{
"value": 1200000,
"operator": ">",
"unit": "none",
"color": "Red",
"format": "Text"
},
{
"value": 1200000,
"operator": "<=",
"unit": "none",
"color": "Green",
"format": "Text"
}
]
}
}

View File

@@ -0,0 +1,23 @@
package model
import "github.com/signoz/common"
kind: "SigNozNumberPanel"
spec: close({
visualization?: #Visualization
formatting?: #Formatting
contextLinks?: [...common.#ContextLinkProps]
thresholds?: [...common.#ComparisonThreshold]
})
#Visualization: {
timePreference?: common.#TimePreference
}
// where to store if alerts can be added or not?
#Formatting: {
// mainly need to discuss optional fields
unit?: string | *""
decimalPrecision?: common.#PrecisionOption
}

View File

@@ -0,0 +1,25 @@
{
"kind": "SigNozPieChartPanel",
"spec": {
"visualization": {
"timePreference": "globalTime"
},
"formatting": {
"unit": "none",
"decimalPrecision": 1
},
"legend": {
"customColors": {
"\"201\"": "#2bc051",
"\"400\"": "#cc462e",
"\"500\"": "#ff0000"
}
},
"contextLinks": [
{
"label": "View breakdown",
"url": "http://localhost:8080/breakdown"
}
]
}
}

View File

@@ -0,0 +1,24 @@
package model
import "github.com/signoz/common"
kind: "SigNozPieChartPanel"
spec: close({
visualization?: #Visualization
formatting?: #Formatting
legend?: #Legend
contextLinks?: [...common.#ContextLinkProps]
})
#Visualization: {
timePreference?: common.#TimePreference
}
#Formatting: {
unit?: string | *""
decimalPrecision?: common.#PrecisionOption
}
#Legend: {
customColors?: [string]: string
}

View File

@@ -0,0 +1,30 @@
{
"kind": "SigNozTablePanel",
"spec": {
"visualization": {
"timePreference": "globalTime"
},
"formatting": {
"columnUnits": {
"A": "s"
},
"decimalPrecision": 2
},
"contextLinks": [
{
"label": "View trace",
"url": "http://localhost:8080/trace/{{traceID}}"
}
],
"thresholds": [
{
"value": 1,
"operator": ">",
"unit": "min",
"color": "Red",
"format": "Text",
"tableOptions": "A"
}
]
}
}

View File

@@ -0,0 +1,31 @@
package model
import "github.com/signoz/common"
kind: "SigNozTablePanel"
spec: close({
visualization?: #Visualization
formatting?: #Formatting
contextLinks?: [...common.#ContextLinkProps]
thresholds?: [...#TableThreshold]
})
#Visualization: {
timePreference?: common.#TimePreference
}
#Formatting: {
columnUnits?: [string]: string
decimalPrecision?: common.#PrecisionOption
}
#TableThreshold: {
common.#ComparisonThreshold
tableOptions: string
}
// missing
columnWidths
customColumnTitles
hiddenColumns
renderColumnCell - can be FE

View File

@@ -0,0 +1,46 @@
{
"kind": "SigNozTimeSeriesPanel",
"spec": {
"visualization": {
"timePreference": "globalTime",
"fillSpans": true
},
"formatting": {
"unit": "By",
"decimalPrecision": 3
},
"axes": {
"softMin": 0,
"softMax": 800,
"isLogScale": true
},
"legend": {
"position": "right",
"customColors": {
"{service.name=\"sampleapp-gateway\"}": "#9ea5f7"
}
},
"contextLinks": [
{
"label": "View service details",
"url": "http://localhost:8080/{{_service.name}}?dfddf=%7B%7Blimit%7D%7D"
}
],
"thresholds": [
{
"value": 1024,
"unit": "By",
"color": "Red",
"format": "Text",
"label": "upper limit"
},
{
"value": 100,
"unit": "By",
"color": "Orange",
"format": "Text",
"label": "kinda bad"
}
]
}
}

View File

@@ -0,0 +1,36 @@
package model
import "github.com/signoz/common"
kind: "SigNozTimeSeriesPanel"
spec: close({
visualization?: #Visualization
formatting?: #Formatting
axes?: common.#Axes
legend?: #Legend
contextLinks?: [...common.#ContextLinkProps]
thresholds?: [...common.#ThresholdWithLabel]
})
#Visualization: {
timePreference?: common.#TimePreference
fillSpans?: bool | *false
}
#Formatting: {
unit?: string | *""
decimalPrecision?: common.#PrecisionOption
}
#Legend: {
position?: common.#LegendPosition
// why call it customColors?
customColors?: [string]: string
}
// chart appearance
- fill mode
- line style
- line interpolation
- show points
- span gaps

View File

@@ -0,0 +1,35 @@
package model
import "github.com/signoz/common"
// Source: pkg/types/querybuildertypes/querybuildertypesv5/builder_query.go — QueryBuilderQuery
kind: "SigNozBuilderQuery"
spec: close({
name: common.#QueryName
signal: "metrics" | "logs" | "traces"
expression: string
disabled?: bool | *false
aggregations?: [...common.#Aggregation]
filter?: common.#FilterExpression
groupBy?: [...common.#GroupByItem]
order?: [...common.#OrderByItem]
selectFields?: [...common.#TelemetryFieldKey]
limit?: common.#Limit
limitBy?: #LimitBy
offset?: common.#Offset
cursor?: string
having?: common.#HavingExpression
// secondaryAggregations not added — not yet implemented.
functions?: [...common.#Function]
legend?: string
stepInterval?: number
reduceTo?: common.#ReduceTo
pageSize?: int & >=1
source?: string
})
#LimitBy: close({
keys: [...string]
value: string
})

View File

@@ -0,0 +1,24 @@
{
"kind": "SigNozBuilderQuery",
"spec": {
"name": "A",
"signal": "metrics",
"expression": "A",
"aggregations": [
{
"metricName": "redis_keyspace_hits",
"timeAggregation": "rate",
"spaceAggregation": "sum",
"reduceTo": "sum"
}
],
"filter": {
"expression": "host_name IN $host_name"
},
"groupBy": [],
"order": [],
"disabled": false,
"legend": "Hit/s across all hosts",
"stepInterval": 60
}
}

View File

@@ -0,0 +1,13 @@
package model
import "github.com/signoz/common"
// Source: pkg/types/querybuildertypes/querybuildertypesv5/clickhouse_query.go — ClickHouseQuery
kind: "SigNozClickHouseSQL"
spec: close({
name: common.#QueryName
query: string & !=""
// required / optional
disabled?: bool | *false
legend?: string
})

View File

@@ -0,0 +1,9 @@
{
"kind": "SigNozClickHouseSQL",
"spec": {
"name": "A",
"query": "SELECT toStartOfInterval(timestamp, INTERVAL 1 MINUTE) AS ts, count() AS total FROM signoz_logs.distributed_logs GROUP BY ts ORDER BY ts",
"disabled": false,
"legend": "Log count"
}
}

View File

@@ -0,0 +1,47 @@
package model
import (
bq "github.com/signoz/schemas-wrapper/schemas/signoz-builder-query:model"
f "github.com/signoz/schemas-wrapper/schemas/signoz-formula:model"
to "github.com/signoz/schemas-wrapper/schemas/signoz-trace-operator:model"
pql "github.com/signoz/schemas-wrapper/schemas/signoz-promql:model"
ch "github.com/signoz/schemas-wrapper/schemas/signoz-clickhouse-sql:model"
)
// Source: pkg/types/querybuildertypes/querybuildertypesv5/req.go — CompositeQuery
// SigNozCompositeQuery groups multiple query plugins into a single
// query request. Each entry is a typed envelope whose spec is
// validated by the corresponding plugin schema.
// this is to be used when there are multiple queries in a panel
// in most cases, there will be only one query, and there it is a better idea to
// use the corresponding kind for that query instead of this composite query
kind: "SigNozCompositeQuery"
spec: close({
queries: [...#QueryEnvelope]
})
// QueryEnvelope wraps a single query plugin with a type discriminator.
// need to verify the types internally are marked required where necessary, so that we can make them optional here and not have to worry about it at the top level
#QueryEnvelope:
close({
type: "builder_query",
spec: bq.spec
}) |
close({
type: "builder_formula",
spec: f.spec
}) |
close({
type: "builder_trace_operator",
spec: to.spec
}) |
close({
type: "promql",
spec: pql.spec
}) |
close({
type: "clickhouse_sql",
spec: ch.spec
})

View File

@@ -0,0 +1,53 @@
{
"kind": "SigNozCompositeQuery",
"spec": {
"queries": [
{
"type": "builder_query",
"spec": {
"name": "A",
"signal": "metrics",
"expression": "A",
"aggregations": [
{
"metricName": "redis_keyspace_hits",
"timeAggregation": "rate",
"spaceAggregation": "sum",
"reduceTo": "sum"
}
],
"filter": {
"expression": "host_name IN $host_name"
}
}
},
{
"type": "builder_query",
"spec": {
"name": "B",
"signal": "metrics",
"expression": "B",
"aggregations": [
{
"metricName": "redis_keyspace_misses",
"timeAggregation": "rate",
"spaceAggregation": "sum",
"reduceTo": "sum"
}
],
"filter": {
"expression": "host_name IN $host_name"
}
}
},
{
"type": "builder_formula",
"spec": {
"name": "F1",
"expression": "A / (A + B) * 100",
"legend": "Hit rate %"
}
}
]
}
}

View File

@@ -0,0 +1,15 @@
package model
import "github.com/signoz/common"
// Source: pkg/types/querybuildertypes/querybuildertypesv5/prom_query.go — PromQuery
kind: "SigNozPromQLQuery"
spec: close({
name: common.#QueryName
query: string & !=""
disabled?: bool | *false
legend?: string
// where are the below items coming from? Don't see types for it. How to arrive at this?
step?: =~"^([0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$" | number
stats?: bool
})

View File

@@ -0,0 +1,9 @@
{
"kind": "SigNozPromQLQuery",
"spec": {
"name": "A",
"query": "rate(http_requests_total{status=\"200\"}[5m])",
"disabled": false,
"legend": "{{method}} {{path}}"
}
}

View File

@@ -0,0 +1 @@
creating folders based on type is helpful instead of a flat folder structure. It is how you would visualise the dashboard as components

View File

@@ -0,0 +1,10 @@
package model
import "github.com/signoz/common"
// defaultValue lives on the Perses ListVariable wrapper (spec level).
kind: "SigNozCustomVariable"
spec: close({
customValue: =~"^[^,]+(,[^,]+)*$"
sort?: common.#VariableSortOrder
})

View File

@@ -0,0 +1,7 @@
{
"kind": "SigNozCustomVariable",
"spec": {
"customValue": "production,staging,development",
"sort": "disabled"
}
}

View File

@@ -0,0 +1,12 @@
package model
import "github.com/signoz/common"
// defaultValue lives on the Perses ListVariable wrapper (spec level).
kind: "SigNozDynamicVariable"
spec: close({
name: string
source: string
sort?: common.#VariableSortOrder
// where does attribute go here?
})

View File

@@ -0,0 +1,8 @@
{
"kind": "SigNozDynamicVariable",
"spec": {
"name": "host_name",
"source": "metrics",
"sort": "asc"
}
}

View File

@@ -0,0 +1,13 @@
package model
import "github.com/signoz/common"
// defaultValue lives on the Perses ListVariable wrapper (spec level).
kind: "SigNozQueryVariable"
spec: close({
queryValue: string
sort?: common.#VariableSortOrder
// This should not be optional
// what happens to the existing https://perses.dev/perses/docs/dac/go/variable/#sortingby inside the ListVariable spec? Do we move that to the plugin spec level?
// same for other list variable cues
})

View File

@@ -0,0 +1,7 @@
{
"kind": "SigNozQueryVariable",
"spec": {
"queryValue": "SELECT DISTINCT host_name FROM signoz_metrics.distributed_time_series_v4_1day WHERE metric_name = 'redis_cpu_time'",
"sort": "asc"
}
}

View File

@@ -0,0 +1,7 @@
package model
// Shouldn't the below line be TextVariable?
// are any of the variables inside this required? How would that validation work?
// defaultValue lives on the Perses ListVariable wrapper (spec level).
kind: "SigNozTextboxVariable"
spec: close({})

View File

@@ -0,0 +1,4 @@
{
"kind": "SigNozTextboxVariable",
"spec": {}
}

2
perses/validate.sh Executable file
View File

@@ -0,0 +1,2 @@
percli lint -f ./examples/perses.json --plugin.path ./schemas-wrapper
rm ./schemas-wrapper/plugin-modules.json

View File

@@ -111,8 +111,8 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/user", handler.New(provider.authZ.AdminAccess(provider.userHandler.ListUsersDeprecated), handler.OpenAPIDef{
ID: "ListUsersDeprecated",
if err := router.Handle("/api/v1/user", handler.New(provider.authZ.AdminAccess(provider.userHandler.ListUsers), handler.OpenAPIDef{
ID: "ListUsers",
Tags: []string{"users"},
Summary: "List users",
Description: "This endpoint lists all users",
@@ -128,25 +128,8 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/users", handler.New(provider.authZ.AdminAccess(provider.userHandler.ListUsers), handler.OpenAPIDef{
ID: "ListUsers",
Tags: []string{"users"},
Summary: "List users v2",
Description: "This endpoint lists all users for the organization",
Request: nil,
RequestContentType: "",
Response: make([]*types.User, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/user/me", handler.New(provider.authZ.OpenAccess(provider.userHandler.GetMyUserDeprecated), handler.OpenAPIDef{
ID: "GetMyUserDeprecated",
if err := router.Handle("/api/v1/user/me", handler.New(provider.authZ.OpenAccess(provider.userHandler.GetMyUser), handler.OpenAPIDef{
ID: "GetMyUser",
Tags: []string{"users"},
Summary: "Get my user",
Description: "This endpoint returns the user I belong to",
@@ -162,42 +145,8 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/users/me", handler.New(provider.authZ.OpenAccess(provider.userHandler.GetMyUser), handler.OpenAPIDef{
ID: "GetMyUser",
Tags: []string{"users"},
Summary: "Get my user v2",
Description: "This endpoint returns the user I belong to",
Request: nil,
RequestContentType: "",
Response: new(authtypes.UserWithRoles),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{{Name: authtypes.IdentNProviderTokenizer.StringValue()}},
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/users/me", handler.New(provider.authZ.OpenAccess(provider.userHandler.UpdateMyUser), handler.OpenAPIDef{
ID: "UpdateMyUserV2",
Tags: []string{"users"},
Summary: "Update my user v2",
Description: "This endpoint updates the user I belong to",
Request: new(types.UpdatableUser),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{{Name: authtypes.IdentNProviderTokenizer.StringValue()}},
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/user/{id}", handler.New(provider.authZ.SelfAccess(provider.userHandler.GetUserDeprecated), handler.OpenAPIDef{
ID: "GetUserDeprecated",
if err := router.Handle("/api/v1/user/{id}", handler.New(provider.authZ.SelfAccess(provider.userHandler.GetUser), handler.OpenAPIDef{
ID: "GetUser",
Tags: []string{"users"},
Summary: "Get user",
Description: "This endpoint returns the user by id",
@@ -213,25 +162,8 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/users/{id}", handler.New(provider.authZ.AdminAccess(provider.userHandler.GetUser), handler.OpenAPIDef{
ID: "GetUser",
Tags: []string{"users"},
Summary: "Get user by user id",
Description: "This endpoint returns the user by id",
Request: nil,
RequestContentType: "",
Response: new(authtypes.UserWithRoles),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/user/{id}", handler.New(provider.authZ.SelfAccess(provider.userHandler.UpdateUserDeprecated), handler.OpenAPIDef{
ID: "UpdateUserDeprecated",
if err := router.Handle("/api/v1/user/{id}", handler.New(provider.authZ.SelfAccess(provider.userHandler.UpdateUser), handler.OpenAPIDef{
ID: "UpdateUser",
Tags: []string{"users"},
Summary: "Update user",
Description: "This endpoint updates the user by id",
@@ -247,23 +179,6 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/users/{id}", handler.New(provider.authZ.AdminAccess(provider.userHandler.UpdateUser), handler.OpenAPIDef{
ID: "UpdateUser",
Tags: []string{"users"},
Summary: "Update user v2",
Description: "This endpoint updates the user by id",
Request: new(types.UpdatableUser),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/user/{id}", handler.New(provider.authZ.AdminAccess(provider.userHandler.DeleteUser), handler.OpenAPIDef{
ID: "DeleteUser",
Tags: []string{"users"},
@@ -349,73 +264,5 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/users/{id}/roles", handler.New(provider.authZ.AdminAccess(provider.userHandler.GetRolesByUserID), handler.OpenAPIDef{
ID: "GetRolesByUserID",
Tags: []string{"users"},
Summary: "Get user roles",
Description: "This endpoint returns the user roles by user id",
Request: nil,
RequestContentType: "",
Response: make([]*authtypes.Role, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/users/{id}/roles", handler.New(provider.authZ.AdminAccess(provider.userHandler.SetRoleByUserID), handler.OpenAPIDef{
ID: "SetRoleByUserID",
Tags: []string{"users"},
Summary: "Set user roles",
Description: "This endpoint assigns the role to the user roles by user id",
Request: new(types.PostableRole),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/users/{id}/roles/{roleId}", handler.New(provider.authZ.AdminAccess(provider.userHandler.RemoveUserRoleByRoleID), handler.OpenAPIDef{
ID: "RemoveUserRoleByUserIDAndRoleID",
Tags: []string{"users"},
Summary: "Remove a role from user",
Description: "This endpoint removes a role from the user by user id and role id",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/roles/{id}/users", handler.New(provider.authZ.AdminAccess(provider.userHandler.GetUsersByRoleID), handler.OpenAPIDef{
ID: "GetUsersByRoleID",
Tags: []string{"users"},
Summary: "Get users by role id",
Description: "This endpoint returns the users having the role by role id",
Request: nil,
RequestContentType: "",
Response: make([]*types.User, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -160,7 +160,7 @@ func (module *module) CreateCallbackAuthNSession(ctx context.Context, authNProvi
return "", errors.WithAdditionalf(err, "root user can only authenticate via password")
}
userRoles, err := module.userGetter.GetRolesByUserID(ctx, newUser.ID)
userRoles, err := module.userGetter.GetUserRoles(ctx, newUser.ID)
if err != nil {
return "", err
}

View File

@@ -37,7 +37,7 @@ func (module *getter) GetRootUserByOrgID(ctx context.Context, orgID valuer.UUID)
return rootUser, userRoles, nil
}
func (module *getter) ListDeprecatedUsersByOrgID(ctx context.Context, orgID valuer.UUID) ([]*types.DeprecatedUser, error) {
func (module *getter) ListByOrgID(ctx context.Context, orgID valuer.UUID) ([]*types.DeprecatedUser, error) {
users, err := module.store.ListUsersByOrgID(ctx, orgID)
if err != nil {
return nil, err
@@ -84,30 +84,13 @@ func (module *getter) ListDeprecatedUsersByOrgID(ctx context.Context, orgID valu
return deprecatedUsers, nil
}
func (module *getter) ListUsersByOrgID(ctx context.Context, orgID valuer.UUID) ([]*types.User, error) {
users, err := module.store.ListUsersByOrgID(ctx, orgID)
if err != nil {
return nil, err
}
// filter root users if feature flag `hide_root_users` is true
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
hideRootUsers := module.flagger.BooleanOrEmpty(ctx, flagger.FeatureHideRootUser, evalCtx)
if hideRootUsers {
users = slices.DeleteFunc(users, func(user *types.User) bool { return user.IsRoot })
}
return users, nil
}
func (module *getter) GetDeprecatedUserByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*types.DeprecatedUser, error) {
user, err := module.store.GetByOrgIDAndID(ctx, orgID, id)
if err != nil {
return nil, err
}
userRoles, err := module.GetRolesByUserID(ctx, id)
userRoles, err := module.GetUserRoles(ctx, id)
if err != nil {
return nil, err
}
@@ -116,26 +99,18 @@ func (module *getter) GetDeprecatedUserByOrgIDAndID(ctx context.Context, orgID v
return nil, errors.New(errors.TypeUnexpected, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found")
}
if userRoles[0].Role == nil {
return nil, errors.New(errors.TypeUnexpected, authtypes.ErrCodeRoleNotFound, "role not found for user role entry")
}
role := authtypes.SigNozManagedRoleToExistingLegacyRole[userRoles[0].Role.Name]
return types.NewDeprecatedUserFromUserAndRole(user, role), nil
}
func (module *getter) GetUserByOrgIDAndID(ctx context.Context, orgID valuer.UUID, userID valuer.UUID) (*types.User, error) {
return module.store.GetByOrgIDAndID(ctx, orgID, userID)
}
func (module *getter) Get(ctx context.Context, id valuer.UUID) (*types.DeprecatedUser, error) {
user, err := module.store.GetUser(ctx, id)
if err != nil {
return nil, err
}
userRoles, err := module.GetRolesByUserID(ctx, id)
userRoles, err := module.GetUserRoles(ctx, id)
if err != nil {
return nil, err
}
@@ -144,10 +119,6 @@ func (module *getter) Get(ctx context.Context, id valuer.UUID) (*types.Deprecate
return nil, errors.New(errors.TypeUnexpected, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found")
}
if userRoles[0].Role == nil {
return nil, errors.New(errors.TypeUnexpected, authtypes.ErrCodeRoleNotFound, "role not found for user role entry")
}
role := authtypes.SigNozManagedRoleToExistingLegacyRole[userRoles[0].Role.Name]
return types.NewDeprecatedUserFromUserAndRole(user, role), nil
@@ -203,21 +174,11 @@ func (module *getter) GetNonDeletedUserByEmailAndOrgID(ctx context.Context, emai
}
func (module *getter) GetRolesByUserID(ctx context.Context, userID valuer.UUID) ([]*authtypes.UserRole, error) {
func (module *getter) GetUserRoles(ctx context.Context, userID valuer.UUID) ([]*authtypes.UserRole, error) {
userRoles, err := module.userRoleStore.GetUserRolesByUserID(ctx, userID)
if err != nil {
return nil, err
}
for _, ur := range userRoles {
if ur.Role == nil {
return nil, errors.New(errors.TypeUnexpected, authtypes.ErrCodeRoleNotFound, "role not found for user role entry")
}
}
return userRoles, nil
}
func (module *getter) GetUsersByOrgIDAndRoleID(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID) ([]*types.User, error) {
return module.store.GetUsersByOrgIDAndRoleID(ctx, orgID, roleID)
}

View File

@@ -85,7 +85,7 @@ func (h *handler) CreateBulkInvite(rw http.ResponseWriter, r *http.Request) {
render.Success(rw, http.StatusCreated, nil)
}
func (h *handler) GetUserDeprecated(w http.ResponseWriter, r *http.Request) {
func (h *handler) GetUser(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
@@ -106,39 +106,7 @@ func (h *handler) GetUserDeprecated(w http.ResponseWriter, r *http.Request) {
render.Success(w, http.StatusOK, user)
}
func (h *handler) GetUser(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
userID := mux.Vars(r)["id"]
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(w, err)
return
}
user, err := h.getter.GetUserByOrgIDAndID(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(userID))
if err != nil {
render.Error(w, err)
return
}
userRoles, err := h.getter.GetRolesByUserID(ctx, user.ID)
if err != nil {
render.Error(w, err)
return
}
userWithRoles := &authtypes.UserWithRoles{
User: user,
UserRoles: userRoles,
}
render.Success(w, http.StatusOK, userWithRoles)
}
func (h *handler) GetMyUserDeprecated(w http.ResponseWriter, r *http.Request) {
func (h *handler) GetMyUser(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
@@ -157,80 +125,6 @@ func (h *handler) GetMyUserDeprecated(w http.ResponseWriter, r *http.Request) {
render.Success(w, http.StatusOK, user)
}
func (h *handler) GetMyUser(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(w, err)
return
}
user, err := h.getter.GetUserByOrgIDAndID(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.UserID))
if err != nil {
render.Error(w, err)
return
}
userRoles, err := h.getter.GetRolesByUserID(ctx, user.ID)
if err != nil {
render.Error(w, err)
return
}
userWithRoles := &authtypes.UserWithRoles{
User: user,
UserRoles: userRoles,
}
render.Success(w, http.StatusOK, userWithRoles)
}
func (h *handler) UpdateMyUser(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(w, err)
return
}
updatableUser := new(types.UpdatableUser)
if err := json.NewDecoder(r.Body).Decode(&updatableUser); err != nil {
render.Error(w, err)
return
}
_, err = h.setter.UpdateUser(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.UserID), updatableUser)
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusNoContent, nil)
}
func (h *handler) ListUsersDeprecated(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(w, err)
return
}
users, err := h.getter.ListDeprecatedUsersByOrgID(ctx, valuer.MustNewUUID(claims.OrgID))
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, users)
}
func (h *handler) ListUsers(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
@@ -241,7 +135,7 @@ func (h *handler) ListUsers(w http.ResponseWriter, r *http.Request) {
return
}
users, err := h.getter.ListUsersByOrgID(ctx, valuer.MustNewUUID(claims.OrgID))
users, err := h.getter.ListByOrgID(ctx, valuer.MustNewUUID(claims.OrgID))
if err != nil {
render.Error(w, err)
return
@@ -250,7 +144,7 @@ func (h *handler) ListUsers(w http.ResponseWriter, r *http.Request) {
render.Success(w, http.StatusOK, users)
}
func (h *handler) UpdateUserDeprecated(w http.ResponseWriter, r *http.Request) {
func (h *handler) UpdateUser(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
@@ -268,7 +162,7 @@ func (h *handler) UpdateUserDeprecated(w http.ResponseWriter, r *http.Request) {
return
}
updatedUser, err := h.setter.UpdateUserDeprecated(ctx, valuer.MustNewUUID(claims.OrgID), id, &user, claims.UserID)
updatedUser, err := h.setter.UpdateUser(ctx, valuer.MustNewUUID(claims.OrgID), id, &user, claims.UserID)
if err != nil {
render.Error(w, err)
return
@@ -277,38 +171,6 @@ func (h *handler) UpdateUserDeprecated(w http.ResponseWriter, r *http.Request) {
render.Success(w, http.StatusOK, updatedUser)
}
func (h *handler) UpdateUser(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
userID := mux.Vars(r)["id"]
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(w, err)
return
}
if userID == claims.UserID {
render.Error(w, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "users cannot call this api on self"))
return
}
updatableUser := new(types.UpdatableUser)
if err := json.NewDecoder(r.Body).Decode(&updatableUser); err != nil {
render.Error(w, err)
return
}
_, err = h.setter.UpdateUser(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(userID), updatableUser)
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusNoContent, nil)
}
func (h *handler) DeleteUser(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
@@ -581,118 +443,3 @@ func (h *handler) RevokeAPIKey(w http.ResponseWriter, r *http.Request) {
render.Success(w, http.StatusNoContent, nil)
}
func (h *handler) GetRolesByUserID(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
userID := mux.Vars(r)["id"]
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(w, err)
return
}
user, err := h.getter.GetUserByOrgIDAndID(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(userID))
if err != nil {
render.Error(w, err)
return
}
userRoles, err := h.getter.GetRolesByUserID(ctx, user.ID)
if err != nil {
render.Error(w, err)
return
}
roles := make([]*authtypes.Role, len(userRoles))
for idx, userRole := range userRoles {
roles[idx] = authtypes.NewRoleFromStorableRole(userRole.Role)
}
render.Success(w, http.StatusOK, roles)
}
func (h *handler) SetRoleByUserID(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
userID := mux.Vars(r)["id"]
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(w, err)
return
}
if userID == claims.UserID {
render.Error(w, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "users cannot call this api on self"))
return
}
postableRole := new(types.PostableRole)
if err := json.NewDecoder(r.Body).Decode(postableRole); err != nil {
render.Error(w, err)
return
}
if postableRole.Name == "" {
render.Error(w, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "role name is required"))
return
}
if err := h.setter.AddUserRole(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(userID), postableRole.Name); err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, nil)
}
func (h *handler) RemoveUserRoleByRoleID(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
userID := mux.Vars(r)["id"]
roleID := mux.Vars(r)["roleId"]
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(w, err)
return
}
if userID == claims.UserID {
render.Error(w, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "users cannot call this api on self"))
return
}
if err := h.setter.RemoveUserRole(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(userID), valuer.MustNewUUID(roleID)); err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusNoContent, nil)
}
func (h *handler) GetUsersByRoleID(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
roleID := mux.Vars(r)["id"]
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(w, err)
return
}
users, err := h.getter.GetUsersByOrgIDAndRoleID(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(roleID))
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, users)
}

View File

@@ -133,7 +133,7 @@ func (s *service) createOrPromoteRootUser(ctx context.Context, orgID valuer.UUID
}
if existingUser != nil {
userRoles, err := s.getter.GetRolesByUserID(ctx, existingUser.ID)
userRoles, err := s.getter.GetUserRoles(ctx, existingUser.ID)
if err != nil {
return err
}
@@ -156,7 +156,9 @@ func (s *service) createOrPromoteRootUser(ctx context.Context, orgID valuer.UUID
existingUser.PromoteToRoot()
err = s.store.RunInTx(ctx, func(ctx context.Context) error {
if err := s.setter.UpdateAnyUser(ctx, orgID, existingUser); err != nil {
// update users table
deprecatedUser := types.NewDeprecatedUserFromUserAndRole(existingUser, types.RoleAdmin)
if err := s.setter.UpdateAnyUser(ctx, orgID, deprecatedUser); err != nil {
return err
}
@@ -199,7 +201,8 @@ func (s *service) updateExistingRootUser(ctx context.Context, orgID valuer.UUID,
if existingRoot.Email != s.config.Email {
existingRoot.UpdateEmail(s.config.Email)
if err := s.setter.UpdateAnyUser(ctx, orgID, existingRoot); err != nil {
deprecatedUser := types.NewDeprecatedUserFromUserAndRole(existingRoot, types.RoleAdmin)
if err := s.setter.UpdateAnyUser(ctx, orgID, deprecatedUser); err != nil {
return err
}
}

View File

@@ -220,7 +220,7 @@ func (module *setter) CreateUser(ctx context.Context, user *types.User, opts ...
return nil
}
func (module *setter) UpdateUserDeprecated(ctx context.Context, orgID valuer.UUID, id string, user *types.DeprecatedUser, updatedBy string) (*types.DeprecatedUser, error) {
func (module *setter) UpdateUser(ctx context.Context, orgID valuer.UUID, id string, user *types.DeprecatedUser, updatedBy string) (*types.DeprecatedUser, error) {
existingUser, err := module.getter.GetDeprecatedUserByOrgIDAndID(ctx, orgID, valuer.MustNewUUID(id))
if err != nil {
return nil, err
@@ -265,7 +265,7 @@ func (module *setter) UpdateUserDeprecated(ctx context.Context, orgID valuer.UUI
existingUser.Update(user.DisplayName, user.Role)
// update the user - idempotent (this does analytics too so keeping it outside txn)
if err := module.UpdateAnyUserDeprecated(ctx, orgID, existingUser); err != nil {
if err := module.UpdateAnyUser(ctx, orgID, existingUser); err != nil {
return nil, err
}
@@ -291,46 +291,7 @@ func (module *setter) UpdateUserDeprecated(ctx context.Context, orgID valuer.UUI
return existingUser, nil
}
func (module *setter) UpdateUser(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, updatable *types.UpdatableUser) (*types.User, error) {
existingUser, err := module.getter.GetUserByOrgIDAndID(ctx, orgID, userID)
if err != nil {
return nil, err
}
if err := existingUser.ErrIfRoot(); err != nil {
return nil, errors.WithAdditionalf(err, "cannot update root user")
}
if err := existingUser.ErrIfDeleted(); err != nil {
return nil, errors.WithAdditionalf(err, "cannot update deleted user")
}
existingUser.Update(updatable.DisplayName)
if err := module.UpdateAnyUser(ctx, orgID, existingUser); err != nil {
return nil, err
}
return existingUser, nil
}
func (module *setter) UpdateAnyUser(ctx context.Context, orgID valuer.UUID, user *types.User) error {
if err := module.store.UpdateUser(ctx, orgID, user); err != nil {
return err
}
if err := module.tokenizer.DeleteIdentity(ctx, user.ID); err != nil {
return err
}
// stats collector things
traits := types.NewTraitsFromUser(user)
module.analytics.IdentifyUser(ctx, user.OrgID.String(), user.ID.String(), traits)
module.analytics.TrackUser(ctx, user.OrgID.String(), user.ID.String(), "User Updated", traits)
return nil
}
func (module *setter) UpdateAnyUserDeprecated(ctx context.Context, orgID valuer.UUID, deprecateUser *types.DeprecatedUser) error {
func (module *setter) UpdateAnyUser(ctx context.Context, orgID valuer.UUID, deprecateUser *types.DeprecatedUser) error {
user := types.NewUserFromDeprecatedUser(deprecateUser)
if err := module.store.UpdateUser(ctx, orgID, user); err != nil {
return err
@@ -374,7 +335,7 @@ func (module *setter) DeleteUser(ctx context.Context, orgID valuer.UUID, id stri
return errors.New(errors.TypeForbidden, errors.CodeForbidden, "cannot self delete")
}
userRoles, err := module.getter.GetRolesByUserID(ctx, user.ID)
userRoles, err := module.getter.GetUserRoles(ctx, user.ID)
if err != nil {
return err
}
@@ -552,7 +513,7 @@ func (module *setter) UpdatePasswordByResetPasswordToken(ctx context.Context, to
return err
}
userRoles, err := module.getter.GetRolesByUserID(ctx, user.ID)
userRoles, err := module.getter.GetUserRoles(ctx, user.ID)
if err != nil {
return err
}
@@ -840,121 +801,16 @@ func (module *setter) activatePendingUser(ctx context.Context, user *types.User,
func (module *setter) UpdateUserRoles(ctx context.Context, orgID, userID valuer.UUID, finalRoleNames []string) error {
return module.store.RunInTx(ctx, func(ctx context.Context) error {
// delete old user_role entries
// delete old user_role entries and create new ones from SSO
if err := module.userRoleStore.DeleteUserRoles(ctx, userID); err != nil {
return err
}
// create fresh ones only if there are roles to assign
if len(finalRoleNames) > 0 {
return module.createUserRoleEntries(ctx, orgID, userID, finalRoleNames)
}
return nil
// create fresh ones
return module.createUserRoleEntries(ctx, orgID, userID, finalRoleNames)
})
}
func (module *setter) AddUserRole(ctx context.Context, orgID, userID valuer.UUID, roleName string) error {
existingUser, err := module.getter.GetUserByOrgIDAndID(ctx, orgID, userID)
if err != nil {
return err
}
if err := existingUser.ErrIfRoot(); err != nil {
return errors.WithAdditionalf(err, "cannot add role for root user")
}
if err := existingUser.ErrIfDeleted(); err != nil {
return errors.WithAdditionalf(err, "cannot add role for deleted user")
}
// validate that the role name exists
foundRoles, err := module.authz.ListByOrgIDAndNames(ctx, orgID, []string{roleName})
if err != nil {
return err
}
if len(foundRoles) != 1 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "role name not found: %s", roleName)
}
// check if user already has this role
existingUserRoles, err := module.getter.GetRolesByUserID(ctx, existingUser.ID)
if err != nil {
return err
}
for _, userRole := range existingUserRoles {
if userRole.Role != nil && userRole.Role.Name == roleName {
return nil // role already assigned no-op
}
}
// grant via authz (idempotent)
if err := module.authz.Grant(
ctx,
orgID,
[]string{roleName},
authtypes.MustNewSubject(authtypes.TypeableUser, existingUser.ID.StringValue(), existingUser.OrgID, nil),
); err != nil {
return err
}
// create user_role entry
userRoles := authtypes.NewUserRoles(userID, foundRoles)
if err := module.userRoleStore.CreateUserRoles(ctx, userRoles); err != nil {
return err
}
return module.tokenizer.DeleteIdentity(ctx, userID)
}
func (module *setter) RemoveUserRole(ctx context.Context, orgID, userID valuer.UUID, roleID valuer.UUID) error {
existingUser, err := module.getter.GetUserByOrgIDAndID(ctx, orgID, userID)
if err != nil {
return err
}
if err := existingUser.ErrIfRoot(); err != nil {
return errors.WithAdditionalf(err, "cannot remove role for root user")
}
if err := existingUser.ErrIfDeleted(); err != nil {
return errors.WithAdditionalf(err, "cannot remove role for deleted user")
}
// resolve role name for authz revoke
existingUserRoles, err := module.getter.GetRolesByUserID(ctx, existingUser.ID)
if err != nil {
return err
}
var roleName string
for _, ur := range existingUserRoles {
if ur.Role != nil && ur.RoleID == roleID {
roleName = ur.Role.Name
break
}
}
if roleName == "" {
return errors.Newf(errors.TypeNotFound, authtypes.ErrCodeUserRolesNotFound, "role %s not found for user %s", roleID, userID)
}
// revoke authz grant
if err := module.authz.Revoke(
ctx,
orgID,
[]string{roleName},
authtypes.MustNewSubject(authtypes.TypeableUser, existingUser.ID.StringValue(), existingUser.OrgID, nil),
); err != nil {
return err
}
if err := module.userRoleStore.DeleteUserRoleByUserIDAndRoleID(ctx, userID, roleID); err != nil {
return err
}
return module.tokenizer.DeleteIdentity(ctx, userID)
}
func roleNamesFromUserRoles(userRoles []*authtypes.UserRole) []string {
names := make([]string, 0, len(userRoles))
for _, ur := range userRoles {

View File

@@ -667,22 +667,3 @@ func (store *store) GetUsersByEmailsOrgIDAndStatuses(ctx context.Context, orgID
return users, nil
}
func (store *store) GetUsersByOrgIDAndRoleID(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID) ([]*types.User, error) {
users := []*types.User{}
err := store.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(&users).
Join(`JOIN user_role ON user_role.user_id = "users".id`).
Where(`"users".org_id = ?`, orgID).
Where("user_role.role_id = ?", roleID).
Scan(ctx)
if err != nil {
return nil, err
}
return users, nil
}

View File

@@ -65,21 +65,6 @@ func (store *userRoleStore) DeleteUserRoles(ctx context.Context, userID valuer.U
return nil
}
func (store *userRoleStore) DeleteUserRoleByUserIDAndRoleID(ctx context.Context, userID valuer.UUID, roleID valuer.UUID) error {
_, err := store.sqlstore.
BunDBCtx(ctx).
NewDelete().
Model(new(authtypes.UserRole)).
Where("user_id = ?", userID).
Where("role_id = ?", roleID).
Exec(ctx)
if err != nil {
return err
}
return nil
}
func (store *userRoleStore) GetUserRolesByUserID(ctx context.Context, userID valuer.UUID) ([]*authtypes.UserRole, error) {
userRoles := make([]*authtypes.UserRole, 0)

View File

@@ -34,12 +34,10 @@ type Setter interface {
// Initiate forgot password flow for a user
ForgotPassword(ctx context.Context, orgID valuer.UUID, email valuer.Email, frontendBaseURL string) error
UpdateUserDeprecated(ctx context.Context, orgID valuer.UUID, id string, user *types.DeprecatedUser, updatedBy string) (*types.DeprecatedUser, error)
UpdateUser(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, updatable *types.UpdatableUser) (*types.User, error)
UpdateUser(ctx context.Context, orgID valuer.UUID, id string, user *types.DeprecatedUser, updatedBy string) (*types.DeprecatedUser, error)
// UpdateAnyUser updates a user and persists the changes to the database along with the analytics and identity deletion.
UpdateAnyUserDeprecated(ctx context.Context, orgID valuer.UUID, deprecateUser *types.DeprecatedUser) error
UpdateAnyUser(ctx context.Context, orgID valuer.UUID, user *types.User) error
UpdateAnyUser(ctx context.Context, orgID valuer.UUID, user *types.DeprecatedUser) error
DeleteUser(ctx context.Context, orgID valuer.UUID, id string, deletedBy string) error
// invite
@@ -54,8 +52,6 @@ type Setter interface {
// Roles
UpdateUserRoles(ctx context.Context, orgID, userID valuer.UUID, finalRoleNames []string) error
AddUserRole(ctx context.Context, orgID, userID valuer.UUID, roleName string) error
RemoveUserRole(ctx context.Context, orgID, userID valuer.UUID, roleID valuer.UUID) error
statsreporter.StatsCollector
}
@@ -64,13 +60,11 @@ type Getter interface {
// Get root user by org id.
GetRootUserByOrgID(context.Context, valuer.UUID) (*types.User, []*authtypes.UserRole, error)
// Get gets the users based on the given org id
ListDeprecatedUsersByOrgID(context.Context, valuer.UUID) ([]*types.DeprecatedUser, error)
ListUsersByOrgID(ctx context.Context, orgID valuer.UUID) ([]*types.User, error)
// Get gets the users based on the given id
ListByOrgID(context.Context, valuer.UUID) ([]*types.DeprecatedUser, error)
// Get deprecated user object by orgID and id.
GetDeprecatedUserByOrgIDAndID(context.Context, valuer.UUID, valuer.UUID) (*types.DeprecatedUser, error)
GetUserByOrgIDAndID(ctx context.Context, orgID valuer.UUID, userID valuer.UUID) (*types.User, error)
// Get user by id.
Get(context.Context, valuer.UUID) (*types.DeprecatedUser, error)
@@ -91,10 +85,7 @@ type Getter interface {
GetNonDeletedUserByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) (*types.User, error)
// Gets user_role with roles entries from db
GetRolesByUserID(ctx context.Context, userID valuer.UUID) ([]*authtypes.UserRole, error)
// Gets all the user with role using role id in an org id
GetUsersByOrgIDAndRoleID(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID) ([]*types.User, error)
GetUserRoles(ctx context.Context, userID valuer.UUID) ([]*authtypes.UserRole, error)
}
type Handler interface {
@@ -102,21 +93,11 @@ type Handler interface {
CreateInvite(http.ResponseWriter, *http.Request)
CreateBulkInvite(http.ResponseWriter, *http.Request)
// users
ListUsersDeprecated(http.ResponseWriter, *http.Request)
ListUsers(http.ResponseWriter, *http.Request)
UpdateUserDeprecated(http.ResponseWriter, *http.Request)
UpdateUser(http.ResponseWriter, *http.Request)
DeleteUser(http.ResponseWriter, *http.Request)
GetUserDeprecated(http.ResponseWriter, *http.Request)
GetUser(http.ResponseWriter, *http.Request)
GetMyUserDeprecated(http.ResponseWriter, *http.Request)
GetMyUser(http.ResponseWriter, *http.Request)
UpdateMyUser(http.ResponseWriter, *http.Request)
GetRolesByUserID(http.ResponseWriter, *http.Request)
SetRoleByUserID(http.ResponseWriter, *http.Request)
RemoveUserRoleByRoleID(http.ResponseWriter, *http.Request)
GetUsersByRoleID(http.ResponseWriter, *http.Request)
// Reset Password
GetResetPasswordToken(http.ResponseWriter, *http.Request)

View File

@@ -1,7 +1,6 @@
package tracedetail
import (
"maps"
"slices"
"sort"
@@ -64,22 +63,26 @@ func findIndexForSelectedSpanFromPreOrder(spans []*model.Span, selectedSpanId st
return selectedSpanIndex
}
func getPathFromRootToSelectedSpanId(node *model.Span, selectedSpanId string) (bool, []string) {
func getPathFromRootToSelectedSpanId(node *model.Span, selectedSpanId string, uncollapsedSpans []string, isSelectedSpanIDUnCollapsed bool) (bool, []string) {
spansFromRootToNode := []string{}
spansFromRootToNode = append(spansFromRootToNode, node.SpanID)
if node.SpanID == selectedSpanId {
if isSelectedSpanIDUnCollapsed && !slices.Contains(uncollapsedSpans, node.SpanID) {
spansFromRootToNode = append(spansFromRootToNode, node.SpanID)
}
return true, spansFromRootToNode
}
isPresentInSubtreeForTheNode := false
for _, child := range node.Children {
isPresentInThisSubtree, _spansFromRootToNode := getPathFromRootToSelectedSpanId(child, selectedSpanId)
isPresentInThisSubtree, _spansFromRootToNode := getPathFromRootToSelectedSpanId(child, selectedSpanId, uncollapsedSpans, isSelectedSpanIDUnCollapsed)
// if the interested node is present in the given subtree then add the span node to uncollapsed node list
if isPresentInThisSubtree {
if !slices.Contains(uncollapsedSpans, node.SpanID) {
spansFromRootToNode = append(spansFromRootToNode, node.SpanID)
}
isPresentInSubtreeForTheNode = true
spansFromRootToNode = append(spansFromRootToNode, _spansFromRootToNode...)
break
}
}
return isPresentInSubtreeForTheNode, spansFromRootToNode
@@ -89,7 +92,7 @@ func getPathFromRootToSelectedSpanId(node *model.Span, selectedSpanId string) (b
// throughout the recursion. Per-call state (level, isPartOfPreOrder, etc.)
// is passed as direct arguments.
type traverseOpts struct {
uncollapsedSpans map[string]struct{}
uncollapsedSpans []string
selectedSpanID string
}
@@ -131,7 +134,7 @@ func traverseTrace(span *model.Span, opts traverseOpts, level uint64, isPartOfPr
preOrderTraversal = append(preOrderTraversal, &nodeWithoutChildren)
}
_, isAlreadyUncollapsed := opts.uncollapsedSpans[span.SpanID]
isAlreadyUncollapsed := slices.Contains(opts.uncollapsedSpans, span.SpanID)
for index, child := range span.Children {
_childTraversal := traverseTrace(child, opts, level+1, isPartOfPreOrder && isAlreadyUncollapsed, index != (len(span.Children)-1))
preOrderTraversal = append(preOrderTraversal, _childTraversal...)
@@ -166,28 +169,16 @@ func GetSelectedSpans(uncollapsedSpans []string, selectedSpanID string, traceRoo
var preOrderTraversal = make([]*model.Span, 0)
var rootServiceName, rootServiceEntryPoint string
// create a map of uncollapsed spans for quick lookup
uncollapsedSpanMap := make(map[string]struct{})
for _, spanID := range uncollapsedSpans {
uncollapsedSpanMap[spanID] = struct{}{}
}
updatedUncollapsedSpans := uncollapsedSpans
selectedSpanIndex := -1
for _, rootSpanID := range traceRoots {
if rootNode, exists := spanIdToSpanNodeMap[rootSpanID.SpanID]; exists {
present, spansFromRootToNode := getPathFromRootToSelectedSpanId(rootNode, selectedSpanID)
if present {
for _, spanID := range spansFromRootToNode {
if selectedSpanID == spanID && !isSelectedSpanIDUnCollapsed {
continue
}
uncollapsedSpanMap[spanID] = struct{}{}
}
}
_, spansFromRootToNode := getPathFromRootToSelectedSpanId(rootNode, selectedSpanID, updatedUncollapsedSpans, isSelectedSpanIDUnCollapsed)
updatedUncollapsedSpans = append(updatedUncollapsedSpans, spansFromRootToNode...)
opts := traverseOpts{
uncollapsedSpans: uncollapsedSpanMap,
uncollapsedSpans: updatedUncollapsedSpans,
selectedSpanID: selectedSpanID,
}
_preOrderTraversal := traverseTrace(rootNode, opts, 0, true, false)
@@ -232,5 +223,5 @@ func GetSelectedSpans(uncollapsedSpans []string, selectedSpanID string, traceRoo
startIndex = 0
}
return preOrderTraversal[startIndex:endIndex], slices.Collect(maps.Keys(uncollapsedSpanMap)), rootServiceName, rootServiceEntryPoint
return preOrderTraversal[startIndex:endIndex], updatedUncollapsedSpans, rootServiceName, rootServiceEntryPoint
}

View File

@@ -183,7 +183,7 @@ func TestGetSelectedSpans_PathReturnedInUncollapsed(t *testing.T) {
spanMap := buildSpanMap(root)
spans, uncollapsed, _, _ := GetSelectedSpans([]string{}, "selected", []*model.Span{root}, spanMap, false)
assert.ElementsMatch(t, []string{"root", "parent"}, uncollapsed)
assert.Equal(t, []string{"root", "parent"}, uncollapsed)
assert.Equal(t, []string{"root", "parent", "selected"}, spanIDs(spans))
}
@@ -206,7 +206,7 @@ func TestGetSelectedSpans_SiblingsNotExpanded(t *testing.T) {
// children of root sort alphabetically: parent < unrelated; unrelated-child stays hidden
assert.Equal(t, []string{"root", "parent", "selected", "unrelated"}, spanIDs(spans))
// only the path nodes are tracked as uncollapsed — unrelated is not
assert.ElementsMatch(t, []string{"root", "parent"}, uncollapsed)
assert.Equal(t, []string{"root", "parent"}, uncollapsed)
}
// An unknown selectedSpanID must not panic; returns a window from index 0.

View File

@@ -165,7 +165,7 @@ func (provider *provider) Report(ctx context.Context) error {
continue
}
users, err := provider.userGetter.ListUsersByOrgID(ctx, org.ID)
users, err := provider.userGetter.ListByOrgID(ctx, org.ID)
if err != nil {
provider.settings.Logger().WarnContext(ctx, "failed to list users", errors.Attr(err), slog.Any("org_id", org.ID))
continue
@@ -178,7 +178,7 @@ func (provider *provider) Report(ctx context.Context) error {
}
for _, user := range users {
traits := types.NewTraitsFromUser(user)
traits := types.NewTraitsFromDeprecatedUser(user)
if maxLastObservedAt, ok := maxLastObservedAtPerUserID[user.ID]; ok {
traits["auth_token.last_observed_at.max.time"] = maxLastObservedAt.UTC()
traits["auth_token.last_observed_at.max.time_unix"] = maxLastObservedAt.Unix()

View File

@@ -4,7 +4,6 @@ import (
"fmt"
"slices"
schemamigrator "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/querybuilder"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
@@ -41,14 +40,8 @@ func (c *jsonConditionBuilder) buildJSONCondition(operator qbtypes.FilterOperato
}
conditions = append(conditions, condition)
}
baseCond := sb.Or(conditions...)
// path index
if operator.AddDefaultExistsFilter() {
pathIndex := fmt.Sprintf(`has(%s, '%s')`, schemamigrator.JSONPathsIndexExpr(LogsV2BodyV2Column), c.key.ArrayParentPaths()[0])
return sb.And(baseCond, pathIndex), nil
}
return baseCond, nil
return sb.Or(conditions...), nil
}
// emitPlannedCondition handles paths with array traversal

View File

@@ -258,7 +258,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((dynamicElement(body_v2.`user.name`, 'String') = ?) AND has(JSONAllPaths(body_v2), 'user.name')) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (dynamicElement(body_v2.`user.name`, 'String') = ?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "x", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
expectedErr: nil,
@@ -300,7 +300,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_v2.education`-> (arrayExists(`body_v2.education[].awards`-> dynamicElement(`body_v2.education[].awards`.`name`, 'String') = ?, dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_v2.education[].awards`-> dynamicElement(`body_v2.education[].awards`.`name`, 'String') = ?, 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))'))) AND has(JSONAllPaths(body_v2), 'education')) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (arrayExists(`body_v2.education`-> (arrayExists(`body_v2.education[].awards`-> dynamicElement(`body_v2.education[].awards`.`name`, 'String') = ?, dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_v2.education[].awards`-> dynamicElement(`body_v2.education[].awards`.`name`, 'String') = ?, 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))'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "Iron Award", "Iron Award", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
expectedErr: nil,
@@ -314,7 +314,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')) OR 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))'))) AND has(JSONAllPaths(body_v2), 'education')) OR ((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> toFloat64(x) = ?, arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')) OR 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 -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> toFloat64(x) = ?, arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "%1.65%", 1.65, "%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,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."},
},
@@ -329,7 +329,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_v2.education`-> LOWER(dynamicElement(`body_v2.education`.`name`, 'String')) LIKE LOWER(?), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (arrayExists(`body_v2.education`-> LOWER(dynamicElement(`body_v2.education`.`name`, 'String')) LIKE LOWER(?), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "%IIT%", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
expectedErr: nil,
@@ -343,7 +343,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> x = ?, dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) OR ((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> 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 -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "%true%", true, "%true%", true, "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,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."},
},
@@ -358,7 +358,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> toString(x) = ?, dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) OR ((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> toString(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 -> LOWER(x) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "%passed%", "passed", "%passed%", "passed", "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,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."},
},
@@ -373,7 +373,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (((arrayExists(`body_v2.education`-> (arrayExists(x -> toFloat64(x) IN (?, ?), dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) OR ((arrayExists(`body_v2.education`-> (arrayExists(x -> x IN (?, ?), arrayMap(x->dynamicElement(x, 'Array(Nullable(Float64))'), arrayFilter(x->(dynamicType(x) = 'Array(Nullable(Float64))'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_v2.education`-> (arrayExists(x -> toFloat64(x) IN (?, ?), 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 -> x IN (?, ?), arrayMap(x->dynamicElement(x, 'Array(Nullable(Float64))'), arrayFilter(x->(dynamicType(x) = 'Array(Nullable(Float64))'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), 1.65, 1.99, 1.65, 1.99, "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,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."},
},
@@ -388,7 +388,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_v2.education`-> (arrayExists(`body_v2.education[].awards`-> toFloat64(dynamicElement(`body_v2.education[].awards`.`semester`, 'Int64')) BETWEEN ? AND ?, dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_v2.education[].awards`-> toFloat64(dynamicElement(`body_v2.education[].awards`.`semester`, 'Int64')) BETWEEN ? AND ?, 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))'))) AND has(JSONAllPaths(body_v2), 'education')) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (arrayExists(`body_v2.education`-> (arrayExists(`body_v2.education[].awards`-> toFloat64(dynamicElement(`body_v2.education[].awards`.`semester`, 'Int64')) BETWEEN ? AND ?, dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_v2.education[].awards`-> toFloat64(dynamicElement(`body_v2.education[].awards`.`semester`, 'Int64')) BETWEEN ? AND ?, 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))'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), float64(2), float64(4), float64(2), float64(4), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
expectedErr: nil,
@@ -402,7 +402,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_v2.education`-> (arrayExists(`body_v2.education[].awards`-> toFloat64(dynamicElement(`body_v2.education[].awards`.`semester`, 'Int64')) IN (?, ?), dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_v2.education[].awards`-> toFloat64(dynamicElement(`body_v2.education[].awards`.`semester`, 'Int64')) IN (?, ?), 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))'))) AND has(JSONAllPaths(body_v2), 'education')) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (arrayExists(`body_v2.education`-> (arrayExists(`body_v2.education[].awards`-> toFloat64(dynamicElement(`body_v2.education[].awards`.`semester`, 'Int64')) IN (?, ?), dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_v2.education[].awards`-> toFloat64(dynamicElement(`body_v2.education[].awards`.`semester`, 'Int64')) IN (?, ?), 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))'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), float64(2), float64(4), float64(2), float64(4), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
expectedErr: nil,
@@ -416,7 +416,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_v2.education`-> (arrayExists(`body_v2.education[].awards`-> dynamicElement(`body_v2.education[].awards`.`type`, 'String') = ?, dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_v2.education[].awards`-> dynamicElement(`body_v2.education[].awards`.`type`, 'String') = ?, 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))'))) AND has(JSONAllPaths(body_v2), 'education')) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (arrayExists(`body_v2.education`-> (arrayExists(`body_v2.education[].awards`-> dynamicElement(`body_v2.education[].awards`.`type`, 'String') = ?, dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_v2.education[].awards`-> dynamicElement(`body_v2.education[].awards`.`type`, 'String') = ?, 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))'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "sports", "sports", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
expectedErr: nil,
@@ -430,7 +430,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (((arrayExists(`body_v2.interests`-> arrayExists(`body_v2.interests[].entities`-> arrayExists(`body_v2.interests[].entities[].reviews`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))')) OR arrayExists(x -> toFloat64(x) = ?, dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))'))), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), 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))'))) AND has(JSONAllPaths(body_v2), 'interests')) OR ((arrayExists(`body_v2.interests`-> arrayExists(`body_v2.interests[].entities`-> arrayExists(`body_v2.interests[].entities[].reviews`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))')) OR arrayExists(x -> toFloat64OrNull(x) = ?, dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))'))), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), 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))'))) AND has(JSONAllPaths(body_v2), 'interests'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_v2.interests`-> arrayExists(`body_v2.interests[].entities`-> arrayExists(`body_v2.interests[].entities[].reviews`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))')) OR arrayExists(x -> toFloat64(x) = ?, dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))'))), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), 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))'))) OR (arrayExists(`body_v2.interests`-> arrayExists(`body_v2.interests[].entities`-> arrayExists(`body_v2.interests[].entities[].reviews`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))')) OR arrayExists(x -> toFloat64OrNull(x) = ?, dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))'))), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), 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))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "%4%", float64(4), "%4%", float64(4), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `interests[].entities[].reviews[].entries[].metadata[].positions[].ratings` is ambiguous, found 2 different combinations of field context / data type: [name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]int64,jsondatatype=Array(Nullable(Int64)) name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]string,jsondatatype=Array(Nullable(String))]."},
},
@@ -445,7 +445,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (((arrayExists(`body_v2.interests`-> arrayExists(`body_v2.interests[].entities`-> arrayExists(`body_v2.interests[].entities[].reviews`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))')) OR arrayExists(x -> toString(x) = ?, dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))'))), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), 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))'))) AND has(JSONAllPaths(body_v2), 'interests')) OR ((arrayExists(`body_v2.interests`-> arrayExists(`body_v2.interests[].entities`-> arrayExists(`body_v2.interests[].entities[].reviews`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))')) OR arrayExists(x -> x = ?, dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))'))), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), 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))'))) AND has(JSONAllPaths(body_v2), 'interests'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_v2.interests`-> arrayExists(`body_v2.interests[].entities`-> arrayExists(`body_v2.interests[].entities[].reviews`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))')) OR arrayExists(x -> toString(x) = ?, dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))'))), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), 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))'))) OR (arrayExists(`body_v2.interests`-> arrayExists(`body_v2.interests[].entities`-> arrayExists(`body_v2.interests[].entities[].reviews`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))')) OR arrayExists(x -> x = ?, dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))'))), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), 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))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "%Good%", "Good", "%Good%", "Good", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `interests[].entities[].reviews[].entries[].metadata[].positions[].ratings` is ambiguous, found 2 different combinations of field context / data type: [name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]int64,jsondatatype=Array(Nullable(Int64)) name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]string,jsondatatype=Array(Nullable(String))]."},
},
@@ -460,7 +460,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_v2.education`-> (arrayExists(`body_v2.education[].awards`-> (arrayExists(`body_v2.education[].awards[].participated`-> arrayExists(`body_v2.education[].awards[].participated[].team`-> LOWER(dynamicElement(`body_v2.education[].awards[].participated[].team`.`branch`, 'String')) LIKE LOWER(?), dynamicElement(`body_v2.education[].awards[].participated`.`team`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')) OR arrayExists(`body_v2.education[].awards[].participated`-> arrayExists(`body_v2.education[].awards[].participated[].team`-> LOWER(dynamicElement(`body_v2.education[].awards[].participated[].team`.`branch`, 'String')) LIKE LOWER(?), dynamicElement(`body_v2.education[].awards[].participated`.`team`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')), 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(`body_v2.education[].awards[].participated[].team`-> LOWER(dynamicElement(`body_v2.education[].awards[].participated[].team`.`branch`, 'String')) LIKE LOWER(?), dynamicElement(`body_v2.education[].awards[].participated`.`team`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=64))')), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')) OR arrayExists(`body_v2.education[].awards[].participated`-> arrayExists(`body_v2.education[].awards[].participated[].team`-> LOWER(dynamicElement(`body_v2.education[].awards[].participated[].team`.`branch`, 'String')) LIKE LOWER(?), dynamicElement(`body_v2.education[].awards[].participated`.`team`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')), 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))'))) AND has(JSONAllPaths(body_v2), 'education')) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (arrayExists(`body_v2.education`-> (arrayExists(`body_v2.education[].awards`-> (arrayExists(`body_v2.education[].awards[].participated`-> arrayExists(`body_v2.education[].awards[].participated[].team`-> LOWER(dynamicElement(`body_v2.education[].awards[].participated[].team`.`branch`, 'String')) LIKE LOWER(?), dynamicElement(`body_v2.education[].awards[].participated`.`team`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')) OR arrayExists(`body_v2.education[].awards[].participated`-> arrayExists(`body_v2.education[].awards[].participated[].team`-> LOWER(dynamicElement(`body_v2.education[].awards[].participated[].team`.`branch`, 'String')) LIKE LOWER(?), dynamicElement(`body_v2.education[].awards[].participated`.`team`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')), 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(`body_v2.education[].awards[].participated[].team`-> LOWER(dynamicElement(`body_v2.education[].awards[].participated[].team`.`branch`, 'String')) LIKE LOWER(?), dynamicElement(`body_v2.education[].awards[].participated`.`team`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=64))')), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')) OR arrayExists(`body_v2.education[].awards[].participated`-> arrayExists(`body_v2.education[].awards[].participated[].team`-> LOWER(dynamicElement(`body_v2.education[].awards[].participated[].team`.`branch`, 'String')) LIKE LOWER(?), dynamicElement(`body_v2.education[].awards[].participated`.`team`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')), 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))'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "%Civil%", "%Civil%", "%Civil%", "%Civil%", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
expectedErr: nil,
@@ -474,7 +474,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (((LOWER(toString(dynamicElement(body_v2.`user.age`, 'Int64'))) LIKE LOWER(?)) AND has(JSONAllPaths(body_v2), 'user.age')) OR ((LOWER(dynamicElement(body_v2.`user.age`, 'String')) LIKE LOWER(?)) AND has(JSONAllPaths(body_v2), 'user.age'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((LOWER(toString(dynamicElement(body_v2.`user.age`, 'Int64'))) LIKE LOWER(?)) OR (LOWER(dynamicElement(body_v2.`user.age`, 'String')) LIKE LOWER(?))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "%25%", "%25%", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `user.age` is ambiguous, found 2 different combinations of field context / data type: [name=user.age,context=body,datatype=int64,jsondatatype=Int64 name=user.age,context=body,datatype=string,jsondatatype=String]."},
},
@@ -489,7 +489,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((LOWER(toString(dynamicElement(body_v2.`user.height`, 'Float64'))) LIKE LOWER(?)) AND has(JSONAllPaths(body_v2), 'user.height')) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (LOWER(toString(dynamicElement(body_v2.`user.height`, 'Float64'))) LIKE LOWER(?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "%5.8%", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
expectedErr: nil,
@@ -503,7 +503,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_v2.education`-> LOWER(toString(dynamicElement(`body_v2.education`.`year`, 'Int64'))) LIKE LOWER(?), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (arrayExists(`body_v2.education`-> LOWER(toString(dynamicElement(`body_v2.education`.`year`, 'Int64'))) LIKE LOWER(?), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "%2020%", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
expectedErr: nil,

View File

@@ -65,10 +65,10 @@ type StorableRole struct {
types.Identifiable
types.TimeAuditable
Name string `bun:"name,type:string" json:"name"`
Description string `bun:"description,type:string" json:"description"`
Type string `bun:"type,type:string" json:"type"`
OrgID string `bun:"org_id,type:string" json:"orgId"`
Name string `bun:"name,type:string"`
Description string `bun:"description,type:string"`
Type string `bun:"type,type:string"`
OrgID string `bun:"org_id,type:string"`
}
type Role struct {

View File

@@ -5,22 +5,21 @@ import (
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
var (
ErrCodeUserRoleAlreadyExists = errors.MustNewCode("user_role_already_exists")
ErrCodeUserRolesNotFound = errors.MustNewCode("user_roles_not_found")
ErrCodeUserRolesNotFound = errors.MustNewCode("user_roles_not_found")
)
type UserRole struct {
bun.BaseModel `bun:"table:user_role,alias:user_role"`
ID valuer.UUID `bun:"id,pk,type:text" json:"id" required:"true"`
UserID valuer.UUID `bun:"user_id" json:"userId"`
RoleID valuer.UUID `bun:"role_id" json:"roleId"`
UserID valuer.UUID `bun:"user_id" json:"user_id"`
RoleID valuer.UUID `bun:"role_id" json:"role_id"`
CreatedAt time.Time `bun:"created_at" json:"createdAt"`
UpdatedAt time.Time `bun:"updated_at" json:"updatedAt"`
@@ -48,11 +47,6 @@ func NewUserRoles(userID valuer.UUID, roles []*Role) []*UserRole {
return userRoles
}
type UserWithRoles struct {
*types.User
UserRoles []*UserRole `json:"userRoles"`
}
type UserRoleStore interface {
// create user roles in bulk
CreateUserRoles(ctx context.Context, userRoles []*UserRole) error
@@ -65,7 +59,4 @@ type UserRoleStore interface {
// delete user role entries by user id
DeleteUserRoles(ctx context.Context, userID valuer.UUID) error
// delete a single user role entry by user id and role id
DeleteUserRoleByUserIDAndRoleID(ctx context.Context, userID valuer.UUID, roleID valuer.UUID) error
}

View File

@@ -102,10 +102,6 @@ func (fn FunctionName) Validate() error {
// ApplyFunction applies the given function to the result data
func ApplyFunction(fn Function, result *TimeSeries) *TimeSeries {
if len(result.Values) == 0 {
return result
}
// Extract the function name and arguments
name := fn.Name
args := fn.Args

View File

@@ -599,14 +599,6 @@ func TestApplyFunction(t *testing.T) {
values []float64
want []float64
}{
{
name: "test with empty series",
function: Function{
Name: FunctionNameRunningDiff,
},
values: []float64{},
want: []float64{},
},
{
name: "cutOffMin function",
function: Function{

View File

@@ -206,11 +206,8 @@ func (q *QueryBuilderQuery[T]) validateAggregations(cfg validationConfig) error
return nil
}
// At least one aggregation required for aggregation queries, even if
// they are disabled, usually because they are used in formula
// regardless of use in formula, it's invalid to have empty Aggregations
// for aggregation request
if len(q.Aggregations) == 0 {
// At least one aggregation required for non-disabled queries
if len(q.Aggregations) == 0 && !q.Disabled {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"at least one aggregation is required",

View File

@@ -4,7 +4,6 @@ import (
"strings"
"testing"
"github.com/SigNoz/signoz/pkg/types/metrictypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
@@ -32,14 +31,7 @@ func TestQueryRangeRequest_ValidateAllQueriesNotDisabled(t *testing.T) {
Spec: QueryBuilderQuery[MetricAggregation]{
Name: "A",
Disabled: true,
Aggregations: []MetricAggregation{
{
MetricName: "test",
TimeAggregation: metrictypes.TimeAggregationAvg,
SpaceAggregation: metrictypes.SpaceAggregationMax,
},
},
Signal: telemetrytypes.SignalMetrics,
Signal: telemetrytypes.SignalMetrics,
},
},
{
@@ -47,12 +39,7 @@ func TestQueryRangeRequest_ValidateAllQueriesNotDisabled(t *testing.T) {
Spec: QueryBuilderQuery[LogAggregation]{
Name: "B",
Disabled: true,
Aggregations: []LogAggregation{
{
Expression: "count()",
},
},
Signal: telemetrytypes.SignalLogs,
Signal: telemetrytypes.SignalLogs,
},
},
},
@@ -74,14 +61,7 @@ func TestQueryRangeRequest_ValidateAllQueriesNotDisabled(t *testing.T) {
Spec: QueryBuilderQuery[MetricAggregation]{
Name: "A",
Disabled: true,
Aggregations: []MetricAggregation{
{
MetricName: "test",
TimeAggregation: metrictypes.TimeAggregationAvg,
SpaceAggregation: metrictypes.SpaceAggregationMax,
},
},
Signal: telemetrytypes.SignalMetrics,
Signal: telemetrytypes.SignalMetrics,
},
},
{
@@ -214,14 +194,7 @@ func TestQueryRangeRequest_ValidateAllQueriesNotDisabled(t *testing.T) {
Spec: QueryBuilderQuery[MetricAggregation]{
Name: "A",
Disabled: true,
Aggregations: []MetricAggregation{
{
MetricName: "test",
TimeAggregation: metrictypes.TimeAggregationAvg,
SpaceAggregation: metrictypes.SpaceAggregationMax,
},
},
Signal: telemetrytypes.SignalMetrics,
Signal: telemetrytypes.SignalMetrics,
},
},
{
@@ -259,12 +232,7 @@ func TestQueryRangeRequest_ValidateAllQueriesNotDisabled(t *testing.T) {
Spec: QueryBuilderQuery[LogAggregation]{
Name: "A",
Disabled: true,
Aggregations: []LogAggregation{
{
Expression: "sum(duration)",
},
},
Signal: telemetrytypes.SignalLogs,
Signal: telemetrytypes.SignalLogs,
},
},
},
@@ -398,12 +366,7 @@ func TestQueryRangeRequest_ValidateCompositeQuery(t *testing.T) {
Spec: QueryBuilderQuery[LogAggregation]{
Name: "A",
Disabled: true,
Aggregations: []LogAggregation{
{
Expression: "count()",
},
},
Signal: telemetrytypes.SignalLogs,
Signal: telemetrytypes.SignalLogs,
},
},
{
@@ -411,12 +374,7 @@ func TestQueryRangeRequest_ValidateCompositeQuery(t *testing.T) {
Spec: QueryBuilderQuery[TraceAggregation]{
Name: "A",
Disabled: true,
Aggregations: []TraceAggregation{
{
Expression: "count()",
},
},
Signal: telemetrytypes.SignalTraces,
Signal: telemetrytypes.SignalTraces,
},
},
},
@@ -438,12 +396,7 @@ func TestQueryRangeRequest_ValidateCompositeQuery(t *testing.T) {
Spec: QueryBuilderQuery[LogAggregation]{
Name: "X",
Disabled: true,
Aggregations: []LogAggregation{
{
Expression: "count()",
},
},
Signal: telemetrytypes.SignalLogs,
Signal: telemetrytypes.SignalLogs,
},
},
{
@@ -451,14 +404,7 @@ func TestQueryRangeRequest_ValidateCompositeQuery(t *testing.T) {
Spec: QueryBuilderQuery[MetricAggregation]{
Name: "X",
Disabled: true,
Aggregations: []MetricAggregation{
{
MetricName: "test",
TimeAggregation: metrictypes.TimeAggregationAvg,
SpaceAggregation: metrictypes.SpaceAggregationMax,
},
},
Signal: telemetrytypes.SignalMetrics,
Signal: telemetrytypes.SignalMetrics,
},
},
},
@@ -481,9 +427,7 @@ func TestQueryRangeRequest_ValidateCompositeQuery(t *testing.T) {
Name: "A",
Signal: telemetrytypes.SignalLogs,
Aggregations: []LogAggregation{
{
Expression: "count()",
},
{Expression: "count()"},
},
},
},
@@ -637,9 +581,7 @@ func TestQueryRangeRequest_ValidateCompositeQuery(t *testing.T) {
Name: "A",
Signal: telemetrytypes.SignalLogs,
Aggregations: []LogAggregation{
{
Expression: "count()",
},
{Expression: "count()"},
},
},
},

View File

@@ -51,14 +51,6 @@ type DeprecatedUser struct {
Role Role `json:"role"`
}
type UpdatableUser struct {
DisplayName string `json:"displayName" required:"true"`
}
type PostableRole struct {
Name string `json:"name" required:"true"`
}
type PostableRegisterOrgAndAdmin struct {
Name string `json:"name"`
Email valuer.Email `json:"email"`
@@ -306,9 +298,6 @@ type UserStore interface {
// Get user by reset password token
GetUserByResetPasswordToken(ctx context.Context, token string) (*User, error)
// Get users having role by org id and role id
GetUsersByOrgIDAndRoleID(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID) ([]*User, error)
// Transaction
RunInTx(ctx context.Context, cb func(ctx context.Context) error) error
}