mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-22 18:00:25 +01:00
Compare commits
24 Commits
ankitnayan
...
infraM/v2_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ec6b760c8 | ||
|
|
09d7c8b909 | ||
|
|
abcdb32c2e | ||
|
|
446355ebfb | ||
|
|
0389550757 | ||
|
|
2f60e6e75c | ||
|
|
896d77e3ec | ||
|
|
981aab436f | ||
|
|
711bbf49f1 | ||
|
|
1cc48405e0 | ||
|
|
e79715bf6d | ||
|
|
2ce86a3f9d | ||
|
|
639e3239a9 | ||
|
|
e83a23e6ab | ||
|
|
f0cff4ae96 | ||
|
|
7274421895 | ||
|
|
0667dc47cb | ||
|
|
4af267ee61 | ||
|
|
9c6656d6b9 | ||
|
|
5c54a2537c | ||
|
|
bf201710a7 | ||
|
|
6f1090818c | ||
|
|
a5adc52276 | ||
|
|
5ddcf33811 |
@@ -2689,7 +2689,6 @@ components:
|
||||
records:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesClusterRecord'
|
||||
nullable: true
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
@@ -2759,7 +2758,6 @@ components:
|
||||
records:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesDaemonSetRecord'
|
||||
nullable: true
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
@@ -2829,7 +2827,6 @@ components:
|
||||
records:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesDeploymentRecord'
|
||||
nullable: true
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
@@ -2908,7 +2905,6 @@ components:
|
||||
records:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesHostRecord'
|
||||
nullable: true
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
@@ -2984,7 +2980,6 @@ components:
|
||||
records:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesJobRecord'
|
||||
nullable: true
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
@@ -3032,7 +3027,6 @@ components:
|
||||
records:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesNamespaceRecord'
|
||||
nullable: true
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
@@ -3110,7 +3104,6 @@ components:
|
||||
records:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesNodeRecord'
|
||||
nullable: true
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
@@ -3209,7 +3202,6 @@ components:
|
||||
records:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodRecord'
|
||||
nullable: true
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
@@ -3554,7 +3546,6 @@ components:
|
||||
records:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesStatefulSetRecord'
|
||||
nullable: true
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
@@ -3615,7 +3606,6 @@ components:
|
||||
records:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesVolumeRecord'
|
||||
nullable: true
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"@signozhq/design-tokens": "2.1.4",
|
||||
"@signozhq/icons": "0.4.0",
|
||||
"@signozhq/resizable": "0.0.2",
|
||||
"@signozhq/ui": "0.0.19",
|
||||
"@signozhq/ui": "0.0.21",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@tanstack/react-virtual": "3.13.22",
|
||||
"@uiw/codemirror-theme-copilot": "4.23.11",
|
||||
|
||||
31
frontend/pnpm-lock.yaml
generated
31
frontend/pnpm-lock.yaml
generated
@@ -77,8 +77,8 @@ importers:
|
||||
specifier: 0.0.2
|
||||
version: 0.0.2(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@signozhq/ui':
|
||||
specifier: 0.0.19
|
||||
version: 0.0.19(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)
|
||||
specifier: 0.0.21
|
||||
version: 0.0.21(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)
|
||||
'@tanstack/react-table':
|
||||
specifier: 8.21.3
|
||||
version: 8.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -3269,8 +3269,8 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^18.2.0
|
||||
|
||||
'@signozhq/ui@0.0.19':
|
||||
resolution: {integrity: sha512-2q6aRxN/PR4PlR2xJZAREEuvLPiDFggfFKzCW2Z5vHVVbrgnvZHWD1jPUuwszfEg0ceH3UvkwqceO7wN4uRJAA==}
|
||||
'@signozhq/ui@0.0.21':
|
||||
resolution: {integrity: sha512-uLM3Vqwxlk2USXbwtb3qRLpjZR9b9QSHFQq/jtcfYNMDmIE/sNjSj0nRkEhX4RqqRgsLRt2PVA33aeWxDOLO3g==}
|
||||
peerDependencies:
|
||||
'@signozhq/icons': 0.3.0
|
||||
react: ^18.2.0
|
||||
@@ -3851,27 +3851,6 @@ packages:
|
||||
peerDependencies:
|
||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||
|
||||
'@webassemblyjs/ast@1.14.1':
|
||||
resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==}
|
||||
|
||||
'@webassemblyjs/floating-point-hex-parser@1.13.2':
|
||||
resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==}
|
||||
|
||||
'@webassemblyjs/helper-api-error@1.13.2':
|
||||
resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==}
|
||||
|
||||
'@webassemblyjs/helper-buffer@1.14.1':
|
||||
resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==}
|
||||
|
||||
'@webassemblyjs/helper-numbers@1.13.2':
|
||||
resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==}
|
||||
|
||||
'@webassemblyjs/helper-wasm-bytecode@1.13.2':
|
||||
resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==}
|
||||
|
||||
'@webassemblyjs/helper-wasm-section@1.14.1':
|
||||
resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==}
|
||||
|
||||
'@xmldom/xmldom@0.8.13':
|
||||
resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
@@ -12034,7 +12013,7 @@ snapshots:
|
||||
- react-dom
|
||||
- tailwindcss
|
||||
|
||||
'@signozhq/ui@0.0.19(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)':
|
||||
'@signozhq/ui@0.0.21(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@chenglou/pretext': 0.0.5
|
||||
'@radix-ui/react-checkbox': 1.3.3(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
|
||||
@@ -3488,9 +3488,9 @@ export interface InframonitoringtypesClustersDTO {
|
||||
*/
|
||||
endTimeBeforeRetention: boolean;
|
||||
/**
|
||||
* @type array,null
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesClusterRecordDTO[] | null;
|
||||
records: InframonitoringtypesClusterRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
@@ -3566,9 +3566,9 @@ export interface InframonitoringtypesDaemonSetsDTO {
|
||||
*/
|
||||
endTimeBeforeRetention: boolean;
|
||||
/**
|
||||
* @type array,null
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesDaemonSetRecordDTO[] | null;
|
||||
records: InframonitoringtypesDaemonSetRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
@@ -3644,9 +3644,9 @@ export interface InframonitoringtypesDeploymentsDTO {
|
||||
*/
|
||||
endTimeBeforeRetention: boolean;
|
||||
/**
|
||||
* @type array,null
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesDeploymentRecordDTO[] | null;
|
||||
records: InframonitoringtypesDeploymentRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
@@ -3730,9 +3730,9 @@ export interface InframonitoringtypesHostsDTO {
|
||||
*/
|
||||
endTimeBeforeRetention: boolean;
|
||||
/**
|
||||
* @type array,null
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesHostRecordDTO[] | null;
|
||||
records: InframonitoringtypesHostRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
@@ -3816,9 +3816,9 @@ export interface InframonitoringtypesJobsDTO {
|
||||
*/
|
||||
endTimeBeforeRetention: boolean;
|
||||
/**
|
||||
* @type array,null
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesJobRecordDTO[] | null;
|
||||
records: InframonitoringtypesJobRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
@@ -3866,9 +3866,9 @@ export interface InframonitoringtypesNamespacesDTO {
|
||||
*/
|
||||
endTimeBeforeRetention: boolean;
|
||||
/**
|
||||
* @type array,null
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesNamespaceRecordDTO[] | null;
|
||||
records: InframonitoringtypesNamespaceRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
@@ -3933,9 +3933,9 @@ export interface InframonitoringtypesNodesDTO {
|
||||
*/
|
||||
endTimeBeforeRetention: boolean;
|
||||
/**
|
||||
* @type array,null
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesNodeRecordDTO[] | null;
|
||||
records: InframonitoringtypesNodeRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
@@ -4017,9 +4017,9 @@ export interface InframonitoringtypesPodsDTO {
|
||||
*/
|
||||
endTimeBeforeRetention: boolean;
|
||||
/**
|
||||
* @type array,null
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesPodRecordDTO[] | null;
|
||||
records: InframonitoringtypesPodRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
@@ -4437,9 +4437,9 @@ export interface InframonitoringtypesStatefulSetsDTO {
|
||||
*/
|
||||
endTimeBeforeRetention: boolean;
|
||||
/**
|
||||
* @type array,null
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesStatefulSetRecordDTO[] | null;
|
||||
records: InframonitoringtypesStatefulSetRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
@@ -4506,9 +4506,9 @@ export interface InframonitoringtypesVolumesDTO {
|
||||
*/
|
||||
endTimeBeforeRetention: boolean;
|
||||
/**
|
||||
* @type array,null
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesVolumeRecordDTO[] | null;
|
||||
records: InframonitoringtypesVolumeRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
|
||||
@@ -137,7 +137,6 @@ function CreateServiceAccountModal(): JSX.Element {
|
||||
<AuthZTooltip checks={[SACreatePermission]}>
|
||||
<Button
|
||||
type="submit"
|
||||
// @ts-expect-error -- form prop not in @signozhq/ui Button type - TODO: Fix this - @SagarRajput
|
||||
form="create-sa-form"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
|
||||
@@ -11,9 +11,6 @@ import { GuardAuthZ } from './GuardAuthZ';
|
||||
describe('GuardAuthZ', () => {
|
||||
const TestChild = (): ReactElement => <div>Protected Content</div>;
|
||||
const LoadingFallback = (): ReactElement => <div>Loading...</div>;
|
||||
const ErrorFallback = (error: Error): ReactElement => (
|
||||
<div>Error occurred: {error.message}</div>
|
||||
);
|
||||
const NoPermissionFallback = (_response: {
|
||||
requiredPermissionName: BrandedPermission;
|
||||
}): ReactElement => <div>Access denied</div>;
|
||||
@@ -90,40 +87,28 @@ describe('GuardAuthZ', () => {
|
||||
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render fallbackOnError when API error occurs', async () => {
|
||||
const errorMessage = 'Internal Server Error';
|
||||
|
||||
it('should render children when API error occurs and no fallbackOnError provided (fail open)', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
|
||||
return res(ctx.status(500), ctx.json({ error: errorMessage }));
|
||||
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
|
||||
}),
|
||||
);
|
||||
|
||||
render(
|
||||
<GuardAuthZ relation="read" object="role:*" fallbackOnError={ErrorFallback}>
|
||||
<GuardAuthZ relation="read" object="role:*">
|
||||
<TestChild />
|
||||
</GuardAuthZ>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Error occurred:/)).toBeInTheDocument();
|
||||
expect(screen.getByText('Protected Content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should pass error object to fallbackOnError function', async () => {
|
||||
const errorMessage = 'Network request failed';
|
||||
let receivedError: Error | null = null;
|
||||
|
||||
const errorFallbackWithCapture = (error: Error): ReactElement => {
|
||||
receivedError = error;
|
||||
return <div>Captured error: {error.message}</div>;
|
||||
};
|
||||
|
||||
it('should render fallbackOnError when API error occurs and fallbackOnError is provided', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
|
||||
return res(ctx.status(500), ctx.json({ error: errorMessage }));
|
||||
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -131,35 +116,14 @@ describe('GuardAuthZ', () => {
|
||||
<GuardAuthZ
|
||||
relation="read"
|
||||
object="role:*"
|
||||
fallbackOnError={errorFallbackWithCapture}
|
||||
fallbackOnError={<div>Custom error fallback</div>}
|
||||
>
|
||||
<TestChild />
|
||||
</GuardAuthZ>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(receivedError).not.toBeNull();
|
||||
});
|
||||
|
||||
expect(receivedError).toBeInstanceOf(Error);
|
||||
expect(screen.getByText(/Captured error:/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render null when error occurs and no fallbackOnError provided', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
|
||||
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
|
||||
}),
|
||||
);
|
||||
|
||||
const { container } = render(
|
||||
<GuardAuthZ relation="read" object="role:*">
|
||||
<TestChild />
|
||||
</GuardAuthZ>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.firstChild).toBeNull();
|
||||
expect(screen.getByText('Custom error fallback')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
|
||||
@@ -12,7 +12,7 @@ export type GuardAuthZProps<R extends AuthZRelation> = {
|
||||
relation: R;
|
||||
object: AuthZObject<R>;
|
||||
fallbackOnLoading?: JSX.Element;
|
||||
fallbackOnError?: (error: Error) => JSX.Element;
|
||||
fallbackOnError?: JSX.Element;
|
||||
fallbackOnNoPermissions?: (response: {
|
||||
requiredPermissionName: BrandedPermission;
|
||||
}) => JSX.Element;
|
||||
@@ -35,7 +35,7 @@ export function GuardAuthZ<R extends AuthZRelation>({
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return fallbackOnError?.(error) ?? null;
|
||||
return fallbackOnError ?? children;
|
||||
}
|
||||
|
||||
if (!permissions?.[permission]?.isGranted) {
|
||||
|
||||
@@ -13,7 +13,7 @@ export function NoAuthBanner(): JSX.Element {
|
||||
Impersonation mode: authentication is disabled. Anyone with access to this
|
||||
instance has admin privileges.{' '}
|
||||
<a
|
||||
href="https://signoz.io/docs/manage/administrator-guide/configuration/no-auth-mode/"
|
||||
href="https://signoz.io/docs/manage/administrator-guide/configuration/impersonation-mode/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
|
||||
@@ -127,7 +127,6 @@ function KeyFormPhase({
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
// @ts-expect-error -- form prop not in @signozhq/ui Button type - TODO: Fix this - @SagarRajput
|
||||
form={FORM_ID}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
|
||||
@@ -190,7 +190,6 @@ function EditKeyForm({
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
// @ts-expect-error -- form prop not in @signozhq/ui Button type - TODO: Fix this - @SagarRajput
|
||||
form={FORM_ID}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
|
||||
@@ -204,7 +204,7 @@ describe('createGuardedRoute', () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render error fallback when API error occurs', async () => {
|
||||
it('should render the component when API error occurs (fail open)', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
|
||||
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
|
||||
@@ -230,12 +230,8 @@ describe('createGuardedRoute', () => {
|
||||
render(<GuardedComponent {...props} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Something went wrong/i)).toBeInTheDocument();
|
||||
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.queryByText('Test Component: test-value'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render no permissions fallback when permission is denied', async () => {
|
||||
@@ -9,14 +9,11 @@ import { parsePermission } from 'hooks/useAuthZ/utils';
|
||||
|
||||
import noDataUrl from '@/assets/Icons/no-data.svg';
|
||||
|
||||
import ErrorBoundaryFallback from '../../pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import AppLoading from '../AppLoading/AppLoading';
|
||||
import { GuardAuthZ } from '../GuardAuthZ/GuardAuthZ';
|
||||
|
||||
import './createGuardedRoute.styles.scss';
|
||||
|
||||
const onErrorFallback = (): JSX.Element => <ErrorBoundaryFallback />;
|
||||
|
||||
function OnNoPermissionsFallback(response: {
|
||||
requiredPermissionName: BrandedPermission;
|
||||
}): ReactElement {
|
||||
@@ -63,7 +60,6 @@ export function createGuardedRoute<P extends object, R extends AuthZRelation>(
|
||||
relation={relation}
|
||||
object={resolvedObject}
|
||||
fallbackOnLoading={<AppLoading />}
|
||||
fallbackOnError={onErrorFallback}
|
||||
fallbackOnNoPermissions={(response): ReactElement => (
|
||||
<OnNoPermissionsFallback {...response} />
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
import { usePanelContextMenu } from '../usePanelContextMenu';
|
||||
|
||||
// The hook composes `useCoordinates` (popover state) and `useGraphContextMenu`
|
||||
// (menu items). We mock both so the test focuses on the `enableDrillDown` gate
|
||||
// rather than the implementation of the menu wiring itself.
|
||||
const onClickMock = jest.fn();
|
||||
jest.mock('periscope/components/ContextMenu', () => ({
|
||||
useCoordinates: (): unknown => ({
|
||||
coordinates: null,
|
||||
popoverPosition: null,
|
||||
clickedData: null,
|
||||
onClose: jest.fn(),
|
||||
subMenu: null,
|
||||
onClick: onClickMock,
|
||||
setSubMenu: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/QueryTable/Drilldown/useGraphContextMenu', () => ({
|
||||
__esModule: true,
|
||||
default: (): { menuItemsConfig: { header: string; items: string } } => ({
|
||||
menuItemsConfig: { header: 'menu-header', items: 'menu-items' },
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/QueryTable/Drilldown/drilldownUtils', () => ({
|
||||
getUplotClickData: jest.fn(() => ({
|
||||
coord: { x: 1, y: 2 },
|
||||
record: { queryName: 'A', filters: [] },
|
||||
label: 'lbl',
|
||||
seriesColor: '#abc',
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('container/PanelWrapper/utils', () => ({
|
||||
isApmMetric: jest.fn(() => false),
|
||||
getTimeRangeFromStepInterval: jest.fn(() => ({ start: 0, end: 0 })),
|
||||
}));
|
||||
|
||||
const mockWidget = { id: 'w-1', query: {} } as unknown as Widgets;
|
||||
const mockQueryResponse = {
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
} as unknown as UseQueryResult<
|
||||
SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||
Error
|
||||
>;
|
||||
|
||||
describe('usePanelContextMenu', () => {
|
||||
beforeEach(() => {
|
||||
onClickMock.mockClear();
|
||||
});
|
||||
|
||||
it('returns empty menuItemsConfig when enableDrillDown is false', () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePanelContextMenu({
|
||||
widget: mockWidget,
|
||||
queryResponse: mockQueryResponse,
|
||||
enableDrillDown: false,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.menuItemsConfig).toStrictEqual({});
|
||||
});
|
||||
|
||||
it('returns wired menuItemsConfig when enableDrillDown is true', () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePanelContextMenu({
|
||||
widget: mockWidget,
|
||||
queryResponse: mockQueryResponse,
|
||||
enableDrillDown: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.menuItemsConfig).toStrictEqual({
|
||||
header: 'menu-header',
|
||||
items: 'menu-items',
|
||||
});
|
||||
});
|
||||
|
||||
it('clickHandlerWithContextMenu is a no-op when enableDrillDown is false', () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePanelContextMenu({
|
||||
widget: mockWidget,
|
||||
queryResponse: mockQueryResponse,
|
||||
enableDrillDown: false,
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.clickHandlerWithContextMenu(
|
||||
100, // xValue
|
||||
200, // yValue
|
||||
0, // mouseX
|
||||
0, // mouseY
|
||||
{ serviceName: 'svc' }, // metric
|
||||
{ queryName: 'A', inFocusOrNot: true }, // queryData
|
||||
10, // absoluteMouseX
|
||||
20, // absoluteMouseY
|
||||
{}, // axesData
|
||||
{ seriesIndex: 0, seriesName: 'A', value: 1, color: '#abc' }, // focusedSeries
|
||||
);
|
||||
|
||||
expect(onClickMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('clickHandlerWithContextMenu opens popover when enableDrillDown is true', () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePanelContextMenu({
|
||||
widget: mockWidget,
|
||||
queryResponse: mockQueryResponse,
|
||||
enableDrillDown: true,
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.clickHandlerWithContextMenu(
|
||||
100,
|
||||
200,
|
||||
0,
|
||||
0,
|
||||
{ serviceName: 'svc' },
|
||||
{ queryName: 'A', inFocusOrNot: true },
|
||||
10,
|
||||
20,
|
||||
{},
|
||||
{ seriesIndex: 0, seriesName: 'A', value: 1, color: '#abc' },
|
||||
);
|
||||
|
||||
expect(onClickMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('defaults to disabled when enableDrillDown is not provided', () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePanelContextMenu({
|
||||
widget: mockWidget,
|
||||
queryResponse: mockQueryResponse,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.menuItemsConfig).toStrictEqual({});
|
||||
});
|
||||
});
|
||||
@@ -21,11 +21,13 @@ interface UseTimeSeriesContextMenuParams {
|
||||
SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||
Error
|
||||
>;
|
||||
enableDrillDown?: boolean;
|
||||
}
|
||||
|
||||
export const usePanelContextMenu = ({
|
||||
widget,
|
||||
queryResponse,
|
||||
enableDrillDown = false,
|
||||
}: UseTimeSeriesContextMenuParams): {
|
||||
coordinates: { x: number; y: number } | null;
|
||||
popoverPosition: PopoverPosition | null;
|
||||
@@ -61,6 +63,9 @@ export const usePanelContextMenu = ({
|
||||
|
||||
const clickHandlerWithContextMenu = useCallback(
|
||||
(...args: any[]) => {
|
||||
if (!enableDrillDown) {
|
||||
return;
|
||||
}
|
||||
const [
|
||||
xValue,
|
||||
_yvalue,
|
||||
@@ -112,14 +117,14 @@ export const usePanelContextMenu = ({
|
||||
});
|
||||
}
|
||||
},
|
||||
[onClick, queryResponse],
|
||||
[enableDrillDown, onClick, queryResponse],
|
||||
);
|
||||
|
||||
return {
|
||||
coordinates,
|
||||
popoverPosition,
|
||||
onClose,
|
||||
menuItemsConfig,
|
||||
menuItemsConfig: enableDrillDown ? menuItemsConfig : {},
|
||||
clickHandlerWithContextMenu,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -31,6 +31,7 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
|
||||
isFullViewMode,
|
||||
onToggleModelHandler,
|
||||
groupByPerQuery,
|
||||
enableDrillDown = false,
|
||||
} = props;
|
||||
const uPlotRef = useRef<uPlot | null>(null);
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
@@ -61,6 +62,7 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
|
||||
} = usePanelContextMenu({
|
||||
widget,
|
||||
queryResponse,
|
||||
enableDrillDown,
|
||||
});
|
||||
|
||||
const config = useMemo(() => {
|
||||
|
||||
@@ -31,6 +31,7 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
|
||||
isFullViewMode,
|
||||
onToggleModelHandler,
|
||||
groupByPerQuery,
|
||||
enableDrillDown = false,
|
||||
} = props;
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||
@@ -60,6 +61,7 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
|
||||
} = usePanelContextMenu({
|
||||
widget,
|
||||
queryResponse,
|
||||
enableDrillDown,
|
||||
});
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
|
||||
@@ -292,6 +292,8 @@ function FullView({
|
||||
return <Spinner height="100%" size="large" tip="Loading..." />;
|
||||
}
|
||||
|
||||
const showEditBtn = editWidget && dashboardEditView;
|
||||
|
||||
return (
|
||||
<div className="full-view-container">
|
||||
<OverlayScrollbar>
|
||||
@@ -306,7 +308,7 @@ function FullView({
|
||||
Reset Query
|
||||
</Button>
|
||||
)}
|
||||
{editWidget && (
|
||||
{showEditBtn && (
|
||||
<Button
|
||||
className="switch-edit-btn"
|
||||
disabled={response.isFetching || response.isLoading}
|
||||
|
||||
@@ -30,7 +30,12 @@ import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
import { GraphTitle, MENU_ITEMS, SERVICE_CHART_ID } from '../constant';
|
||||
import {
|
||||
GraphTitle,
|
||||
MENU_ITEMS,
|
||||
SERVICE_CHART_ID,
|
||||
SERVICE_DETAIL_DRILLDOWN_ENABLED,
|
||||
} from '../constant';
|
||||
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
|
||||
import { Card, GraphContainer, Row } from '../styles';
|
||||
import { Button } from './styles';
|
||||
@@ -206,6 +211,7 @@ function DBCall(): JSX.Element {
|
||||
}}
|
||||
onDragSelect={onDragSelect}
|
||||
version={ENTITY_VERSION_V4}
|
||||
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
@@ -244,6 +250,7 @@ function DBCall(): JSX.Element {
|
||||
}}
|
||||
onDragSelect={onDragSelect}
|
||||
version={ENTITY_VERSION_V4}
|
||||
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
|
||||
@@ -32,7 +32,12 @@ import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
import { GraphTitle, legend, MENU_ITEMS } from '../constant';
|
||||
import {
|
||||
GraphTitle,
|
||||
legend,
|
||||
MENU_ITEMS,
|
||||
SERVICE_DETAIL_DRILLDOWN_ENABLED,
|
||||
} from '../constant';
|
||||
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
|
||||
import { Card, GraphContainer, Row } from '../styles';
|
||||
import GraphControlsPanel from './Overview/GraphControlsPanel/GraphControlsPanel';
|
||||
@@ -279,6 +284,7 @@ function External(): JSX.Element {
|
||||
}}
|
||||
onDragSelect={onDragSelect}
|
||||
version={ENTITY_VERSION_V4}
|
||||
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
@@ -322,6 +328,7 @@ function External(): JSX.Element {
|
||||
}}
|
||||
onDragSelect={onDragSelect}
|
||||
version={ENTITY_VERSION_V4}
|
||||
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
@@ -366,6 +373,7 @@ function External(): JSX.Element {
|
||||
}
|
||||
onDragSelect={onDragSelect}
|
||||
version={ENTITY_VERSION_V4}
|
||||
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
@@ -409,6 +417,7 @@ function External(): JSX.Element {
|
||||
}}
|
||||
onDragSelect={onDragSelect}
|
||||
version={ENTITY_VERSION_V4}
|
||||
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
|
||||
@@ -15,6 +15,7 @@ import DisplayThreshold from 'container/GridCardLayout/WidgetHeader/DisplayThres
|
||||
import {
|
||||
GraphTitle,
|
||||
SERVICE_CHART_ID,
|
||||
SERVICE_DETAIL_DRILLDOWN_ENABLED,
|
||||
} from 'container/MetricsApplication/constant';
|
||||
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
|
||||
import { apDexMetricsQueryBuilderQueries } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries';
|
||||
@@ -105,6 +106,7 @@ function ApDexMetrics({
|
||||
threshold={threshold}
|
||||
isQueryEnabled={isQueryEnabled}
|
||||
version={ENTITY_VERSION_V4}
|
||||
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import Graph from 'container/GridCardLayout/GridCard';
|
||||
import {
|
||||
GraphTitle,
|
||||
SERVICE_CHART_ID,
|
||||
SERVICE_DETAIL_DRILLDOWN_ENABLED,
|
||||
} from 'container/MetricsApplication/constant';
|
||||
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
|
||||
import { latency } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries';
|
||||
@@ -138,6 +139,7 @@ function ServiceOverview({
|
||||
onClickHandler={handleGraphClick('Service')}
|
||||
isQueryEnabled={isQueryEnabled}
|
||||
version={ENTITY_VERSION_V4}
|
||||
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
|
||||
/>
|
||||
)}
|
||||
</GraphContainer>
|
||||
|
||||
@@ -4,6 +4,7 @@ import axios from 'axios';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import Graph from 'container/GridCardLayout/GridCard';
|
||||
import { SERVICE_DETAIL_DRILLDOWN_ENABLED } from 'container/MetricsApplication/constant';
|
||||
import { Card, GraphContainer } from 'container/MetricsApplication/styles';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
@@ -43,6 +44,7 @@ function TopLevelOperation({
|
||||
onDragSelect={onDragSelect}
|
||||
isQueryEnabled={!topLevelOperationsIsLoading}
|
||||
version={ENTITY_VERSION_V4}
|
||||
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
|
||||
/>
|
||||
)}
|
||||
</GraphContainer>
|
||||
|
||||
@@ -25,6 +25,8 @@ export const OPERATION_LEGENDS = ['Operations'];
|
||||
|
||||
export const MENU_ITEMS = [MenuItemKeys.View, MenuItemKeys.CreateAlerts];
|
||||
|
||||
export const SERVICE_DETAIL_DRILLDOWN_ENABLED = true;
|
||||
|
||||
export enum FORMULA {
|
||||
ERROR_PERCENTAGE = 'A*100/B',
|
||||
DATABASE_CALLS_AVG_DURATION = 'A/B',
|
||||
|
||||
@@ -127,6 +127,12 @@
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding-bottom: 16px;
|
||||
|
||||
.password-error-text {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--bg-cherry-400);
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-color-picker-trigger {
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Input, Modal } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import {
|
||||
updateMyPassword,
|
||||
useUpdateMyUserV2,
|
||||
} from 'api/generated/services/users';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { Check, FileTerminal, Mail, User } from '@signozhq/icons';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
import { ErrorV2Resp } from 'types/api';
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
import '../MySettings.styles.scss';
|
||||
import './UserInfo.styles.scss';
|
||||
|
||||
function UserInfo(): JSX.Element {
|
||||
const { user, org, updateUser } = useAppContext();
|
||||
const { t } = useTranslation(['routes', 'settings', 'common']);
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const { mutateAsync: updateMyUser } = useUpdateMyUserV2();
|
||||
|
||||
const [currentPassword, setCurrentPassword] = useState<string>('');
|
||||
@@ -47,6 +49,8 @@ function UserInfo(): JSX.Element {
|
||||
|
||||
const hideResetPasswordModal = (): void => {
|
||||
setIsResetPasswordModalOpen(false);
|
||||
setCurrentPassword('');
|
||||
setUpdatePassword('');
|
||||
};
|
||||
|
||||
const onChangePasswordClickHandler = async (): Promise<void> => {
|
||||
@@ -57,27 +61,29 @@ function UserInfo(): JSX.Element {
|
||||
newPassword: updatePassword,
|
||||
oldPassword: currentPassword,
|
||||
});
|
||||
notifications.success({
|
||||
message: t('success', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
toast.success('Password updated successfully');
|
||||
hideResetPasswordModal();
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
notifications.error({
|
||||
message: (error as APIError).error.error.code,
|
||||
description: (error as APIError).error.error.message,
|
||||
});
|
||||
try {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
} catch (apiError) {
|
||||
showErrorModal(apiError as APIError);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const passwordsMatch =
|
||||
currentPassword.length > 0 &&
|
||||
updatePassword.length > 0 &&
|
||||
currentPassword === updatePassword;
|
||||
|
||||
const isResetPasswordDisabled =
|
||||
isLoading ||
|
||||
currentPassword.length === 0 ||
|
||||
updatePassword.length === 0 ||
|
||||
currentPassword === updatePassword;
|
||||
passwordsMatch;
|
||||
|
||||
const onSaveHandler = async (): Promise<void> => {
|
||||
void logEvent('Account Settings: Name Updated', {
|
||||
@@ -94,11 +100,7 @@ function UserInfo(): JSX.Element {
|
||||
setIsLoading(true);
|
||||
await updateMyUser({ data: { displayName: changedName } });
|
||||
|
||||
notifications.success({
|
||||
message: t('success', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
toast.success('Name updated successfully');
|
||||
updateUser({
|
||||
...user,
|
||||
displayName: changedName,
|
||||
@@ -106,10 +108,11 @@ function UserInfo(): JSX.Element {
|
||||
setIsLoading(false);
|
||||
hideUpdateNameModal();
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: (error as APIError).getErrorCode(),
|
||||
description: (error as APIError).getErrorMessage(),
|
||||
});
|
||||
try {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
} catch (apiError) {
|
||||
showErrorModal(apiError as APIError);
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
@@ -166,7 +169,7 @@ function UserInfo(): JSX.Element {
|
||||
type="primary"
|
||||
icon={<Check size={16} />}
|
||||
onClick={onSaveHandler}
|
||||
disabled={isLoading}
|
||||
loading={isLoading}
|
||||
data-testid="update-name-btn"
|
||||
>
|
||||
Update name
|
||||
@@ -178,7 +181,11 @@ function UserInfo(): JSX.Element {
|
||||
<Input
|
||||
placeholder="e.g. John Doe"
|
||||
value={changedName}
|
||||
disabled={isLoading}
|
||||
onChange={(e): void => setChangedName(e.target.value)}
|
||||
onPressEnter={(): void => {
|
||||
void onSaveHandler();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -188,6 +195,7 @@ function UserInfo(): JSX.Element {
|
||||
title={<span className="title">Reset password</span>}
|
||||
open={isResetPasswordModalOpen}
|
||||
closable
|
||||
destroyOnClose
|
||||
onCancel={hideResetPasswordModal}
|
||||
footer={[
|
||||
<Button
|
||||
@@ -197,7 +205,8 @@ function UserInfo(): JSX.Element {
|
||||
}`}
|
||||
icon={<Check size={16} />}
|
||||
onClick={onChangePasswordClickHandler}
|
||||
disabled={isLoading || isResetPasswordDisabled}
|
||||
loading={isLoading}
|
||||
disabled={isResetPasswordDisabled}
|
||||
data-testid="reset-password-btn"
|
||||
>
|
||||
Reset password
|
||||
@@ -218,6 +227,11 @@ function UserInfo(): JSX.Element {
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
visibilityToggle
|
||||
onPressEnter={(): void => {
|
||||
if (!isResetPasswordDisabled) {
|
||||
void onChangePasswordClickHandler();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -235,7 +249,18 @@ function UserInfo(): JSX.Element {
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
visibilityToggle={false}
|
||||
status={passwordsMatch ? 'error' : ''}
|
||||
onPressEnter={(): void => {
|
||||
if (!isResetPasswordDisabled) {
|
||||
void onChangePasswordClickHandler();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{passwordsMatch && (
|
||||
<span className="password-error-text">
|
||||
New password must be different from current password
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -8,11 +8,23 @@ import {
|
||||
waitFor,
|
||||
within,
|
||||
} from 'tests/test-utils';
|
||||
import APIError from 'types/api/error';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
|
||||
const toggleThemeFunction = jest.fn();
|
||||
const logEventFunction = jest.fn();
|
||||
const copyToClipboardFn = jest.fn();
|
||||
const editUserFn = jest.fn();
|
||||
const updateMyPasswordFn = jest.fn();
|
||||
const showErrorModalFn = jest.fn();
|
||||
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
...jest.requireActual('@signozhq/ui/sonner'),
|
||||
toast: {
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('react-use', () => ({
|
||||
__esModule: true,
|
||||
@@ -24,12 +36,21 @@ jest.mock('react-use', () => ({
|
||||
|
||||
jest.mock('api/generated/services/users', () => ({
|
||||
...jest.requireActual('api/generated/services/users'),
|
||||
updateMyPassword: (...args: unknown[]): Promise<unknown> =>
|
||||
updateMyPasswordFn(...args),
|
||||
useUpdateMyUserV2: jest.fn(() => ({
|
||||
mutateAsync: (...args: unknown[]): Promise<unknown> => editUserFn(...args),
|
||||
isLoading: false,
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('providers/ErrorModalProvider', () => ({
|
||||
...jest.requireActual('providers/ErrorModalProvider'),
|
||||
useErrorModal: jest.fn(() => ({
|
||||
showErrorModal: showErrorModalFn,
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useDarkMode', () => ({
|
||||
__esModule: true,
|
||||
useIsDarkMode: jest.fn(() => true),
|
||||
@@ -65,12 +86,12 @@ const NEW_PASSWORD_TEST_ID = 'new-password-textbox';
|
||||
const UPDATE_NAME_BUTTON_TEST_ID = 'update-name-btn';
|
||||
const RESET_PASSWORD_BUTTON_TEST_ID = 'reset-password-btn';
|
||||
const UPDATE_NAME_BUTTON_TEXT = 'Update name';
|
||||
const PASSWORD_VALIDATION_MESSAGE_TEST_ID = 'password-validation-message';
|
||||
|
||||
describe('MySettings Flows', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
editUserFn.mockResolvedValue({});
|
||||
updateMyPasswordFn.mockResolvedValue({});
|
||||
render(<MySettingsContainer />);
|
||||
});
|
||||
|
||||
@@ -152,9 +173,7 @@ describe('MySettings Flows', () => {
|
||||
fireEvent.click(modalUpdateNameButton);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(successNotification).toHaveBeenCalledWith({
|
||||
message: 'success',
|
||||
}),
|
||||
expect(toast.success).toHaveBeenCalledWith('Name updated successfully'),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -181,22 +200,131 @@ describe('MySettings Flows', () => {
|
||||
expect(screen.getByTestId(NEW_PASSWORD_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should display validation error if password is less than 8 characters', async () => {
|
||||
it('Should show inline error when new password matches current password', async () => {
|
||||
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
|
||||
fireEvent.click(resetPasswordButtons[0]);
|
||||
|
||||
const currentPasswordTextbox = screen.getByTestId(CURRENT_PASSWORD_TEST_ID);
|
||||
act(() => {
|
||||
fireEvent.change(currentPasswordTextbox, { target: { value: '123' } });
|
||||
fireEvent.change(screen.getByTestId(CURRENT_PASSWORD_TEST_ID), {
|
||||
target: { value: 'samePassword1' },
|
||||
});
|
||||
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
|
||||
target: { value: 'samePassword1' },
|
||||
});
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText('New password must be different from current password'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId(RESET_PASSWORD_BUTTON_TEST_ID)).toBeDisabled();
|
||||
});
|
||||
|
||||
it('Should hide inline error when passwords are changed to be different', async () => {
|
||||
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
|
||||
fireEvent.click(resetPasswordButtons[0]);
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(screen.getByTestId(CURRENT_PASSWORD_TEST_ID), {
|
||||
target: { value: 'samePassword1' },
|
||||
});
|
||||
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
|
||||
target: { value: 'samePassword1' },
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
|
||||
target: { value: 'differentPassword1' },
|
||||
});
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.queryByText('New password must be different from current password'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should show error modal when password reset API returns an error', async () => {
|
||||
updateMyPasswordFn.mockRejectedValue(
|
||||
new Error('Current password is incorrect'),
|
||||
);
|
||||
|
||||
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
|
||||
fireEvent.click(resetPasswordButtons[0]);
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(screen.getByTestId(CURRENT_PASSWORD_TEST_ID), {
|
||||
target: { value: 'oldPassword1' },
|
||||
});
|
||||
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
|
||||
target: { value: 'newPassword1' },
|
||||
});
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId(RESET_PASSWORD_BUTTON_TEST_ID));
|
||||
|
||||
await waitFor(() => {
|
||||
// Use getByTestId for the validation message (if present in your modal/component)
|
||||
if (screen.queryByTestId(PASSWORD_VALIDATION_MESSAGE_TEST_ID)) {
|
||||
expect(
|
||||
screen.getByTestId(PASSWORD_VALIDATION_MESSAGE_TEST_ID),
|
||||
).toBeInTheDocument();
|
||||
}
|
||||
expect(showErrorModalFn).toHaveBeenCalledWith(expect.any(APIError));
|
||||
});
|
||||
});
|
||||
|
||||
it('Should show success toast and close modal on successful password reset', async () => {
|
||||
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
|
||||
fireEvent.click(resetPasswordButtons[0]);
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(screen.getByTestId(CURRENT_PASSWORD_TEST_ID), {
|
||||
target: { value: 'oldPassword1' },
|
||||
});
|
||||
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
|
||||
target: { value: 'newPassword1' },
|
||||
});
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId(RESET_PASSWORD_BUTTON_TEST_ID));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.success).toHaveBeenCalledWith('Password updated successfully');
|
||||
expect(
|
||||
screen.queryByTestId(CURRENT_PASSWORD_TEST_ID),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Should clear password fields when modal is cancelled', async () => {
|
||||
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
|
||||
fireEvent.click(resetPasswordButtons[0]);
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(screen.getByTestId(CURRENT_PASSWORD_TEST_ID), {
|
||||
target: { value: 'somePassword' },
|
||||
});
|
||||
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
|
||||
target: { value: 'otherPassword' },
|
||||
});
|
||||
});
|
||||
|
||||
expect(screen.getByTestId(CURRENT_PASSWORD_TEST_ID)).toHaveValue(
|
||||
'somePassword',
|
||||
);
|
||||
|
||||
// Close the modal
|
||||
const closeButton = document.querySelector(
|
||||
'.reset-password-modal .ant-modal-close',
|
||||
) as HTMLElement;
|
||||
fireEvent.click(closeButton);
|
||||
|
||||
// Reopen the modal
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByTestId(CURRENT_PASSWORD_TEST_ID),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT)[0]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(CURRENT_PASSWORD_TEST_ID)).toHaveValue('');
|
||||
expect(screen.getByTestId(NEW_PASSWORD_TEST_ID)).toHaveValue('');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -169,9 +169,10 @@ describe('drilldownUtils', () => {
|
||||
|
||||
// Verify transformations were applied
|
||||
if (filterExpression) {
|
||||
// Rule 2: operation → name
|
||||
expect(filterExpression).toContain(`name = 'GET'`);
|
||||
// `operation` rewrites to `name` via source-side pass, then `name`
|
||||
// is dropped by the logs target-side pass (logs has no span-name).
|
||||
expect(filterExpression).not.toContain(`operation = 'GET'`);
|
||||
expect(filterExpression).not.toContain(`name = 'GET'`);
|
||||
|
||||
// Rule 3: span.kind → kind
|
||||
expect(filterExpression).toContain(`${spanKindKey} = '${spanKindServer}'`);
|
||||
@@ -262,8 +263,9 @@ describe('drilldownUtils', () => {
|
||||
const filterExpression = result?.builder.queryData[0]?.filter?.expression;
|
||||
|
||||
if (filterExpression) {
|
||||
// All transformations should be applied
|
||||
expect(filterExpression).toContain(`name = 'POST'`);
|
||||
// `operation` rewrites to `name` then drops for logs target.
|
||||
expect(filterExpression).not.toContain(`operation = 'POST'`);
|
||||
expect(filterExpression).not.toContain(`name = 'POST'`);
|
||||
expect(filterExpression).toContain(`${spanKindKey} = '${spanKindClient}'`);
|
||||
expect(filterExpression).toContain(`status_code_string = 'Error'`);
|
||||
expect(filterExpression).toContain(`http.status_code = 500`);
|
||||
@@ -410,8 +412,9 @@ describe('drilldownUtils', () => {
|
||||
const filterExpression = result?.builder.queryData[0]?.filter?.expression;
|
||||
|
||||
if (filterExpression) {
|
||||
// Transformed attributes
|
||||
expect(filterExpression).toContain(`name = 'GET'`);
|
||||
// `operation` rewrites to `name` then drops for logs target.
|
||||
expect(filterExpression).not.toContain(`operation = 'GET'`);
|
||||
expect(filterExpression).not.toContain(`name = 'GET'`);
|
||||
expect(filterExpression).toContain(`${spanKindKey} = '${spanKindServer}'`);
|
||||
|
||||
// Preserved non-metric attributes
|
||||
@@ -499,4 +502,189 @@ describe('drilldownUtils', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getViewQuery target-aware sanitisation (serviceName / name)', () => {
|
||||
const makeQuery = (
|
||||
expression: string,
|
||||
dataSource: 'traces' | 'logs' | 'metrics' = 'traces',
|
||||
): Query => ({
|
||||
id: 'src-query',
|
||||
queryType: 'builder' as any,
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
queryName: 'src',
|
||||
dataSource: dataSource as any,
|
||||
aggregations: [{ metricName: 'non_apm_metric' }] as any,
|
||||
groupBy: [],
|
||||
expression: '',
|
||||
disabled: false,
|
||||
functions: [],
|
||||
legend: '',
|
||||
having: [],
|
||||
limit: null,
|
||||
stepInterval: undefined,
|
||||
orderBy: [],
|
||||
filter: { expression },
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
});
|
||||
|
||||
it('rewrites serviceName -> service.name when drilling to logs', () => {
|
||||
const result = getViewQuery(
|
||||
makeQuery(`serviceName = 'svc'`),
|
||||
[],
|
||||
'view_logs',
|
||||
'src',
|
||||
);
|
||||
const expr = result?.builder.queryData[0]?.filter?.expression || '';
|
||||
expect(expr).toContain(`service.name = 'svc'`);
|
||||
expect(expr).not.toContain('serviceName');
|
||||
});
|
||||
|
||||
it('rewrites serviceName -> service.name when drilling to traces', () => {
|
||||
const result = getViewQuery(
|
||||
makeQuery(`serviceName = 'svc'`),
|
||||
[],
|
||||
'view_traces',
|
||||
'src',
|
||||
);
|
||||
const expr = result?.builder.queryData[0]?.filter?.expression || '';
|
||||
expect(expr).toContain(`service.name = 'svc'`);
|
||||
expect(expr).not.toContain('serviceName');
|
||||
});
|
||||
|
||||
it('drops `name` clause when drilling to logs', () => {
|
||||
const result = getViewQuery(
|
||||
makeQuery(`name = 'GET /api'`),
|
||||
[],
|
||||
'view_logs',
|
||||
'src',
|
||||
);
|
||||
const expr = result?.builder.queryData[0]?.filter?.expression || '';
|
||||
expect(expr).not.toContain(`name = 'GET /api'`);
|
||||
});
|
||||
|
||||
it('keeps `name` clause when drilling to traces', () => {
|
||||
const result = getViewQuery(
|
||||
makeQuery(`name = 'GET /api'`),
|
||||
[],
|
||||
'view_traces',
|
||||
'src',
|
||||
);
|
||||
const expr = result?.builder.queryData[0]?.filter?.expression || '';
|
||||
expect(expr).toContain(`name = 'GET /api'`);
|
||||
});
|
||||
|
||||
it('combined: drilling to logs rewrites serviceName and drops name', () => {
|
||||
const result = getViewQuery(
|
||||
makeQuery(`serviceName = 'svc' AND name = 'GET /api'`),
|
||||
[],
|
||||
'view_logs',
|
||||
'src',
|
||||
);
|
||||
const expr = result?.builder.queryData[0]?.filter?.expression || '';
|
||||
expect(expr).toContain(`service.name = 'svc'`);
|
||||
expect(expr).not.toContain('serviceName');
|
||||
expect(expr).not.toContain(`name = 'GET /api'`);
|
||||
});
|
||||
|
||||
it('combined: drilling to traces rewrites serviceName and keeps name', () => {
|
||||
const result = getViewQuery(
|
||||
makeQuery(`serviceName = 'svc' AND name = 'GET /api'`),
|
||||
[],
|
||||
'view_traces',
|
||||
'src',
|
||||
);
|
||||
const expr = result?.builder.queryData[0]?.filter?.expression || '';
|
||||
expect(expr).toContain(`service.name = 'svc'`);
|
||||
expect(expr).toContain(`name = 'GET /api'`);
|
||||
expect(expr).not.toContain('serviceName');
|
||||
});
|
||||
|
||||
it('metric-APM source -> traces target preserves existing operation -> name rewrite', () => {
|
||||
const metricsQuery: Query = {
|
||||
id: 'apm-metrics',
|
||||
queryType: 'builder' as any,
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
queryName: 'm',
|
||||
dataSource: 'metrics' as any,
|
||||
aggregations: [{ metricName: 'signoz_calls_total' }] as any,
|
||||
groupBy: [],
|
||||
expression: '',
|
||||
disabled: false,
|
||||
functions: [],
|
||||
legend: '',
|
||||
having: [],
|
||||
limit: null,
|
||||
stepInterval: undefined,
|
||||
orderBy: [],
|
||||
filter: { expression: `operation = 'GET'` },
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
};
|
||||
const result = getViewQuery(metricsQuery, [], 'view_traces', 'm');
|
||||
const expr = result?.builder.queryData[0]?.filter?.expression || '';
|
||||
expect(expr).toContain(`name = 'GET'`);
|
||||
expect(expr).not.toContain(`operation = 'GET'`);
|
||||
});
|
||||
|
||||
it('metric-APM source -> logs target: operation rewrites to name, then dropped', () => {
|
||||
const metricsQuery: Query = {
|
||||
id: 'apm-metrics',
|
||||
queryType: 'builder' as any,
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
queryName: 'm',
|
||||
dataSource: 'metrics' as any,
|
||||
aggregations: [{ metricName: 'signoz_calls_total' }] as any,
|
||||
groupBy: [],
|
||||
expression: '',
|
||||
disabled: false,
|
||||
functions: [],
|
||||
legend: '',
|
||||
having: [],
|
||||
limit: null,
|
||||
stepInterval: undefined,
|
||||
orderBy: [],
|
||||
filter: { expression: `operation = 'GET'` },
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
};
|
||||
const result = getViewQuery(metricsQuery, [], 'view_logs', 'm');
|
||||
const expr = result?.builder.queryData[0]?.filter?.expression || '';
|
||||
expect(expr).not.toContain(`operation = 'GET'`);
|
||||
expect(expr).not.toContain(`name = 'GET'`);
|
||||
});
|
||||
|
||||
it('drilling to metrics does not apply target-side sanitisation', () => {
|
||||
const result = getViewQuery(
|
||||
makeQuery(`serviceName = 'svc' AND name = 'GET /api'`),
|
||||
[],
|
||||
'view_metrics',
|
||||
'src',
|
||||
);
|
||||
const expr = result?.builder.queryData[0]?.filter?.expression || '';
|
||||
expect(expr).toContain('serviceName');
|
||||
expect(expr).toContain(`name = 'GET /api'`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,8 +7,10 @@ import {
|
||||
import ROUTES from 'constants/routes';
|
||||
import { isApmMetric } from 'container/PanelWrapper/utils';
|
||||
import {
|
||||
applyMappingsToExpression,
|
||||
DRILLDOWN_TO_LOGS_MAPPINGS,
|
||||
DRILLDOWN_TO_TRACES_MAPPINGS,
|
||||
METRIC_TO_LOGS_TRACES_MAPPINGS,
|
||||
replaceKeysAndValuesInExpression,
|
||||
} from 'container/QueryTable/Drilldown/metricsCorrelationUtils';
|
||||
import cloneDeep from 'lodash-es/cloneDeep';
|
||||
import {
|
||||
@@ -347,27 +349,41 @@ export const getViewQuery = (
|
||||
newQuery.builder.queryData[0].filter = newFilterExpression;
|
||||
|
||||
try {
|
||||
// ===========================================
|
||||
// TEMP LOGIC - TO BE REMOVED LATER
|
||||
// ===========================================
|
||||
// Apply metric-to-logs/traces transformations
|
||||
// Drill-down filter sanitisation. Two stages:
|
||||
// 1. Source-side: rewrite metric-APM-specific keys (operation, span.kind,
|
||||
// status.code) so they map onto trace/log columns.
|
||||
// 2. Target-side: normalise legacy keys to OTel-canonical (`serviceName`
|
||||
// -> `service.name`) and drop keys with no equivalent in the target
|
||||
// datasource (e.g. `name` for logs).
|
||||
let expression = newFilterExpression?.expression || '';
|
||||
|
||||
const specificQuery = getQueryData(query, queryName);
|
||||
const isMetricQuery = specificQuery?.dataSource === 'metrics';
|
||||
const metricName = (specificQuery?.aggregations?.[0] as MetricAggregation)
|
||||
?.metricName;
|
||||
|
||||
if (isMetricQuery && isApmMetric(metricName || '')) {
|
||||
const transformedExpression = replaceKeysAndValuesInExpression(
|
||||
newFilterExpression?.expression || '',
|
||||
expression = applyMappingsToExpression(
|
||||
expression,
|
||||
METRIC_TO_LOGS_TRACES_MAPPINGS,
|
||||
);
|
||||
newQuery.builder.queryData[0].filter = {
|
||||
expression: transformedExpression || '',
|
||||
};
|
||||
}
|
||||
// ===========================================
|
||||
|
||||
if (key === 'view_logs') {
|
||||
expression = applyMappingsToExpression(
|
||||
expression,
|
||||
DRILLDOWN_TO_LOGS_MAPPINGS,
|
||||
);
|
||||
} else if (key === 'view_traces') {
|
||||
expression = applyMappingsToExpression(
|
||||
expression,
|
||||
DRILLDOWN_TO_TRACES_MAPPINGS,
|
||||
);
|
||||
}
|
||||
|
||||
newQuery.builder.queryData[0].filter = { expression };
|
||||
} catch (error) {
|
||||
console.error('Error transforming metrics to logs/traces:', error);
|
||||
console.error('Error sanitising drilldown filter expression:', error);
|
||||
}
|
||||
|
||||
return newQuery;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
|
||||
import {
|
||||
formatValueForExpression,
|
||||
removeKeysFromExpression,
|
||||
} from 'components/QueryBuilderV2/utils';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { IQueryPair } from 'types/antlrQueryTypes';
|
||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
@@ -8,7 +11,7 @@ import { isFunctionOperator, isNonValueOperator } from 'utils/tokenUtils';
|
||||
|
||||
type KeyValueMapping = {
|
||||
attribute: string;
|
||||
newAttribute: string;
|
||||
newAttribute: string | null;
|
||||
valueMappings: Record<string, string>;
|
||||
};
|
||||
|
||||
@@ -40,8 +43,33 @@ export const METRIC_TO_LOGS_TRACES_MAPPINGS: KeyValueMapping[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const DRILLDOWN_TO_LOGS_MAPPINGS: KeyValueMapping[] = [
|
||||
{
|
||||
attribute: 'serviceName',
|
||||
newAttribute: 'service.name',
|
||||
valueMappings: {},
|
||||
},
|
||||
{
|
||||
attribute: 'name',
|
||||
newAttribute: null,
|
||||
valueMappings: {},
|
||||
},
|
||||
];
|
||||
|
||||
export const DRILLDOWN_TO_TRACES_MAPPINGS: KeyValueMapping[] = [
|
||||
{
|
||||
attribute: 'serviceName',
|
||||
newAttribute: 'service.name',
|
||||
valueMappings: {},
|
||||
},
|
||||
];
|
||||
|
||||
// Logic for rewriting key/values in an expression using provided mappings.
|
||||
function modifyKeyVal(pair: IQueryPair, mapping: KeyValueMapping): string {
|
||||
// Callers must pre-filter mappings to ensure newAttribute is non-null.
|
||||
function modifyKeyVal(
|
||||
pair: IQueryPair,
|
||||
mapping: KeyValueMapping & { newAttribute: string },
|
||||
): string {
|
||||
const newKey = mapping.newAttribute;
|
||||
const op = pair.operator;
|
||||
|
||||
@@ -107,8 +135,18 @@ export function replaceKeysAndValuesInExpression(
|
||||
return expression;
|
||||
}
|
||||
|
||||
const attributeToMapping = new Map<string, KeyValueMapping>(
|
||||
mappingList.map((m) => [m.attribute.trim().toLowerCase(), m]),
|
||||
// Only rewrite mappings (newAttribute non-null) are processed here.
|
||||
// Drops are handled separately by applyMappingsToExpression via removeKeysFromExpression.
|
||||
const attributeToMapping = new Map<
|
||||
string,
|
||||
KeyValueMapping & { newAttribute: string }
|
||||
>(
|
||||
mappingList
|
||||
.filter(
|
||||
(m): m is KeyValueMapping & { newAttribute: string } =>
|
||||
m.newAttribute !== null,
|
||||
)
|
||||
.map((m) => [m.attribute.trim().toLowerCase(), m]),
|
||||
);
|
||||
|
||||
const pairs: IQueryPair[] = extractQueryPairs(expression);
|
||||
@@ -179,3 +217,26 @@ export function replaceKeysAndValuesInExpression(
|
||||
|
||||
return resultParts.join('');
|
||||
}
|
||||
|
||||
// Apply a list of mappings to a filter expression. Rewrites are applied first
|
||||
// (newAttribute is a string), then drops (newAttribute is null) via the
|
||||
// ANTLR-parser-based removeKeysFromExpression which handles AND/OR/NOT/paren
|
||||
// elision correctly.
|
||||
export function applyMappingsToExpression(
|
||||
expression: string,
|
||||
mappings: KeyValueMapping[],
|
||||
): string {
|
||||
if (!expression || !mappings || mappings.length === 0) {
|
||||
return expression;
|
||||
}
|
||||
|
||||
const dropKeys = mappings
|
||||
.filter((m) => m.newAttribute === null)
|
||||
.map((m) => m.attribute);
|
||||
|
||||
let result = replaceKeysAndValuesInExpression(expression, mappings);
|
||||
if (dropKeys.length > 0) {
|
||||
result = removeKeysFromExpression(result, dropKeys);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -48,11 +48,10 @@ describe('useAuthZ', () => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.error).toBeNull();
|
||||
expect(result.current.permissions).toStrictEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it('should handle API errors', async () => {
|
||||
it('should return error and null permissions when API errors', async () => {
|
||||
const permission = buildPermission('read', 'role:*');
|
||||
|
||||
server.use(
|
||||
@@ -191,7 +190,6 @@ describe('useAuthZ', () => {
|
||||
});
|
||||
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.error).toBeNull();
|
||||
expect(result.current.permissions).toStrictEqual({});
|
||||
});
|
||||
|
||||
@@ -119,6 +119,12 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.statusMessageBadge {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.traceId {
|
||||
color: var(--accent-primary);
|
||||
overflow: hidden;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import ExpandableValue from 'periscope/components/ExpandableValue';
|
||||
import { SpanV3 } from 'types/api/trace/getTraceV3';
|
||||
|
||||
import styles from './SpanDetailsPanel.module.scss';
|
||||
@@ -48,7 +49,15 @@ export const HIGHLIGHTED_OPTIONS: HighlightedOption[] = [
|
||||
label: 'STATUS MESSAGE',
|
||||
render: (span): ReactNode | null =>
|
||||
span.status_message ? (
|
||||
<Badge color="vanilla">{span.status_message}</Badge>
|
||||
<ExpandableValue value={span.status_message} title="Status message">
|
||||
<Badge
|
||||
color="vanilla"
|
||||
textEllipsis="end"
|
||||
className={styles.statusMessageBadge}
|
||||
>
|
||||
{span.status_message}
|
||||
</Badge>
|
||||
</ExpandableValue>
|
||||
) : null,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.traceOptionsDropdown {
|
||||
z-index: 1100;
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import { Ellipsis } from '@signozhq/icons';
|
||||
|
||||
import { useTraceStore } from '../stores/traceStore';
|
||||
|
||||
import styles from './TraceOptionsMenu.module.scss';
|
||||
|
||||
interface TraceOptionsMenuProps {
|
||||
showTraceDetails: boolean;
|
||||
onToggleTraceDetails: () => void;
|
||||
@@ -82,7 +84,11 @@ function TraceOptionsMenu({
|
||||
]);
|
||||
|
||||
return (
|
||||
<Dropdown menu={{ items: menuItems }} align="start">
|
||||
<Dropdown
|
||||
menu={{ items: menuItems }}
|
||||
align="start"
|
||||
className={styles.traceOptionsDropdown}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
.trigger {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
[data-truncated='true'] {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltipContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-width: 480px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.preview {
|
||||
margin: 0;
|
||||
max-height: 220px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: var(--font-family-mono, monospace);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.expandButton {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
max-width: 80vw;
|
||||
width: 80vw;
|
||||
}
|
||||
|
||||
.fullValue {
|
||||
margin: 0;
|
||||
max-height: 70vh;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: var(--font-family-mono, monospace);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { ReactNode, useState } from 'react';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DialogWrapper } from '@signozhq/ui/dialog';
|
||||
import {
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipRoot,
|
||||
TooltipTrigger,
|
||||
} from '@signozhq/ui/tooltip';
|
||||
import { Fullscreen } from '@signozhq/icons';
|
||||
|
||||
import styles from './ExpandableValue.module.scss';
|
||||
|
||||
const DEFAULT_THRESHOLD = 100;
|
||||
const DEFAULT_DIALOG_TITLE = 'Value';
|
||||
|
||||
const DEFAULT_Z_INDEX = 1100;
|
||||
|
||||
interface ExpandableValueProps {
|
||||
value: string;
|
||||
title?: string;
|
||||
threshold?: number;
|
||||
zIndex?: number;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function ExpandableValue({
|
||||
value,
|
||||
title = DEFAULT_DIALOG_TITLE,
|
||||
threshold = DEFAULT_THRESHOLD,
|
||||
zIndex = DEFAULT_Z_INDEX,
|
||||
children,
|
||||
}: ExpandableValueProps): JSX.Element {
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
|
||||
if (value.length <= threshold) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipRoot>
|
||||
<TooltipTrigger asChild>
|
||||
<span className={styles.trigger}>{children}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className={styles.tooltipContent}
|
||||
side="top"
|
||||
style={{ zIndex }}
|
||||
>
|
||||
<pre className={styles.preview}>{value}</pre>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
prefix={<Fullscreen size={14} />}
|
||||
onClick={(): void => setIsDialogOpen(true)}
|
||||
className={styles.expandButton}
|
||||
>
|
||||
Expand
|
||||
</Button>
|
||||
</TooltipContent>
|
||||
</TooltipRoot>
|
||||
|
||||
<DialogWrapper
|
||||
title={title}
|
||||
open={isDialogOpen}
|
||||
onOpenChange={setIsDialogOpen}
|
||||
className={styles.dialog}
|
||||
style={{ zIndex }}
|
||||
>
|
||||
<pre className={styles.fullValue}>{value}</pre>
|
||||
</DialogWrapper>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExpandableValue;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './ExpandableValue';
|
||||
@@ -227,10 +227,8 @@ function PrettyView({
|
||||
menu={{ items: menuItems }}
|
||||
align="start"
|
||||
className="pretty-view-actions-dropdown"
|
||||
// onClick on the dropdown content is forwarded to the underlying div via ...props
|
||||
// but is not in the public type. Stop click bubbling so item clicks don't reach
|
||||
// clickable ancestors of the trigger through the React tree.
|
||||
// @ts-expect-error see comment above
|
||||
// Stop click bubbling so item clicks don't reach clickable ancestors
|
||||
// of the trigger through the React tree.
|
||||
onClick={(e: React.MouseEvent): void => e.stopPropagation()}
|
||||
>
|
||||
<span
|
||||
|
||||
@@ -129,14 +129,13 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
const {
|
||||
permissions: permissionsResult,
|
||||
isFetching: isFetchingPermissions,
|
||||
error: errorOnPermissions,
|
||||
refetchPermissions,
|
||||
} = useAuthZ([IsAdminPermission, IsEditorPermission, IsViewerPermission], {
|
||||
enabled: isLoggedIn,
|
||||
});
|
||||
|
||||
const isFetchingUser = isFetchingUserData || isFetchingPermissions;
|
||||
const userFetchError = userFetchDataError || errorOnPermissions;
|
||||
const userFetchError = userFetchDataError;
|
||||
|
||||
const userRole = useMemo(() => {
|
||||
if (permissionsResult?.[IsAdminPermission]?.isGranted) {
|
||||
|
||||
@@ -298,9 +298,32 @@ describe('AppProvider when authz/check fails', () => {
|
||||
beforeEach(() => {
|
||||
queryClient.clear();
|
||||
setLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN, 'true');
|
||||
server.use(
|
||||
rest.get(MY_USER_URL, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: {
|
||||
id: 'u-1',
|
||||
displayName: 'Test User',
|
||||
email: 'test@signoz.io',
|
||||
orgId: 'org-1',
|
||||
isRoot: false,
|
||||
status: 'active',
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.get(MY_ORG_URL, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ data: { id: 'org-1', displayName: 'Org' } }),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('sets userFetchError when authz/check returns 500 (same as user fetch error)', async () => {
|
||||
it('does not set userFetchError when authz/check returns 500 (authz errors are ignored)', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, (_, res, ctx) =>
|
||||
res(ctx.status(500), ctx.json({ error: 'Internal Server Error' })),
|
||||
@@ -314,13 +337,13 @@ describe('AppProvider when authz/check fails', () => {
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result.current.userFetchError).toBeTruthy();
|
||||
expect(result.current.userFetchError).toBeFalsy();
|
||||
},
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
});
|
||||
|
||||
it('sets userFetchError when authz/check fails with network error (same as user fetch error)', async () => {
|
||||
it('does not set userFetchError when authz/check fails with network error (authz errors are ignored)', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, (_, res) => res.networkError('Network error')),
|
||||
);
|
||||
@@ -332,7 +355,7 @@ describe('AppProvider when authz/check fails', () => {
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result.current.userFetchError).toBeTruthy();
|
||||
expect(result.current.userFetchError).toBeFalsy();
|
||||
},
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
|
||||
@@ -26,7 +26,7 @@ func buildClusterRecords(
|
||||
records := make([]inframonitoringtypes.ClusterRecord, 0, len(pageGroups))
|
||||
for _, labels := range pageGroups {
|
||||
compositeKey := compositeKeyFromLabels(labels, groupBy)
|
||||
clusterName := labels[clusterNameAttrKey]
|
||||
clusterName := labels[inframonitoringtypes.ClusterNameAttrKey]
|
||||
|
||||
record := inframonitoringtypes.ClusterRecord{ // initialize with default values
|
||||
ClusterName: clusterName,
|
||||
@@ -87,6 +87,9 @@ func (m *module) getTopClusterGroups(
|
||||
metadataMap map[string]map[string]string,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.ClusterNameAttrKey {
|
||||
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.ClusterNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToClustersQueryNames[orderByKey]
|
||||
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
|
||||
|
||||
|
||||
@@ -7,14 +7,9 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
// TODO(nikhilmantri0902): change to k8s.cluster.uid after showing the missing
|
||||
// data banner. Carried forward from v1 (see k8sClusterUIDAttrKey in
|
||||
// pkg/query-service/app/inframetrics/clusters.go).
|
||||
const clusterNameAttrKey = "k8s.cluster.name"
|
||||
|
||||
var clusterNameGroupByKey = qbtypes.GroupByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: clusterNameAttrKey,
|
||||
Name: inframonitoringtypes.ClusterNameAttrKey,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@ func buildDaemonSetRecords(
|
||||
records := make([]inframonitoringtypes.DaemonSetRecord, 0, len(pageGroups))
|
||||
for _, labels := range pageGroups {
|
||||
compositeKey := compositeKeyFromLabels(labels, groupBy)
|
||||
daemonSetName := labels[daemonSetNameAttrKey]
|
||||
daemonSetName := labels[inframonitoringtypes.DaemonSetNameAttrKey]
|
||||
|
||||
record := inframonitoringtypes.DaemonSetRecord{ // initialize with default values
|
||||
DaemonSetName: daemonSetName,
|
||||
@@ -95,6 +95,9 @@ func (m *module) getTopDaemonSetGroups(
|
||||
metadataMap map[string]map[string]string,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.DaemonSetNameAttrKey {
|
||||
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.DaemonSetNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToDaemonSetsQueryNames[orderByKey]
|
||||
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
|
||||
|
||||
|
||||
@@ -7,14 +7,11 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
const (
|
||||
daemonSetNameAttrKey = "k8s.daemonset.name"
|
||||
daemonSetsBaseFilterExpr = "k8s.daemonset.name != ''"
|
||||
)
|
||||
const daemonSetsBaseFilterExpr = "k8s.daemonset.name != ''"
|
||||
|
||||
var daemonSetNameGroupByKey = qbtypes.GroupByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: daemonSetNameAttrKey,
|
||||
Name: inframonitoringtypes.DaemonSetNameAttrKey,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@ func buildDeploymentRecords(
|
||||
records := make([]inframonitoringtypes.DeploymentRecord, 0, len(pageGroups))
|
||||
for _, labels := range pageGroups {
|
||||
compositeKey := compositeKeyFromLabels(labels, groupBy)
|
||||
deploymentName := labels[deploymentNameAttrKey]
|
||||
deploymentName := labels[inframonitoringtypes.DeploymentNameAttrKey]
|
||||
|
||||
record := inframonitoringtypes.DeploymentRecord{ // initialize with default values
|
||||
DeploymentName: deploymentName,
|
||||
@@ -95,6 +95,9 @@ func (m *module) getTopDeploymentGroups(
|
||||
metadataMap map[string]map[string]string,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.DeploymentNameAttrKey {
|
||||
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.DeploymentNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToDeploymentsQueryNames[orderByKey]
|
||||
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
|
||||
|
||||
|
||||
@@ -7,14 +7,11 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
const (
|
||||
deploymentNameAttrKey = "k8s.deployment.name"
|
||||
deploymentsBaseFilterExpr = "k8s.deployment.name != ''"
|
||||
)
|
||||
const deploymentsBaseFilterExpr = "k8s.deployment.name != ''"
|
||||
|
||||
var deploymentNameGroupByKey = qbtypes.GroupByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: deploymentNameAttrKey,
|
||||
Name: inframonitoringtypes.DeploymentNameAttrKey,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
|
||||
@@ -149,6 +149,7 @@ func paginateWithBackfill(
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
offset, limit int,
|
||||
) []map[string]string {
|
||||
|
||||
metricKeySet := make(map[string]bool, len(metricGroups))
|
||||
for _, g := range metricGroups {
|
||||
metricKeySet[g.compositeKey] = true
|
||||
@@ -163,7 +164,7 @@ func paginateWithBackfill(
|
||||
sort.Strings(metadataOnlyKeys)
|
||||
|
||||
totalMetric := len(metricGroups)
|
||||
totalAll := totalMetric + len(metadataOnlyKeys)
|
||||
totalAll := len(metadataMap)
|
||||
|
||||
end := offset + limit
|
||||
if end > totalAll {
|
||||
@@ -307,23 +308,57 @@ func parseFullQueryResponse(
|
||||
return result
|
||||
}
|
||||
|
||||
// buildSamplesTblFingerprintSubQuery returns a SelectBuilder that selects distinct fingerprints
|
||||
// from the samples table for the given metric names andtime range.
|
||||
func (m *module) buildSamplesTblFingerprintSubQuery(metricNames []string, startMs, endMs int64) *sqlbuilder.SelectBuilder {
|
||||
samplesTableName := telemetrymetrics.WhichSamplesTableToUse(
|
||||
uint64(startMs), uint64(endMs),
|
||||
metrictypes.UnspecifiedType,
|
||||
metrictypes.TimeAggregationUnspecified,
|
||||
nil,
|
||||
// alignedMetricWindow returns step-floored time bounds and the metric tables
|
||||
// to use for the given window. The floor matches what the QB v5 metric
|
||||
// querier does internally (see querybuilder.AdjustedMetricTimeRange).
|
||||
// Please use the samplesAdjustedStartMs with samples table and tsAdjustedStartMs with ts tables.
|
||||
// Both can use the same flooredEndMs.
|
||||
func alignedMetricWindow(startMs, endMs int64) (
|
||||
samplesAdjustedStartMs uint64,
|
||||
flooredEndMs uint64,
|
||||
tsAdjustedStartMs uint64,
|
||||
distributedTSTable string,
|
||||
localTSTable string,
|
||||
distributedSamplesTable string,
|
||||
localSamplesTable string,
|
||||
) {
|
||||
samplesAdjustedStartMs = uint64(startMs)
|
||||
flooredEndMs = uint64(endMs)
|
||||
stepSecs := querybuilder.RecommendedStepIntervalForMetric(samplesAdjustedStartMs, flooredEndMs)
|
||||
// note: this is the same flooring logic as in querybuilder.AdjustedMetricTimeRange
|
||||
if stepSecs > 0 {
|
||||
samplesAdjustedStartMs = samplesAdjustedStartMs - (samplesAdjustedStartMs % (stepSecs * 1000))
|
||||
adjustStep := stepSecs
|
||||
if adjustStep > 60 {
|
||||
adjustStep = 60
|
||||
}
|
||||
flooredEndMs = flooredEndMs - (flooredEndMs % (adjustStep * 1000))
|
||||
}
|
||||
|
||||
tsAdjustedStartMs, _, distributedTSTable, localTSTable = telemetrymetrics.WhichTSTableToUse(
|
||||
samplesAdjustedStartMs, flooredEndMs, nil,
|
||||
)
|
||||
localSamplesTable := strings.TrimPrefix(samplesTableName, "distributed_")
|
||||
|
||||
distributedSamplesTable = telemetrymetrics.WhichSamplesTableToUse(
|
||||
samplesAdjustedStartMs, flooredEndMs,
|
||||
metrictypes.UnspecifiedType, metrictypes.TimeAggregationUnspecified, nil,
|
||||
)
|
||||
localSamplesTable = strings.TrimPrefix(distributedSamplesTable, "distributed_")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// buildSamplesTblFingerprintSubQuery returns a SelectBuilder that selects distinct fingerprints
|
||||
// from the samples table for the given metric names and time range.
|
||||
// Bounds must already be step-floored by the caller via alignedMetricWindow.
|
||||
func (m *module) buildSamplesTblFingerprintSubQuery(metricNames []string, samplesTable string, flooredStart, flooredEnd uint64) *sqlbuilder.SelectBuilder {
|
||||
fpSB := sqlbuilder.NewSelectBuilder()
|
||||
fpSB.Select("DISTINCT fingerprint")
|
||||
fpSB.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, localSamplesTable))
|
||||
fpSB.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, samplesTable))
|
||||
fpSB.Where(
|
||||
fpSB.In("metric_name", sqlbuilder.List(metricNames)),
|
||||
fpSB.GE("unix_milli", startMs),
|
||||
fpSB.L("unix_milli", endMs),
|
||||
fpSB.GE("unix_milli", flooredStart),
|
||||
fpSB.L("unix_milli", flooredEnd),
|
||||
)
|
||||
return fpSB
|
||||
}
|
||||
@@ -454,12 +489,12 @@ func (m *module) getMetadata(
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "groupBy must not be empty")
|
||||
}
|
||||
|
||||
// Pick the optimal timeseries table based on time range; also get adjusted start.
|
||||
adjustedStart, adjustedEnd, distributedTableName, _ := telemetrymetrics.WhichTSTableToUse(
|
||||
uint64(startMs), uint64(endMs), nil,
|
||||
)
|
||||
// Step-floor the window and pick the right tables — matches the bounds the
|
||||
// QB v5 metric querier uses, so metadataMap covers the same universe the
|
||||
// ranking sees (see alignedMetricWindow doc).
|
||||
samplesStartMs, flooredEndMs, tsAdjustedStartMs, distributedTableName, _, _, localSamplesTable := alignedMetricWindow(startMs, endMs)
|
||||
|
||||
fpSB := m.buildSamplesTblFingerprintSubQuery(metricNames, startMs, endMs)
|
||||
fpSB := m.buildSamplesTblFingerprintSubQuery(metricNames, localSamplesTable, samplesStartMs, flooredEndMs)
|
||||
|
||||
// Flatten groupBy keys to string names for SQL expressions and result scanning.
|
||||
groupByCols := make([]string, len(groupBy))
|
||||
@@ -494,8 +529,8 @@ func (m *module) getMetadata(
|
||||
innerSB.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, distributedTableName))
|
||||
innerSB.Where(
|
||||
innerSB.In("metric_name", sqlbuilder.List(metricNames)),
|
||||
innerSB.GE("unix_milli", adjustedStart),
|
||||
innerSB.L("unix_milli", adjustedEnd),
|
||||
innerSB.GE("unix_milli", tsAdjustedStartMs),
|
||||
innerSB.LE("unix_milli", flooredEndMs),
|
||||
fmt.Sprintf("fingerprint IN (%s)", innerSB.Var(fpSB)),
|
||||
)
|
||||
|
||||
|
||||
@@ -34,11 +34,10 @@ func (m *module) getPerGroupHostStatusCounts(
|
||||
pageGroupsFilterExpr := buildPageGroupsFilterExpr(pageGroups)
|
||||
filterExpr := mergeFilterExpressions(reqFilterExpr, pageGroupsFilterExpr)
|
||||
|
||||
adjustedStart, adjustedEnd, distributedTimeSeriesTableName, _ := telemetrymetrics.WhichTSTableToUse(
|
||||
uint64(req.Start), uint64(req.End), nil,
|
||||
)
|
||||
// Step-floor bounds + resolve tables in one shot to match QB v5 querier.
|
||||
samplesStartMs, flooredEndMs, tsAdjustedStartMs, distributedTimeSeriesTableName, _, _, localSamplesTable := alignedMetricWindow(req.Start, req.End)
|
||||
|
||||
hostNameExpr := fmt.Sprintf("JSONExtractString(labels, '%s')", hostNameAttrKey)
|
||||
hostNameExpr := fmt.Sprintf("JSONExtractString(labels, '%s')", inframonitoringtypes.HostNameAttrKey)
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
selectCols := make([]string, 0, len(req.GroupBy)+2)
|
||||
@@ -48,22 +47,22 @@ func (m *module) getPerGroupHostStatusCounts(
|
||||
)
|
||||
}
|
||||
|
||||
activeHostsSQ := m.getActiveHostsQuery(metricNames, hostNameAttrKey, sinceUnixMilli)
|
||||
activeHostsSQ := m.getActiveHostsQuery(metricNames, inframonitoringtypes.HostNameAttrKey, sinceUnixMilli)
|
||||
selectCols = append(selectCols,
|
||||
fmt.Sprintf("uniqExactIf(%s, %s GLOBAL IN (%s)) AS active_host_count", hostNameExpr, hostNameExpr, sb.Var(activeHostsSQ)),
|
||||
fmt.Sprintf("uniqExactIf(%s, %s != '') AS total_host_count", hostNameExpr, hostNameExpr),
|
||||
)
|
||||
|
||||
// Build a fingerprint subquery to restrict to fingerprints with actual sample
|
||||
// data in the original time range (not the wider timeseries table window).
|
||||
fpSB := m.buildSamplesTblFingerprintSubQuery(metricNames, req.Start, req.End)
|
||||
// data in the floored time range.
|
||||
fpSB := m.buildSamplesTblFingerprintSubQuery(metricNames, localSamplesTable, samplesStartMs, flooredEndMs)
|
||||
|
||||
sb.Select(selectCols...)
|
||||
sb.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, distributedTimeSeriesTableName))
|
||||
sb.Where(
|
||||
sb.In("metric_name", sqlbuilder.List(metricNames)),
|
||||
sb.GE("unix_milli", adjustedStart),
|
||||
sb.L("unix_milli", adjustedEnd),
|
||||
sb.GE("unix_milli", tsAdjustedStartMs),
|
||||
sb.LE("unix_milli", flooredEndMs),
|
||||
fmt.Sprintf("fingerprint IN (%s)", sb.Var(fpSB)),
|
||||
)
|
||||
|
||||
@@ -142,7 +141,7 @@ func buildHostRecords(
|
||||
records := make([]inframonitoringtypes.HostRecord, 0, len(pageGroups))
|
||||
for _, labels := range pageGroups {
|
||||
compositeKey := compositeKeyFromLabels(labels, groupBy)
|
||||
hostName := labels[hostNameAttrKey]
|
||||
hostName := labels[inframonitoringtypes.HostNameAttrKey]
|
||||
|
||||
activeStatus := inframonitoringtypes.HostStatusNone
|
||||
activeHostCount := 0
|
||||
@@ -216,6 +215,9 @@ func (m *module) getTopHostGroups(
|
||||
metadataMap map[string]map[string]string,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.HostNameAttrKey {
|
||||
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.HostNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToHostsQueryNames[orderByKey]
|
||||
// The last entry is the formula/query whose value we sort by.
|
||||
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
|
||||
@@ -281,7 +283,7 @@ func (m *module) applyHostsActiveStatusFilter(req *inframonitoringtypes.Postable
|
||||
if req.Filter.FilterByStatus == inframonitoringtypes.HostStatusInactive {
|
||||
op = "NOT IN"
|
||||
}
|
||||
statusClause := fmt.Sprintf("%s %s (%s)", hostNameAttrKey, op, strings.Join(activeHosts, ", "))
|
||||
statusClause := fmt.Sprintf("%s %s (%s)", inframonitoringtypes.HostNameAttrKey, op, strings.Join(activeHosts, ", "))
|
||||
req.Filter.Expression = mergeFilterExpressions(req.Filter.Expression, statusClause)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -7,14 +7,10 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
const (
|
||||
hostNameAttrKey = "host.name"
|
||||
)
|
||||
|
||||
// Helper group-by key used across all queries.
|
||||
var hostNameGroupByKey = qbtypes.GroupByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: hostNameAttrKey,
|
||||
Name: inframonitoringtypes.HostNameAttrKey,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@ func buildJobRecords(
|
||||
records := make([]inframonitoringtypes.JobRecord, 0, len(pageGroups))
|
||||
for _, labels := range pageGroups {
|
||||
compositeKey := compositeKeyFromLabels(labels, groupBy)
|
||||
jobName := labels[jobNameAttrKey]
|
||||
jobName := labels[inframonitoringtypes.JobNameAttrKey]
|
||||
|
||||
record := inframonitoringtypes.JobRecord{ // initialize with default values
|
||||
JobName: jobName,
|
||||
@@ -103,6 +103,9 @@ func (m *module) getTopJobGroups(
|
||||
metadataMap map[string]map[string]string,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.JobNameAttrKey {
|
||||
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.JobNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToJobsQueryNames[orderByKey]
|
||||
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
|
||||
|
||||
|
||||
@@ -7,14 +7,11 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
const (
|
||||
jobNameAttrKey = "k8s.job.name"
|
||||
jobsBaseFilterExpr = "k8s.job.name != ''"
|
||||
)
|
||||
const jobsBaseFilterExpr = "k8s.job.name != ''"
|
||||
|
||||
var jobNameGroupByKey = qbtypes.GroupByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: jobNameAttrKey,
|
||||
Name: inframonitoringtypes.JobNameAttrKey,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
|
||||
@@ -100,7 +100,7 @@ func (m *module) ListHosts(ctx context.Context, orgID valuer.UUID, req *inframon
|
||||
// Determine active hosts: those with metrics reported in the last 10 minutes.
|
||||
// Compute the cutoff once so every downstream query/subquery agrees on what "active" means.
|
||||
sinceUnixMilli := time.Now().Add(-10 * time.Minute).UTC().UnixMilli()
|
||||
activeHostsMap, err := m.getActiveHosts(ctx, hostsTableMetricNamesList, hostNameAttrKey, sinceUnixMilli)
|
||||
activeHostsMap, err := m.getActiveHosts(ctx, hostsTableMetricNamesList, inframonitoringtypes.HostNameAttrKey, sinceUnixMilli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -146,7 +146,7 @@ func (m *module) ListHosts(ctx context.Context, orgID valuer.UUID, req *inframon
|
||||
// When host.name is not in groupBy, we need to run an additional query to get the counts per group for the current page,
|
||||
// using the same filter expression as the main query (including user filters + page groups IN clause).
|
||||
hostCounts := make(map[string]groupHostStatusCounts)
|
||||
isHostNameInGroupBy := isKeyInGroupByAttrs(req.GroupBy, hostNameAttrKey)
|
||||
isHostNameInGroupBy := isKeyInGroupByAttrs(req.GroupBy, inframonitoringtypes.HostNameAttrKey)
|
||||
if !isHostNameInGroupBy {
|
||||
hostCounts, err = m.getPerGroupHostStatusCounts(ctx, req, hostsTableMetricNamesList, pageGroups, sinceUnixMilli)
|
||||
if err != nil {
|
||||
@@ -324,7 +324,7 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isNodeNameInGroupBy := isKeyInGroupByAttrs(req.GroupBy, nodeNameAttrKey)
|
||||
isNodeNameInGroupBy := isKeyInGroupByAttrs(req.GroupBy, inframonitoringtypes.NodeNameAttrKey)
|
||||
resp.Records = buildNodeRecords(isNodeNameInGroupBy, queryResp, pageGroups, req.GroupBy, metadataMap, nodeConditionCounts, podPhaseCounts)
|
||||
resp.Warning = queryResp.Warning
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ func buildNamespaceRecords(
|
||||
records := make([]inframonitoringtypes.NamespaceRecord, 0, len(pageGroups))
|
||||
for _, labels := range pageGroups {
|
||||
compositeKey := compositeKeyFromLabels(labels, groupBy)
|
||||
namespaceName := labels[namespaceNameAttrKey]
|
||||
namespaceName := labels[inframonitoringtypes.NamespaceNameAttrKey]
|
||||
|
||||
record := inframonitoringtypes.NamespaceRecord{ // initialize with default values
|
||||
NamespaceName: namespaceName,
|
||||
@@ -70,6 +70,9 @@ func (m *module) getTopNamespaceGroups(
|
||||
metadataMap map[string]map[string]string,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.NamespaceNameAttrKey {
|
||||
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.NamespaceNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToNamespacesQueryNames[orderByKey]
|
||||
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
|
||||
|
||||
|
||||
@@ -7,13 +7,9 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
const (
|
||||
namespaceNameAttrKey = "k8s.namespace.name"
|
||||
)
|
||||
|
||||
var namespaceNameGroupByKey = qbtypes.GroupByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: namespaceNameAttrKey,
|
||||
Name: inframonitoringtypes.NamespaceNameAttrKey,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
@@ -33,7 +32,7 @@ func buildNodeRecords(
|
||||
records := make([]inframonitoringtypes.NodeRecord, 0, len(pageGroups))
|
||||
for _, labels := range pageGroups {
|
||||
compositeKey := compositeKeyFromLabels(labels, groupBy)
|
||||
nodeName := labels[nodeNameAttrKey]
|
||||
nodeName := labels[inframonitoringtypes.NodeNameAttrKey]
|
||||
|
||||
record := inframonitoringtypes.NodeRecord{ // initialize with default values
|
||||
NodeName: nodeName,
|
||||
@@ -105,6 +104,9 @@ func (m *module) getTopNodeGroups(
|
||||
metadataMap map[string]map[string]string,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.NodeNameAttrKey {
|
||||
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.NodeNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToNodesQueryNames[orderByKey]
|
||||
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
|
||||
|
||||
@@ -187,21 +189,15 @@ func (m *module) getPerGroupNodeConditionCounts(
|
||||
pageGroupsFilterExpr := buildPageGroupsFilterExpr(pageGroups)
|
||||
mergedFilterExpr := mergeFilterExpressions(userFilterExpr, pageGroupsFilterExpr)
|
||||
|
||||
// Resolve tables. Same convention as pods.
|
||||
adjustedStart, adjustedEnd, _, localTimeSeriesTable := telemetrymetrics.WhichTSTableToUse(
|
||||
uint64(start), uint64(end), nil,
|
||||
)
|
||||
samplesTable := telemetrymetrics.WhichSamplesTableToUse(
|
||||
uint64(start), uint64(end),
|
||||
metrictypes.UnspecifiedType, metrictypes.TimeAggregationUnspecified, nil,
|
||||
)
|
||||
valueCol := telemetrymetrics.ValueColumnForSamplesTable(samplesTable)
|
||||
// Step-floor bounds + resolve tables in one shot to match QB v5 querier.
|
||||
samplesStartMs, flooredEndMs, tsAdjustedStartMs, _, localTimeSeriesTable, distributedSamplesTable, _ := alignedMetricWindow(start, end)
|
||||
valueCol := telemetrymetrics.ValueColumnForSamplesTable(distributedSamplesTable)
|
||||
|
||||
// ----- timeSeriesFPs -----
|
||||
timeSeriesFPs := sqlbuilder.NewSelectBuilder()
|
||||
timeSeriesFPsSelectCols := []string{
|
||||
"fingerprint",
|
||||
fmt.Sprintf("JSONExtractString(labels, %s) AS node_name", timeSeriesFPs.Var(nodeNameAttrKey)),
|
||||
fmt.Sprintf("JSONExtractString(labels, %s) AS node_name", timeSeriesFPs.Var(inframonitoringtypes.NodeNameAttrKey)),
|
||||
}
|
||||
for _, key := range groupBy {
|
||||
timeSeriesFPsSelectCols = append(timeSeriesFPsSelectCols,
|
||||
@@ -212,8 +208,8 @@ func (m *module) getPerGroupNodeConditionCounts(
|
||||
timeSeriesFPs.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, localTimeSeriesTable))
|
||||
timeSeriesFPs.Where(
|
||||
timeSeriesFPs.E("metric_name", nodeConditionMetricName),
|
||||
timeSeriesFPs.GE("unix_milli", adjustedStart),
|
||||
timeSeriesFPs.L("unix_milli", adjustedEnd),
|
||||
timeSeriesFPs.GE("unix_milli", tsAdjustedStartMs),
|
||||
timeSeriesFPs.LE("unix_milli", flooredEndMs),
|
||||
)
|
||||
if mergedFilterExpr != "" {
|
||||
filterClause, err := m.buildFilterClause(ctx, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
|
||||
@@ -246,12 +242,12 @@ func (m *module) getPerGroupNodeConditionCounts(
|
||||
latestConditionPerNode.Select(latestConditionPerNodeSelectCols...)
|
||||
latestConditionPerNode.From(fmt.Sprintf(
|
||||
"%s.%s AS samples INNER JOIN time_series_fps AS tsfp ON samples.fingerprint = tsfp.fingerprint",
|
||||
telemetrymetrics.DBName, samplesTable,
|
||||
telemetrymetrics.DBName, distributedSamplesTable,
|
||||
))
|
||||
latestConditionPerNode.Where(
|
||||
latestConditionPerNode.E("samples.metric_name", nodeConditionMetricName),
|
||||
latestConditionPerNode.GE("samples.unix_milli", start),
|
||||
latestConditionPerNode.L("samples.unix_milli", end),
|
||||
latestConditionPerNode.GE("samples.unix_milli", samplesStartMs),
|
||||
latestConditionPerNode.L("samples.unix_milli", flooredEndMs),
|
||||
"tsfp.node_name != ''",
|
||||
)
|
||||
latestConditionPerNode.GroupBy(latestConditionPerNodeGroupBy...)
|
||||
|
||||
@@ -7,14 +7,11 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
const (
|
||||
nodeNameAttrKey = "k8s.node.name"
|
||||
nodeConditionMetricName = "k8s.node.condition_ready"
|
||||
)
|
||||
const nodeConditionMetricName = "k8s.node.condition_ready"
|
||||
|
||||
var nodeNameGroupByKey = qbtypes.GroupByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: nodeNameAttrKey,
|
||||
Name: inframonitoringtypes.NodeNameAttrKey,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
@@ -124,6 +123,9 @@ func (m *module) getTopPodGroups(
|
||||
metadataMap map[string]map[string]string,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.PodNameAttrKey {
|
||||
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.PodNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToPodsQueryNames[orderByKey]
|
||||
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
|
||||
|
||||
@@ -206,15 +208,9 @@ func (m *module) getPerGroupPodPhaseCounts(
|
||||
pageGroupsFilterExpr := buildPageGroupsFilterExpr(pageGroups)
|
||||
mergedFilterExpr := mergeFilterExpressions(userFilterExpr, pageGroupsFilterExpr)
|
||||
|
||||
// Resolve tables. Same convention as hosts (distributed names from helpers).
|
||||
adjustedStart, adjustedEnd, _, localTimeSeriesTable := telemetrymetrics.WhichTSTableToUse(
|
||||
uint64(start), uint64(end), nil,
|
||||
)
|
||||
samplesTable := telemetrymetrics.WhichSamplesTableToUse(
|
||||
uint64(start), uint64(end),
|
||||
metrictypes.UnspecifiedType, metrictypes.TimeAggregationUnspecified, nil,
|
||||
)
|
||||
valueCol := telemetrymetrics.ValueColumnForSamplesTable(samplesTable)
|
||||
// Step-floor bounds + resolve tables in one shot to match QB v5 querier.
|
||||
samplesStartMs, flooredEndMs, tsAdjustedStart, _, localTimeSeriesTable, distributedSamplesTable, _ := alignedMetricWindow(start, end)
|
||||
valueCol := telemetrymetrics.ValueColumnForSamplesTable(distributedSamplesTable)
|
||||
|
||||
// ----- timeSeriesFPs -----
|
||||
timeSeriesFPs := sqlbuilder.NewSelectBuilder()
|
||||
@@ -231,8 +227,8 @@ func (m *module) getPerGroupPodPhaseCounts(
|
||||
timeSeriesFPs.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, localTimeSeriesTable))
|
||||
timeSeriesFPs.Where(
|
||||
timeSeriesFPs.E("metric_name", podPhaseMetricName),
|
||||
timeSeriesFPs.GE("unix_milli", adjustedStart),
|
||||
timeSeriesFPs.L("unix_milli", adjustedEnd),
|
||||
timeSeriesFPs.GE("unix_milli", tsAdjustedStart),
|
||||
timeSeriesFPs.LE("unix_milli", flooredEndMs),
|
||||
)
|
||||
if mergedFilterExpr != "" {
|
||||
filterClause, err := m.buildFilterClause(ctx, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
|
||||
@@ -264,12 +260,12 @@ func (m *module) getPerGroupPodPhaseCounts(
|
||||
latestPhasePerPod.Select(latestPhasePerPodSelectCols...)
|
||||
latestPhasePerPod.From(fmt.Sprintf(
|
||||
"%s.%s AS samples INNER JOIN time_series_fps AS tsfp ON samples.fingerprint = tsfp.fingerprint",
|
||||
telemetrymetrics.DBName, samplesTable,
|
||||
telemetrymetrics.DBName, distributedSamplesTable,
|
||||
))
|
||||
latestPhasePerPod.Where(
|
||||
latestPhasePerPod.E("samples.metric_name", podPhaseMetricName),
|
||||
latestPhasePerPod.GE("samples.unix_milli", start),
|
||||
latestPhasePerPod.L("samples.unix_milli", end),
|
||||
latestPhasePerPod.GE("samples.unix_milli", samplesStartMs),
|
||||
latestPhasePerPod.L("samples.unix_milli", flooredEndMs),
|
||||
"tsfp.pod_uid != ''",
|
||||
)
|
||||
latestPhasePerPod.GroupBy(latestPhasePerPodGroupBy...)
|
||||
|
||||
@@ -25,7 +25,7 @@ func buildStatefulSetRecords(
|
||||
records := make([]inframonitoringtypes.StatefulSetRecord, 0, len(pageGroups))
|
||||
for _, labels := range pageGroups {
|
||||
compositeKey := compositeKeyFromLabels(labels, groupBy)
|
||||
statefulSetName := labels[statefulSetNameAttrKey]
|
||||
statefulSetName := labels[inframonitoringtypes.StatefulSetNameAttrKey]
|
||||
|
||||
record := inframonitoringtypes.StatefulSetRecord{ // initialize with default values
|
||||
StatefulSetName: statefulSetName,
|
||||
@@ -95,6 +95,9 @@ func (m *module) getTopStatefulSetGroups(
|
||||
metadataMap map[string]map[string]string,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.StatefulSetNameAttrKey {
|
||||
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.StatefulSetNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToStatefulSetsQueryNames[orderByKey]
|
||||
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
|
||||
|
||||
|
||||
@@ -7,14 +7,11 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
const (
|
||||
statefulSetNameAttrKey = "k8s.statefulset.name"
|
||||
statefulSetsBaseFilterExpr = "k8s.statefulset.name != ''"
|
||||
)
|
||||
const statefulSetsBaseFilterExpr = "k8s.statefulset.name != ''"
|
||||
|
||||
var statefulSetNameGroupByKey = qbtypes.GroupByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: statefulSetNameAttrKey,
|
||||
Name: inframonitoringtypes.StatefulSetNameAttrKey,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
|
||||
@@ -23,7 +23,7 @@ func buildVolumeRecords(
|
||||
records := make([]inframonitoringtypes.VolumeRecord, 0, len(pageGroups))
|
||||
for _, labels := range pageGroups {
|
||||
compositeKey := compositeKeyFromLabels(labels, groupBy)
|
||||
pvcName := labels[persistentVolumeClaimNameAttrKey]
|
||||
pvcName := labels[inframonitoringtypes.PersistentVolumeClaimNameAttrKey]
|
||||
|
||||
record := inframonitoringtypes.VolumeRecord{ // initialize with default values
|
||||
PersistentVolumeClaimName: pvcName,
|
||||
@@ -75,6 +75,9 @@ func (m *module) getTopVolumeGroups(
|
||||
metadataMap map[string]map[string]string,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.PersistentVolumeClaimNameAttrKey {
|
||||
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.PersistentVolumeClaimNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToVolumesQueryNames[orderByKey]
|
||||
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
|
||||
|
||||
|
||||
@@ -7,14 +7,11 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
const (
|
||||
persistentVolumeClaimNameAttrKey = "k8s.persistentvolumeclaim.name"
|
||||
volumesBaseFilterExpr = "k8s.persistentvolumeclaim.name != ''"
|
||||
)
|
||||
const volumesBaseFilterExpr = "k8s.persistentvolumeclaim.name != ''"
|
||||
|
||||
var pvcNameGroupByKey = qbtypes.GroupByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: persistentVolumeClaimNameAttrKey,
|
||||
Name: inframonitoringtypes.PersistentVolumeClaimNameAttrKey,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
type Clusters struct {
|
||||
Type ResponseType `json:"type" required:"true"`
|
||||
Records []ClusterRecord `json:"records" required:"true"`
|
||||
Records []ClusterRecord `json:"records" required:"true" nullable:"false"`
|
||||
Total int `json:"total" required:"true"`
|
||||
RequiredMetricsCheck RequiredMetricsCheck `json:"requiredMetricsCheck" required:"true"`
|
||||
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention" required:"true"`
|
||||
@@ -19,8 +19,8 @@ type Clusters struct {
|
||||
|
||||
type ClusterRecord struct {
|
||||
// TODO(nikhilmantri0902): once the underlying attr key is migrated to
|
||||
// k8s.cluster.uid (see clusterNameAttrKey TODO in implinframonitoring),
|
||||
// surface ClusterUID alongside (or replace) ClusterName.
|
||||
// k8s.cluster.uid (see ClusterNameAttrKey), surface ClusterUID alongside
|
||||
// (or replace) ClusterName.
|
||||
ClusterName string `json:"clusterName" required:"true"`
|
||||
ClusterCPU float64 `json:"clusterCPU" required:"true"`
|
||||
ClusterCPUAllocatable float64 `json:"clusterCPUAllocatable" required:"true"`
|
||||
@@ -88,6 +88,9 @@ func (req *PostableClusters) Validate() error {
|
||||
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
|
||||
}
|
||||
if req.OrderBy.Key.Name == ClusterNameAttrKey && len(req.GroupBy) > 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", ClusterNameAttrKey)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
const ClusterNameAttrKey = "k8s.cluster.name"
|
||||
|
||||
const (
|
||||
ClustersOrderByCPU = "cpu"
|
||||
ClustersOrderByCPUAllocatable = "cpu_allocatable"
|
||||
@@ -12,4 +14,5 @@ var ClustersValidOrderByKeys = []string{
|
||||
ClustersOrderByCPUAllocatable,
|
||||
ClustersOrderByMemory,
|
||||
ClustersOrderByMemoryAllocatable,
|
||||
ClusterNameAttrKey,
|
||||
}
|
||||
|
||||
@@ -275,6 +275,57 @@ func TestPostableClusters_Validate(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy name asc with empty groupBy is valid",
|
||||
req: &PostableClusters{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: ClusterNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name desc with empty groupBy is valid",
|
||||
req: &PostableClusters{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: ClusterNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name with non-empty groupBy is rejected",
|
||||
req: &PostableClusters{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "k8s.cluster.name"}},
|
||||
},
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: ClusterNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
type DaemonSets struct {
|
||||
Type ResponseType `json:"type" required:"true"`
|
||||
Records []DaemonSetRecord `json:"records" required:"true"`
|
||||
Records []DaemonSetRecord `json:"records" required:"true" nullable:"false"`
|
||||
Total int `json:"total" required:"true"`
|
||||
RequiredMetricsCheck RequiredMetricsCheck `json:"requiredMetricsCheck" required:"true"`
|
||||
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention" required:"true"`
|
||||
@@ -88,6 +88,9 @@ func (req *PostableDaemonSets) Validate() error {
|
||||
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
|
||||
}
|
||||
if req.OrderBy.Key.Name == DaemonSetNameAttrKey && len(req.GroupBy) > 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", DaemonSetNameAttrKey)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
const DaemonSetNameAttrKey = "k8s.daemonset.name"
|
||||
|
||||
const (
|
||||
DaemonSetsOrderByCPU = "cpu"
|
||||
DaemonSetsOrderByCPURequest = "cpu_request"
|
||||
DaemonSetsOrderByCPULimit = "cpu_limit"
|
||||
DaemonSetsOrderByMemory = "memory"
|
||||
DaemonSetsOrderByMemoryRequest = "memory_request"
|
||||
DaemonSetsOrderByMemoryLimit = "memory_limit"
|
||||
DaemonSetsOrderByCPU = "cpu"
|
||||
DaemonSetsOrderByCPURequest = "cpu_request"
|
||||
DaemonSetsOrderByCPULimit = "cpu_limit"
|
||||
DaemonSetsOrderByMemory = "memory"
|
||||
DaemonSetsOrderByMemoryRequest = "memory_request"
|
||||
DaemonSetsOrderByMemoryLimit = "memory_limit"
|
||||
DaemonSetsOrderByDesiredNodes = "desired_nodes"
|
||||
DaemonSetsOrderByCurrentNodes = "current_nodes"
|
||||
)
|
||||
@@ -20,4 +22,5 @@ var DaemonSetsValidOrderByKeys = []string{
|
||||
DaemonSetsOrderByMemoryLimit,
|
||||
DaemonSetsOrderByDesiredNodes,
|
||||
DaemonSetsOrderByCurrentNodes,
|
||||
DaemonSetNameAttrKey,
|
||||
}
|
||||
|
||||
@@ -257,6 +257,63 @@ func TestPostableDaemonSets_Validate(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy name asc with empty groupBy is valid",
|
||||
req: &PostableDaemonSets{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: DaemonSetNameAttrKey,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name desc with empty groupBy is valid",
|
||||
req: &PostableDaemonSets{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: DaemonSetNameAttrKey,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name with non-empty groupBy is rejected",
|
||||
req: &PostableDaemonSets{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "k8s.namespace.name"}},
|
||||
},
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: DaemonSetNameAttrKey,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
type Deployments struct {
|
||||
Type ResponseType `json:"type" required:"true"`
|
||||
Records []DeploymentRecord `json:"records" required:"true"`
|
||||
Records []DeploymentRecord `json:"records" required:"true" nullable:"false"`
|
||||
Total int `json:"total" required:"true"`
|
||||
RequiredMetricsCheck RequiredMetricsCheck `json:"requiredMetricsCheck" required:"true"`
|
||||
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention" required:"true"`
|
||||
@@ -88,6 +88,9 @@ func (req *PostableDeployments) Validate() error {
|
||||
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
|
||||
}
|
||||
if req.OrderBy.Key.Name == DeploymentNameAttrKey && len(req.GroupBy) > 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", DeploymentNameAttrKey)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
const DeploymentNameAttrKey = "k8s.deployment.name"
|
||||
|
||||
const (
|
||||
DeploymentsOrderByCPU = "cpu"
|
||||
DeploymentsOrderByCPURequest = "cpu_request"
|
||||
@@ -20,4 +22,5 @@ var DeploymentsValidOrderByKeys = []string{
|
||||
DeploymentsOrderByMemoryLimit,
|
||||
DeploymentsOrderByDesiredPods,
|
||||
DeploymentsOrderByAvailablePods,
|
||||
DeploymentNameAttrKey,
|
||||
}
|
||||
|
||||
@@ -257,6 +257,57 @@ func TestPostableDeployments_Validate(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy name asc with empty groupBy is valid",
|
||||
req: &PostableDeployments{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: DeploymentNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name desc with empty groupBy is valid",
|
||||
req: &PostableDeployments{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: DeploymentNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name with non-empty groupBy is rejected",
|
||||
req: &PostableDeployments{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "k8s.namespace.name"}},
|
||||
},
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: DeploymentNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
type Hosts struct {
|
||||
Type ResponseType `json:"type" required:"true"`
|
||||
Records []HostRecord `json:"records" required:"true"`
|
||||
Records []HostRecord `json:"records" required:"true" nullable:"false"`
|
||||
Total int `json:"total" required:"true"`
|
||||
RequiredMetricsCheck RequiredMetricsCheck `json:"requiredMetricsCheck" required:"true"`
|
||||
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention" required:"true"`
|
||||
@@ -100,6 +100,9 @@ func (req *PostableHosts) Validate() error {
|
||||
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
|
||||
}
|
||||
if req.OrderBy.Key.Name == HostNameAttrKey && len(req.GroupBy) > 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", HostNameAttrKey)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -20,6 +20,8 @@ func (HostStatus) Enum() []any {
|
||||
}
|
||||
}
|
||||
|
||||
const HostNameAttrKey = "host.name"
|
||||
|
||||
const (
|
||||
HostsOrderByCPU = "cpu"
|
||||
HostsOrderByMemory = "memory"
|
||||
@@ -34,4 +36,5 @@ var HostsValidOrderByKeys = []string{
|
||||
HostsOrderByWait,
|
||||
HostsOrderByDiskUsage,
|
||||
HostsOrderByLoad15,
|
||||
HostNameAttrKey,
|
||||
}
|
||||
|
||||
@@ -228,6 +228,57 @@ func TestHostsListRequest_Validate(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy name asc with empty groupBy is valid",
|
||||
req: &PostableHosts{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: HostNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name desc with empty groupBy is valid",
|
||||
req: &PostableHosts{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: HostNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name with non-empty groupBy is rejected",
|
||||
req: &PostableHosts{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "os.type"}},
|
||||
},
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: HostNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
type Jobs struct {
|
||||
Type ResponseType `json:"type" required:"true"`
|
||||
Records []JobRecord `json:"records" required:"true"`
|
||||
Records []JobRecord `json:"records" required:"true" nullable:"false"`
|
||||
Total int `json:"total" required:"true"`
|
||||
RequiredMetricsCheck RequiredMetricsCheck `json:"requiredMetricsCheck" required:"true"`
|
||||
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention" required:"true"`
|
||||
@@ -90,6 +90,9 @@ func (req *PostableJobs) Validate() error {
|
||||
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
|
||||
}
|
||||
if req.OrderBy.Key.Name == JobNameAttrKey && len(req.GroupBy) > 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", JobNameAttrKey)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
const JobNameAttrKey = "k8s.job.name"
|
||||
|
||||
const (
|
||||
JobsOrderByCPU = "cpu"
|
||||
JobsOrderByCPURequest = "cpu_request"
|
||||
@@ -24,4 +26,5 @@ var JobsValidOrderByKeys = []string{
|
||||
JobsOrderByActivePods,
|
||||
JobsOrderByFailedPods,
|
||||
JobsOrderBySuccessfulPods,
|
||||
JobNameAttrKey,
|
||||
}
|
||||
|
||||
@@ -293,6 +293,57 @@ func TestPostableJobs_Validate(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy name asc with empty groupBy is valid",
|
||||
req: &PostableJobs{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: JobNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name desc with empty groupBy is valid",
|
||||
req: &PostableJobs{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: JobNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name with non-empty groupBy is rejected",
|
||||
req: &PostableJobs{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "k8s.namespace.name"}},
|
||||
},
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: JobNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
type Namespaces struct {
|
||||
Type ResponseType `json:"type" required:"true"`
|
||||
Records []NamespaceRecord `json:"records" required:"true"`
|
||||
Records []NamespaceRecord `json:"records" required:"true" nullable:"false"`
|
||||
Total int `json:"total" required:"true"`
|
||||
RequiredMetricsCheck RequiredMetricsCheck `json:"requiredMetricsCheck" required:"true"`
|
||||
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention" required:"true"`
|
||||
@@ -82,6 +82,9 @@ func (req *PostableNamespaces) Validate() error {
|
||||
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
|
||||
}
|
||||
if req.OrderBy.Key.Name == NamespaceNameAttrKey && len(req.GroupBy) > 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", NamespaceNameAttrKey)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
const NamespaceNameAttrKey = "k8s.namespace.name"
|
||||
|
||||
const (
|
||||
NamespacesOrderByCPU = "cpu"
|
||||
NamespacesOrderByMemory = "memory"
|
||||
@@ -8,4 +10,5 @@ const (
|
||||
var NamespacesValidOrderByKeys = []string{
|
||||
NamespacesOrderByCPU,
|
||||
NamespacesOrderByMemory,
|
||||
NamespaceNameAttrKey,
|
||||
}
|
||||
|
||||
@@ -221,6 +221,57 @@ func TestPostableNamespaces_Validate(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy name asc with empty groupBy is valid",
|
||||
req: &PostableNamespaces{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: NamespaceNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name desc with empty groupBy is valid",
|
||||
req: &PostableNamespaces{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: NamespaceNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name with non-empty groupBy is rejected",
|
||||
req: &PostableNamespaces{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "k8s.cluster.name"}},
|
||||
},
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: NamespaceNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
type Nodes struct {
|
||||
Type ResponseType `json:"type" required:"true"`
|
||||
Records []NodeRecord `json:"records" required:"true"`
|
||||
Records []NodeRecord `json:"records" required:"true" nullable:"false"`
|
||||
Total int `json:"total" required:"true"`
|
||||
RequiredMetricsCheck RequiredMetricsCheck `json:"requiredMetricsCheck" required:"true"`
|
||||
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention" required:"true"`
|
||||
@@ -93,6 +93,9 @@ func (req *PostableNodes) Validate() error {
|
||||
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
|
||||
}
|
||||
if req.OrderBy.Key.Name == NodeNameAttrKey && len(req.GroupBy) > 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", NodeNameAttrKey)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -27,6 +27,8 @@ const (
|
||||
NodeConditionNumNotReady = 0
|
||||
)
|
||||
|
||||
const NodeNameAttrKey = "k8s.node.name"
|
||||
|
||||
const (
|
||||
NodesOrderByCPU = "cpu"
|
||||
NodesOrderByCPUAllocatable = "cpu_allocatable"
|
||||
@@ -39,4 +41,5 @@ var NodesValidOrderByKeys = []string{
|
||||
NodesOrderByCPUAllocatable,
|
||||
NodesOrderByMemory,
|
||||
NodesOrderByMemoryAllocatable,
|
||||
NodeNameAttrKey,
|
||||
}
|
||||
|
||||
@@ -239,6 +239,57 @@ func TestPostableNodes_Validate(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy name asc with empty groupBy is valid",
|
||||
req: &PostableNodes{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: NodeNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name desc with empty groupBy is valid",
|
||||
req: &PostableNodes{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: NodeNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name with non-empty groupBy is rejected",
|
||||
req: &PostableNodes{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "k8s.cluster.name"}},
|
||||
},
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: NodeNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
55
pkg/types/inframonitoringtypes/pagination.go
Normal file
55
pkg/types/inframonitoringtypes/pagination.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
)
|
||||
|
||||
// PaginateMetadataByName returns metadataMap groups sorted by name
|
||||
// (lexicographic, asc or desc), paginated by offset/limit, and rebuilt into
|
||||
// label maps using groupBy. When sortByMetaKey is non-empty, groups are sorted
|
||||
// by metadataMap[k][sortByMetaKey]; otherwise sorted by composite key directly.
|
||||
func PaginateMetadataByName(
|
||||
metadataMap map[string]map[string]string,
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
direction qbtypes.OrderDirection,
|
||||
offset, limit int,
|
||||
sortByMetaKey string,
|
||||
) []map[string]string {
|
||||
|
||||
pageGroups := make([]map[string]string, 0)
|
||||
if offset >= len(metadataMap) {
|
||||
return pageGroups
|
||||
}
|
||||
type entry struct{ compositeKey, sortVal string }
|
||||
entries := make([]entry, 0, len(metadataMap))
|
||||
for ck, meta := range metadataMap {
|
||||
sv := ck
|
||||
if sortByMetaKey != "" {
|
||||
sv = meta[sortByMetaKey]
|
||||
}
|
||||
entries = append(entries, entry{compositeKey: ck, sortVal: sv})
|
||||
}
|
||||
sort.Slice(entries, func(i, j int) bool {
|
||||
if entries[i].sortVal != entries[j].sortVal {
|
||||
if direction == qbtypes.OrderDirectionAsc {
|
||||
return entries[i].sortVal < entries[j].sortVal
|
||||
}
|
||||
return entries[i].sortVal > entries[j].sortVal
|
||||
}
|
||||
return entries[i].compositeKey < entries[j].compositeKey
|
||||
})
|
||||
|
||||
end := min(offset+limit, len(entries))
|
||||
|
||||
for _, e := range entries[offset:end] {
|
||||
attrs := metadataMap[e.compositeKey]
|
||||
labels := make(map[string]string, len(groupBy))
|
||||
for _, gb := range groupBy {
|
||||
labels[gb.Name] = attrs[gb.Name]
|
||||
}
|
||||
pageGroups = append(pageGroups, labels)
|
||||
}
|
||||
return pageGroups
|
||||
}
|
||||
371
pkg/types/inframonitoringtypes/pagination_test.go
Normal file
371
pkg/types/inframonitoringtypes/pagination_test.go
Normal file
@@ -0,0 +1,371 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func gbKey(name string) qbtypes.GroupByKey {
|
||||
return qbtypes.GroupByKey{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: name}}
|
||||
}
|
||||
|
||||
// fiveHostMap returns a metadataMap with 5 host entries h1..h5.
|
||||
func fiveHostMap() map[string]map[string]string {
|
||||
m := make(map[string]map[string]string, 5)
|
||||
for _, n := range []string{"h1", "h2", "h3", "h4", "h5"} {
|
||||
m[n] = map[string]string{"host.name": n}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func TestPaginateMetadataByName(t *testing.T) {
|
||||
hostGB := []qbtypes.GroupByKey{gbKey("host.name")}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
metadataMap map[string]map[string]string
|
||||
groupBy []qbtypes.GroupByKey
|
||||
direction qbtypes.OrderDirection
|
||||
offset, limit int
|
||||
sortByMetaKey string
|
||||
want []map[string]string
|
||||
wantNotNil bool // assert empty non-nil slice instead of comparing to want
|
||||
}{
|
||||
// A. Array out of bounds
|
||||
{
|
||||
name: "offset_equals_len",
|
||||
metadataMap: fiveHostMap(),
|
||||
groupBy: hostGB,
|
||||
direction: qbtypes.OrderDirectionAsc,
|
||||
offset: 5,
|
||||
limit: 10,
|
||||
sortByMetaKey: "host.name",
|
||||
wantNotNil: true,
|
||||
},
|
||||
{
|
||||
name: "offset_way_past_len",
|
||||
metadataMap: fiveHostMap(),
|
||||
groupBy: hostGB,
|
||||
direction: qbtypes.OrderDirectionAsc,
|
||||
offset: 100,
|
||||
limit: 10,
|
||||
sortByMetaKey: "host.name",
|
||||
wantNotNil: true,
|
||||
},
|
||||
{
|
||||
name: "offset_plus_limit_exceeds_len",
|
||||
metadataMap: fiveHostMap(),
|
||||
groupBy: hostGB,
|
||||
direction: qbtypes.OrderDirectionAsc,
|
||||
offset: 3,
|
||||
limit: 10,
|
||||
sortByMetaKey: "host.name",
|
||||
want: []map[string]string{
|
||||
{"host.name": "h4"},
|
||||
{"host.name": "h5"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "limit_exceeds_len",
|
||||
metadataMap: fiveHostMap(),
|
||||
groupBy: hostGB,
|
||||
direction: qbtypes.OrderDirectionAsc,
|
||||
offset: 0,
|
||||
limit: 100,
|
||||
sortByMetaKey: "host.name",
|
||||
want: []map[string]string{
|
||||
{"host.name": "h1"},
|
||||
{"host.name": "h2"},
|
||||
{"host.name": "h3"},
|
||||
{"host.name": "h4"},
|
||||
{"host.name": "h5"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "limit_zero",
|
||||
metadataMap: fiveHostMap(),
|
||||
groupBy: hostGB,
|
||||
direction: qbtypes.OrderDirectionAsc,
|
||||
offset: 0,
|
||||
limit: 0,
|
||||
sortByMetaKey: "host.name",
|
||||
wantNotNil: true, // expect empty non-nil slice
|
||||
},
|
||||
{
|
||||
name: "empty_map",
|
||||
metadataMap: map[string]map[string]string{},
|
||||
groupBy: hostGB,
|
||||
direction: qbtypes.OrderDirectionAsc,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
sortByMetaKey: "host.name",
|
||||
wantNotNil: true,
|
||||
},
|
||||
{
|
||||
name: "exact_page",
|
||||
metadataMap: fiveHostMap(),
|
||||
groupBy: hostGB,
|
||||
direction: qbtypes.OrderDirectionAsc,
|
||||
offset: 0,
|
||||
limit: 5,
|
||||
sortByMetaKey: "host.name",
|
||||
want: []map[string]string{
|
||||
{"host.name": "h1"},
|
||||
{"host.name": "h2"},
|
||||
{"host.name": "h3"},
|
||||
{"host.name": "h4"},
|
||||
{"host.name": "h5"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mid_page",
|
||||
metadataMap: fiveHostMap(),
|
||||
groupBy: hostGB,
|
||||
direction: qbtypes.OrderDirectionAsc,
|
||||
offset: 2,
|
||||
limit: 2,
|
||||
sortByMetaKey: "host.name",
|
||||
want: []map[string]string{
|
||||
{"host.name": "h3"},
|
||||
{"host.name": "h4"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "last_single",
|
||||
metadataMap: fiveHostMap(),
|
||||
groupBy: hostGB,
|
||||
direction: qbtypes.OrderDirectionAsc,
|
||||
offset: 4,
|
||||
limit: 1,
|
||||
sortByMetaKey: "host.name",
|
||||
want: []map[string]string{
|
||||
{"host.name": "h5"},
|
||||
},
|
||||
},
|
||||
|
||||
// B. Nil / missing
|
||||
{
|
||||
name: "nil_map",
|
||||
metadataMap: nil,
|
||||
groupBy: hostGB,
|
||||
direction: qbtypes.OrderDirectionAsc,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
sortByMetaKey: "host.name",
|
||||
wantNotNil: true,
|
||||
},
|
||||
{
|
||||
name: "nil_groupBy",
|
||||
metadataMap: map[string]map[string]string{
|
||||
"h1": {"host.name": "h1"},
|
||||
"h2": {"host.name": "h2"},
|
||||
},
|
||||
groupBy: nil,
|
||||
direction: qbtypes.OrderDirectionAsc,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
sortByMetaKey: "host.name",
|
||||
want: []map[string]string{
|
||||
{},
|
||||
{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty_groupBy",
|
||||
metadataMap: map[string]map[string]string{
|
||||
"h1": {"host.name": "h1"},
|
||||
"h2": {"host.name": "h2"},
|
||||
},
|
||||
groupBy: []qbtypes.GroupByKey{},
|
||||
direction: qbtypes.OrderDirectionAsc,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
sortByMetaKey: "host.name",
|
||||
want: []map[string]string{
|
||||
{},
|
||||
{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing_key_in_entry",
|
||||
metadataMap: map[string]map[string]string{
|
||||
"h1": {"host.name": "h1", "os.type": "linux"},
|
||||
"h2": {"host.name": "h2"}, // os.type absent
|
||||
},
|
||||
groupBy: []qbtypes.GroupByKey{gbKey("host.name"), gbKey("os.type")},
|
||||
direction: qbtypes.OrderDirectionAsc,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
sortByMetaKey: "host.name",
|
||||
want: []map[string]string{
|
||||
{"host.name": "h1", "os.type": "linux"},
|
||||
{"host.name": "h2", "os.type": ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nil_inner_map",
|
||||
metadataMap: map[string]map[string]string{
|
||||
"h1": nil,
|
||||
"h2": {"host.name": "h2"},
|
||||
},
|
||||
groupBy: hostGB,
|
||||
direction: qbtypes.OrderDirectionAsc,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
sortByMetaKey: "host.name",
|
||||
// sortVal for h1 = "" (nil map read), for h2 = "h2".
|
||||
// Asc order: "" < "h2", so h1 first.
|
||||
want: []map[string]string{
|
||||
{"host.name": ""},
|
||||
{"host.name": "h2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sortByMetaKey_empty_sorts_by_compositeKey",
|
||||
metadataMap: map[string]map[string]string{
|
||||
"x\x00z": {"a": "x", "b": "z"},
|
||||
"x\x00y": {"a": "x", "b": "y"},
|
||||
},
|
||||
groupBy: []qbtypes.GroupByKey{gbKey("a"), gbKey("b")},
|
||||
direction: qbtypes.OrderDirectionAsc,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
sortByMetaKey: "",
|
||||
want: []map[string]string{
|
||||
{"a": "x", "b": "y"},
|
||||
{"a": "x", "b": "z"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sortByMetaKey_absent_in_some_entries",
|
||||
metadataMap: map[string]map[string]string{
|
||||
"h1": {"host.name": "h1"},
|
||||
"h2": {}, // host.name absent
|
||||
"h3": {"host.name": "h3"},
|
||||
},
|
||||
groupBy: hostGB,
|
||||
direction: qbtypes.OrderDirectionAsc,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
sortByMetaKey: "host.name",
|
||||
// h2 sortVal="" sorts first; then h1, h3.
|
||||
want: []map[string]string{
|
||||
{"host.name": ""},
|
||||
{"host.name": "h1"},
|
||||
{"host.name": "h3"},
|
||||
},
|
||||
},
|
||||
|
||||
// C. Sort direction
|
||||
{
|
||||
name: "asc",
|
||||
metadataMap: map[string]map[string]string{
|
||||
"h2": {"host.name": "h2"},
|
||||
"h1": {"host.name": "h1"},
|
||||
"h3": {"host.name": "h3"},
|
||||
},
|
||||
groupBy: hostGB,
|
||||
direction: qbtypes.OrderDirectionAsc,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
sortByMetaKey: "host.name",
|
||||
want: []map[string]string{
|
||||
{"host.name": "h1"},
|
||||
{"host.name": "h2"},
|
||||
{"host.name": "h3"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "desc",
|
||||
metadataMap: map[string]map[string]string{
|
||||
"h2": {"host.name": "h2"},
|
||||
"h1": {"host.name": "h1"},
|
||||
"h3": {"host.name": "h3"},
|
||||
},
|
||||
groupBy: hostGB,
|
||||
direction: qbtypes.OrderDirectionDesc,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
sortByMetaKey: "host.name",
|
||||
want: []map[string]string{
|
||||
{"host.name": "h3"},
|
||||
{"host.name": "h2"},
|
||||
{"host.name": "h1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "zero_value_direction_falls_into_desc",
|
||||
metadataMap: map[string]map[string]string{
|
||||
"h2": {"host.name": "h2"},
|
||||
"h1": {"host.name": "h1"},
|
||||
"h3": {"host.name": "h3"},
|
||||
},
|
||||
groupBy: hostGB,
|
||||
direction: qbtypes.OrderDirection{},
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
sortByMetaKey: "host.name",
|
||||
want: []map[string]string{
|
||||
{"host.name": "h3"},
|
||||
{"host.name": "h2"},
|
||||
{"host.name": "h1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tie_breaks_on_compositeKey_asc",
|
||||
metadataMap: map[string]map[string]string{
|
||||
"a\x00b": {"k": "a", "x": "tie"},
|
||||
"a\x00c": {"k": "a", "x": "tie"},
|
||||
},
|
||||
groupBy: []qbtypes.GroupByKey{gbKey("k"), gbKey("x")},
|
||||
direction: qbtypes.OrderDirectionDesc,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
sortByMetaKey: "x",
|
||||
// Both sortVals "tie"; tie-break on compositeKey asc:
|
||||
// "a\x00b" < "a\x00c", so b first.
|
||||
want: []map[string]string{
|
||||
{"k": "a", "x": "tie"},
|
||||
{"k": "a", "x": "tie"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := PaginateMetadataByName(
|
||||
tt.metadataMap,
|
||||
tt.groupBy,
|
||||
tt.direction,
|
||||
tt.offset,
|
||||
tt.limit,
|
||||
tt.sortByMetaKey,
|
||||
)
|
||||
if tt.wantNotNil {
|
||||
assert.NotNil(t, got)
|
||||
assert.Len(t, got, 0)
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaginateMetadataByName_Deterministic(t *testing.T) {
|
||||
m := make(map[string]map[string]string, 10)
|
||||
// 10 entries, several with tied host.name values to force compositeKey tie-break.
|
||||
for i, n := range []string{"a", "a", "b", "b", "c", "d", "e", "e", "f", "g"} {
|
||||
ck := n + "\x00" + string(rune('0'+i))
|
||||
m[ck] = map[string]string{"host.name": n, "id": string(rune('0' + i))}
|
||||
}
|
||||
gb := []qbtypes.GroupByKey{gbKey("host.name"), gbKey("id")}
|
||||
|
||||
first := PaginateMetadataByName(m, gb, qbtypes.OrderDirectionAsc, 0, 10, "host.name")
|
||||
for i := range 50 {
|
||||
got := PaginateMetadataByName(m, gb, qbtypes.OrderDirectionAsc, 0, 10, "host.name")
|
||||
assert.Equal(t, first, got, "iteration %d differed — map-iteration nondeterminism leaked through sort", i)
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
type Pods struct {
|
||||
Type ResponseType `json:"type" required:"true"`
|
||||
Records []PodRecord `json:"records" required:"true"`
|
||||
Records []PodRecord `json:"records" required:"true" nullable:"false"`
|
||||
Total int `json:"total" required:"true"`
|
||||
RequiredMetricsCheck RequiredMetricsCheck `json:"requiredMetricsCheck" required:"true"`
|
||||
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention" required:"true"`
|
||||
@@ -98,6 +98,9 @@ func (req *PostablePods) Validate() error {
|
||||
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
|
||||
}
|
||||
if req.OrderBy.Key.Name == PodNameAttrKey && len(req.GroupBy) > 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", PodNameAttrKey)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -36,6 +36,8 @@ const (
|
||||
PodPhaseNumUnknown = 5
|
||||
)
|
||||
|
||||
const PodNameAttrKey = "k8s.pod.name"
|
||||
|
||||
const (
|
||||
PodsOrderByCPU = "cpu"
|
||||
PodsOrderByCPURequest = "cpu_request"
|
||||
@@ -52,4 +54,5 @@ var PodsValidOrderByKeys = []string{
|
||||
PodsOrderByMemory,
|
||||
PodsOrderByMemoryRequest,
|
||||
PodsOrderByMemoryLimit,
|
||||
PodNameAttrKey,
|
||||
}
|
||||
|
||||
@@ -203,6 +203,57 @@ func TestPostablePods_Validate(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy name asc with empty groupBy is valid",
|
||||
req: &PostablePods{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: PodNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name desc with empty groupBy is valid",
|
||||
req: &PostablePods{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: PodNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name with non-empty groupBy is rejected",
|
||||
req: &PostablePods{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "k8s.namespace.name"}},
|
||||
},
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: PodNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
type StatefulSets struct {
|
||||
Type ResponseType `json:"type" required:"true"`
|
||||
Records []StatefulSetRecord `json:"records" required:"true"`
|
||||
Records []StatefulSetRecord `json:"records" required:"true" nullable:"false"`
|
||||
Total int `json:"total" required:"true"`
|
||||
RequiredMetricsCheck RequiredMetricsCheck `json:"requiredMetricsCheck" required:"true"`
|
||||
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention" required:"true"`
|
||||
@@ -88,6 +88,9 @@ func (req *PostableStatefulSets) Validate() error {
|
||||
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
|
||||
}
|
||||
if req.OrderBy.Key.Name == StatefulSetNameAttrKey && len(req.GroupBy) > 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", StatefulSetNameAttrKey)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
const StatefulSetNameAttrKey = "k8s.statefulset.name"
|
||||
|
||||
const (
|
||||
StatefulSetsOrderByCPU = "cpu"
|
||||
StatefulSetsOrderByCPURequest = "cpu_request"
|
||||
@@ -20,4 +22,5 @@ var StatefulSetsValidOrderByKeys = []string{
|
||||
StatefulSetsOrderByMemoryLimit,
|
||||
StatefulSetsOrderByDesiredPods,
|
||||
StatefulSetsOrderByCurrentPods,
|
||||
StatefulSetNameAttrKey,
|
||||
}
|
||||
|
||||
@@ -257,6 +257,57 @@ func TestPostableStatefulSets_Validate(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy name asc with empty groupBy is valid",
|
||||
req: &PostableStatefulSets{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: StatefulSetNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name desc with empty groupBy is valid",
|
||||
req: &PostableStatefulSets{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: StatefulSetNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name with non-empty groupBy is rejected",
|
||||
req: &PostableStatefulSets{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "k8s.namespace.name"}},
|
||||
},
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: StatefulSetNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
type Volumes struct {
|
||||
Type ResponseType `json:"type" required:"true"`
|
||||
Records []VolumeRecord `json:"records" required:"true"`
|
||||
Records []VolumeRecord `json:"records" required:"true" nullable:"false"`
|
||||
Total int `json:"total" required:"true"`
|
||||
RequiredMetricsCheck RequiredMetricsCheck `json:"requiredMetricsCheck" required:"true"`
|
||||
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention" required:"true"`
|
||||
@@ -85,6 +85,9 @@ func (req *PostableVolumes) Validate() error {
|
||||
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
|
||||
}
|
||||
if req.OrderBy.Key.Name == PersistentVolumeClaimNameAttrKey && len(req.GroupBy) > 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", PersistentVolumeClaimNameAttrKey)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
const PersistentVolumeClaimNameAttrKey = "k8s.persistentvolumeclaim.name"
|
||||
|
||||
const (
|
||||
VolumesOrderByAvailable = "available"
|
||||
VolumesOrderByCapacity = "capacity"
|
||||
@@ -16,4 +18,5 @@ var VolumesValidOrderByKeys = []string{
|
||||
VolumesOrderByInodes,
|
||||
VolumesOrderByInodesFree,
|
||||
VolumesOrderByInodesUsed,
|
||||
PersistentVolumeClaimNameAttrKey,
|
||||
}
|
||||
|
||||
@@ -221,6 +221,57 @@ func TestPostableVolumes_Validate(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy name asc with empty groupBy is valid",
|
||||
req: &PostableVolumes{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: PersistentVolumeClaimNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name desc with empty groupBy is valid",
|
||||
req: &PostableVolumes{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: PersistentVolumeClaimNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name with non-empty groupBy is rejected",
|
||||
req: &PostableVolumes{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "k8s.namespace.name"}},
|
||||
},
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: PersistentVolumeClaimNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
84
tests/integration/testdata/inframonitoring/hosts_filter_dataset.jsonl
vendored
Normal file
84
tests/integration/testdata/inframonitoring/hosts_filter_dataset.jsonl
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
{"metric_name":"system.cpu.time","labels":{"host.name":"prod-linux-1","os.type":"linux","state":"user"},"timestamp":"2025-01-10T10:00:00+00:00","value":100,"temporality":"Cumulative","type_":"Sum","is_monotonic":true}
|
||||
{"metric_name":"system.cpu.time","labels":{"host.name":"prod-linux-1","os.type":"linux","state":"user"},"timestamp":"2025-01-10T10:02:00+00:00","value":200,"temporality":"Cumulative","type_":"Sum","is_monotonic":true}
|
||||
{"metric_name":"system.cpu.time","labels":{"host.name":"prod-linux-1","os.type":"linux","state":"user"},"timestamp":"2025-01-10T10:04:00+00:00","value":300,"temporality":"Cumulative","type_":"Sum","is_monotonic":true}
|
||||
{"metric_name":"system.cpu.time","labels":{"host.name":"prod-linux-1","os.type":"linux","state":"idle"},"timestamp":"2025-01-10T10:00:00+00:00","value":400,"temporality":"Cumulative","type_":"Sum","is_monotonic":true}
|
||||
{"metric_name":"system.cpu.time","labels":{"host.name":"prod-linux-1","os.type":"linux","state":"idle"},"timestamp":"2025-01-10T10:02:00+00:00","value":600,"temporality":"Cumulative","type_":"Sum","is_monotonic":true}
|
||||
{"metric_name":"system.cpu.time","labels":{"host.name":"prod-linux-1","os.type":"linux","state":"idle"},"timestamp":"2025-01-10T10:04:00+00:00","value":800,"temporality":"Cumulative","type_":"Sum","is_monotonic":true}
|
||||
{"metric_name":"system.memory.usage","labels":{"host.name":"prod-linux-1","os.type":"linux","state":"used"},"timestamp":"2025-01-10T10:00:00+00:00","value":2000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.memory.usage","labels":{"host.name":"prod-linux-1","os.type":"linux","state":"used"},"timestamp":"2025-01-10T10:02:00+00:00","value":2100000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.memory.usage","labels":{"host.name":"prod-linux-1","os.type":"linux","state":"used"},"timestamp":"2025-01-10T10:04:00+00:00","value":2200000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.memory.usage","labels":{"host.name":"prod-linux-1","os.type":"linux","state":"free"},"timestamp":"2025-01-10T10:00:00+00:00","value":6000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.memory.usage","labels":{"host.name":"prod-linux-1","os.type":"linux","state":"free"},"timestamp":"2025-01-10T10:02:00+00:00","value":5900000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.memory.usage","labels":{"host.name":"prod-linux-1","os.type":"linux","state":"free"},"timestamp":"2025-01-10T10:04:00+00:00","value":5800000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.cpu.load_average.15m","labels":{"host.name":"prod-linux-1","os.type":"linux"},"timestamp":"2025-01-10T10:00:00+00:00","value":1.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"system.cpu.load_average.15m","labels":{"host.name":"prod-linux-1","os.type":"linux"},"timestamp":"2025-01-10T10:02:00+00:00","value":1.55,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"system.cpu.load_average.15m","labels":{"host.name":"prod-linux-1","os.type":"linux"},"timestamp":"2025-01-10T10:04:00+00:00","value":1.6,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"system.filesystem.usage","labels":{"host.name":"prod-linux-1","os.type":"linux","state":"used"},"timestamp":"2025-01-10T10:00:00+00:00","value":50000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.filesystem.usage","labels":{"host.name":"prod-linux-1","os.type":"linux","state":"used"},"timestamp":"2025-01-10T10:02:00+00:00","value":51000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.filesystem.usage","labels":{"host.name":"prod-linux-1","os.type":"linux","state":"used"},"timestamp":"2025-01-10T10:04:00+00:00","value":52000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.filesystem.usage","labels":{"host.name":"prod-linux-1","os.type":"linux","state":"free"},"timestamp":"2025-01-10T10:00:00+00:00","value":50000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.filesystem.usage","labels":{"host.name":"prod-linux-1","os.type":"linux","state":"free"},"timestamp":"2025-01-10T10:02:00+00:00","value":49000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.filesystem.usage","labels":{"host.name":"prod-linux-1","os.type":"linux","state":"free"},"timestamp":"2025-01-10T10:04:00+00:00","value":48000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.cpu.time","labels":{"host.name":"prod-windows-1","os.type":"windows","state":"user"},"timestamp":"2025-01-10T10:00:00+00:00","value":100,"temporality":"Cumulative","type_":"Sum","is_monotonic":true}
|
||||
{"metric_name":"system.cpu.time","labels":{"host.name":"prod-windows-1","os.type":"windows","state":"user"},"timestamp":"2025-01-10T10:02:00+00:00","value":200,"temporality":"Cumulative","type_":"Sum","is_monotonic":true}
|
||||
{"metric_name":"system.cpu.time","labels":{"host.name":"prod-windows-1","os.type":"windows","state":"user"},"timestamp":"2025-01-10T10:04:00+00:00","value":300,"temporality":"Cumulative","type_":"Sum","is_monotonic":true}
|
||||
{"metric_name":"system.cpu.time","labels":{"host.name":"prod-windows-1","os.type":"windows","state":"idle"},"timestamp":"2025-01-10T10:00:00+00:00","value":400,"temporality":"Cumulative","type_":"Sum","is_monotonic":true}
|
||||
{"metric_name":"system.cpu.time","labels":{"host.name":"prod-windows-1","os.type":"windows","state":"idle"},"timestamp":"2025-01-10T10:02:00+00:00","value":600,"temporality":"Cumulative","type_":"Sum","is_monotonic":true}
|
||||
{"metric_name":"system.cpu.time","labels":{"host.name":"prod-windows-1","os.type":"windows","state":"idle"},"timestamp":"2025-01-10T10:04:00+00:00","value":800,"temporality":"Cumulative","type_":"Sum","is_monotonic":true}
|
||||
{"metric_name":"system.memory.usage","labels":{"host.name":"prod-windows-1","os.type":"windows","state":"used"},"timestamp":"2025-01-10T10:00:00+00:00","value":2000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.memory.usage","labels":{"host.name":"prod-windows-1","os.type":"windows","state":"used"},"timestamp":"2025-01-10T10:02:00+00:00","value":2100000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.memory.usage","labels":{"host.name":"prod-windows-1","os.type":"windows","state":"used"},"timestamp":"2025-01-10T10:04:00+00:00","value":2200000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.memory.usage","labels":{"host.name":"prod-windows-1","os.type":"windows","state":"free"},"timestamp":"2025-01-10T10:00:00+00:00","value":6000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.memory.usage","labels":{"host.name":"prod-windows-1","os.type":"windows","state":"free"},"timestamp":"2025-01-10T10:02:00+00:00","value":5900000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.memory.usage","labels":{"host.name":"prod-windows-1","os.type":"windows","state":"free"},"timestamp":"2025-01-10T10:04:00+00:00","value":5800000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.cpu.load_average.15m","labels":{"host.name":"prod-windows-1","os.type":"windows"},"timestamp":"2025-01-10T10:00:00+00:00","value":1.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"system.cpu.load_average.15m","labels":{"host.name":"prod-windows-1","os.type":"windows"},"timestamp":"2025-01-10T10:02:00+00:00","value":1.55,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"system.cpu.load_average.15m","labels":{"host.name":"prod-windows-1","os.type":"windows"},"timestamp":"2025-01-10T10:04:00+00:00","value":1.6,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"system.filesystem.usage","labels":{"host.name":"prod-windows-1","os.type":"windows","state":"used"},"timestamp":"2025-01-10T10:00:00+00:00","value":50000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.filesystem.usage","labels":{"host.name":"prod-windows-1","os.type":"windows","state":"used"},"timestamp":"2025-01-10T10:02:00+00:00","value":51000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.filesystem.usage","labels":{"host.name":"prod-windows-1","os.type":"windows","state":"used"},"timestamp":"2025-01-10T10:04:00+00:00","value":52000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.filesystem.usage","labels":{"host.name":"prod-windows-1","os.type":"windows","state":"free"},"timestamp":"2025-01-10T10:00:00+00:00","value":50000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.filesystem.usage","labels":{"host.name":"prod-windows-1","os.type":"windows","state":"free"},"timestamp":"2025-01-10T10:02:00+00:00","value":49000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.filesystem.usage","labels":{"host.name":"prod-windows-1","os.type":"windows","state":"free"},"timestamp":"2025-01-10T10:04:00+00:00","value":48000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.cpu.time","labels":{"host.name":"dev-linux-1","os.type":"linux","state":"user"},"timestamp":"2025-01-10T10:00:00+00:00","value":100,"temporality":"Cumulative","type_":"Sum","is_monotonic":true}
|
||||
{"metric_name":"system.cpu.time","labels":{"host.name":"dev-linux-1","os.type":"linux","state":"user"},"timestamp":"2025-01-10T10:02:00+00:00","value":200,"temporality":"Cumulative","type_":"Sum","is_monotonic":true}
|
||||
{"metric_name":"system.cpu.time","labels":{"host.name":"dev-linux-1","os.type":"linux","state":"user"},"timestamp":"2025-01-10T10:04:00+00:00","value":300,"temporality":"Cumulative","type_":"Sum","is_monotonic":true}
|
||||
{"metric_name":"system.cpu.time","labels":{"host.name":"dev-linux-1","os.type":"linux","state":"idle"},"timestamp":"2025-01-10T10:00:00+00:00","value":400,"temporality":"Cumulative","type_":"Sum","is_monotonic":true}
|
||||
{"metric_name":"system.cpu.time","labels":{"host.name":"dev-linux-1","os.type":"linux","state":"idle"},"timestamp":"2025-01-10T10:02:00+00:00","value":600,"temporality":"Cumulative","type_":"Sum","is_monotonic":true}
|
||||
{"metric_name":"system.cpu.time","labels":{"host.name":"dev-linux-1","os.type":"linux","state":"idle"},"timestamp":"2025-01-10T10:04:00+00:00","value":800,"temporality":"Cumulative","type_":"Sum","is_monotonic":true}
|
||||
{"metric_name":"system.memory.usage","labels":{"host.name":"dev-linux-1","os.type":"linux","state":"used"},"timestamp":"2025-01-10T10:00:00+00:00","value":2000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.memory.usage","labels":{"host.name":"dev-linux-1","os.type":"linux","state":"used"},"timestamp":"2025-01-10T10:02:00+00:00","value":2100000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.memory.usage","labels":{"host.name":"dev-linux-1","os.type":"linux","state":"used"},"timestamp":"2025-01-10T10:04:00+00:00","value":2200000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.memory.usage","labels":{"host.name":"dev-linux-1","os.type":"linux","state":"free"},"timestamp":"2025-01-10T10:00:00+00:00","value":6000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.memory.usage","labels":{"host.name":"dev-linux-1","os.type":"linux","state":"free"},"timestamp":"2025-01-10T10:02:00+00:00","value":5900000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.memory.usage","labels":{"host.name":"dev-linux-1","os.type":"linux","state":"free"},"timestamp":"2025-01-10T10:04:00+00:00","value":5800000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.cpu.load_average.15m","labels":{"host.name":"dev-linux-1","os.type":"linux"},"timestamp":"2025-01-10T10:00:00+00:00","value":1.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"system.cpu.load_average.15m","labels":{"host.name":"dev-linux-1","os.type":"linux"},"timestamp":"2025-01-10T10:02:00+00:00","value":1.55,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"system.cpu.load_average.15m","labels":{"host.name":"dev-linux-1","os.type":"linux"},"timestamp":"2025-01-10T10:04:00+00:00","value":1.6,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"system.filesystem.usage","labels":{"host.name":"dev-linux-1","os.type":"linux","state":"used"},"timestamp":"2025-01-10T10:00:00+00:00","value":50000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.filesystem.usage","labels":{"host.name":"dev-linux-1","os.type":"linux","state":"used"},"timestamp":"2025-01-10T10:02:00+00:00","value":51000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.filesystem.usage","labels":{"host.name":"dev-linux-1","os.type":"linux","state":"used"},"timestamp":"2025-01-10T10:04:00+00:00","value":52000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.filesystem.usage","labels":{"host.name":"dev-linux-1","os.type":"linux","state":"free"},"timestamp":"2025-01-10T10:00:00+00:00","value":50000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.filesystem.usage","labels":{"host.name":"dev-linux-1","os.type":"linux","state":"free"},"timestamp":"2025-01-10T10:02:00+00:00","value":49000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.filesystem.usage","labels":{"host.name":"dev-linux-1","os.type":"linux","state":"free"},"timestamp":"2025-01-10T10:04:00+00:00","value":48000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.cpu.time","labels":{"host.name":"dev-windows-1","os.type":"windows","state":"user"},"timestamp":"2025-01-10T10:00:00+00:00","value":100,"temporality":"Cumulative","type_":"Sum","is_monotonic":true}
|
||||
{"metric_name":"system.cpu.time","labels":{"host.name":"dev-windows-1","os.type":"windows","state":"user"},"timestamp":"2025-01-10T10:02:00+00:00","value":200,"temporality":"Cumulative","type_":"Sum","is_monotonic":true}
|
||||
{"metric_name":"system.cpu.time","labels":{"host.name":"dev-windows-1","os.type":"windows","state":"user"},"timestamp":"2025-01-10T10:04:00+00:00","value":300,"temporality":"Cumulative","type_":"Sum","is_monotonic":true}
|
||||
{"metric_name":"system.cpu.time","labels":{"host.name":"dev-windows-1","os.type":"windows","state":"idle"},"timestamp":"2025-01-10T10:00:00+00:00","value":400,"temporality":"Cumulative","type_":"Sum","is_monotonic":true}
|
||||
{"metric_name":"system.cpu.time","labels":{"host.name":"dev-windows-1","os.type":"windows","state":"idle"},"timestamp":"2025-01-10T10:02:00+00:00","value":600,"temporality":"Cumulative","type_":"Sum","is_monotonic":true}
|
||||
{"metric_name":"system.cpu.time","labels":{"host.name":"dev-windows-1","os.type":"windows","state":"idle"},"timestamp":"2025-01-10T10:04:00+00:00","value":800,"temporality":"Cumulative","type_":"Sum","is_monotonic":true}
|
||||
{"metric_name":"system.memory.usage","labels":{"host.name":"dev-windows-1","os.type":"windows","state":"used"},"timestamp":"2025-01-10T10:00:00+00:00","value":2000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.memory.usage","labels":{"host.name":"dev-windows-1","os.type":"windows","state":"used"},"timestamp":"2025-01-10T10:02:00+00:00","value":2100000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.memory.usage","labels":{"host.name":"dev-windows-1","os.type":"windows","state":"used"},"timestamp":"2025-01-10T10:04:00+00:00","value":2200000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.memory.usage","labels":{"host.name":"dev-windows-1","os.type":"windows","state":"free"},"timestamp":"2025-01-10T10:00:00+00:00","value":6000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.memory.usage","labels":{"host.name":"dev-windows-1","os.type":"windows","state":"free"},"timestamp":"2025-01-10T10:02:00+00:00","value":5900000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.memory.usage","labels":{"host.name":"dev-windows-1","os.type":"windows","state":"free"},"timestamp":"2025-01-10T10:04:00+00:00","value":5800000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.cpu.load_average.15m","labels":{"host.name":"dev-windows-1","os.type":"windows"},"timestamp":"2025-01-10T10:00:00+00:00","value":1.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"system.cpu.load_average.15m","labels":{"host.name":"dev-windows-1","os.type":"windows"},"timestamp":"2025-01-10T10:02:00+00:00","value":1.55,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"system.cpu.load_average.15m","labels":{"host.name":"dev-windows-1","os.type":"windows"},"timestamp":"2025-01-10T10:04:00+00:00","value":1.6,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"system.filesystem.usage","labels":{"host.name":"dev-windows-1","os.type":"windows","state":"used"},"timestamp":"2025-01-10T10:00:00+00:00","value":50000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.filesystem.usage","labels":{"host.name":"dev-windows-1","os.type":"windows","state":"used"},"timestamp":"2025-01-10T10:02:00+00:00","value":51000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.filesystem.usage","labels":{"host.name":"dev-windows-1","os.type":"windows","state":"used"},"timestamp":"2025-01-10T10:04:00+00:00","value":52000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.filesystem.usage","labels":{"host.name":"dev-windows-1","os.type":"windows","state":"free"},"timestamp":"2025-01-10T10:00:00+00:00","value":50000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.filesystem.usage","labels":{"host.name":"dev-windows-1","os.type":"windows","state":"free"},"timestamp":"2025-01-10T10:02:00+00:00","value":49000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
{"metric_name":"system.filesystem.usage","labels":{"host.name":"dev-windows-1","os.type":"windows","state":"free"},"timestamp":"2025-01-10T10:04:00+00:00","value":48000000000,"temporality":"Unspecified","type_":"Sum","is_monotonic":false}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user