Compare commits

...

10 Commits

Author SHA1 Message Date
vikrantgupta25
b5ccbe7558 chore(user): remove deprecated user endpoints
Assisted-by: Claude Opus 5
2026-08-12 16:53:36 +05:30
Swapnil Nakade
2616885d22 feat: adding mysql GCP service (#12514)
<!--A few plain bullets saying what changed and why, for a reviewer
skimming it - not a wall of text, not a restatement of the diff, not
generated boilerplate.-->
#### Description
- Adding GCP integration MySQL service
- Related fix: adding formula to convert CPU utilization fraction into
percentage for Postgres dashboard

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR
https://github.com/SigNoz/platform-pod/issues/2942
2026-08-12 09:53:16 +00:00
Aditya Singh
52dd57074e feat: filter fields with no name in field selector (#12512)
<!--A few plain bullets saying what changed and why, for a reviewer
skimming it - not a wall of text, not a restatement of the diff, not
generated boilerplate.-->
#### Description
Filter field selector options with name field empty

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR
closes https://github.com/SigNoz/engineering-pod/issues/5890

<!--If applicable, include screenshots or screen recordings that clearly
show the behavior before the change and the result after the change. -->
#### Screenshots / Screen Recordings
No screen recording as this is hard to reproduce.
2026-08-12 07:54:51 +00:00
Naman Verma
fe94b817db fix(promql): remove NaN and Inf values from PromQL query range response (#12388)
## Pull Request

---

### 📄 Summary

Currently, builder and clickhouse queries remove NaN and Inf values, but
PromQL does not. This way, it ends up in the final response. While the
UI handles these values, a lot of other places in the flow do not, such
as our query response caching. This can lead to unexpected issues.

The current issue at hand is that while the first query range call shows
the correct data, the second call (that fetches from cache) does not.

Instead of fixing the caching, better to solve the problem at root level
and not return non-finite values for PromQL altogether.

#### Recordings

On local data before the change:


https://github.com/user-attachments/assets/c08ec796-a7e5-47d8-8cc5-3dfd302dba49

After the change:


https://github.com/user-attachments/assets/9162c963-1a98-4ebc-83ce-759a9127b772


#### Issues closed by this PR

Closes https://github.com/SigNoz/pulse-pod/issues/185

---

### 🧪 Testing Strategy

- Tests added/updated: Yes, integration and unit tests
- Manual verification: Added data locally to reproduce the exact
scenario

---

### ⚠️ Risk & Impact Assessment

- Blast radius: PromQL queries
- Rollback plan: Revert PR or just add a fix

---
2026-08-12 07:35:38 +00:00
Aditya Singh
ea36032d96 fix(sentry): drop benign cancellation errors from reporting on sentry (#12524)
<!--A few plain bullets saying what changed and why, for a reviewer
skimming it - not a wall of text, not a restatement of the diff, not
generated boilerplate.-->
#### Description
This PR drops Cancelation error from monaco on sentry to reduce noise



<!--Anything reviewers should keep in mind while reviewing -->
#### Additional Information
Pager: https://signoz-1.pagerduty.com/incidents/Q1JG9MJ5DRA4LW
Sentry: https://signoz-io.sentry.io/issues/7491905006
2026-08-12 07:22:33 +00:00
Abhi kumar
6ecfa839f3 fix(service-map): stop the resource attribute filter bar from clearing (#12521)
#### Description

Selecting a filter on the Service Map cleared the filter bar instead of
applying it, and the same filter then turned up applied on the Services
tab. Three separate causes:

- The resource attribute context filtered its queries by the current
route, so a filter the map cannot apply vanished from the bar while
staying in state and in the `resourceAttribute` URL param — which the
sidebar carries across routes, hence it reappearing on Services. The
context now exposes whatever is in the URL, and the Service Map narrows
the queries for its own `/dependency_graph` request, so the request
payload is unchanged.
- `ServiceMap` returned early with the filter bar under a different
parent element in each branch, so React tore the bar down and rebuilt it
whenever the map flipped between having services and being empty (and it
wasn't rendered at all while loading). It now renders once, above the
loading / empty / map states. As a side effect the graph tooltip styles
in `Container` finally wrap the graph rather than only the empty state.
- The environment `Select` was keyed on its own value, remounting an
already-controlled select on every pick and closing the dropdown before
a second environment could be chosen.

#### Issues closed by this PR

Closes SigNoz/pulse-pod#199

#### Additional Information

- Related but deliberately left out of scope: `whilelistedKeys` lists
`resource_k8s_cluster_namespace`, while the backend column is
`k8s_namespace_name` (`pkg/query-service/app/services/map.go`), so that
filter is accepted by the UI and silently dropped server side.
2026-08-12 06:53:55 +00:00
Ashwin Bhatkal
62d382b3cc fix(alerts): tolerate a null channels field when editing an existing alert rule (#12510)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
## Summary

Opening an existing alert rule for editing crashes the whole page with
`Cannot read properties of null (reading 'length')`. It happens when a
rule's threshold has `channels: null`, which is the case for rules not
created through the UI.

The generated type is right — `RuletypesBasicRuleThresholdDTO.channels`
is `string[] | null`. The problem is our own `BasicThreshold` wrapper
type, which we keep because v1 and v2 alert shapes both exist. It says
`channels: string[]`, and `fromRuleDTOToPostableRuleV2` casts the DTO
straight into it with `as unknown as`. So the null reaches our code
while the compiler thinks it can't.

`getThresholdStateFromAlertDef` copied that null into state, and the
footer validator then read `.length` on it during render, which takes
down the page instead of failing one field.

This PR defaults `channels` to `[]` where the API data becomes local
state, so the validator, the payload builder and both channel dropdowns
are all safe. The validator also gets an optional chain, since a throw
there can't be recovered.

This is a guard, not the real fix. The cast in
`fromRuleDTOToPostableRuleV2` is the actual gap, and the same wrapper
also claims `spec` is non-nullable when the generated type allows null —
so `spec.map` and `spec[0].op` in the same function can still crash.
Worth fixing at the converter.

## Test plan

One test per guard. Both fail with the original error when the fix is
reverted.

- `pnpm jest src/container/CreateAlertV2/` — 420 pass, 28 suites
- `oxfmt`, `oxlint`, `tsgo --noEmit` clean

Closes https://github.com/SigNoz/pulse-pod/issues/261
2026-08-11 20:40:30 +00:00
Ashwin Bhatkal
cc07e2fa24 fix(alert-channel-integrations): de-flake the Google Chat alert channel save tests (#12509)
## Summary

The Google Chat save test fails on CI now and then with `Exceeded
timeout of 5000 ms for a test`.

The two Google Chat tests fill the form with `userEvent.type()`, which
sends one keystroke at a time. Each keystroke re-renders the whole form.
The payload test types 91 characters, so it takes ~850ms locally. CI is
about 5x slower, which puts it near the 5s limit. A busy runner then
pushes it over.

This PR pastes the values instead of typing them. One event per field,
same assertions.

| Test | Before | After |
| --- | --- | --- |
| `saving sends a googlechat_configs payload` | 847 ms | 283 ms |
| `saving with a webhook url outside chat.googleapis.com` | 590 ms | 326
ms |

Nothing regressed. The new test was added recently to an already
existing suite. It was always close to the limit.

## Test plan

- `pnpm jest
src/container/AllAlertChannels/__tests__/CreateAlertChannel.test.tsx` —
57/57 pass, 3 runs
- `oxfmt`, `oxlint`, `tsgo --noEmit` clean

Closes https://github.com/SigNoz/pulse-pod/issues/259
2026-08-11 20:17:56 +00:00
Vinicius Lourenço
d7aa63f1bc fix(infrastructure-monitoring): migrate having clause to new format (#12467)
<!--A few plain bullets saying what changed and why, for a reviewer
skimming it - not a wall of text, not a restatement of the diff, not
generated boilerplate.-->
#### Description

This fixes the bad migration I did at
https://github.com/SigNoz/signoz/pull/11060, and correctly fixes the
expressions for `having` clause inside the charts.

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR

Closes https://github.com/SigNoz/platform-pod/issues/2905

Closes https://github.com/SigNoz/pulse-pod/issues/212

<!--If applicable, include screenshots or screen recordings that clearly
show the behavior before the change and the result after the change. -->
#### Screenshots / Screen Recordings

| Before | After |
|--------|--------|
| <img width="2156" height="1081" alt="screenshot-2026-08-07_17-15-41"
src="https://github.com/user-attachments/assets/b5617e88-2d63-4a21-915f-d21ed78f9f8b"
/> | <img width="2148" height="1075"
alt="screenshot-2026-08-07_17-12-06"
src="https://github.com/user-attachments/assets/4de3ffd0-533f-4b3b-81ac-df1515b4a076"
/> |
| <img width="2145" height="357" alt="screenshot-2026-08-07_17-16-08"
src="https://github.com/user-attachments/assets/0bfb92f3-e0c4-4cf6-9e4e-62068501da96"
/> | <img width="2146" height="360" alt="screenshot-2026-08-07_17-11-54"
src="https://github.com/user-attachments/assets/bba8ee82-68cd-4205-951f-711adaad07e0"
/> |
| <img width="1074" height="356" alt="screenshot-2026-08-07_17-15-55"
src="https://github.com/user-attachments/assets/574430b4-8547-4a1e-b53a-982ef33344ae"
/> | <img width="1074" height="363" alt="screenshot-2026-08-07_17-11-44"
src="https://github.com/user-attachments/assets/bf5698af-f7fb-45b6-886a-bc8ed4aba808"
/> |
2026-08-11 19:59:53 +00:00
Nityananda Gohain
c36b748370 feat: ai-011y quickfilters support (#12406)
## Pull Request

---

### 📄 Summary
AI 011y quickfilter

Will add the migration later.



#### Issues closed by this PR
Part of https://github.com/SigNoz/engineering-pod/issues/5714

---

###  Change Type
_Select all that apply_

- [x]  Feature
- [ ] 🐛 Bug fix
- [ ] ♻️ Refactor
- [ ] 🛠️ Infra / Tooling
- [ ] 🧪 Test-only

---

### 🧪 Testing Strategy
> How was this change validated?

- Tests added/updated:
- Manual verification:
- Edge cases covered:

---

### ⚠️ Risk & Impact Assessment
> What could break? How do we recover?

- Blast radius: None
- Potential regressions:
- Rollback plan:

---
2026-08-11 19:19:35 +00:00
45 changed files with 2907 additions and 2051 deletions

View File

@@ -1499,6 +1499,7 @@ components:
- computeengine
- gke
- cloudstorage
- cloudsql_mysql
type: string
CloudintegrationtypesServiceMetadata:
properties:
@@ -8865,31 +8866,6 @@ components:
oldPassword:
type: string
type: object
TypesDeprecatedUser:
properties:
createdAt:
format: date-time
type: string
displayName:
type: string
email:
type: string
id:
type: string
isRoot:
type: boolean
orgId:
type: string
role:
type: string
status:
type: string
updatedAt:
format: date-time
type: string
required:
- id
type: object
TypesIdentifiable:
properties:
id:
@@ -8897,31 +8873,6 @@ components:
required:
- id
type: object
TypesInvite:
properties:
createdAt:
format: date-time
type: string
email:
type: string
id:
type: string
inviteLink:
type: string
name:
type: string
orgId:
type: string
role:
type: string
token:
type: string
updatedAt:
format: date-time
type: string
required:
- id
type: object
TypesOrganization:
properties:
alias:
@@ -8956,17 +8907,6 @@ components:
- orgId
- email
type: object
TypesPostableInvite:
properties:
email:
type: string
frontendBaseUrl:
type: string
name:
type: string
role:
type: string
type: object
TypesPostableResetPassword:
properties:
password:
@@ -8974,13 +8914,6 @@ components:
token:
type: string
type: object
TypesPostableRole:
properties:
name:
type: string
required:
- name
type: object
TypesPostableVerifyResetPasswordToken:
properties:
token:
@@ -11395,70 +11328,6 @@ paths:
summary: Get field values
tags:
- fields
/api/v1/getResetPasswordToken/{id}:
get:
deprecated: true
description: This endpoint returns the reset password token by id
operationId: GetResetPasswordTokenDeprecated
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/TypesResetPasswordToken'
status:
type: string
required:
- status
- data
type: object
description: OK
"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: Get reset password token
tags:
- users
/api/v1/global/config:
get:
deprecated: false
@@ -11488,69 +11357,6 @@ paths:
summary: Get global config
tags:
- global
/api/v1/invite:
post:
deprecated: true
description: This endpoint creates an invite for a user
operationId: CreateInvite
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TypesPostableInvite'
responses:
"201":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/TypesInvite'
status:
type: string
required:
- status
- data
type: object
description: Created
"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
"409":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Conflict
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Create invite
tags:
- users
/api/v1/llm_pricing_rules:
get:
deprecated: false
@@ -12202,40 +12008,6 @@ paths:
summary: Get query range result
tags:
- dashboard
/api/v1/resetPassword:
post:
deprecated: true
description: This endpoint resets the password by token
operationId: ResetPasswordDeprecated
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TypesPostableResetPassword'
responses:
"204":
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"409":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Conflict
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
summary: Reset password
tags:
- users
/api/v1/roles:
get:
deprecated: false
@@ -14426,97 +14198,6 @@ paths:
summary: Get aggregations for a trace
tags:
- tracedetail
/api/v1/user:
get:
deprecated: true
description: This endpoint lists all users
operationId: ListUsersDeprecated
responses:
"200":
content:
application/json:
schema:
properties:
data:
items:
$ref: '#/components/schemas/TypesDeprecatedUser'
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
tags:
- users
/api/v1/user/me:
get:
deprecated: true
description: This endpoint returns the user I belong to
operationId: GetMyUserDeprecated
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/TypesDeprecatedUser'
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
tags:
- users
/api/v1/user/preferences:
get:
deprecated: false
@@ -23761,108 +23442,6 @@ paths:
summary: Get user roles
tags:
- users
post:
deprecated: true
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: true
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

View File

@@ -388,6 +388,10 @@ function App(): JSX.Element {
if (error?.name === 'AbortError') {
return null;
}
// Ignore benign Monaco cancellation errors (name 'Canceled').
if (error?.name === 'Canceled') {
return null;
}
// Drop the event if its level is 'warning' or 'info'
if (event.level === 'warning' || event.level === 'info') {

View File

@@ -2818,6 +2818,7 @@ export enum CloudintegrationtypesServiceIDDTO {
computeengine = 'computeengine',
gke = 'gke',
cloudstorage = 'cloudstorage',
cloudsql_mysql = 'cloudsql_mysql',
}
export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
/**
@@ -9933,47 +9934,6 @@ export interface TypesChangePasswordRequestDTO {
oldPassword?: string;
}
export interface TypesDeprecatedUserDTO {
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
displayName?: string;
/**
* @type string
*/
email?: string;
/**
* @type string
*/
id: string;
/**
* @type boolean
*/
isRoot?: boolean;
/**
* @type string
*/
orgId?: string;
/**
* @type string
*/
role?: string;
/**
* @type string
*/
status?: string;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
}
export interface TypesIdentifiableDTO {
/**
* @type string
@@ -9981,47 +9941,6 @@ export interface TypesIdentifiableDTO {
id: string;
}
export interface TypesInviteDTO {
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
email?: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
inviteLink?: string;
/**
* @type string
*/
name?: string;
/**
* @type string
*/
orgId?: string;
/**
* @type string
*/
role?: string;
/**
* @type string
*/
token?: string;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
}
export interface TypesOrganizationDTO {
/**
* @type string
@@ -10071,25 +9990,6 @@ export interface TypesPostableForgotPasswordDTO {
orgId: string;
}
export interface TypesPostableInviteDTO {
/**
* @type string
*/
email?: string;
/**
* @type string
*/
frontendBaseUrl?: string;
/**
* @type string
*/
name?: string;
/**
* @type string
*/
role?: string;
}
export interface TypesPostableResetPasswordDTO {
/**
* @type string
@@ -10101,13 +10001,6 @@ export interface TypesPostableResetPasswordDTO {
token?: string;
}
export interface TypesPostableRoleDTO {
/**
* @type string
*/
name: string;
}
export interface TypesPostableVerifyResetPasswordTokenDTO {
/**
* @type string
@@ -10750,17 +10643,6 @@ export type GetFieldsValues200 = {
status: string;
};
export type GetResetPasswordTokenDeprecatedPathParameters = {
id: string;
};
export type GetResetPasswordTokenDeprecated200 = {
data: TypesResetPasswordTokenDTO;
/**
* @type string
*/
status: string;
};
export type GetGlobalConfig200 = {
data: GlobaltypesConfigDTO;
/**
@@ -10769,14 +10651,6 @@ export type GetGlobalConfig200 = {
status: string;
};
export type CreateInvite201 = {
data: TypesInviteDTO;
/**
* @type string
*/
status: string;
};
export type ListLLMPricingRulesParams = {
/**
* @type integer
@@ -11189,25 +11063,6 @@ export type GetTraceAggregations200 = {
status: string;
};
export type ListUsersDeprecated200 = {
/**
* @type array
*/
data: TypesDeprecatedUserDTO[];
/**
* @type string
*/
status: string;
};
export type GetMyUserDeprecated200 = {
data: TypesDeprecatedUserDTO;
/**
* @type string
*/
status: string;
};
export type ListUserPreferences200 = {
/**
* @type array
@@ -12402,13 +12257,6 @@ export type GetRolesByUserID200 = {
status: string;
};
export type SetRoleByUserIDPathParameters = {
id: string;
};
export type RemoveUserRoleByUserIDAndRoleIDPathParameters = {
id: string;
roleId: string;
};
export type GetMyUser200 = {
data: AuthtypesUserWithRolesDTO;
/**

View File

@@ -20,7 +20,6 @@ import type {
import type {
AuthtypesPostableUserDTO,
AuthtypesPostableUserRoleDTO,
CreateInvite201,
CreateResetPasswordToken201,
CreateResetPasswordTokenPathParameters,
CreateUser201,
@@ -28,10 +27,7 @@ import type {
DeleteUserPathParameters,
DeleteUserRolePathParameters,
GetMyUser200,
GetMyUserDeprecated200,
GetResetPasswordToken200,
GetResetPasswordTokenDeprecated200,
GetResetPasswordTokenDeprecatedPathParameters,
GetResetPasswordTokenPathParameters,
GetRolesByUserID200,
GetRolesByUserIDPathParameters,
@@ -42,15 +38,10 @@ import type {
GetUsersByRoleID200,
GetUsersByRoleIDPathParameters,
ListUsers200,
ListUsersDeprecated200,
RemoveUserRoleByUserIDAndRoleIDPathParameters,
RenderErrorResponseDTO,
SetRoleByUserIDPathParameters,
TypesChangePasswordRequestDTO,
TypesPostableForgotPasswordDTO,
TypesPostableInviteDTO,
TypesPostableResetPasswordDTO,
TypesPostableRoleDTO,
TypesPostableVerifyResetPasswordTokenDTO,
TypesUpdatableUserDTO,
UpdateUserPathParameters,
@@ -59,460 +50,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* This endpoint returns the reset password token by id
* @deprecated
* @summary Get reset password token
*/
export const getResetPasswordTokenDeprecated = (
{ id }: GetResetPasswordTokenDeprecatedPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetResetPasswordTokenDeprecated200>({
url: `/api/v1/getResetPasswordToken/${id}`,
method: 'GET',
signal,
});
};
export const getGetResetPasswordTokenDeprecatedQueryKey = ({
id,
}: GetResetPasswordTokenDeprecatedPathParameters) => {
return [`/api/v1/getResetPasswordToken/${id}`] as const;
};
export const getGetResetPasswordTokenDeprecatedQueryOptions = <
TData = Awaited<ReturnType<typeof getResetPasswordTokenDeprecated>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetResetPasswordTokenDeprecatedPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getResetPasswordTokenDeprecated>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetResetPasswordTokenDeprecatedQueryKey({ id });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getResetPasswordTokenDeprecated>>
> = ({ signal }) => getResetPasswordTokenDeprecated({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getResetPasswordTokenDeprecated>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetResetPasswordTokenDeprecatedQueryResult = NonNullable<
Awaited<ReturnType<typeof getResetPasswordTokenDeprecated>>
>;
export type GetResetPasswordTokenDeprecatedQueryError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Get reset password token
*/
export function useGetResetPasswordTokenDeprecated<
TData = Awaited<ReturnType<typeof getResetPasswordTokenDeprecated>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetResetPasswordTokenDeprecatedPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getResetPasswordTokenDeprecated>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetResetPasswordTokenDeprecatedQueryOptions(
{ id },
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @deprecated
* @summary Get reset password token
*/
export const invalidateGetResetPasswordTokenDeprecated = async (
queryClient: QueryClient,
{ id }: GetResetPasswordTokenDeprecatedPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetResetPasswordTokenDeprecatedQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint creates an invite for a user
* @deprecated
* @summary Create invite
*/
export const createInvite = (
typesPostableInviteDTO?: BodyType<TypesPostableInviteDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateInvite201>({
url: `/api/v1/invite`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: typesPostableInviteDTO,
signal,
});
};
export const getCreateInviteMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createInvite>>,
TError,
{ data?: BodyType<TypesPostableInviteDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createInvite>>,
TError,
{ data?: BodyType<TypesPostableInviteDTO> },
TContext
> => {
const mutationKey = ['createInvite'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createInvite>>,
{ data?: BodyType<TypesPostableInviteDTO> }
> = (props) => {
const { data } = props ?? {};
return createInvite(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateInviteMutationResult = NonNullable<
Awaited<ReturnType<typeof createInvite>>
>;
export type CreateInviteMutationBody =
| BodyType<TypesPostableInviteDTO>
| undefined;
export type CreateInviteMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Create invite
*/
export const useCreateInvite = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createInvite>>,
TError,
{ data?: BodyType<TypesPostableInviteDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createInvite>>,
TError,
{ data?: BodyType<TypesPostableInviteDTO> },
TContext
> => {
return useMutation(getCreateInviteMutationOptions(options));
};
/**
* This endpoint resets the password by token
* @deprecated
* @summary Reset password
*/
export const resetPasswordDeprecated = (
typesPostableResetPasswordDTO?: BodyType<TypesPostableResetPasswordDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/resetPassword`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: typesPostableResetPasswordDTO,
signal,
});
};
export const getResetPasswordDeprecatedMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof resetPasswordDeprecated>>,
TError,
{ data?: BodyType<TypesPostableResetPasswordDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof resetPasswordDeprecated>>,
TError,
{ data?: BodyType<TypesPostableResetPasswordDTO> },
TContext
> => {
const mutationKey = ['resetPasswordDeprecated'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof resetPasswordDeprecated>>,
{ data?: BodyType<TypesPostableResetPasswordDTO> }
> = (props) => {
const { data } = props ?? {};
return resetPasswordDeprecated(data);
};
return { mutationFn, ...mutationOptions };
};
export type ResetPasswordDeprecatedMutationResult = NonNullable<
Awaited<ReturnType<typeof resetPasswordDeprecated>>
>;
export type ResetPasswordDeprecatedMutationBody =
| BodyType<TypesPostableResetPasswordDTO>
| undefined;
export type ResetPasswordDeprecatedMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Reset password
*/
export const useResetPasswordDeprecated = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof resetPasswordDeprecated>>,
TError,
{ data?: BodyType<TypesPostableResetPasswordDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof resetPasswordDeprecated>>,
TError,
{ data?: BodyType<TypesPostableResetPasswordDTO> },
TContext
> => {
return useMutation(getResetPasswordDeprecatedMutationOptions(options));
};
/**
* This endpoint lists all users
* @deprecated
* @summary List users
*/
export const listUsersDeprecated = (signal?: AbortSignal) => {
return GeneratedAPIInstance<ListUsersDeprecated200>({
url: `/api/v1/user`,
method: 'GET',
signal,
});
};
export const getListUsersDeprecatedQueryKey = () => {
return [`/api/v1/user`] as const;
};
export const getListUsersDeprecatedQueryOptions = <
TData = Awaited<ReturnType<typeof listUsersDeprecated>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listUsersDeprecated>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListUsersDeprecatedQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listUsersDeprecated>>
> = ({ signal }) => listUsersDeprecated(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listUsersDeprecated>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListUsersDeprecatedQueryResult = NonNullable<
Awaited<ReturnType<typeof listUsersDeprecated>>
>;
export type ListUsersDeprecatedQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary List users
*/
export function useListUsersDeprecated<
TData = Awaited<ReturnType<typeof listUsersDeprecated>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listUsersDeprecated>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListUsersDeprecatedQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @deprecated
* @summary List users
*/
export const invalidateListUsersDeprecated = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListUsersDeprecatedQueryKey() },
options,
);
return queryClient;
};
/**
* This endpoint returns the user I belong to
* @deprecated
* @summary Get my user
*/
export const getMyUserDeprecated = (signal?: AbortSignal) => {
return GeneratedAPIInstance<GetMyUserDeprecated200>({
url: `/api/v1/user/me`,
method: 'GET',
signal,
});
};
export const getGetMyUserDeprecatedQueryKey = () => {
return [`/api/v1/user/me`] as const;
};
export const getGetMyUserDeprecatedQueryOptions = <
TData = Awaited<ReturnType<typeof getMyUserDeprecated>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMyUserDeprecated>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetMyUserDeprecatedQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getMyUserDeprecated>>
> = ({ signal }) => getMyUserDeprecated(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getMyUserDeprecated>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetMyUserDeprecatedQueryResult = NonNullable<
Awaited<ReturnType<typeof getMyUserDeprecated>>
>;
export type GetMyUserDeprecatedQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Get my user
*/
export function useGetMyUserDeprecated<
TData = Awaited<ReturnType<typeof getMyUserDeprecated>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMyUserDeprecated>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMyUserDeprecatedQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @deprecated
* @summary Get my user
*/
export const invalidateGetMyUserDeprecated = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMyUserDeprecatedQueryKey() },
options,
);
return queryClient;
};
/**
* This endpoint initiates the forgot password flow by sending a reset password email
* @summary Forgot password
@@ -1834,189 +1371,6 @@ export const invalidateGetRolesByUserID = async (
return queryClient;
};
/**
* This endpoint assigns the role to the user roles by user id
* @deprecated
* @summary Set user roles
*/
export const setRoleByUserID = (
{ id }: SetRoleByUserIDPathParameters,
typesPostableRoleDTO?: BodyType<TypesPostableRoleDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/users/${id}/roles`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: typesPostableRoleDTO,
signal,
});
};
export const getSetRoleByUserIDMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof setRoleByUserID>>,
TError,
{
pathParams: SetRoleByUserIDPathParameters;
data?: BodyType<TypesPostableRoleDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof setRoleByUserID>>,
TError,
{
pathParams: SetRoleByUserIDPathParameters;
data?: BodyType<TypesPostableRoleDTO>;
},
TContext
> => {
const mutationKey = ['setRoleByUserID'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof setRoleByUserID>>,
{
pathParams: SetRoleByUserIDPathParameters;
data?: BodyType<TypesPostableRoleDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return setRoleByUserID(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type SetRoleByUserIDMutationResult = NonNullable<
Awaited<ReturnType<typeof setRoleByUserID>>
>;
export type SetRoleByUserIDMutationBody =
| BodyType<TypesPostableRoleDTO>
| undefined;
export type SetRoleByUserIDMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Set user roles
*/
export const useSetRoleByUserID = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof setRoleByUserID>>,
TError,
{
pathParams: SetRoleByUserIDPathParameters;
data?: BodyType<TypesPostableRoleDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof setRoleByUserID>>,
TError,
{
pathParams: SetRoleByUserIDPathParameters;
data?: BodyType<TypesPostableRoleDTO>;
},
TContext
> => {
return useMutation(getSetRoleByUserIDMutationOptions(options));
};
/**
* This endpoint removes a role from the user by user id and role id
* @deprecated
* @summary Remove a role from user
*/
export const removeUserRoleByUserIDAndRoleID = (
{ id, roleId }: RemoveUserRoleByUserIDAndRoleIDPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/users/${id}/roles/${roleId}`,
method: 'DELETE',
signal,
});
};
export const getRemoveUserRoleByUserIDAndRoleIDMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof removeUserRoleByUserIDAndRoleID>>,
TError,
{ pathParams: RemoveUserRoleByUserIDAndRoleIDPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof removeUserRoleByUserIDAndRoleID>>,
TError,
{ pathParams: RemoveUserRoleByUserIDAndRoleIDPathParameters },
TContext
> => {
const mutationKey = ['removeUserRoleByUserIDAndRoleID'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof removeUserRoleByUserIDAndRoleID>>,
{ pathParams: RemoveUserRoleByUserIDAndRoleIDPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return removeUserRoleByUserIDAndRoleID(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type RemoveUserRoleByUserIDAndRoleIDMutationResult = NonNullable<
Awaited<ReturnType<typeof removeUserRoleByUserIDAndRoleID>>
>;
export type RemoveUserRoleByUserIDAndRoleIDMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Remove a role from user
*/
export const useRemoveUserRoleByUserIDAndRoleID = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof removeUserRoleByUserIDAndRoleID>>,
TError,
{ pathParams: RemoveUserRoleByUserIDAndRoleIDPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof removeUserRoleByUserIDAndRoleID>>,
TError,
{ pathParams: RemoveUserRoleByUserIDAndRoleIDPathParameters },
TContext
> => {
return useMutation(getRemoveUserRoleByUserIDAndRoleIDMutationOptions(options));
};
/**
* This endpoint returns the user I belong to
* @summary Get my user v2

View File

@@ -437,6 +437,17 @@ describe('Create Alert Channel', () => {
render(<CreateAlertChannels preType={ChannelType.GoogleChat} />);
});
// paste instead of type: a per-keystroke re-render of the whole form
// pushes these tests past the 5s jest timeout on slower CI runners
async function fillField(
user: ReturnType<typeof userEvent.setup>,
testId: string,
value: string,
): Promise<void> {
await user.click(screen.getByTestId(testId));
await user.paste(value);
}
it('Should check if the selected item in the type dropdown has text "Google Chat"', () => {
expect(screen.getByText('Google Chat')).toBeInTheDocument();
});
@@ -463,14 +474,8 @@ describe('Create Alert Channel', () => {
it('Should check if saving with a webhook url outside chat.googleapis.com displays error notification', async () => {
const user = userEvent.setup();
await user.type(
screen.getByTestId('channel-name-textbox'),
'gchat-channel',
);
await user.type(
screen.getByTestId('webhook-url-textbox'),
'https://example.com/webhook',
);
await fillField(user, 'channel-name-textbox', 'gchat-channel');
await fillField(user, 'webhook-url-textbox', 'https://example.com/webhook');
await user.click(screen.getByTestId('save-channel-button'));
@@ -496,11 +501,8 @@ describe('Create Alert Channel', () => {
const user = userEvent.setup();
await user.type(
screen.getByTestId('channel-name-textbox'),
'gchat-channel',
);
await user.type(screen.getByTestId('webhook-url-textbox'), validWebhookUrl);
await fillField(user, 'channel-name-textbox', 'gchat-channel');
await fillField(user, 'webhook-url-textbox', validWebhookUrl);
await user.click(screen.getByTestId('save-channel-button'));

View File

@@ -130,6 +130,28 @@ describe('Footer utils', () => {
};
expect(validateCreateAlertState(currentArgs)).toBeNull();
});
it('when threshold channels are null', () => {
const currentArgs: BuildCreateAlertRulePayloadArgs = {
...args,
basicAlertState: {
...args.basicAlertState,
name: 'test name',
},
thresholdState: {
...args.thresholdState,
thresholds: [
{
...args.thresholdState.thresholds[0],
channels: null as unknown as string[],
},
],
},
};
expect(validateCreateAlertState(currentArgs)).toBe(
'Please select at least one channel for each threshold or enable routing policies',
);
});
});
describe('getNotificationSettingsProps', () => {

View File

@@ -44,7 +44,8 @@ export function validateCreateAlertState(
if (!threshold.label) {
return 'Please enter a label for each threshold';
}
if (!notificationSettings.routingPolicies && !threshold.channels.length) {
// this runs during render, so a throw here takes down the whole page
if (!notificationSettings.routingPolicies && !threshold.channels?.length) {
return 'Please select at least one channel for each threshold or enable routing policies';
}
}

View File

@@ -316,6 +316,34 @@ describe('CreateAlertV2 utils', () => {
});
});
describe('getThresholdStateFromAlertDef null channels', () => {
it('falls back to an empty array so downstream consumers never see null', () => {
const def: PostableAlertRuleV2 = {
...defaultPostableAlertRuleV2,
condition: {
...defaultPostableAlertRuleV2.condition,
thresholds: {
kind: 'basic',
spec: [
{
name: 'critical',
target: 1,
targetUnit: UniversalYAxisUnit.MINUTES,
channels: null as unknown as string[],
matchType: AlertThresholdMatchType.AT_LEAST_ONCE,
op: AlertThresholdOperator.IS_ABOVE,
},
],
},
},
};
expect(
getThresholdStateFromAlertDef(def).thresholds[0].channels,
).toStrictEqual([]);
});
});
describe('normalizeOperator', () => {
it.each([
['1', AlertThresholdOperator.IS_ABOVE],

View File

@@ -258,7 +258,9 @@ export function getThresholdStateFromAlertDef(
recoveryThresholdValue: null,
unit: threshold.targetUnit,
color: getColorForThreshold(threshold.name),
channels: threshold.channels,
// rules created outside the UI can come back with a null channels
// field; drop the guard once the API enforces the schema
channels: threshold.channels ?? [],
})) || [],
selectedQuery: alertDef.condition.selectedQueryName || '',
operator:

View File

@@ -82,7 +82,7 @@ export function getHostMetricsQueryPayload(
start: number,
end: number,
): ReturnType<typeof getHostQueryPayload> {
return getHostQueryPayload(host.hostName, start, end);
return getHostQueryPayload(host.hostName, start, end, true);
}
export { hostWidgetInfo };

View File

@@ -562,13 +562,9 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
],
having: [
{
columnName: `MAX(${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_CONDITION_READY})`,
op: '=',
value: 1,
},
],
having: {
expression: `max(${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_CONDITION_READY}) = 1`,
},
legend: `{{${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME}}}`,
limit: null,
orderBy: [],
@@ -648,13 +644,9 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
],
having: [
{
columnName: `MAX(${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_CONDITION_READY})`,
op: '=',
value: 0,
},
],
having: {
expression: `max(${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_CONDITION_READY}) = 0`,
},
legend: `{{${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME}}}`,
limit: null,
orderBy: [],

View File

@@ -1208,13 +1208,9 @@ export const getNamespaceMetricsQueryPayload = (
type: 'tag',
},
],
having: [
{
columnName: `MAX(${INFRA_MONITORING_ATTR_KEYS.K8S_REPLICASET_DESIRED})`,
op: '>',
value: 0,
},
],
having: {
expression: `max(${INFRA_MONITORING_ATTR_KEYS.K8S_REPLICASET_DESIRED}) > 0`,
},
legend: 'desired',
limit: null,
orderBy: [],
@@ -1261,13 +1257,9 @@ export const getNamespaceMetricsQueryPayload = (
type: 'tag',
},
],
having: [
{
columnName: `MAX(${INFRA_MONITORING_ATTR_KEYS.K8S_REPLICASET_DESIRED})`,
op: '>',
value: 0,
},
],
having: {
expression: `max(${INFRA_MONITORING_ATTR_KEYS.K8S_REPLICASET_AVAILABLE}) > 0`,
},
legend: 'available',
limit: null,
orderBy: [],

View File

@@ -1,9 +1,19 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import type { Having } from 'types/api/queryBuilder/queryBuilderData';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import type { Having as HavingV5 } from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
const buildSumGreaterThanZeroHaving = (
metricKey: string,
useV5HavingFormat: boolean,
): Having[] | HavingV5 =>
useV5HavingFormat
? { expression: `sum(${metricKey}) > 0` }
: [{ columnName: `SUM(${metricKey})`, op: '>', value: 0 }];
export const getPodQueryPayload = (
clusterName: string,
podName: string,
@@ -1540,6 +1550,7 @@ export const getHostQueryPayload = (
hostName: string,
start: number,
end: number,
useV5HavingFormat = false,
): GetQueryResultsProps[] => {
const hostNameKey = 'host.name';
const cpuTimeKey = 'system.cpu.time';
@@ -1802,13 +1813,7 @@ export const getHostQueryPayload = (
type: 'tag',
},
],
having: [
{
columnName: `SUM(${fsUsageKey})`,
op: '>',
value: 0,
},
],
having: buildSumGreaterThanZeroHaving(fsUsageKey, useV5HavingFormat),
legend: '{{mountpoint}}',
limit: null,
orderBy: [],
@@ -1857,13 +1862,7 @@ export const getHostQueryPayload = (
type: 'tag',
},
],
having: [
{
columnName: `SUM(${fsUsageKey})`,
op: '>',
value: 0,
},
],
having: buildSumGreaterThanZeroHaving(fsUsageKey, useV5HavingFormat),
legend: '{{mountpoint}}',
limit: null,
orderBy: [],
@@ -2089,13 +2088,7 @@ export const getHostQueryPayload = (
type: 'tag',
},
],
having: [
{
columnName: `SUM(${netIoKey})`,
op: '>',
value: 0,
},
],
having: buildSumGreaterThanZeroHaving(netIoKey, useV5HavingFormat),
legend: '{{device}}::{{direction}}',
limit: 30,
orderBy: [],
@@ -2551,13 +2544,7 @@ export const getHostQueryPayload = (
type: 'tag',
},
],
having: [
{
columnName: `SUM(${diskOpsKey})`,
op: '>',
value: 0,
},
],
having: buildSumGreaterThanZeroHaving(diskOpsKey, useV5HavingFormat),
legend: '{{device}}::{{direction}}',
limit: null,
orderBy: [],
@@ -2626,13 +2613,7 @@ export const getHostQueryPayload = (
type: 'tag',
},
],
having: [
{
columnName: `SUM(${diskPendingKey})`,
op: '>',
value: 0,
},
],
having: buildSumGreaterThanZeroHaving(diskPendingKey, useV5HavingFormat),
legend: '{{device}}',
limit: null,
orderBy: [],
@@ -2708,13 +2689,7 @@ export const getHostQueryPayload = (
type: 'tag',
},
],
having: [
{
columnName: `SUM(${diskOpTimeKey})`,
op: '>',
value: 0,
},
],
having: buildSumGreaterThanZeroHaving(diskOpTimeKey, useV5HavingFormat),
legend: '{{device}}::{{direction}}',
limit: null,
orderBy: [],

View File

@@ -387,4 +387,42 @@ describe('useOptionsMenu', () => {
expect(remaining).toHaveLength(seedColumns.length);
});
});
describe('fieldsSelector.value drops legacy columns without a name', () => {
it('excludes entries missing name while keeping valid columns', () => {
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
data: { data: { data: { keys: {} } } },
isFetching: false,
});
(usePreferenceContext as jest.Mock).mockReturnValue({
traces: {
preferences: {
columns: [
{ name: 'body', fieldContext: 'log' },
{ key: 'legacy-key-no-name', fieldContext: 'log' },
{ name: 'timestamp', fieldContext: 'log' },
],
formatting: { format: 'table', maxLines: 1, fontSize: 'small' },
},
updateColumns: mockUpdateColumns,
updateFormatting: mockUpdateFormatting,
},
logs: {
preferences: { columns: [], formatting: {} },
updateColumns: mockUpdateColumns,
updateFormatting: mockUpdateFormatting,
},
});
const { result } = renderHook(() =>
useOptionsMenu({
dataSource: DataSource.TRACES,
aggregateOperator: 'count',
}),
);
const fields = result.current.config.fieldsSelector?.value ?? [];
expect(fields.map((f) => f.name)).toStrictEqual(['body', 'timestamp']);
});
});
});

View File

@@ -399,7 +399,7 @@ const useOptionsMenu = ({
onReorder: reorderSelectColumns,
},
fieldsSelector: {
value: preferences?.columns ?? [],
value: preferences?.columns?.filter((item) => has(item, 'name')) ?? [],
onFieldsChange: updateColumns,
},
format: {

View File

@@ -80,7 +80,6 @@ function ResourceAttributesFilter({
<div className="environment-selector">
<Select
getPopupContainer={popupContainer}
key={selectedEnvironments.join('')}
showSearch
mode="multiple"
value={selectedEnvironments}

View File

@@ -0,0 +1,175 @@
import { ReactNode } from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Router } from 'react-router-dom';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ROUTES from 'constants/routes';
import { createMemoryHistory, MemoryHistory } from 'history';
import { ResourceProvider } from 'hooks/useResourceAttribute';
import { IResourceAttribute } from 'hooks/useResourceAttribute/types';
import { encode } from 'js-base64';
import ResourceAttributesFilter from '../ResourceAttributesFilter';
jest.mock('lib/history', () => ({
__esModule: true,
default: {
push: jest.fn(),
location: { search: '', pathname: '/' },
},
}));
jest.mock('api/metrics/getResourceAttributes', () => ({
getResourceAttributesTagKeys: jest.fn(),
getResourceAttributesTagValues: jest.fn(),
}));
// eslint-disable-next-line import/first, import/order
import {
getResourceAttributesTagKeys,
getResourceAttributesTagValues,
// eslint-disable-next-line import/newline-after-import
} from 'api/metrics/getResourceAttributes';
// eslint-disable-next-line import/first, import/order
import history from 'lib/history';
const mockTagKeys = getResourceAttributesTagKeys as jest.MockedFunction<
typeof getResourceAttributesTagKeys
>;
const mockTagValues = getResourceAttributesTagValues as jest.MockedFunction<
typeof getResourceAttributesTagValues
>;
function tagKeysPayload(keys: string[]): never {
return {
statusCode: 200,
error: null,
message: 'ok',
payload: {
data: {
attributeKeys: keys.map((key) => ({
key,
dataType: 'string',
type: 'resource',
isColumn: false,
})),
},
},
} as unknown as never;
}
function tagValuesPayload(values: string[]): never {
return {
statusCode: 200,
error: null,
message: 'ok',
payload: { data: { stringAttributeValues: values } },
} as unknown as never;
}
function seedUrl(queries: IResourceAttribute[], pathname: string): void {
const location = history.location as { search: string; pathname: string };
location.search = queries.length
? `?resourceAttribute=${encode(JSON.stringify(queries))}`
: '';
location.pathname = pathname;
}
function renderFilter(pathname: string): MemoryHistory {
const routerHistory = createMemoryHistory({
initialEntries: [`${pathname}${history.location.search}`],
});
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
function Wrapper({ children }: { children: ReactNode }): JSX.Element {
return (
<QueryClientProvider client={queryClient}>
<Router history={routerHistory}>
<ResourceProvider>{children}</ResourceProvider>
</Router>
</QueryClientProvider>
);
}
render(
<Wrapper>
<ResourceAttributesFilter />
</Wrapper>,
);
return routerHistory;
}
describe('ResourceAttributesFilter', () => {
beforeEach(() => {
mockTagKeys.mockReset();
mockTagValues.mockReset();
mockTagKeys.mockResolvedValue(
tagKeysPayload(['resource_deployment.environment']),
);
mockTagValues.mockResolvedValue(tagValuesPayload(['production', 'staging']));
seedUrl([], '/');
});
it('shows every applied filter on the service map, including ones it cannot apply', async () => {
seedUrl(
[
{
id: 'svc',
tagKey: 'resource_service_name',
operator: 'IN',
tagValue: ['frontend'],
},
{
id: 'env',
tagKey: 'resource_deployment.environment',
operator: 'IN',
tagValue: ['production'],
},
],
ROUTES.SERVICE_MAP,
);
renderFilter(ROUTES.SERVICE_MAP);
await waitFor(() =>
expect(screen.getByText(/service\.name/)).toBeInTheDocument(),
);
await waitFor(() =>
expect(
screen
.getByTestId('resource-environment-filter')
.querySelector('.ant-select-selection-item'),
).toHaveTextContent('production'),
);
});
it('keeps the environment dropdown open so more than one environment can be picked', async () => {
const user = userEvent.setup();
renderFilter('/services');
const environmentFilter = screen.getByTestId('resource-environment-filter');
await user.click(
environmentFilter.querySelector('input') as HTMLInputElement,
);
await user.click(await screen.findByTitle('production'));
await waitFor(() =>
expect(
screen.getByTitle('staging').closest('.ant-select-dropdown'),
).not.toHaveClass('ant-select-dropdown-hidden'),
);
await user.click(screen.getByTitle('staging'));
await waitFor(() => {
const selected = Array.from(
environmentFilter.querySelectorAll('.ant-select-selection-item-content'),
).map((node) => node.textContent);
expect(selected).toStrictEqual(['production', 'staging']);
});
});
});

View File

@@ -1,12 +1,10 @@
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { encode } from 'js-base64';
import { whilelistedKeys } from './config';
import { ResourceContext } from './context';
import {
IResourceAttribute,
@@ -195,16 +193,9 @@ function ResourceProvider({ children }: Props): JSX.Element {
setOptionsData({ mode: undefined, options: [] });
}, [dispatchQueries]);
const getVisibleQueries = useMemo(() => {
if (pathname === ROUTES.SERVICE_MAP) {
return queries.filter((query) => whilelistedKeys.includes(query.tagKey));
}
return queries;
}, [queries, pathname]);
const value: IResourceAttributeProps = useMemo(
() => ({
queries: getVisibleQueries,
queries,
staging,
handleClearAll,
handleClose,
@@ -227,7 +218,7 @@ function ResourceProvider({ children }: Props): JSX.Element {
staging,
selectedQuery,
optionsData,
getVisibleQueries,
queries,
],
);

View File

@@ -504,22 +504,23 @@ describe('ResourceProvider', () => {
});
});
describe('getVisibleQueries (SERVICE_MAP filtering)', () => {
it('filters queries down to whitelisted keys on SERVICE_MAP', () => {
const seeded = [
{
id: 'a',
tagKey: 'resource_service_name',
operator: 'IN',
tagValue: ['frontend'],
},
{
id: 'b',
tagKey: 'resource_k8s_cluster_name',
operator: 'IN',
tagValue: ['prod'],
},
];
describe('SERVICE_MAP', () => {
const seeded = [
{
id: 'a',
tagKey: 'resource_service_name',
operator: 'IN',
tagValue: ['frontend'],
},
{
id: 'b',
tagKey: 'resource_k8s_cluster_name',
operator: 'IN',
tagValue: ['prod'],
},
];
it('exposes every query from the URL, including ones the map cannot apply', () => {
mockLibHistory(
`?resourceAttribute=${encode(JSON.stringify(seeded))}`,
ROUTES.SERVICE_MAP,
@@ -532,24 +533,10 @@ describe('ResourceProvider', () => {
wrapper: createWrapper({ routerHistory }),
});
expect(result.current.queries).toStrictEqual([seeded[1]]);
expect(result.current.queries).toStrictEqual(seeded);
});
it('returns all queries on non-SERVICE_MAP routes', () => {
const seeded = [
{
id: 'a',
tagKey: 'resource_service_name',
operator: 'IN',
tagValue: ['frontend'],
},
{
id: 'b',
tagKey: 'resource_k8s_cluster_name',
operator: 'IN',
tagValue: ['prod'],
},
];
mockLibHistory(
`?resourceAttribute=${encode(JSON.stringify(seeded))}`,
'/services',

View File

@@ -1,7 +1,10 @@
import ROUTES from 'constants/routes';
import { whilelistedKeys } from '../config';
import { mappingWithRoutesAndKeys } from '../utils';
import {
filterServiceMapSupportedQueries,
mappingWithRoutesAndKeys,
} from '../utils';
describe('useResourceAttribute config', () => {
describe('whilelistedKeys', () => {
@@ -74,4 +77,29 @@ describe('useResourceAttribute config', () => {
expect(result).toStrictEqual(allFilters);
});
});
describe('filterServiceMapSupportedQueries', () => {
const environmentQuery = {
id: 'env',
tagKey: 'resource_deployment_environment',
operator: 'IN',
tagValue: ['production'],
};
const serviceQuery = {
id: 'svc',
tagKey: 'resource_service_name',
operator: 'IN',
tagValue: ['frontend'],
};
it('should keep only the queries the service map can filter on', () => {
expect(
filterServiceMapSupportedQueries([environmentQuery, serviceQuery]),
).toStrictEqual([environmentQuery]);
});
it('should return an empty list when no query is supported', () => {
expect(filterServiceMapSupportedQueries([serviceQuery])).toStrictEqual([]);
});
});
});

View File

@@ -281,3 +281,8 @@ export const mappingWithRoutesAndKeys = (
}
return filters;
};
export const filterServiceMapSupportedQueries = (
queries: IResourceAttribute[],
): IResourceAttribute[] =>
queries.filter((query) => whilelistedKeys.includes(query.tagKey));

View File

@@ -1,6 +1,6 @@
//@ts-nocheck
import { useEffect, useRef } from 'react';
import { useEffect, useMemo, useRef } from 'react';
// eslint-disable-next-line no-restricted-imports
import { connect } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router-dom';
@@ -11,6 +11,7 @@ import ResourceAttributesFilter from 'container/ResourceAttributesFilter';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { whilelistedKeys } from 'hooks/useResourceAttribute/config';
import { IResourceAttribute } from 'hooks/useResourceAttribute/types';
import { filterServiceMapSupportedQueries } from 'hooks/useResourceAttribute/utils';
import { getDetailedServiceMapItems, ServiceMapStore } from 'store/actions';
import { AppState } from 'store/reducers';
import styled from 'styled-components';
@@ -70,32 +71,37 @@ function ServiceMap(props: ServiceMapProps): JSX.Element {
const { queries } = useResourceAttribute();
const supportedQueries = useMemo(
() => filterServiceMapSupportedQueries(queries),
[queries],
);
useEffect(() => {
/*
Call the apis only when the route is loaded.
Check this issue: https://github.com/SigNoz/signoz/issues/110
*/
getDetailedServiceMapItems(globalTime, queries);
}, [globalTime, getDetailedServiceMapItems, queries]);
getDetailedServiceMapItems(globalTime, supportedQueries);
}, [globalTime, getDetailedServiceMapItems, supportedQueries]);
useEffect(() => {
fgRef.current && fgRef.current.d3Force('charge').strength(-400);
});
if (serviceMap.loading) {
return <Spinner size="large" tip="Loading..." />;
}
const renderBody = (): JSX.Element => {
if (serviceMap.loading) {
return <Spinner size="large" tip="Loading..." />;
}
if (serviceMap.items.length === 0) {
return <Card>No Service Found</Card>;
}
return <Map fgRef={fgRef} serviceMap={serviceMap} />;
};
if (!serviceMap.loading && serviceMap.items.length === 0) {
return (
<Container>
<ResourceAttributesFilter />
<Card>No Service Found</Card>
</Container>
);
}
return (
<div className="service-map-container">
<Container className="service-map-container">
<ResourceAttributesFilter
suffixIcon={
<TextToolTip
@@ -108,8 +114,8 @@ function ServiceMap(props: ServiceMapProps): JSX.Element {
}
/>
<Map fgRef={fgRef} serviceMap={serviceMap} />
</div>
{renderBody()}
</Container>
);
}

View File

@@ -10,40 +10,6 @@ import (
)
func (provider *provider) addUserRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/invite", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.CreateInvite), handler.OpenAPIDef{
ID: "CreateInvite",
Tags: []string{"users"},
Summary: "Create invite",
Description: "This endpoint creates an invite for a user",
Request: new(types.PostableInvite),
RequestContentType: "application/json",
Response: new(types.Invite),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: true,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/user", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.ListUsersDeprecated), handler.OpenAPIDef{
ID: "ListUsersDeprecated",
Tags: []string{"users"},
Summary: "List users",
Description: "This endpoint lists all users",
Request: nil,
RequestContentType: "",
Response: make([]*types.DeprecatedUser, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: true,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/users", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.ListUsers), handler.OpenAPIDef{
ID: "ListUsers",
Tags: []string{"users"},
@@ -61,23 +27,6 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/user/me", handler.New(provider.authzMiddleware.OpenAccess(provider.userHandler.GetMyUserDeprecated), handler.OpenAPIDef{
ID: "GetMyUserDeprecated",
Tags: []string{"users"},
Summary: "Get my user",
Description: "This endpoint returns the user I belong to",
Request: nil,
RequestContentType: "",
Response: new(types.DeprecatedUser),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: true,
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.authzMiddleware.OpenAccess(provider.userHandler.GetMyUser), handler.OpenAPIDef{
ID: "GetMyUser",
Tags: []string{"users"},
@@ -180,23 +129,6 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/getResetPasswordToken/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.GetResetPasswordTokenDeprecated), handler.OpenAPIDef{
ID: "GetResetPasswordTokenDeprecated",
Tags: []string{"users"},
Summary: "Get reset password token",
Description: "This endpoint returns the reset password token by id",
Request: nil,
RequestContentType: "",
Response: new(types.ResetPasswordToken),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: true,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/users/{id}/reset_password_tokens", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.GetResetPasswordToken), handler.OpenAPIDef{
ID: "GetResetPasswordToken",
Tags: []string{"users"},
@@ -248,23 +180,6 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/resetPassword", handler.New(provider.authzMiddleware.OpenAccess(provider.userHandler.ResetPassword), handler.OpenAPIDef{
ID: "ResetPasswordDeprecated",
Tags: []string{"users"},
Summary: "Reset password",
Description: "This endpoint resets the password by token",
Request: new(types.PostableResetPassword),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: true,
SecuritySchemes: []handler.OpenAPISecurityScheme{},
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/users/me/factor_password", handler.New(provider.authzMiddleware.OpenAccess(provider.userHandler.ChangePassword), handler.OpenAPIDef{
ID: "UpdateMyPassword",
Tags: []string{"users"},
@@ -333,40 +248,6 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/users/{id}/roles", handler.New(provider.authzMiddleware.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: true,
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.authzMiddleware.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: true,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/roles/{id}/users", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.GetUsersByRoleID), handler.OpenAPIDef{
ID: "GetUsersByRoleID",
Tags: []string{"users"},

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24"><defs><style>.cls-1{fill:#aecbfa;}.cls-1,.cls-2,.cls-3{fill-rule:evenodd;}.cls-2{fill:#669df6;}.cls-3{fill:#4285f4;}</style></defs><title>Icon_24px_SQL_Color</title><g data-name="Product Icons"><g ><polygon class="cls-1" points="4.67 10.44 4.67 13.45 12 17.35 12 14.34 4.67 10.44"/><polygon class="cls-1" points="4.67 15.09 4.67 18.1 12 22 12 18.99 4.67 15.09"/><polygon class="cls-2" points="12 17.35 19.33 13.45 19.33 10.44 12 14.34 12 17.35"/><polygon class="cls-2" points="12 22 19.33 18.1 19.33 15.09 12 18.99 12 22"/><polygon class="cls-3" points="19.33 8.91 19.33 5.9 12 2 12 5.01 19.33 8.91"/><polygon class="cls-2" points="12 2 4.67 5.9 4.67 8.91 12 5.01 12 2"/><polygon class="cls-1" points="4.67 5.87 4.67 8.89 12 12.79 12 9.77 4.67 5.87"/><polygon class="cls-2" points="12 12.79 19.33 8.89 19.33 5.87 12 9.77 12 12.79"/></g></g></svg>

After

Width:  |  Height:  |  Size: 933 B

View File

@@ -0,0 +1,136 @@
{
"id": "cloudsql_mysql",
"title": "GCP Cloud SQL for MySQL",
"icon": "file://icon.svg",
"overview": "file://overview.md",
"supportedSignals": {
"metrics": true,
"logs": true
},
"dataCollected": {
"metrics": [
{
"name": "cloudsql.googleapis.com/database/up",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "cloudsql.googleapis.com/database/instance_state",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "cloudsql.googleapis.com/database/replication/replica_lag",
"unit": "Seconds",
"type": "Gauge",
"description": ""
},
{
"name": "cloudsql.googleapis.com/database/cpu/utilization",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "cloudsql.googleapis.com/database/memory/utilization",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "cloudsql.googleapis.com/database/disk/utilization",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "cloudsql.googleapis.com/database/network/connections",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "cloudsql.googleapis.com/database/mysql/queries",
"unit": "Count",
"type": "Sum",
"description": ""
},
{
"name": "cloudsql.googleapis.com/database/mysql/dml_operations_count",
"unit": "Count",
"type": "Sum",
"description": ""
},
{
"name": "cloudsql.googleapis.com/database/mysql/threads",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "cloudsql.googleapis.com/database/mysql/innodb/buffer_pool_reads_count",
"unit": "Count",
"type": "Sum",
"description": ""
},
{
"name": "cloudsql.googleapis.com/database/mysql/innodb/buffer_pool_read_requests_count",
"unit": "Count",
"type": "Sum",
"description": ""
},
{
"name": "cloudsql.googleapis.com/database/mysql/slow_queries_count",
"unit": "Count",
"type": "Sum",
"description": ""
},
{
"name": "cloudsql.googleapis.com/database/mysql/aborted_connects_count",
"unit": "Count",
"type": "Sum",
"description": ""
},
{
"name": "cloudsql.googleapis.com/database/mysql/innodb/deadlocks_count",
"unit": "Count",
"type": "Sum",
"description": ""
},
{
"name": "cloudsql.googleapis.com/database/mysql/innodb/row_lock_waits_count",
"unit": "Count",
"type": "Sum",
"description": ""
},
{
"name": "cloudsql.googleapis.com/database/disk/read_ops_count",
"unit": "Count",
"type": "Sum",
"description": ""
},
{
"name": "cloudsql.googleapis.com/database/disk/write_ops_count",
"unit": "Count",
"type": "Sum",
"description": ""
}
],
"logs": []
},
"telemetryCollectionStrategy": {
"gcp": {}
},
"assets": {
"dashboards": [
{
"id": "overview",
"title": "GCP Cloud SQL for MySQL Overview",
"description": "Overview of GCP Cloud SQL for MySQL metrics",
"definition": "file://assets/dashboards/overview.json"
}
]
}
}

View File

@@ -0,0 +1,3 @@
### Monitor GCP Cloud SQL for MySQL with SigNoz
Collect key GCP Cloud SQL for MySQL metrics and view them with an out of the box dashboard.

View File

@@ -784,40 +784,57 @@
{
"kind": "time_series",
"spec": {
"name": "A",
"plugin": {
"kind": "signoz/BuilderQuery",
"kind": "signoz/CompositeQuery",
"spec": {
"name": "A",
"signal": "metrics",
"source": "",
"aggregations": [
"queries": [
{
"metricName": "cloudsql.googleapis.com/database/cpu/utilization",
"temporality": "",
"timeAggregation": "max",
"spaceAggregation": "max",
"reduceTo": "avg"
}
],
"disabled": false,
"filter": {
"expression": "project_id = $project_id AND database_id in $database_id AND gcp.resource_type = 'cloudsql_database' "
},
"groupBy": [
"type": "builder_query",
"spec": {
"name": "A",
"stepInterval": 0,
"signal": "metrics",
"source": "",
"aggregations": [
{
"metricName": "cloudsql.googleapis.com/database/cpu/utilization",
"temporality": "",
"timeAggregation": "max",
"spaceAggregation": "max",
"reduceTo": ""
}
],
"disabled": false,
"filter": {
"expression": "project_id = $project_id AND database_id in $database_id AND gcp.resource_type = 'cloudsql_database' "
},
"groupBy": [
{
"name": "database_id",
"signal": "",
"fieldContext": "attribute",
"fieldDataType": "string"
}
],
"order": null,
"selectFields": null,
"secondaryAggregations": null,
"functions": null,
"legend": "{{database_id}}"
}
},
{
"name": "database_id",
"signal": "",
"fieldContext": "attribute",
"fieldDataType": "string"
"type": "builder_formula",
"spec": {
"name": "F1",
"expression": "100 * A",
"disabled": false,
"order": null,
"functions": null,
"legend": "{{database_id}}"
}
}
],
"order": [],
"having": {
"expression": ""
},
"functions": [],
"legend": "{{database_id}}"
]
}
}
}
@@ -1417,4 +1434,4 @@
"refreshInterval": "",
"links": []
}
}
}

View File

@@ -37,53 +37,6 @@ 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) {
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 })
}
userIDs := make([]valuer.UUID, len(users))
for idx, user := range users {
userIDs[idx] = user.ID
}
userRoles, err := module.userRoleStore.ListUserRolesByOrgIDAndUserIDs(ctx, orgID, userIDs)
if err != nil {
return nil, err
}
// Build userID → role name mapping directly from the joined Role
userIDToRoleNames := make(map[valuer.UUID][]string)
for _, ur := range userRoles {
if ur.Role != nil {
userIDToRoleNames[ur.UserID] = append(userIDToRoleNames[ur.UserID], ur.Role.Name)
}
}
deprecatedUsers := make([]*types.DeprecatedUser, 0, len(users))
for _, user := range users {
roleNames := userIDToRoleNames[user.ID]
if len(roleNames) == 0 {
return nil, errors.Newf(errors.TypeInternal, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found for user: %s", user.ID.String())
}
role := authtypes.SigNozManagedRoleToExistingLegacyRole[roleNames[0]]
deprecatedUsers = append(deprecatedUsers, types.NewDeprecatedUserFromUserAndRole(user, role))
}
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 {
@@ -101,58 +54,10 @@ func (module *getter) ListUsersByOrgID(ctx context.Context, orgID valuer.UUID) (
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)
if err != nil {
return nil, err
}
if len(userRoles) == 0 {
return nil, errors.New(errors.TypeInternal, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found")
}
if userRoles[0].Role == nil {
return nil, errors.New(errors.TypeInternal, 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)
if err != nil {
return nil, err
}
if len(userRoles) == 0 {
return nil, errors.New(errors.TypeInternal, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found")
}
if userRoles[0].Role == nil {
return nil, errors.New(errors.TypeInternal, 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) ListUsersByEmailAndOrgIDs(ctx context.Context, email valuer.Email, orgIDs []valuer.UUID) ([]*types.User, error) {
return module.store.ListUsersByEmailAndOrgIDs(ctx, email, orgIDs)
}

View File

@@ -61,33 +61,6 @@ func (handler *handler) CreateUser(rw http.ResponseWriter, r *http.Request) {
render.Success(rw, http.StatusCreated, types.Identifiable{ID: user.ID})
}
func (handler *handler) CreateInvite(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
var req types.PostableInvite
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
render.Error(rw, err)
return
}
invites, err := handler.setter.CreateBulkInvite(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.IdentityID()), valuer.MustNewEmail(claims.Email), &types.PostableBulkInviteRequest{
Invites: []types.PostableInvite{req},
})
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusCreated, invites[0])
}
func (handler *handler) GetUser(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
@@ -120,25 +93,6 @@ func (handler *handler) GetUser(w http.ResponseWriter, r *http.Request) {
render.Success(w, http.StatusOK, userWithRoles)
}
func (handler *handler) GetMyUserDeprecated(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 := handler.getter.GetDeprecatedUserByOrgIDAndID(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.UserID))
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, user)
}
func (handler *handler) GetMyUser(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
@@ -194,25 +148,6 @@ func (handler *handler) UpdateMyUser(w http.ResponseWriter, r *http.Request) {
render.Success(w, http.StatusNoContent, nil)
}
func (handler *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 := handler.getter.ListDeprecatedUsersByOrgID(ctx, valuer.MustNewUUID(claims.OrgID))
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, users)
}
func (handler *handler) ListUsers(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
@@ -284,33 +219,6 @@ func (handler *handler) DeleteUser(w http.ResponseWriter, r *http.Request) {
render.Success(w, http.StatusNoContent, nil)
}
func (handler *handler) GetResetPasswordTokenDeprecated(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
id := mux.Vars(r)["id"]
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(w, err)
return
}
user, err := handler.getter.GetDeprecatedUserByOrgIDAndID(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(id))
if err != nil {
render.Error(w, err)
return
}
token, err := handler.setter.GetOrCreateResetPasswordToken(ctx, user.ID)
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, token)
}
func (handler *handler) GetResetPasswordToken(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
@@ -481,68 +389,6 @@ func (handler *handler) GetRolesByUserID(w http.ResponseWriter, r *http.Request)
render.Success(w, http.StatusOK, roles)
}
func (handler *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 := handler.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 (handler *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 := handler.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 (handler *handler) GetUsersByRoleID(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()

View File

@@ -55,119 +55,6 @@ func NewSetter(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing em
}
}
// CreateBulk implements invite.Module.
func (module *setter) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, identityID valuer.UUID, identityEmail valuer.Email, bulkInvites *types.PostableBulkInviteRequest) ([]*types.Invite, error) {
// validate all emails to be invited
emails := make([]string, len(bulkInvites.Invites))
for idx, invite := range bulkInvites.Invites {
emails[idx] = invite.Email.StringValue()
}
users, err := module.store.GetUsersByEmailsOrgIDAndStatuses(ctx, orgID, emails, []string{types.UserStatusActive.StringValue(), types.UserStatusPendingInvite.StringValue()})
if err != nil {
return nil, err
}
if len(users) > 0 {
if err := users[0].ErrIfRoot(); err != nil {
return nil, errors.WithAdditionalf(err, "Cannot send invite to root user")
}
if users[0].Status == types.UserStatusPendingInvite {
return nil, errors.Newf(errors.TypeAlreadyExists, errors.CodeAlreadyExists, "An invite already exists for this email: %s", users[0].Email.StringValue())
}
return nil, errors.Newf(errors.TypeAlreadyExists, errors.CodeAlreadyExists, "User already exists with this email: %s", users[0].Email.StringValue())
}
type userWithResetToken struct {
User *types.User
ResetPasswordToken *types.ResetPasswordToken
Role types.Role
}
newUsersWithResetToken := make([]*userWithResetToken, len(bulkInvites.Invites))
if err := module.store.RunInTx(ctx, func(ctx context.Context) error {
for idx, invite := range bulkInvites.Invites {
// create a new user with pending invite status
newUser, err := types.NewUser(invite.Name, invite.Email, orgID, types.UserStatusPendingInvite)
if err != nil {
return err
}
// store the user and password in db
err = module.createUserWithoutGrant(ctx, newUser, root.WithRoleNames([]string{authtypes.MustGetSigNozManagedRoleFromExistingRole(invite.Role)}))
if err != nil {
return err
}
// generate reset password token
resetPasswordToken, err := module.GetOrCreateResetPasswordToken(ctx, newUser.ID)
if err != nil {
module.settings.Logger().ErrorContext(ctx, "failed to create reset password token for invited user", errors.Attr(err))
return err
}
newUsersWithResetToken[idx] = &userWithResetToken{
User: newUser,
ResetPasswordToken: resetPasswordToken,
Role: invite.Role,
}
}
return nil
}); err != nil {
return nil, err
}
invites := make([]*types.Invite, len(bulkInvites.Invites))
// send password reset emails to all the invited users
for idx, userWithToken := range newUsersWithResetToken {
module.analytics.TrackUser(ctx, orgID.String(), identityID.String(), "Invite Sent", map[string]any{
"invitee_email": userWithToken.User.Email,
"invitee_role": userWithToken.Role,
})
invite := &types.Invite{
Identifiable: types.Identifiable{
ID: userWithToken.User.ID,
},
Name: userWithToken.User.DisplayName,
Email: userWithToken.User.Email,
Token: userWithToken.ResetPasswordToken.Token,
Role: userWithToken.Role,
OrgID: userWithToken.User.OrgID,
TimeAuditable: types.TimeAuditable{
CreatedAt: userWithToken.User.CreatedAt,
UpdatedAt: userWithToken.User.UpdatedAt,
},
}
invites[idx] = invite
frontendBaseUrl := bulkInvites.Invites[idx].FrontendBaseUrl
if frontendBaseUrl == "" {
module.settings.Logger().InfoContext(ctx, "frontend base url is not provided, skipping email", slog.Any("invitee_email", userWithToken.User.Email))
continue
}
resetLink := userWithToken.ResetPasswordToken.FactorPasswordResetLink(frontendBaseUrl)
tokenLifetime := module.config.Password.Invite.MaxTokenLifetime
humanizedTokenLifetime := strings.TrimSpace(humanize.RelTime(time.Now(), time.Now().Add(tokenLifetime), "", ""))
if err := module.emailing.SendHTML(ctx, userWithToken.User.Email.String(), "You're Invited to Join SigNoz", emailtypes.TemplateNameInvitationEmail, map[string]any{
"inviter_email": identityEmail.StringValue(),
"link": resetLink,
"Expiry": humanizedTokenLifetime,
}); err != nil {
module.settings.Logger().ErrorContext(ctx, "failed to send invite email", errors.Attr(err))
}
}
return invites, nil
}
func (module *setter) CreateUser(ctx context.Context, user *types.User, opts ...root.CreateUserOption) error {
createUserOpts := root.NewCreateUserOptions(opts...)

View File

@@ -7,7 +7,6 @@ import (
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
type userRoleStore struct {
@@ -19,26 +18,6 @@ func NewUserRoleStore(sqlstore sqlstore.SQLStore, settings factory.ProviderSetti
return &userRoleStore{sqlstore: sqlstore, settings: settings}
}
func (store *userRoleStore) ListUserRolesByOrgIDAndUserIDs(ctx context.Context, orgID valuer.UUID, userIDs []valuer.UUID) ([]*authtypes.UserRole, error) {
userRoles := make([]*authtypes.UserRole, 0)
err := store.sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(&userRoles).
Join("JOIN users").
JoinOn("users.id = user_role.user_id").
Where("users.org_id = ?", orgID).
Where("users.id IN (?)", bun.In(userIDs)).
Relation("Role").
Scan(ctx)
if err != nil {
return nil, err
}
return userRoles, nil
}
func (store *userRoleStore) GetUserRoleByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.UserRole, error) {
userRole := new(authtypes.UserRole)

View File

@@ -40,9 +40,6 @@ type Setter interface {
UpdateAnyUser(ctx context.Context, orgID valuer.UUID, user *types.User) error
DeleteUser(ctx context.Context, orgID valuer.UUID, id string, deletedBy string) error
// invite
CreateBulkInvite(ctx context.Context, orgID valuer.UUID, identityID valuer.UUID, identityEmail valuer.Email, bulkInvites *types.PostableBulkInviteRequest) ([]*types.Invite, error)
// Creates a pending invite user with the roles given via opts and emails them the invite link.
CreatePendingInviteUser(ctx context.Context, identityID valuer.UUID, identityEmail valuer.Email, frontendBaseURL string, user *types.User, opts ...CreateUserOption) (*types.User, error)
@@ -60,16 +57,11 @@ type Getter interface {
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 deprecated user object by orgID and id.
GetDeprecatedUserByOrgIDAndID(context.Context, valuer.UUID, valuer.UUID) (*types.DeprecatedUser, error)
// Get user by orgID and id.
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)
// List users by email and org ids.
ListUsersByEmailAndOrgIDs(context.Context, valuer.Email, []valuer.UUID) ([]*types.User, error)
@@ -105,22 +97,15 @@ type Getter interface {
}
type Handler interface {
// invite
CreateInvite(http.ResponseWriter, *http.Request)
// users
ListUsersDeprecated(http.ResponseWriter, *http.Request)
ListUsers(http.ResponseWriter, *http.Request)
CreateUser(http.ResponseWriter, *http.Request)
UpdateUser(http.ResponseWriter, *http.Request)
DeleteUser(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)
// user roles
@@ -129,7 +114,6 @@ type Handler interface {
DeleteUserRole(http.ResponseWriter, *http.Request)
// Reset Password
GetResetPasswordTokenDeprecated(http.ResponseWriter, *http.Request)
GetResetPasswordToken(http.ResponseWriter, *http.Request)
CreateResetPasswordToken(http.ResponseWriter, *http.Request)
VerifyResetPasswordToken(http.ResponseWriter, *http.Request)

View File

@@ -5,6 +5,7 @@ import (
"context"
"fmt"
"log/slog"
"math"
"regexp"
"sort"
"strings"
@@ -478,11 +479,19 @@ func (q *promqlQuery) toResult(matrix promql.Matrix, warnings []string, began ti
for idx := range v.Floats {
p := v.Floats[idx]
// NaN and +/-Inf have no JSON number form and nothing to plot; the
// builder path drops them while scanning rows (see consume.go).
if math.IsNaN(p.F) || math.IsInf(p.F, 0) {
continue
}
s.Values = append(s.Values, &qbv5.TimeSeriesValue{
Timestamp: p.T,
Value: p.F,
})
}
if len(s.Values) == 0 {
continue
}
series = append(series, &s)
}
@@ -494,13 +503,11 @@ func (q *promqlQuery) toResult(matrix promql.Matrix, warnings []string, began ti
}
statsMu.Unlock()
tsData := &qbv5.TimeSeriesData{
QueryName: q.query.Name,
Aggregations: []*qbv5.AggregationBucket{
{
Series: series,
},
},
tsData := &qbv5.TimeSeriesData{QueryName: q.query.Name}
// No bucket at all when nothing survived: a bucket holding no series reads
// as "filtered to empty" to the cache, which stores it as a real result.
if len(series) > 0 {
tsData.Aggregations = []*qbv5.AggregationBucket{{Series: series}}
}
var payload any = tsData

View File

@@ -2,14 +2,21 @@ package querier
import (
"log/slog"
"math"
"strings"
"sync"
"testing"
"time"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/promql"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/prometheus/prometheustest"
qbv5 "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRemoveAllVarMatchers(t *testing.T) {
@@ -453,3 +460,82 @@ func TestFingerprint_PinnedProviderBypassesCache(t *testing.T) {
}
assert.Empty(t, q.Fingerprint())
}
func TestToResultDropsNonFiniteValues(t *testing.T) {
tests := []struct {
description string
floats []promql.FPoint
expectedTimestamps []int64
expectedValues []float64
}{
{
description: "finite values pass through untouched",
floats: []promql.FPoint{{T: 1000, F: 1.5}, {T: 2000, F: 2.5}},
expectedTimestamps: []int64{1000, 2000},
expectedValues: []float64{1.5, 2.5},
},
{
description: "a ratio's 0/0 points are dropped, the rest kept",
floats: []promql.FPoint{{T: 1000, F: 1.5}, {T: 2000, F: math.NaN()}, {T: 3000, F: 2.5}},
expectedTimestamps: []int64{1000, 3000},
expectedValues: []float64{1.5, 2.5},
},
{
description: "both infinities are dropped",
floats: []promql.FPoint{{T: 1000, F: math.Inf(1)}, {T: 2000, F: 4.5}, {T: 3000, F: math.Inf(-1)}},
expectedTimestamps: []int64{2000},
expectedValues: []float64{4.5},
},
}
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
q := &promqlQuery{query: qbv5.PromQuery{Name: "A"}, requestType: qbv5.RequestTypeTimeSeries}
matrix := promql.Matrix{{Metric: labels.FromStrings("job_name", "dbBloatMonitorJob"), Floats: test.floats}}
var mu sync.Mutex
var rows, bytes uint64
result := q.toResult(matrix, nil, time.Now(), &mu, &rows, &bytes)
tsData, ok := result.Value.(*qbv5.TimeSeriesData)
require.True(t, ok)
require.Len(t, tsData.Aggregations, 1)
require.Len(t, tsData.Aggregations[0].Series, 1)
timestamps := make([]int64, 0, len(test.expectedTimestamps))
values := make([]float64, 0, len(test.expectedValues))
for _, v := range tsData.Aggregations[0].Series[0].Values {
timestamps = append(timestamps, v.Timestamp)
values = append(values, v.Value)
}
assert.Equal(t, test.expectedTimestamps, timestamps)
assert.Equal(t, test.expectedValues, values)
})
}
}
// A series left with nothing must not surface as an empty series, and a result
// left with no series must carry no aggregation bucket at all — the cache reads
// a bucket holding no series as a real, filtered-to-empty result and stores it.
func TestToResultDropsSeriesAndBucketLeftEmpty(t *testing.T) {
q := &promqlQuery{query: qbv5.PromQuery{Name: "A"}, requestType: qbv5.RequestTypeTimeSeries}
matrix := promql.Matrix{
{Metric: labels.FromStrings("job_name", "idleJob"), Floats: []promql.FPoint{{T: 1000, F: math.NaN()}}},
{Metric: labels.FromStrings("job_name", "activeJob"), Floats: []promql.FPoint{{T: 1000, F: 7.5}}},
}
var mu sync.Mutex
var rows, bytes uint64
tsData, ok := q.toResult(matrix, nil, time.Now(), &mu, &rows, &bytes).Value.(*qbv5.TimeSeriesData)
require.True(t, ok)
require.Len(t, tsData.Aggregations, 1)
require.Len(t, tsData.Aggregations[0].Series, 1, "the all-NaN series is gone")
assert.Equal(t, "activeJob", tsData.Aggregations[0].Series[0].Labels[0].Value)
allNaN := promql.Matrix{
{Metric: labels.FromStrings("job_name", "idleJob"), Floats: []promql.FPoint{{T: 1000, F: math.NaN()}}},
}
tsData, ok = q.toResult(allNaN, nil, time.Now(), &mu, &rows, &bytes).Value.(*qbv5.TimeSeriesData)
require.True(t, ok)
assert.Empty(t, tsData.Aggregations)
}

View File

@@ -53,12 +53,6 @@ var (
types.RoleEditor: SigNozEditorRoleName,
types.RoleViewer: SigNozViewerRoleName,
}
SigNozManagedRoleToExistingLegacyRole = map[string]types.Role{
SigNozAdminRoleName: types.RoleAdmin,
SigNozEditorRoleName: types.RoleEditor,
SigNozViewerRoleName: types.RoleViewer,
}
)
type Role struct {

View File

@@ -119,9 +119,6 @@ type UserRoleStore interface {
// get a single user role entry by org id and its own id
GetUserRoleByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*UserRole, error)
// list all user_role entries for
ListUserRolesByOrgIDAndUserIDs(ctx context.Context, orgID valuer.UUID, userIDs []valuer.UUID) ([]*UserRole, error)
// delete user role entries by user id
DeleteUserRoles(ctx context.Context, userID valuer.UUID) error

View File

@@ -46,6 +46,7 @@ var (
GCPServiceComputeEngine = ServiceID{valuer.NewString("computeengine")}
GCPServiceGKE = ServiceID{valuer.NewString("gke")}
GCPServiceCloudStorage = ServiceID{valuer.NewString("cloudstorage")}
GCPServiceCloudSQLMySQL = ServiceID{valuer.NewString("cloudsql_mysql")}
)
func (ServiceID) Enum() []any {
@@ -82,6 +83,7 @@ func (ServiceID) Enum() []any {
GCPServiceComputeEngine,
GCPServiceGKE,
GCPServiceCloudStorage,
GCPServiceCloudSQLMySQL,
}
}
@@ -124,6 +126,7 @@ var SupportedServices = map[CloudProviderType][]ServiceID{
GCPServiceComputeEngine,
GCPServiceGKE,
GCPServiceCloudStorage,
GCPServiceCloudSQLMySQL,
},
}

View File

@@ -1,83 +0,0 @@
package types
import (
"encoding/json"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
var (
ErrInviteAlreadyExists = errors.MustNewCode("invite_already_exists")
ErrInviteNotFound = errors.MustNewCode("invite_not_found")
)
type GettableInvite = Invite
type Invite struct {
bun.BaseModel `bun:"table:user_invite"`
Identifiable
TimeAuditable
Name string `bun:"name,type:text" json:"name"`
Email valuer.Email `bun:"email,type:text" json:"email"`
Token string `bun:"token,type:text" json:"token"`
Role Role `bun:"role,type:text" json:"role"`
OrgID valuer.UUID `bun:"org_id,type:text" json:"orgId"`
InviteLink string `bun:"-" json:"inviteLink"`
}
type PostableInvite struct {
Name string `json:"name"`
Email valuer.Email `json:"email"`
Role Role `json:"role"`
FrontendBaseUrl string `json:"frontendBaseUrl"`
}
type PostableBulkInviteRequest struct {
Invites []PostableInvite `json:"invites" required:"true" nullable:"false"`
}
func (request *PostableBulkInviteRequest) UnmarshalJSON(data []byte) error {
type Alias PostableBulkInviteRequest
var temp Alias
if err := json.Unmarshal(data, &temp); err != nil {
return err
}
// check for duplicate emails in the same request
seen := make(map[string]struct{}, len(temp.Invites))
for _, invite := range temp.Invites {
email := invite.Email.StringValue()
if _, exists := seen[email]; exists {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "Duplicate email in request: %s", email)
}
seen[email] = struct{}{}
}
*request = PostableBulkInviteRequest(temp)
return nil
}
func NewInvite(name string, role Role, orgID valuer.UUID, email valuer.Email) (*Invite, error) {
invite := &Invite{
Identifiable: Identifiable{
ID: valuer.GenerateUUID(),
},
Name: name,
Email: email,
Token: valuer.GenerateUUID().String(),
Role: role,
OrgID: orgID,
TimeAuditable: TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
}
return invite, nil
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
@@ -31,11 +32,12 @@ func (enum *Signal) UnmarshalJSON(data []byte) error {
}
var (
SignalTraces = Signal{valuer.NewString("traces")}
SignalLogs = Signal{valuer.NewString("logs")}
SignalApiMonitoring = Signal{valuer.NewString("api_monitoring")}
SignalExceptions = Signal{valuer.NewString("exceptions")}
SignalMeter = Signal{valuer.NewString("meter")}
SignalTraces = Signal{valuer.NewString("traces")}
SignalLogs = Signal{valuer.NewString("logs")}
SignalApiMonitoring = Signal{valuer.NewString("api_monitoring")}
SignalExceptions = Signal{valuer.NewString("exceptions")}
SignalMeter = Signal{valuer.NewString("meter")}
SignalAiObservability = Signal{valuer.NewString("ai_observability")}
)
// NewSignal creates a Signal from a string.
@@ -51,6 +53,8 @@ func NewSignal(s string) (Signal, error) {
return SignalExceptions, nil
case "meter":
return SignalMeter, nil
case "ai_observability":
return SignalAiObservability, nil
default:
return Signal{}, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid signal: %s", s)
}
@@ -187,6 +191,18 @@ func NewDefaultQuickFilter(orgID valuer.UUID) ([]*StorableQuickFilter, error) {
{"key": "host.name", "dataType": "float64", "type": "Sum"},
}
// AI observability (builder_ai_query trace explorer), ordered by expected
// usage: env scoping, the LLM identity keys, then service and the rest.
aiObservabilityFilters := []map[string]interface{}{
{"key": "deployment.environment", "dataType": "string", "type": "resource"},
{"key": telemetrytypes.GenAIOperationName, "dataType": "string", "type": "tag"},
{"key": telemetrytypes.GenAIProviderName, "dataType": "string", "type": "tag"},
{"key": telemetrytypes.GenAIRequestModel, "dataType": "string", "type": "tag"},
{"key": "service.name", "dataType": "string", "type": "resource"},
{"key": telemetrytypes.GenAIToolName, "dataType": "string", "type": "tag"},
{"key": telemetrytypes.GenAIAgentName, "dataType": "string", "type": "tag"},
}
tracesJSON, err := json.Marshal(tracesFilters)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to marshal traces filters")
@@ -212,6 +228,11 @@ func NewDefaultQuickFilter(orgID valuer.UUID) ([]*StorableQuickFilter, error) {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to marshal meter filters")
}
aiObservabilityJSON, err := json.Marshal(aiObservabilityFilters)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to marshal ai observability filters")
}
timeRightNow := time.Now()
return []*StorableQuickFilter{
@@ -275,5 +296,17 @@ func NewDefaultQuickFilter(orgID valuer.UUID) ([]*StorableQuickFilter, error) {
UpdatedAt: timeRightNow,
},
},
{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
OrgID: orgID,
Filter: string(aiObservabilityJSON),
Signal: SignalAiObservability,
TimeAuditable: types.TimeAuditable{
CreatedAt: timeRightNow,
UpdatedAt: timeRightNow,
},
},
}, nil
}

View File

@@ -3,10 +3,11 @@ package telemetrytypes
// OpenTelemetry gen_ai semantic-convention attribute keys. Single source of truth
// shared by the AI query builder and the LLM pricing pipeline.
const (
GenAIRequestModel = "gen_ai.request.model"
GenAIToolName = "gen_ai.tool.name"
GenAIAgentName = "gen_ai.agent.name"
GenAIProviderName = "gen_ai.provider.name"
GenAIRequestModel = "gen_ai.request.model"
GenAIOperationName = "gen_ai.operation.name"
GenAIToolName = "gen_ai.tool.name"
GenAIAgentName = "gen_ai.agent.name"
GenAIProviderName = "gen_ai.provider.name"
GenAIUsageInputTokens = "gen_ai.usage.input_tokens"
GenAIUsageOutputTokens = "gen_ai.usage.output_tokens"
@@ -25,10 +26,11 @@ const (
// on, surfaced by the metadata store even before ingestion so the AI gate/columns
// resolve on a fresh install.
var GenAIFieldDefinitions = map[string]TelemetryFieldKey{
GenAIRequestModel: {Name: GenAIRequestModel, Signal: SignalTraces, FieldContext: FieldContextAttribute, FieldDataType: FieldDataTypeString},
GenAIToolName: {Name: GenAIToolName, Signal: SignalTraces, FieldContext: FieldContextAttribute, FieldDataType: FieldDataTypeString},
GenAIAgentName: {Name: GenAIAgentName, Signal: SignalTraces, FieldContext: FieldContextAttribute, FieldDataType: FieldDataTypeString},
GenAIProviderName: {Name: GenAIProviderName, Signal: SignalTraces, FieldContext: FieldContextAttribute, FieldDataType: FieldDataTypeString},
GenAIRequestModel: {Name: GenAIRequestModel, Signal: SignalTraces, FieldContext: FieldContextAttribute, FieldDataType: FieldDataTypeString},
GenAIOperationName: {Name: GenAIOperationName, Signal: SignalTraces, FieldContext: FieldContextAttribute, FieldDataType: FieldDataTypeString},
GenAIToolName: {Name: GenAIToolName, Signal: SignalTraces, FieldContext: FieldContextAttribute, FieldDataType: FieldDataTypeString},
GenAIAgentName: {Name: GenAIAgentName, Signal: SignalTraces, FieldContext: FieldContextAttribute, FieldDataType: FieldDataTypeString},
GenAIProviderName: {Name: GenAIProviderName, Signal: SignalTraces, FieldContext: FieldContextAttribute, FieldDataType: FieldDataTypeString},
GenAIUsageInputTokens: {Name: GenAIUsageInputTokens, Signal: SignalTraces, FieldContext: FieldContextAttribute, FieldDataType: FieldDataTypeFloat64},
GenAIUsageOutputTokens: {Name: GenAIUsageOutputTokens, Signal: SignalTraces, FieldContext: FieldContextAttribute, FieldDataType: FieldDataTypeFloat64},

View File

@@ -46,19 +46,10 @@ type User struct {
TimeAuditable
}
type DeprecatedUser struct {
*User
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"`
@@ -121,13 +112,6 @@ func NewRootUser(displayName string, email valuer.Email, orgID valuer.UUID) (*Us
}, nil
}
func NewDeprecatedUserFromUserAndRole(user *User, role Role) *DeprecatedUser {
return &DeprecatedUser{
user,
role,
}
}
// Update applies mutable fields from the input to the user. Immutable fields
// (email, is_root, org_id, id) are preserved. Only non-zero input fields are applied.
func (u *User) Update(displayName string) {

View File

@@ -1,4 +1,116 @@
{
"note": "Divergences of the CURRENT promql serving path from the upstream reference engine, enforced exactly by 01_upstream_corpus.py in both directions. These document shipped defects, not test debt: the dominant class is the v1 remote-read fetch injecting a synthetic 'fingerprint' label into every series (pkg/prometheus/clickhouseprometheus/json.go), which breaks without() grouping and default vector matching. Entries must be REMOVED as the serving path is fixed or swapped.",
"divergences": {}
}
"note": "Divergences of the CURRENT promql serving path from the upstream reference engine, enforced exactly by 01_upstream_corpus.py in both directions. These document shipped defects, not test debt: the dominant class is the v1 remote-read fetch injecting a synthetic 'fingerprint' label into every series (pkg/prometheus/clickhouseprometheus/json.go), which breaks without() grouping and default vector matching. Entries must be REMOVED as the serving path is fixed or swapped. Second class, and the bulk of the entries below: promql_query.go drops NaN and +/-Inf from results, mirroring the builder path in consume.go, so every case whose expected output carries a non-finite value diverges on both legs. That class is a product decision rather than a defect, so it is not part of the burn-down.",
"divergences": {
"aggregators.test:630[base]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:630[instant-coarse]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:633[base]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:633[instant-coarse]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:636[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:636[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:639[base]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:639[instant-coarse]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:642[base]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:642[instant-coarse]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:645[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:645[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:648[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:648[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:661[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:661[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:698[base]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:698[instant-coarse]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:702[base]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:702[instant-coarse]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:706[base]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:706[instant-coarse]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:710[base]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:710[instant-coarse]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:714[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:714[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:717[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:717[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:720[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:720[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:724[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:724[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:862[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:862[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:865[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:865[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:868[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:868[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:873[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:873[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:885[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:885[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:888[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:888[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:891[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:891[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:896[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:896[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:906[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:906[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:909[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:909[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:919[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:919[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:922[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:922[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:925[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:925[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:930[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:930[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:942[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:942[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:945[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:945[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:948[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:948[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:953[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:953[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:963[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:963[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:966[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:966[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"operators.test:533[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"operators.test:533[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"operators.test:539[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"trig_functions.test:13[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:13[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:18[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:18[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:23[base]": "2 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:23[instant-coarse]": "2 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:28[base]": "2 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:28[instant-coarse]": "2 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:33[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:33[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:38[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:38[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:43[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:43[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:48[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:48[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:53[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:53[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:58[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:58[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:63[base]": "2 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:63[instant-coarse]": "2 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:68[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:68[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:73[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:73[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:78[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:78[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:83[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:83[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:88[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:88[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:8[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:8[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:93[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:93[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing"
}
}

View File

@@ -1,17 +1,128 @@
{
"note": "Divergences of the clickhousev2 provider (pinned via X-SigNoz-PromQL-Provider) from the upstream reference engine, enforced exactly by 01_upstream_corpus.py in both directions. This ledger is the rollout scorecard for the provider swap: the default provider cannot be replaced by clickhousev2 while anything is listed here. Entries must carry the defect's cause and be REMOVED as the provider is fixed. Current class: the engine aggregates floats with Kahan compensated summation (sum, sum_over_time) and an overflow-free incremental mean (avg); ClickHouse's sumForEach/avgForEach/arraySum are naive, so extreme-magnitude corpus data (±1e100 cancellation, ±1.8e308 overflow) diverges on transpiled plans. Burn-down candidates: sumKahanForEach for the cancellation class; the overflow class needs an incremental-mean aggregate ClickHouse does not have.",
"note": "Divergences of the clickhousev2 provider (pinned via X-SigNoz-PromQL-Provider) from the upstream reference engine, enforced exactly by 01_upstream_corpus.py in both directions. This ledger is the rollout scorecard for the provider swap: the default provider cannot be replaced by clickhousev2 while anything is listed here. Entries must carry the defect's cause and be REMOVED as the provider is fixed. Current class: the engine aggregates floats with Kahan compensated summation (sum, sum_over_time) and an overflow-free incremental mean (avg); ClickHouse's sumForEach/avgForEach/arraySum are naive, so extreme-magnitude corpus data (±1e100 cancellation, ±1.8e308 overflow) diverges on transpiled plans. Burn-down candidates: sumKahanForEach for the cancellation class; the overflow class needs an incremental-mean aggregate ClickHouse does not have. Second class, and the bulk of the entries below: promql_query.go drops NaN and +/-Inf from results, mirroring the builder path in consume.go, so every case whose expected output carries a non-finite value diverges on both legs. That class is a product decision rather than a defect, so it is not part of the burn-down.",
"divergences": {
"aggregators.test:630[base]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:630[instant-coarse]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:633[base]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:633[instant-coarse]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:636[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:636[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:639[base]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:639[instant-coarse]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:642[base]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:642[instant-coarse]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:645[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:645[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:648[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:648[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:651[base]": "avg over near-max-float64 values: engine's incremental mean never forms the overflowing sum; avgForEach sums then divides, overflowing to +Inf",
"aggregators.test:651[instant-coarse]": "same as aggregators.test:651[base] on the coarse-step grid variant",
"aggregators.test:654[base]": "avg over near-min-float64 values: engine's incremental mean never forms the overflowing sum; avgForEach overflows to -Inf",
"aggregators.test:654[instant-coarse]": "same as aggregators.test:654[base] on the coarse-step grid variant",
"aggregators.test:661[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:661[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:687[base]": "sum over {1e100, -1e100, small}: engine uses Kahan compensated summation; sumForEach's naive summation loses the small terms to cancellation and returns 0",
"aggregators.test:687[instant-coarse]": "same as aggregators.test:687[base] on the coarse-step grid variant",
"aggregators.test:695[base]": "avg over {1e100, -1e100, small}: same Kahan-vs-naive cancellation as aggregators.test:687, divided by count",
"aggregators.test:695[instant-coarse]": "same as aggregators.test:695[base] on the coarse-step grid variant",
"aggregators.test:698[base]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:698[instant-coarse]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:702[base]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:702[instant-coarse]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:706[base]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:706[instant-coarse]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:710[base]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:710[instant-coarse]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:714[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:714[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:717[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:717[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:720[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:720[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:724[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:724[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:862[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:862[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:865[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:865[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:868[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:868[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:873[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:873[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:885[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:885[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:888[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:888[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:891[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:891[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:896[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:896[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:906[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:906[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:909[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:909[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:919[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:919[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:922[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:922[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:925[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:925[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:930[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:930[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:942[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:942[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:945[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:945[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:948[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:948[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:953[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:953[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"aggregators.test:963[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:963[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:966[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"aggregators.test:966[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"functions.test:1084[instant-coarse]": "sum_over_time over a window containing ±1e100: the disjoint coarse-step form's arraySum slide is naive summation, cancelling to 0 (the base variant's W>64 shape falls back to the engine and is exact)",
"functions.test:1087[instant-coarse]": "avg_over_time, same window and cancellation as functions.test:1084[instant-coarse]",
"functions.test:1149[base]": "avg_over_time over ±2.258e220-magnitude samples: engine's Kahan-compensated incremental mean cancels exactly to 0; the bucketed form's naive slide summation leaves a ~1e202 residue",
"functions.test:1149[instant-coarse]": "same as functions.test:1149[base] through the disjoint coarse-step form"
"functions.test:1149[instant-coarse]": "same as functions.test:1149[base] through the disjoint coarse-step form",
"operators.test:533[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"operators.test:533[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"operators.test:539[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
"trig_functions.test:13[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:13[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:18[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:18[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:23[base]": "2 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:23[instant-coarse]": "2 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:28[base]": "2 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:28[instant-coarse]": "2 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:33[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:33[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:38[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:38[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:43[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:43[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:48[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:48[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:53[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:53[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:58[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:58[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:63[base]": "2 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:63[instant-coarse]": "2 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:68[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:68[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:73[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:73[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:78[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:78[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:83[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:83[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:88[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:88[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:8[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:8[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:93[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
"trig_functions.test:93[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing"
}
}

View File

@@ -0,0 +1,65 @@
from collections.abc import Callable
from datetime import UTC, datetime, timedelta
from http import HTTPStatus
from uuid import uuid4
from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.metrics import Metrics
from fixtures.querier import get_all_series, make_query_request
HOUR_MS = 3_600_000
SAMPLE_INTERVAL_MS = 60_000
def test_promql_ratio_with_zero_denominator_is_dropped_and_cached(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_metrics: Callable[[list[Metrics]], None],
) -> None:
# 12h ending on an hour boundary 15m ago — old enough to be cached.
end_ms = (int((datetime.now(tz=UTC) - timedelta(minutes=15)).timestamp() * 1000) // HOUR_MS) * HOUR_MS
start_ms = end_ms - 12 * HOUR_MS
sum_metric = f"job_duration_sum_{uuid4().hex[:8]}"
count_metric = f"job_duration_count_{uuid4().hex[:8]}"
# active_job divides finite; idle_job is 0/0 at every step.
series = {"active_job": (100.0, 4.0), "idle_job": (0.0, 0.0)}
metrics: list[Metrics] = []
for job_name, (sum_value, count_value) in series.items():
for ts_ms in range(start_ms, end_ms + 1, SAMPLE_INTERVAL_MS):
timestamp = datetime.fromtimestamp(ts_ms / 1000, tz=UTC)
metrics.append(Metrics(metric_name=sum_metric, labels={"job_name": job_name}, timestamp=timestamp, value=sum_value))
metrics.append(Metrics(metric_name=count_metric, labels={"job_name": job_name}, timestamp=timestamp, value=count_value))
insert_metrics(metrics)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
promql = f"sum by (job_name) ({sum_metric}) / sum by (job_name) ({count_metric})"
def run() -> tuple[dict[str, dict[int, object]], int]:
query = {"type": "promql", "spec": {"name": "A", "query": promql}}
response = make_query_request(signoz, token, start_ms, end_ms, [query], no_cache=False)
assert response.status_code == HTTPStatus.OK, response.text[:300]
body = response.json()
out: dict[str, dict[int, object]] = {}
for entry in get_all_series(body, "A") or []:
labels = {l["key"]["name"]: str(l["value"]) for l in entry.get("labels") or []}
out[labels["job_name"]] = {v["timestamp"]: v["value"] for v in entry.get("values") or []}
return out, int(body["data"]["meta"]["stepIntervals"]["A"])
# First populates the cache, second must be served from it.
first, step_seconds = run()
second, _ = run()
expected_points = (end_ms - start_ms) // (step_seconds * 1000) + 1
assert set(first) == {"active_job"}, f"the 0/0 series must not reach the response: {sorted(first)}"
assert set(first["active_job"].values()) == {25.0}, sorted(set(first["active_job"].values()))
assert len(first["active_job"]) == expected_points, f"expected {expected_points} points, got {len(first['active_job'])}"
# The cached read excludes end_ms, the one legitimate difference.
assert set(second) == set(first), sorted(second)
for job_name, points in first.items():
expected = {ts: value for ts, value in points.items() if ts < end_ms}
assert second[job_name] == expected, f"{job_name}: got {len(second[job_name])} of {len(expected)} points"