mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-22 14:10:30 +01:00
Compare commits
6 Commits
fix/genera
...
infraM/met
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dff443f68c | ||
|
|
dadec2acc4 | ||
|
|
51edea502d | ||
|
|
e93792d427 | ||
|
|
6d0f618a9e | ||
|
|
564d479aae |
@@ -4247,6 +4247,15 @@ components:
|
||||
- missingOptionalMetrics
|
||||
- missingRequiredAttributes
|
||||
type: object
|
||||
InframonitoringtypesClusterMeta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
properties:
|
||||
k8s.cluster.name:
|
||||
type: string
|
||||
required:
|
||||
- k8s.cluster.name
|
||||
type: object
|
||||
InframonitoringtypesClusterRecord:
|
||||
properties:
|
||||
clusterCPU:
|
||||
@@ -4292,10 +4301,7 @@ components:
|
||||
- statefulSets
|
||||
type: object
|
||||
meta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
$ref: '#/components/schemas/InframonitoringtypesClusterMeta'
|
||||
nodeCountsByReadiness:
|
||||
$ref: '#/components/schemas/InframonitoringtypesNodeCountsByReadiness'
|
||||
podCountsByPhase:
|
||||
@@ -4387,6 +4393,51 @@ components:
|
||||
- containerCannotRun
|
||||
- unknown
|
||||
type: object
|
||||
InframonitoringtypesContainerMeta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
properties:
|
||||
container.image.name:
|
||||
type: string
|
||||
container.image.tag:
|
||||
type: string
|
||||
k8s.cluster.name:
|
||||
type: string
|
||||
k8s.container.name:
|
||||
type: string
|
||||
k8s.cronjob.name:
|
||||
type: string
|
||||
k8s.daemonset.name:
|
||||
type: string
|
||||
k8s.deployment.name:
|
||||
type: string
|
||||
k8s.job.name:
|
||||
type: string
|
||||
k8s.namespace.name:
|
||||
type: string
|
||||
k8s.node.name:
|
||||
type: string
|
||||
k8s.pod.name:
|
||||
type: string
|
||||
k8s.pod.uid:
|
||||
type: string
|
||||
k8s.statefulset.name:
|
||||
type: string
|
||||
required:
|
||||
- k8s.pod.uid
|
||||
- k8s.container.name
|
||||
- k8s.pod.name
|
||||
- container.image.name
|
||||
- container.image.tag
|
||||
- k8s.namespace.name
|
||||
- k8s.node.name
|
||||
- k8s.deployment.name
|
||||
- k8s.statefulset.name
|
||||
- k8s.daemonset.name
|
||||
- k8s.job.name
|
||||
- k8s.cronjob.name
|
||||
- k8s.cluster.name
|
||||
type: object
|
||||
InframonitoringtypesContainerReady:
|
||||
enum:
|
||||
- ready
|
||||
@@ -4420,10 +4471,7 @@ components:
|
||||
format: double
|
||||
type: number
|
||||
meta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
$ref: '#/components/schemas/InframonitoringtypesContainerMeta'
|
||||
podUID:
|
||||
type: string
|
||||
ready:
|
||||
@@ -4486,6 +4534,21 @@ components:
|
||||
- total
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesDaemonSetMeta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
properties:
|
||||
k8s.cluster.name:
|
||||
type: string
|
||||
k8s.daemonset.name:
|
||||
type: string
|
||||
k8s.namespace.name:
|
||||
type: string
|
||||
required:
|
||||
- k8s.daemonset.name
|
||||
- k8s.namespace.name
|
||||
- k8s.cluster.name
|
||||
type: object
|
||||
InframonitoringtypesDaemonSetRecord:
|
||||
properties:
|
||||
currentNodes:
|
||||
@@ -4513,10 +4576,7 @@ components:
|
||||
desiredNodes:
|
||||
type: integer
|
||||
meta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
$ref: '#/components/schemas/InframonitoringtypesDaemonSetMeta'
|
||||
misscheduledNodes:
|
||||
type: integer
|
||||
podCountsByPhase:
|
||||
@@ -4561,6 +4621,21 @@ components:
|
||||
- total
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesDeploymentMeta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
properties:
|
||||
k8s.cluster.name:
|
||||
type: string
|
||||
k8s.deployment.name:
|
||||
type: string
|
||||
k8s.namespace.name:
|
||||
type: string
|
||||
required:
|
||||
- k8s.deployment.name
|
||||
- k8s.namespace.name
|
||||
- k8s.cluster.name
|
||||
type: object
|
||||
InframonitoringtypesDeploymentRecord:
|
||||
properties:
|
||||
availablePods:
|
||||
@@ -4588,10 +4663,7 @@ components:
|
||||
desiredPods:
|
||||
type: integer
|
||||
meta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
$ref: '#/components/schemas/InframonitoringtypesDeploymentMeta'
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
@@ -4637,6 +4709,18 @@ components:
|
||||
filterByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesHostStatus'
|
||||
type: object
|
||||
InframonitoringtypesHostMeta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
properties:
|
||||
host.name:
|
||||
type: string
|
||||
os.type:
|
||||
type: string
|
||||
required:
|
||||
- os.type
|
||||
- host.name
|
||||
type: object
|
||||
InframonitoringtypesHostRecord:
|
||||
properties:
|
||||
activeHostCount:
|
||||
@@ -4658,10 +4742,7 @@ components:
|
||||
format: double
|
||||
type: number
|
||||
meta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
$ref: '#/components/schemas/InframonitoringtypesHostMeta'
|
||||
status:
|
||||
$ref: '#/components/schemas/InframonitoringtypesHostStatus'
|
||||
wait:
|
||||
@@ -4705,6 +4786,21 @@ components:
|
||||
- total
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesJobMeta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
properties:
|
||||
k8s.cluster.name:
|
||||
type: string
|
||||
k8s.job.name:
|
||||
type: string
|
||||
k8s.namespace.name:
|
||||
type: string
|
||||
required:
|
||||
- k8s.job.name
|
||||
- k8s.namespace.name
|
||||
- k8s.cluster.name
|
||||
type: object
|
||||
InframonitoringtypesJobRecord:
|
||||
properties:
|
||||
activePods:
|
||||
@@ -4734,10 +4830,7 @@ components:
|
||||
jobName:
|
||||
type: string
|
||||
meta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
$ref: '#/components/schemas/InframonitoringtypesJobMeta'
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
@@ -4831,6 +4924,18 @@ components:
|
||||
- message
|
||||
- documentationLink
|
||||
type: object
|
||||
InframonitoringtypesNamespaceMeta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
properties:
|
||||
k8s.cluster.name:
|
||||
type: string
|
||||
k8s.namespace.name:
|
||||
type: string
|
||||
required:
|
||||
- k8s.namespace.name
|
||||
- k8s.cluster.name
|
||||
type: object
|
||||
InframonitoringtypesNamespaceRecord:
|
||||
properties:
|
||||
counts:
|
||||
@@ -4854,10 +4959,7 @@ components:
|
||||
- statefulSets
|
||||
type: object
|
||||
meta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
$ref: '#/components/schemas/InframonitoringtypesNamespaceMeta'
|
||||
namespaceCPU:
|
||||
format: double
|
||||
type: number
|
||||
@@ -4915,15 +5017,27 @@ components:
|
||||
- ready
|
||||
- notReady
|
||||
type: object
|
||||
InframonitoringtypesNodeMeta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
properties:
|
||||
k8s.cluster.name:
|
||||
type: string
|
||||
k8s.node.name:
|
||||
type: string
|
||||
k8s.node.uid:
|
||||
type: string
|
||||
required:
|
||||
- k8s.node.uid
|
||||
- k8s.cluster.name
|
||||
- k8s.node.name
|
||||
type: object
|
||||
InframonitoringtypesNodeRecord:
|
||||
properties:
|
||||
condition:
|
||||
$ref: '#/components/schemas/InframonitoringtypesNodeCondition'
|
||||
meta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
$ref: '#/components/schemas/InframonitoringtypesNodeMeta'
|
||||
nodeCPU:
|
||||
format: double
|
||||
type: number
|
||||
@@ -5053,6 +5167,45 @@ components:
|
||||
- shutdown
|
||||
- unexpectedAdmissionError
|
||||
type: object
|
||||
InframonitoringtypesPodMeta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
properties:
|
||||
k8s.cluster.name:
|
||||
type: string
|
||||
k8s.cronjob.name:
|
||||
type: string
|
||||
k8s.daemonset.name:
|
||||
type: string
|
||||
k8s.deployment.name:
|
||||
type: string
|
||||
k8s.job.name:
|
||||
type: string
|
||||
k8s.namespace.name:
|
||||
type: string
|
||||
k8s.node.name:
|
||||
type: string
|
||||
k8s.pod.name:
|
||||
type: string
|
||||
k8s.pod.start_time:
|
||||
type: string
|
||||
k8s.pod.uid:
|
||||
type: string
|
||||
k8s.statefulset.name:
|
||||
type: string
|
||||
required:
|
||||
- k8s.pod.uid
|
||||
- k8s.pod.name
|
||||
- k8s.namespace.name
|
||||
- k8s.node.name
|
||||
- k8s.deployment.name
|
||||
- k8s.statefulset.name
|
||||
- k8s.daemonset.name
|
||||
- k8s.job.name
|
||||
- k8s.cronjob.name
|
||||
- k8s.cluster.name
|
||||
- k8s.pod.start_time
|
||||
type: object
|
||||
InframonitoringtypesPodPhase:
|
||||
enum:
|
||||
- pending
|
||||
@@ -5065,10 +5218,7 @@ components:
|
||||
InframonitoringtypesPodRecord:
|
||||
properties:
|
||||
meta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodMeta'
|
||||
podAge:
|
||||
format: int64
|
||||
type: integer
|
||||
@@ -5452,6 +5602,21 @@ components:
|
||||
- list
|
||||
- grouped_list
|
||||
type: string
|
||||
InframonitoringtypesStatefulSetMeta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
properties:
|
||||
k8s.cluster.name:
|
||||
type: string
|
||||
k8s.namespace.name:
|
||||
type: string
|
||||
k8s.statefulset.name:
|
||||
type: string
|
||||
required:
|
||||
- k8s.statefulset.name
|
||||
- k8s.namespace.name
|
||||
- k8s.cluster.name
|
||||
type: object
|
||||
InframonitoringtypesStatefulSetRecord:
|
||||
properties:
|
||||
currentPods:
|
||||
@@ -5459,10 +5624,7 @@ components:
|
||||
desiredPods:
|
||||
type: integer
|
||||
meta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
$ref: '#/components/schemas/InframonitoringtypesStatefulSetMeta'
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
@@ -5521,13 +5683,37 @@ components:
|
||||
- total
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesVolumeMeta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
properties:
|
||||
k8s.cluster.name:
|
||||
type: string
|
||||
k8s.namespace.name:
|
||||
type: string
|
||||
k8s.node.name:
|
||||
type: string
|
||||
k8s.persistentvolumeclaim.name:
|
||||
type: string
|
||||
k8s.pod.name:
|
||||
type: string
|
||||
k8s.pod.uid:
|
||||
type: string
|
||||
k8s.statefulset.name:
|
||||
type: string
|
||||
required:
|
||||
- k8s.persistentvolumeclaim.name
|
||||
- k8s.pod.uid
|
||||
- k8s.pod.name
|
||||
- k8s.namespace.name
|
||||
- k8s.node.name
|
||||
- k8s.statefulset.name
|
||||
- k8s.cluster.name
|
||||
type: object
|
||||
InframonitoringtypesVolumeRecord:
|
||||
properties:
|
||||
meta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
$ref: '#/components/schemas/InframonitoringtypesVolumeMeta'
|
||||
persistentVolumeClaimName:
|
||||
type: string
|
||||
volumeAvailable:
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
"lodash-es": "^4.17.21",
|
||||
"motion": "12.4.13",
|
||||
"nuqs": "2.8.8",
|
||||
"overlayscrollbars": "^2.8.1",
|
||||
"overlayscrollbars": "^2.16.0",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
"papaparse": "5.4.1",
|
||||
"posthog-js": "1.298.0",
|
||||
@@ -221,5 +221,18 @@
|
||||
"*.(scss|css)": [
|
||||
"stylelint"
|
||||
]
|
||||
},
|
||||
"overrides": {
|
||||
"@babel/core@<=7.29.0": ">=7.29.6 <8",
|
||||
"@istanbuljs/load-nyc-config>js-yaml": ">=4.2.0 <5",
|
||||
"cookie@<0.7.0": ">=0.7.1 <1",
|
||||
"dompurify@<=3.4.10": ">=3.4.11 <4",
|
||||
"esbuild@>=0.27.3 <0.28.1": ">=0.28.1 <0.29.0",
|
||||
"js-cookie@<=3.0.5": ">=3.0.7 <4",
|
||||
"js-yaml@>=4.0.0 <=4.1.1": ">=4.2.0 <5",
|
||||
"prismjs@<1.30.0": ">=1.30.0 <2",
|
||||
"react-router@>=6.7.0 <6.30.4": ">=6.30.4 <7",
|
||||
"tmp@<0.2.6": ">=0.2.6 <0.3.0",
|
||||
"yaml@>=1.0.0 <1.10.3": ">=1.10.3 <2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
frontend/pnpm-lock.yaml
generated
16
frontend/pnpm-lock.yaml
generated
@@ -187,11 +187,11 @@ importers:
|
||||
specifier: 2.8.8
|
||||
version: 2.8.8(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.4(react@18.2.0))(react@18.2.0)
|
||||
overlayscrollbars:
|
||||
specifier: ^2.8.1
|
||||
version: 2.9.2
|
||||
specifier: ^2.16.0
|
||||
version: 2.16.0
|
||||
overlayscrollbars-react:
|
||||
specifier: ^0.5.6
|
||||
version: 0.5.6(overlayscrollbars@2.9.2)(react@18.2.0)
|
||||
version: 0.5.6(overlayscrollbars@2.16.0)(react@18.2.0)
|
||||
papaparse:
|
||||
specifier: 5.4.1
|
||||
version: 5.4.1
|
||||
@@ -6941,8 +6941,8 @@ packages:
|
||||
overlayscrollbars: ^2.0.0
|
||||
react: '>=16.8.0'
|
||||
|
||||
overlayscrollbars@2.9.2:
|
||||
resolution: {integrity: sha512-iDT84r39i7oWP72diZN2mbJUsn/taCq568aQaIrc84S87PunBT7qtsVltAF2esk7ORTRjQDnfjVYoqqTzgs8QA==}
|
||||
overlayscrollbars@2.16.0:
|
||||
resolution: {integrity: sha512-N03oje/q7j93D0aLZtoCdsDSYLmhheSsv8H7oSLE7HhdV9P/bmCURtLV/KbPye7P/bpfyt/obSfDpGUYoJ0OWg==}
|
||||
|
||||
oxfmt@0.54.0:
|
||||
resolution: {integrity: sha512-DjnMwn7smSLF+Mc2+pRItnuPftm/dkUFpY/d4+33y9TfKrsHZo8GLhmUg9BrOIUEy94Rlom1Q11N6vuhE+e0oQ==}
|
||||
@@ -16456,12 +16456,12 @@ snapshots:
|
||||
|
||||
outvariant@1.4.0: {}
|
||||
|
||||
overlayscrollbars-react@0.5.6(overlayscrollbars@2.9.2)(react@18.2.0):
|
||||
overlayscrollbars-react@0.5.6(overlayscrollbars@2.16.0)(react@18.2.0):
|
||||
dependencies:
|
||||
overlayscrollbars: 2.9.2
|
||||
overlayscrollbars: 2.16.0
|
||||
react: 18.2.0
|
||||
|
||||
overlayscrollbars@2.9.2: {}
|
||||
overlayscrollbars@2.16.0: {}
|
||||
|
||||
oxfmt@0.54.0:
|
||||
dependencies:
|
||||
|
||||
20
frontend/src/__tests__/fields_api.util.ts
Normal file
20
frontend/src/__tests__/fields_api.util.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
|
||||
export function mockFieldsAPIsWithEmptyResponse(): void {
|
||||
server.use(
|
||||
rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/keys`, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ status: 'success', data: { keys: {}, complete: true } }),
|
||||
),
|
||||
),
|
||||
rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/values`, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ status: 'success', data: { values: {}, complete: true } }),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -2,29 +2,6 @@ import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
// The wire shape these handlers can actually rely on. The generated
|
||||
// RenderErrorResponseDTO marks code/message/url/errors as required, but the
|
||||
// server omits any of them even on valid errors (e.g. a 400 with just a
|
||||
// message), so a present `error` object is all the guard can guarantee.
|
||||
type ErrorEnvelope = {
|
||||
error: {
|
||||
code?: string;
|
||||
message?: string;
|
||||
url?: string;
|
||||
errors?: { message?: string }[];
|
||||
};
|
||||
};
|
||||
|
||||
function isErrorEnvelope(data: unknown): data is ErrorEnvelope {
|
||||
return (
|
||||
typeof data === 'object' &&
|
||||
data !== null &&
|
||||
'error' in data &&
|
||||
typeof (data as ErrorEnvelope).error === 'object' &&
|
||||
(data as ErrorEnvelope).error !== null
|
||||
);
|
||||
}
|
||||
|
||||
// @deprecated Use convertToApiError instead
|
||||
export function ErrorResponseHandlerForGeneratedAPIs(
|
||||
error: AxiosError<RenderErrorResponseDTO>,
|
||||
@@ -33,29 +10,15 @@ export function ErrorResponseHandlerForGeneratedAPIs(
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
if (response) {
|
||||
// The body isn't guaranteed to be an error envelope — e.g. a gateway 5xx
|
||||
// with an HTML/empty body during a deploy. Verify the shape before reading
|
||||
// it; otherwise synthesize a consistent error from the status.
|
||||
const data: unknown = response.data;
|
||||
if (isErrorEnvelope(data)) {
|
||||
const { code, message, url, errors } = data.error;
|
||||
throw new APIError({
|
||||
httpStatusCode: response.status || 500,
|
||||
error: {
|
||||
code: code ?? '',
|
||||
message: message ?? '',
|
||||
url: url ?? '',
|
||||
errors: (errors ?? []).map((e) => ({ message: e.message ?? '' })),
|
||||
},
|
||||
});
|
||||
}
|
||||
throw new APIError({
|
||||
httpStatusCode: response.status || 500,
|
||||
error: {
|
||||
code: 'UPSTREAM_UNAVAILABLE',
|
||||
message: error.message || 'Something went wrong',
|
||||
url: '',
|
||||
errors: [],
|
||||
code: response.data.error.code,
|
||||
message: response.data.error.message,
|
||||
url: response.data.error.url ?? '',
|
||||
errors: (response.data.error.errors ?? []).map((e) => ({
|
||||
message: e.message ?? '',
|
||||
})),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -99,7 +62,9 @@ export function convertToApiError(
|
||||
return new APIError({
|
||||
httpStatusCode: response?.status || error.status || 500,
|
||||
error: {
|
||||
code: errorData?.code || 'UPSTREAM_UNAVAILABLE',
|
||||
code:
|
||||
errorData?.code ||
|
||||
String(response?.status || error.code || 'unknown_error'),
|
||||
message:
|
||||
errorData?.message ||
|
||||
response?.statusText ||
|
||||
|
||||
@@ -2,38 +2,19 @@ import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
function isErrorV2Resp(data: unknown): data is ErrorV2Resp {
|
||||
return (
|
||||
typeof data === 'object' &&
|
||||
data !== null &&
|
||||
'error' in data &&
|
||||
typeof (data as ErrorV2Resp).error?.code === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
// reference - https://axios-http.com/docs/handling_errors
|
||||
export function ErrorResponseHandlerV2(error: AxiosError<ErrorV2Resp>): never {
|
||||
const { response, request } = error;
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
if (response) {
|
||||
// response.data isn't guaranteed to be a V2 envelope (e.g. a gateway 5xx
|
||||
// with an HTML/empty body during a deploy), so verify the shape first.
|
||||
const data: unknown = response.data;
|
||||
if (isErrorV2Resp(data)) {
|
||||
const { code, message, url, errors } = data.error;
|
||||
throw new APIError({
|
||||
httpStatusCode: response.status || 500,
|
||||
error: { code, message, url, errors },
|
||||
});
|
||||
}
|
||||
throw new APIError({
|
||||
httpStatusCode: response.status || 500,
|
||||
error: {
|
||||
code: 'UPSTREAM_UNAVAILABLE',
|
||||
message: error.message || 'Something went wrong',
|
||||
url: '',
|
||||
errors: [],
|
||||
code: response.data.error.code,
|
||||
message: response.data.error.message,
|
||||
url: response.data.error.url,
|
||||
errors: response.data.error.errors,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
function asAxiosError(partial: Partial<AxiosError>): AxiosError<ErrorV2Resp> {
|
||||
return partial as AxiosError<ErrorV2Resp>;
|
||||
}
|
||||
|
||||
// ErrorResponseHandlerV2 always throws — capture the APIError so assertions stay
|
||||
// unconditional.
|
||||
function runHandler(error: AxiosError<ErrorV2Resp>): APIError {
|
||||
try {
|
||||
ErrorResponseHandlerV2(error);
|
||||
} catch (thrown) {
|
||||
return thrown as APIError;
|
||||
}
|
||||
throw new Error('expected ErrorResponseHandlerV2 to throw');
|
||||
}
|
||||
|
||||
type ExpectedError = {
|
||||
httpStatusCode: number;
|
||||
code: string;
|
||||
message: string;
|
||||
errors: { message: string }[];
|
||||
};
|
||||
|
||||
// One row per response shape the handler must normalize. New shapes (with
|
||||
// different bodies) can be added here without a new test block.
|
||||
const cases: {
|
||||
name: string;
|
||||
error: AxiosError<ErrorV2Resp>;
|
||||
expected: ExpectedError;
|
||||
}[] = [
|
||||
{
|
||||
name: 'well-formed V2 error envelope',
|
||||
error: asAxiosError({
|
||||
message: 'Request failed with status code 400',
|
||||
response: {
|
||||
status: 400,
|
||||
data: {
|
||||
error: {
|
||||
code: 'bad_request',
|
||||
message: 'Invalid dashboard payload',
|
||||
url: 'https://signoz.io/docs',
|
||||
errors: [{ message: 'name is required' }, { message: 'name too long' }],
|
||||
},
|
||||
},
|
||||
} as AxiosError['response'],
|
||||
}),
|
||||
expected: {
|
||||
httpStatusCode: 400,
|
||||
code: 'bad_request',
|
||||
message: 'Invalid dashboard payload',
|
||||
errors: [{ message: 'name is required' }, { message: 'name too long' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Regression: during a deployment the gateway returns a 5xx with a
|
||||
// non-envelope body. Reading response.data.error.code used to throw a
|
||||
// TypeError from inside the handler itself. See engineering-pod#5760.
|
||||
name: '5xx with a non-envelope HTML body',
|
||||
error: asAxiosError({
|
||||
message: 'Request failed with status code 503',
|
||||
response: {
|
||||
status: 503,
|
||||
data: '<html><body>503 Service Temporarily Unavailable</body></html>',
|
||||
} as AxiosError['response'],
|
||||
}),
|
||||
expected: {
|
||||
httpStatusCode: 503,
|
||||
code: 'UPSTREAM_UNAVAILABLE',
|
||||
message: 'Request failed with status code 503',
|
||||
errors: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '5xx with an empty body',
|
||||
error: asAxiosError({
|
||||
message: 'Request failed with status code 502',
|
||||
response: {
|
||||
status: 502,
|
||||
data: undefined,
|
||||
} as AxiosError['response'],
|
||||
}),
|
||||
expected: {
|
||||
httpStatusCode: 502,
|
||||
code: 'UPSTREAM_UNAVAILABLE',
|
||||
message: 'Request failed with status code 502',
|
||||
errors: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'no response received (network error)',
|
||||
error: asAxiosError({
|
||||
message: 'Network Error',
|
||||
code: 'ERR_NETWORK',
|
||||
name: 'AxiosError',
|
||||
request: {},
|
||||
}),
|
||||
expected: {
|
||||
httpStatusCode: 500,
|
||||
code: 'ERR_NETWORK',
|
||||
message: 'Network Error',
|
||||
errors: [],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('ErrorResponseHandlerV2', () => {
|
||||
it.each(cases)(
|
||||
'normalizes $name into a consistent APIError',
|
||||
({ error, expected }) => {
|
||||
const apiError = runHandler(error);
|
||||
|
||||
expect(apiError).toBeInstanceOf(APIError);
|
||||
expect(apiError.getHttpStatusCode()).toBe(expected.httpStatusCode);
|
||||
expect(apiError.getErrorCode()).toBe(expected.code);
|
||||
expect(apiError.getErrorMessage()).toBe(expected.message);
|
||||
// The sub-error messages feed several parts of the UI, so assert them.
|
||||
expect(apiError.getErrorDetails().error.errors).toStrictEqual(
|
||||
expected.errors,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -5681,6 +5681,14 @@ export interface InframonitoringtypesChecksDTO {
|
||||
type: InframonitoringtypesCheckTypeDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesClusterMetaDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.cluster.name': string;
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesClusterRecordDTOCounts = {
|
||||
/**
|
||||
* @type integer
|
||||
@@ -5714,16 +5722,6 @@ export type InframonitoringtypesClusterRecordDTOCounts = {
|
||||
statefulSets: number;
|
||||
};
|
||||
|
||||
export type InframonitoringtypesClusterRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesClusterRecordDTOMeta =
|
||||
InframonitoringtypesClusterRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesNodeCountsByReadinessDTO {
|
||||
/**
|
||||
* @type integer
|
||||
@@ -5862,10 +5860,7 @@ export interface InframonitoringtypesClusterRecordDTO {
|
||||
* @type object
|
||||
*/
|
||||
counts: InframonitoringtypesClusterRecordDTOCounts;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
meta: InframonitoringtypesClusterRecordDTOMeta;
|
||||
meta: InframonitoringtypesClusterMetaDTO;
|
||||
nodeCountsByReadiness: InframonitoringtypesNodeCountsByReadinessDTO;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
@@ -5980,21 +5975,67 @@ export interface InframonitoringtypesContainerCountsByStatusDTO {
|
||||
waiting: number;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesContainerMetaDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'container.image.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'container.image.tag': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.cluster.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.container.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.cronjob.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.daemonset.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.deployment.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.job.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.namespace.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.node.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.pod.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.pod.uid': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.statefulset.name': string;
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export enum InframonitoringtypesContainerReadyDTO {
|
||||
ready = 'ready',
|
||||
not_ready = 'not_ready',
|
||||
no_data = 'no_data',
|
||||
}
|
||||
export type InframonitoringtypesContainerRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesContainerRecordDTOMeta =
|
||||
InframonitoringtypesContainerRecordDTOMetaAnyOf | null;
|
||||
|
||||
export enum InframonitoringtypesContainerStatusDTO {
|
||||
running = 'running',
|
||||
waiting = 'waiting',
|
||||
@@ -6048,10 +6089,7 @@ export interface InframonitoringtypesContainerRecordDTO {
|
||||
* @format double
|
||||
*/
|
||||
memoryRequestUtilization: number;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
meta: InframonitoringtypesContainerRecordDTOMeta;
|
||||
meta: InframonitoringtypesContainerMetaDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -6082,15 +6120,21 @@ export interface InframonitoringtypesContainersDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesDaemonSetRecordDTOMetaAnyOf = {
|
||||
export interface InframonitoringtypesDaemonSetMetaDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.cluster.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.daemonset.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.namespace.name': string;
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesDaemonSetRecordDTOMeta =
|
||||
InframonitoringtypesDaemonSetRecordDTOMetaAnyOf | null;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesDaemonSetRecordDTO {
|
||||
/**
|
||||
@@ -6135,10 +6179,7 @@ export interface InframonitoringtypesDaemonSetRecordDTO {
|
||||
* @type integer
|
||||
*/
|
||||
desiredNodes: number;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
meta: InframonitoringtypesDaemonSetRecordDTOMeta;
|
||||
meta: InframonitoringtypesDaemonSetMetaDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
@@ -6168,15 +6209,21 @@ export interface InframonitoringtypesDaemonSetsDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesDeploymentRecordDTOMetaAnyOf = {
|
||||
export interface InframonitoringtypesDeploymentMetaDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.cluster.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.deployment.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.namespace.name': string;
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesDeploymentRecordDTOMeta =
|
||||
InframonitoringtypesDeploymentRecordDTOMetaAnyOf | null;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesDeploymentRecordDTO {
|
||||
/**
|
||||
@@ -6221,10 +6268,7 @@ export interface InframonitoringtypesDeploymentRecordDTO {
|
||||
* @type integer
|
||||
*/
|
||||
desiredPods: number;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
meta: InframonitoringtypesDeploymentRecordDTOMeta;
|
||||
meta: InframonitoringtypesDeploymentMetaDTO;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
}
|
||||
@@ -6259,15 +6303,17 @@ export interface InframonitoringtypesHostFilterDTO {
|
||||
filterByStatus?: InframonitoringtypesHostStatusDTO;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesHostRecordDTOMetaAnyOf = {
|
||||
export interface InframonitoringtypesHostMetaDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'host.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'os.type': string;
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesHostRecordDTOMeta =
|
||||
InframonitoringtypesHostRecordDTOMetaAnyOf | null;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesHostRecordDTO {
|
||||
/**
|
||||
@@ -6302,10 +6348,7 @@ export interface InframonitoringtypesHostRecordDTO {
|
||||
* @format double
|
||||
*/
|
||||
memory: number;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
meta: InframonitoringtypesHostRecordDTOMeta;
|
||||
meta: InframonitoringtypesHostMetaDTO;
|
||||
status: InframonitoringtypesHostStatusDTO;
|
||||
/**
|
||||
* @type number
|
||||
@@ -6331,15 +6374,21 @@ export interface InframonitoringtypesHostsDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesJobRecordDTOMetaAnyOf = {
|
||||
export interface InframonitoringtypesJobMetaDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.cluster.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.job.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.namespace.name': string;
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesJobRecordDTOMeta =
|
||||
InframonitoringtypesJobRecordDTOMetaAnyOf | null;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesJobRecordDTO {
|
||||
/**
|
||||
@@ -6388,10 +6437,7 @@ export interface InframonitoringtypesJobRecordDTO {
|
||||
* @type string
|
||||
*/
|
||||
jobName: string;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
meta: InframonitoringtypesJobRecordDTOMeta;
|
||||
meta: InframonitoringtypesJobMetaDTO;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
/**
|
||||
@@ -6417,6 +6463,18 @@ export interface InframonitoringtypesJobsDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesNamespaceMetaDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.cluster.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.namespace.name': string;
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesNamespaceRecordDTOCounts = {
|
||||
/**
|
||||
* @type integer
|
||||
@@ -6440,25 +6498,12 @@ export type InframonitoringtypesNamespaceRecordDTOCounts = {
|
||||
statefulSets: number;
|
||||
};
|
||||
|
||||
export type InframonitoringtypesNamespaceRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesNamespaceRecordDTOMeta =
|
||||
InframonitoringtypesNamespaceRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesNamespaceRecordDTO {
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
counts: InframonitoringtypesNamespaceRecordDTOCounts;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
meta: InframonitoringtypesNamespaceRecordDTOMeta;
|
||||
meta: InframonitoringtypesNamespaceMetaDTO;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
@@ -6499,22 +6544,25 @@ export enum InframonitoringtypesNodeConditionDTO {
|
||||
not_ready = 'not_ready',
|
||||
no_data = 'no_data',
|
||||
}
|
||||
export type InframonitoringtypesNodeRecordDTOMetaAnyOf = {
|
||||
export interface InframonitoringtypesNodeMetaDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.cluster.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.node.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.node.uid': string;
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesNodeRecordDTOMeta =
|
||||
InframonitoringtypesNodeRecordDTOMetaAnyOf | null;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesNodeRecordDTO {
|
||||
condition: InframonitoringtypesNodeConditionDTO;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
meta: InframonitoringtypesNodeRecordDTOMeta;
|
||||
meta: InframonitoringtypesNodeMetaDTO;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
@@ -6561,6 +6609,54 @@ export interface InframonitoringtypesNodesDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesPodMetaDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.cluster.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.cronjob.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.daemonset.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.deployment.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.job.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.namespace.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.node.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.pod.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.pod.start_time': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.pod.uid': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.statefulset.name': string;
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export enum InframonitoringtypesPodPhaseDTO {
|
||||
pending = 'pending',
|
||||
running = 'running',
|
||||
@@ -6569,16 +6665,6 @@ export enum InframonitoringtypesPodPhaseDTO {
|
||||
unknown = 'unknown',
|
||||
no_data = 'no_data',
|
||||
}
|
||||
export type InframonitoringtypesPodRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesPodRecordDTOMeta =
|
||||
InframonitoringtypesPodRecordDTOMetaAnyOf | null;
|
||||
|
||||
export enum InframonitoringtypesPodStatusDTO {
|
||||
pending = 'pending',
|
||||
running = 'running',
|
||||
@@ -6601,10 +6687,7 @@ export enum InframonitoringtypesPodStatusDTO {
|
||||
no_data = 'no_data',
|
||||
}
|
||||
export interface InframonitoringtypesPodRecordDTO {
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
meta: InframonitoringtypesPodRecordDTOMeta;
|
||||
meta: InframonitoringtypesPodMetaDTO;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
@@ -6969,15 +7052,21 @@ export interface InframonitoringtypesPostableVolumesDTO {
|
||||
start: number;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesStatefulSetRecordDTOMetaAnyOf = {
|
||||
export interface InframonitoringtypesStatefulSetMetaDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.cluster.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.namespace.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.statefulset.name': string;
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesStatefulSetRecordDTOMeta =
|
||||
InframonitoringtypesStatefulSetRecordDTOMetaAnyOf | null;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesStatefulSetRecordDTO {
|
||||
/**
|
||||
@@ -6988,10 +7077,7 @@ export interface InframonitoringtypesStatefulSetRecordDTO {
|
||||
* @type integer
|
||||
*/
|
||||
desiredPods: number;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
meta: InframonitoringtypesStatefulSetRecordDTOMeta;
|
||||
meta: InframonitoringtypesStatefulSetMetaDTO;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
/**
|
||||
@@ -7047,21 +7133,40 @@ export interface InframonitoringtypesStatefulSetsDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesVolumeRecordDTOMetaAnyOf = {
|
||||
export interface InframonitoringtypesVolumeMetaDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.cluster.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.namespace.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.node.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.persistentvolumeclaim.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.pod.name': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.pod.uid': string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'k8s.statefulset.name': string;
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesVolumeRecordDTOMeta =
|
||||
InframonitoringtypesVolumeRecordDTOMetaAnyOf | null;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesVolumeRecordDTO {
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
meta: InframonitoringtypesVolumeRecordDTOMeta;
|
||||
meta: InframonitoringtypesVolumeMetaDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
|
||||
@@ -1,148 +1,39 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
import { Button, Drawer, Tooltip } from 'antd';
|
||||
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
|
||||
import { X } from '@signozhq/icons';
|
||||
import { Divider } from '@signozhq/ui/divider';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Drawer } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
|
||||
import APIError from 'types/api/error';
|
||||
import { combineInitialAndUserExpression } from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
initialQueryState,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import {
|
||||
BarChart,
|
||||
ChevronsLeftRight,
|
||||
Compass,
|
||||
DraftingCompass,
|
||||
ScrollText,
|
||||
X,
|
||||
} from '@signozhq/icons';
|
||||
import { isCustomTimeRange, useGlobalTimeStore } from 'store/globalTime';
|
||||
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
|
||||
import {
|
||||
LogsAggregatorOperator,
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
GlobalTimeProvider,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
useGlobalTimeStore,
|
||||
} from 'store/globalTime';
|
||||
|
||||
import { InfraMonitoringEntity, VIEW_TYPES } from '../constants';
|
||||
import EntityEvents from '../EntityDetailsUtils/EntityEvents';
|
||||
import EntityLogs from '../EntityDetailsUtils/EntityLogs';
|
||||
import { K8S_ENTITY_LOGS_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityLogs/hooks';
|
||||
import EntityMetrics from '../EntityDetailsUtils/EntityMetrics';
|
||||
import EntityTraces from '../EntityDetailsUtils/EntityTraces';
|
||||
import { K8S_ENTITY_TRACES_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityTraces/hooks';
|
||||
import {
|
||||
SelectedItemParams,
|
||||
useInfraMonitoringEventsFilters,
|
||||
useInfraMonitoringLogFilters,
|
||||
useInfraMonitoringSelectedItemParams,
|
||||
useInfraMonitoringTracesFilters,
|
||||
useInfraMonitoringView,
|
||||
} from '../hooks';
|
||||
import { INFRA_MONITORING_K8S_PARAMS_KEYS } from '../constants';
|
||||
import { useInfraMonitoringSelectedItemParams } from '../hooks';
|
||||
import LoadingContainer from '../LoadingContainer';
|
||||
|
||||
import K8sBaseDetailsContent from './K8sBaseDetailsContent';
|
||||
import { K8sBaseDetailsProps } from './types';
|
||||
|
||||
import '../EntityDetailsUtils/entityDetails.styles.scss';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import {
|
||||
EntityCountConfig,
|
||||
EntityCountsSection,
|
||||
} from './components/EntityCountsSection/EntityCountsSection';
|
||||
|
||||
const TimeRangeOffset = 1000000000;
|
||||
export type {
|
||||
CustomTab,
|
||||
CustomTabRenderProps,
|
||||
K8sBaseDetailsProps,
|
||||
K8sDetailsCountConfig,
|
||||
K8sDetailsFilters,
|
||||
K8sDetailsMetadataConfig,
|
||||
} from './types';
|
||||
|
||||
export interface K8sDetailsMetadataConfig<T> {
|
||||
label: string;
|
||||
getValue: (entity: T) => string | number;
|
||||
render?: (value: string | number, entity: T) => React.ReactNode;
|
||||
}
|
||||
|
||||
export type K8sDetailsCountConfig<T> = EntityCountConfig<T>;
|
||||
|
||||
export interface K8sDetailsFilters {
|
||||
filter: { expression: string };
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export interface CustomTabRenderProps<T> {
|
||||
entity: T;
|
||||
timeRange: { startTime: number; endTime: number };
|
||||
selectedInterval: Time;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface CustomTab<T> {
|
||||
key: string;
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
render: (props: CustomTabRenderProps<T>) => React.ReactNode;
|
||||
}
|
||||
|
||||
export interface K8sBaseDetailsProps<T> {
|
||||
category: InfraMonitoringEntity;
|
||||
eventCategory: string;
|
||||
// Data fetching configuration
|
||||
getSelectedItemExpression: (params: SelectedItemParams) => string;
|
||||
fetchEntityData: (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<{ data: T | null; error?: APIError | null }>;
|
||||
// Entity configuration
|
||||
getEntityName: (entity: T) => string;
|
||||
getInitialLogTracesExpression: (entity: T) => string;
|
||||
getInitialEventsExpression: (entity: T) => string;
|
||||
metadataConfig: K8sDetailsMetadataConfig<T>[];
|
||||
countsConfig?: K8sDetailsCountConfig<T>[];
|
||||
getCountsFilterExpression?: (entity: T) => string;
|
||||
entityWidgetInfo: {
|
||||
title: string;
|
||||
yAxisUnit: string;
|
||||
docPath?: string;
|
||||
}[];
|
||||
getEntityQueryPayload: (
|
||||
entity: T,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
) => GetQueryResultsProps[];
|
||||
queryKeyPrefix: string;
|
||||
/** When true, only metrics are shown and the Metrics/Logs/Traces/Events tab bar is hidden. */
|
||||
hideDetailViewTabs?: boolean;
|
||||
tabsConfig?: {
|
||||
showMetrics?: boolean;
|
||||
showLogs?: boolean;
|
||||
showTraces?: boolean;
|
||||
showEvents?: boolean;
|
||||
};
|
||||
customTabs?: Array<CustomTab<T>>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export default function K8sBaseDetails<T>({
|
||||
category,
|
||||
eventCategory,
|
||||
@@ -163,7 +54,6 @@ export default function K8sBaseDetails<T>({
|
||||
}: K8sBaseDetailsProps<T>): JSX.Element {
|
||||
const selectedTime = useGlobalTimeStore((s) => s.selectedTime);
|
||||
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
|
||||
const lastComputedMinMax = useGlobalTimeStore((s) => s.lastComputedMinMax);
|
||||
const getAutoRefreshQueryKey = useGlobalTimeStore(
|
||||
(s) => s.getAutoRefreshQueryKey,
|
||||
);
|
||||
@@ -237,86 +127,9 @@ export default function K8sBaseDetails<T>({
|
||||
|
||||
const entityName = entity ? getEntityName(entity) : '';
|
||||
|
||||
// Content state (previously in K8sBaseDetailsContent)
|
||||
const tabVisibility = useMemo(
|
||||
() => ({
|
||||
showMetrics: true,
|
||||
showLogs: true,
|
||||
showTraces: true,
|
||||
showEvents: true,
|
||||
...tabsConfig,
|
||||
}),
|
||||
[tabsConfig],
|
||||
);
|
||||
|
||||
const { startMs, endMs } = useMemo(
|
||||
() => ({
|
||||
startMs: Math.floor(lastComputedMinMax.minTime / NANO_SECOND_MULTIPLIER),
|
||||
endMs: Math.floor(lastComputedMinMax.maxTime / NANO_SECOND_MULTIPLIER),
|
||||
}),
|
||||
[lastComputedMinMax],
|
||||
);
|
||||
|
||||
const [modalTimeRange, setModalTimeRange] = useState(() => ({
|
||||
startTime: startMs,
|
||||
endTime: endMs,
|
||||
}));
|
||||
|
||||
// TODO(h4ad): Remove this and use context/zustand
|
||||
const lastSelectedInterval = useRef<Time | null>(null);
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
lastSelectedInterval.current
|
||||
? lastSelectedInterval.current
|
||||
: isCustomTimeRange(selectedTime)
|
||||
? DEFAULT_TIME_RANGE
|
||||
: selectedTime,
|
||||
);
|
||||
|
||||
const [selectedView, setSelectedView] = useInfraMonitoringView();
|
||||
const effectiveView = hideDetailViewTabs ? VIEW_TYPES.METRICS : selectedView;
|
||||
|
||||
const validTabs = useMemo(() => {
|
||||
const tabs: string[] = [];
|
||||
if (tabVisibility.showMetrics) {
|
||||
tabs.push(VIEW_TYPES.METRICS);
|
||||
}
|
||||
if (tabVisibility.showLogs) {
|
||||
tabs.push(VIEW_TYPES.LOGS);
|
||||
}
|
||||
if (tabVisibility.showTraces) {
|
||||
tabs.push(VIEW_TYPES.TRACES);
|
||||
}
|
||||
if (tabVisibility.showEvents) {
|
||||
tabs.push(VIEW_TYPES.EVENTS);
|
||||
}
|
||||
if (customTabs) {
|
||||
tabs.push(...customTabs.map((t) => t.key));
|
||||
}
|
||||
return tabs;
|
||||
}, [tabVisibility, customTabs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hideDetailViewTabs && !validTabs.includes(selectedView)) {
|
||||
const firstValid = validTabs[0] || VIEW_TYPES.METRICS;
|
||||
void setSelectedView(firstValid);
|
||||
}
|
||||
}, [hideDetailViewTabs, selectedView, validTabs, setSelectedView]);
|
||||
|
||||
const [, setLogFiltersParam] = useInfraMonitoringLogFilters();
|
||||
const [, setTracesFiltersParam] = useInfraMonitoringTracesFilters();
|
||||
const [, setEventsFiltersParam] = useInfraMonitoringEventsFilters();
|
||||
const [userLogsExpression] = useQueryState(
|
||||
K8S_ENTITY_LOGS_EXPRESSION_KEY,
|
||||
parseAsString,
|
||||
);
|
||||
const [userTracesExpression] = useQueryState(
|
||||
K8S_ENTITY_TRACES_EXPRESSION_KEY,
|
||||
parseAsString,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (entity) {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
void logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: eventCategory,
|
||||
@@ -324,136 +137,6 @@ export default function K8sBaseDetails<T>({
|
||||
}
|
||||
}, [entity, eventCategory]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
||||
if (!isCustomTimeRange(currentSelectedInterval)) {
|
||||
setSelectedInterval(currentSelectedInterval);
|
||||
const { minTime, maxTime } = getMinMaxTime();
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / TimeRangeOffset),
|
||||
endTime: Math.floor(maxTime / TimeRangeOffset),
|
||||
});
|
||||
}
|
||||
}, [getMinMaxTime, selectedTime]);
|
||||
|
||||
const handleTabChange = (value: string | null): void => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
setSelectedView(value);
|
||||
setLogFiltersParam(null);
|
||||
setTracesFiltersParam(null);
|
||||
setEventsFiltersParam(null);
|
||||
logEvent(InfraMonitoringEvents.TabChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: eventCategory,
|
||||
view: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
lastSelectedInterval.current = interval as Time;
|
||||
setSelectedInterval(interval as Time);
|
||||
|
||||
if (interval === 'custom' && dateTimeRange) {
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(dateTimeRange[0] / 1000),
|
||||
endTime: Math.floor(dateTimeRange[1] / 1000),
|
||||
});
|
||||
} else {
|
||||
const { maxTime, minTime } = GetMinMax(interval);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / TimeRangeOffset),
|
||||
endTime: Math.floor(maxTime / TimeRangeOffset),
|
||||
});
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.TimeUpdated, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: eventCategory,
|
||||
interval,
|
||||
view: effectiveView,
|
||||
});
|
||||
},
|
||||
[eventCategory, effectiveView],
|
||||
);
|
||||
|
||||
const handleExplorePagesRedirect = (): void => {
|
||||
const urlQuery = new URLSearchParams();
|
||||
|
||||
if (selectedInterval !== 'custom') {
|
||||
urlQuery.set(QueryParams.relativeTime, selectedInterval);
|
||||
} else {
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
urlQuery.set(QueryParams.startTime, modalTimeRange.startTime.toString());
|
||||
urlQuery.set(QueryParams.endTime, modalTimeRange.endTime.toString());
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.ExploreClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: eventCategory,
|
||||
view: selectedView,
|
||||
});
|
||||
|
||||
if (selectedView === VIEW_TYPES.LOGS) {
|
||||
const fullExpression = combineInitialAndUserExpression(
|
||||
logsAndTracesInitialExpression,
|
||||
userLogsExpression || '',
|
||||
);
|
||||
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
queryType: 'builder',
|
||||
builder: {
|
||||
...initialQueryState.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.logs,
|
||||
aggregateOperator: LogsAggregatorOperator.NOOP,
|
||||
expression: fullExpression,
|
||||
filter: { expression: fullExpression },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
|
||||
} else if (selectedView === VIEW_TYPES.TRACES) {
|
||||
const fullExpression = combineInitialAndUserExpression(
|
||||
logsAndTracesInitialExpression,
|
||||
userTracesExpression || '',
|
||||
);
|
||||
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
queryType: 'builder',
|
||||
builder: {
|
||||
...initialQueryState.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.traces,
|
||||
aggregateOperator: TracesAggregatorOperator.NOOP,
|
||||
expression: fullExpression,
|
||||
filter: { expression: fullExpression },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
width="70%"
|
||||
@@ -498,207 +181,33 @@ export default function K8sBaseDetails<T>({
|
||||
</div>
|
||||
)}
|
||||
{entity && !isEntityLoading && !hasResponseError && (
|
||||
<>
|
||||
<div className="entity-detail-drawer__entity">
|
||||
<div className="entity-details-grid">
|
||||
<div className="labels-row">
|
||||
{metadataConfig.map((config) => (
|
||||
<Typography.Text
|
||||
key={config.label}
|
||||
color="muted"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
{config.label}
|
||||
</Typography.Text>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="values-row">
|
||||
{metadataConfig.map((config) => {
|
||||
const value = config.getValue(entity);
|
||||
const displayValue = String(value);
|
||||
return (
|
||||
<Typography.Text
|
||||
key={config.label}
|
||||
className="entity-details-metadata-value"
|
||||
>
|
||||
{config.render ? (
|
||||
config.render(value, entity)
|
||||
) : (
|
||||
<Tooltip title={displayValue}>{displayValue}</Tooltip>
|
||||
)}
|
||||
</Typography.Text>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{countsConfig &&
|
||||
countsConfig.length > 0 &&
|
||||
selectedItem &&
|
||||
getCountsFilterExpression && (
|
||||
<EntityCountsSection
|
||||
entity={entity}
|
||||
countsConfig={countsConfig}
|
||||
selectedItem={selectedItem}
|
||||
filterExpression={getCountsFilterExpression(entity)}
|
||||
closeDrawer={handleClose}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!hideDetailViewTabs && (
|
||||
<div className="views-tabs-container">
|
||||
<ToggleGroupSimple
|
||||
type="single"
|
||||
className="views-tabs"
|
||||
onChange={handleTabChange}
|
||||
value={selectedView}
|
||||
items={[
|
||||
...(tabVisibility.showMetrics
|
||||
? [
|
||||
{
|
||||
value: VIEW_TYPES.METRICS,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<BarChart size={14} />
|
||||
Metrics
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(tabVisibility.showLogs
|
||||
? [
|
||||
{
|
||||
value: VIEW_TYPES.LOGS,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<ScrollText size={14} />
|
||||
Logs
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(tabVisibility.showTraces
|
||||
? [
|
||||
{
|
||||
value: VIEW_TYPES.TRACES,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<DraftingCompass size={14} />
|
||||
Traces
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(tabVisibility.showEvents
|
||||
? [
|
||||
{
|
||||
value: VIEW_TYPES.EVENTS,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<ChevronsLeftRight size={14} />
|
||||
Events
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(customTabs?.map((tab) => ({
|
||||
value: tab.key,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
{tab.icon}
|
||||
{tab.label}
|
||||
</div>
|
||||
),
|
||||
})) ?? []),
|
||||
]}
|
||||
/>
|
||||
|
||||
{selectedView === VIEW_TYPES.LOGS && (
|
||||
<Tooltip title="Go to Logs Explorer" placement="left">
|
||||
<Button
|
||||
icon={<Compass size={18} />}
|
||||
className="compass-button"
|
||||
onClick={handleExplorePagesRedirect}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.TRACES && (
|
||||
<Tooltip title="Go to Traces Explorer" placement="left">
|
||||
<Button
|
||||
icon={<Compass size={18} />}
|
||||
className="compass-button"
|
||||
onClick={handleExplorePagesRedirect}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{effectiveView === VIEW_TYPES.METRICS && (
|
||||
<EntityMetrics<T>
|
||||
entity={entity}
|
||||
selectedInterval={selectedInterval}
|
||||
timeRange={modalTimeRange}
|
||||
handleTimeChange={handleTimeChange}
|
||||
isModalTimeSelection
|
||||
entityWidgetInfo={entityWidgetInfo}
|
||||
getEntityQueryPayload={getEntityQueryPayload}
|
||||
category={category}
|
||||
queryKey={`${queryKeyPrefix}Metrics`}
|
||||
/>
|
||||
)}
|
||||
{effectiveView === VIEW_TYPES.LOGS && (
|
||||
<EntityLogs
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKey={`${queryKeyPrefix}Logs`}
|
||||
category={category}
|
||||
initialExpression={logsAndTracesInitialExpression}
|
||||
/>
|
||||
)}
|
||||
{effectiveView === VIEW_TYPES.TRACES && (
|
||||
<EntityTraces
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKey={`${queryKeyPrefix}Traces`}
|
||||
category={category}
|
||||
initialExpression={logsAndTracesInitialExpression}
|
||||
/>
|
||||
)}
|
||||
{effectiveView === VIEW_TYPES.EVENTS && tabVisibility.showEvents && (
|
||||
<EntityEvents
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
category={category}
|
||||
queryKey={`${queryKeyPrefix}Events`}
|
||||
initialExpression={eventsInitialExpression}
|
||||
/>
|
||||
)}
|
||||
{customTabs?.map((tab) =>
|
||||
selectedView === tab.key ? (
|
||||
<React.Fragment key={tab.key}>
|
||||
{tab.render({
|
||||
entity,
|
||||
timeRange: modalTimeRange,
|
||||
selectedInterval,
|
||||
handleTimeChange,
|
||||
})}
|
||||
</React.Fragment>
|
||||
) : null,
|
||||
)}
|
||||
</>
|
||||
<GlobalTimeProvider
|
||||
inheritGlobalTime
|
||||
enableUrlParams={{
|
||||
relativeTimeKey: INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_RELATIVE_TIME,
|
||||
startTimeKey: INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_START_TIME,
|
||||
endTimeKey: INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_END_TIME,
|
||||
}}
|
||||
>
|
||||
<K8sBaseDetailsContent<T>
|
||||
entity={entity}
|
||||
category={category}
|
||||
eventCategory={eventCategory}
|
||||
metadataConfig={metadataConfig}
|
||||
countsConfig={countsConfig}
|
||||
getCountsFilterExpression={getCountsFilterExpression}
|
||||
selectedItem={selectedItem}
|
||||
handleClose={handleClose}
|
||||
entityWidgetInfo={entityWidgetInfo}
|
||||
getEntityQueryPayload={getEntityQueryPayload}
|
||||
queryKeyPrefix={queryKeyPrefix}
|
||||
hideDetailViewTabs={hideDetailViewTabs}
|
||||
tabsConfig={tabsConfig}
|
||||
customTabs={customTabs}
|
||||
logsAndTracesInitialExpression={logsAndTracesInitialExpression}
|
||||
eventsInitialExpression={eventsInitialExpression}
|
||||
/>
|
||||
</GlobalTimeProvider>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,401 @@
|
||||
import { Fragment, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
BarChart,
|
||||
ChevronsLeftRight,
|
||||
Compass,
|
||||
DraftingCompass,
|
||||
ScrollText,
|
||||
} from '@signozhq/icons';
|
||||
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { combineInitialAndUserExpression } from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
initialQueryState,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import {
|
||||
LogsAggregatorOperator,
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { VIEW_TYPES } from '../constants';
|
||||
import { useEntityDetailsTime } from '../EntityDetailsUtils/EntityDateTimeSelector/useEntityDetailsTime';
|
||||
import EntityEvents from '../EntityDetailsUtils/EntityEvents';
|
||||
import EntityLogs from '../EntityDetailsUtils/EntityLogs';
|
||||
import { K8S_ENTITY_LOGS_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityLogs/hooks';
|
||||
import EntityMetrics from '../EntityDetailsUtils/EntityMetrics';
|
||||
import EntityTraces from '../EntityDetailsUtils/EntityTraces';
|
||||
import { K8S_ENTITY_TRACES_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityTraces/hooks';
|
||||
import {
|
||||
useInfraMonitoringEventsFilters,
|
||||
useInfraMonitoringLogFilters,
|
||||
useInfraMonitoringTracesFilters,
|
||||
useInfraMonitoringView,
|
||||
} from '../hooks';
|
||||
|
||||
import { EntityCountsSection } from './components/EntityCountsSection/EntityCountsSection';
|
||||
import { K8sBaseDetailsContentProps } from './types';
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export default function K8sBaseDetailsContent<T>({
|
||||
entity,
|
||||
category,
|
||||
eventCategory,
|
||||
metadataConfig,
|
||||
countsConfig,
|
||||
getCountsFilterExpression,
|
||||
selectedItem,
|
||||
handleClose,
|
||||
entityWidgetInfo,
|
||||
getEntityQueryPayload,
|
||||
queryKeyPrefix,
|
||||
hideDetailViewTabs,
|
||||
tabsConfig,
|
||||
customTabs,
|
||||
logsAndTracesInitialExpression,
|
||||
eventsInitialExpression,
|
||||
}: K8sBaseDetailsContentProps<T>): JSX.Element {
|
||||
const { timeRange, selectedInterval, handleTimeChange } =
|
||||
useEntityDetailsTime();
|
||||
|
||||
const tabVisibility = useMemo(
|
||||
() => ({
|
||||
showMetrics: true,
|
||||
showLogs: true,
|
||||
showTraces: true,
|
||||
showEvents: true,
|
||||
...tabsConfig,
|
||||
}),
|
||||
[tabsConfig],
|
||||
);
|
||||
|
||||
const [selectedView, setSelectedView] = useInfraMonitoringView();
|
||||
|
||||
const validTabs = useMemo(() => {
|
||||
const tabs: string[] = [];
|
||||
if (tabVisibility.showMetrics) {
|
||||
tabs.push(VIEW_TYPES.METRICS);
|
||||
}
|
||||
if (tabVisibility.showLogs) {
|
||||
tabs.push(VIEW_TYPES.LOGS);
|
||||
}
|
||||
if (tabVisibility.showTraces) {
|
||||
tabs.push(VIEW_TYPES.TRACES);
|
||||
}
|
||||
if (tabVisibility.showEvents) {
|
||||
tabs.push(VIEW_TYPES.EVENTS);
|
||||
}
|
||||
if (customTabs) {
|
||||
tabs.push(...customTabs.map((t) => t.key));
|
||||
}
|
||||
return tabs;
|
||||
}, [tabVisibility, customTabs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hideDetailViewTabs && !validTabs.includes(selectedView)) {
|
||||
const firstValid = validTabs[0] || VIEW_TYPES.METRICS;
|
||||
void setSelectedView(firstValid);
|
||||
}
|
||||
}, [hideDetailViewTabs, selectedView, validTabs, setSelectedView]);
|
||||
|
||||
const effectiveView = hideDetailViewTabs ? VIEW_TYPES.METRICS : selectedView;
|
||||
|
||||
const [, setLogFiltersParam] = useInfraMonitoringLogFilters();
|
||||
const [, setTracesFiltersParam] = useInfraMonitoringTracesFilters();
|
||||
const [, setEventsFiltersParam] = useInfraMonitoringEventsFilters();
|
||||
const [userLogsExpression] = useQueryState(
|
||||
K8S_ENTITY_LOGS_EXPRESSION_KEY,
|
||||
parseAsString,
|
||||
);
|
||||
const [userTracesExpression] = useQueryState(
|
||||
K8S_ENTITY_TRACES_EXPRESSION_KEY,
|
||||
parseAsString,
|
||||
);
|
||||
|
||||
const handleTabChange = (value: string | null): void => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
void setSelectedView(value);
|
||||
void setLogFiltersParam(null);
|
||||
void setTracesFiltersParam(null);
|
||||
void setEventsFiltersParam(null);
|
||||
void logEvent(InfraMonitoringEvents.TabChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: eventCategory,
|
||||
view: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleExplorePagesRedirect = (): void => {
|
||||
const urlQuery = new URLSearchParams();
|
||||
|
||||
if (selectedInterval !== 'custom') {
|
||||
urlQuery.set(QueryParams.relativeTime, selectedInterval);
|
||||
} else {
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
// Explorer URL params are in milliseconds, timeRange is in seconds
|
||||
urlQuery.set(QueryParams.startTime, (timeRange.startTime * 1000).toString());
|
||||
urlQuery.set(QueryParams.endTime, (timeRange.endTime * 1000).toString());
|
||||
}
|
||||
|
||||
void logEvent(InfraMonitoringEvents.ExploreClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: eventCategory,
|
||||
view: selectedView,
|
||||
});
|
||||
|
||||
if (selectedView === VIEW_TYPES.LOGS) {
|
||||
const fullExpression = combineInitialAndUserExpression(
|
||||
logsAndTracesInitialExpression,
|
||||
userLogsExpression || '',
|
||||
);
|
||||
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
queryType: 'builder',
|
||||
builder: {
|
||||
...initialQueryState.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.logs,
|
||||
aggregateOperator: LogsAggregatorOperator.NOOP,
|
||||
expression: fullExpression,
|
||||
filter: { expression: fullExpression },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
|
||||
} else if (selectedView === VIEW_TYPES.TRACES) {
|
||||
const fullExpression = combineInitialAndUserExpression(
|
||||
logsAndTracesInitialExpression,
|
||||
userTracesExpression || '',
|
||||
);
|
||||
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
queryType: 'builder',
|
||||
builder: {
|
||||
...initialQueryState.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.traces,
|
||||
aggregateOperator: TracesAggregatorOperator.NOOP,
|
||||
expression: fullExpression,
|
||||
filter: { expression: fullExpression },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="entity-detail-drawer__entity">
|
||||
<div className="entity-details-grid">
|
||||
<div className="labels-row">
|
||||
{metadataConfig.map((config) => (
|
||||
<Typography.Text
|
||||
key={config.label}
|
||||
color="muted"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
{config.label}
|
||||
</Typography.Text>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="values-row">
|
||||
{metadataConfig.map((config) => {
|
||||
const value = config.getValue(entity);
|
||||
const displayValue = String(value);
|
||||
return (
|
||||
<Typography.Text
|
||||
key={config.label}
|
||||
className="entity-details-metadata-value"
|
||||
>
|
||||
{config.render ? (
|
||||
config.render(value, entity)
|
||||
) : (
|
||||
<Tooltip title={displayValue}>{displayValue}</Tooltip>
|
||||
)}
|
||||
</Typography.Text>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{countsConfig &&
|
||||
countsConfig.length > 0 &&
|
||||
selectedItem &&
|
||||
getCountsFilterExpression && (
|
||||
<EntityCountsSection
|
||||
entity={entity}
|
||||
countsConfig={countsConfig}
|
||||
selectedItem={selectedItem}
|
||||
filterExpression={getCountsFilterExpression(entity)}
|
||||
closeDrawer={handleClose}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!hideDetailViewTabs && (
|
||||
<div className="views-tabs-container">
|
||||
<ToggleGroupSimple
|
||||
type="single"
|
||||
className="views-tabs"
|
||||
onChange={handleTabChange}
|
||||
value={selectedView}
|
||||
items={[
|
||||
...(tabVisibility.showMetrics
|
||||
? [
|
||||
{
|
||||
value: VIEW_TYPES.METRICS,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<BarChart size={14} />
|
||||
Metrics
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(tabVisibility.showLogs
|
||||
? [
|
||||
{
|
||||
value: VIEW_TYPES.LOGS,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<ScrollText size={14} />
|
||||
Logs
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(tabVisibility.showTraces
|
||||
? [
|
||||
{
|
||||
value: VIEW_TYPES.TRACES,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<DraftingCompass size={14} />
|
||||
Traces
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(tabVisibility.showEvents
|
||||
? [
|
||||
{
|
||||
value: VIEW_TYPES.EVENTS,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<ChevronsLeftRight size={14} />
|
||||
Events
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(customTabs?.map((tab) => ({
|
||||
value: tab.key,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
{tab.icon}
|
||||
{tab.label}
|
||||
</div>
|
||||
),
|
||||
})) ?? []),
|
||||
]}
|
||||
/>
|
||||
|
||||
{selectedView === VIEW_TYPES.LOGS && (
|
||||
<Tooltip title="Go to Logs Explorer" placement="left">
|
||||
<Button
|
||||
icon={<Compass size={18} />}
|
||||
className="compass-button"
|
||||
onClick={handleExplorePagesRedirect}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.TRACES && (
|
||||
<Tooltip title="Go to Traces Explorer" placement="left">
|
||||
<Button
|
||||
icon={<Compass size={18} />}
|
||||
className="compass-button"
|
||||
onClick={handleExplorePagesRedirect}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{effectiveView === VIEW_TYPES.METRICS && (
|
||||
<EntityMetrics<T>
|
||||
entity={entity}
|
||||
eventEntity={eventCategory}
|
||||
entityWidgetInfo={entityWidgetInfo}
|
||||
getEntityQueryPayload={getEntityQueryPayload}
|
||||
category={category}
|
||||
queryKey={`${queryKeyPrefix}Metrics`}
|
||||
/>
|
||||
)}
|
||||
{effectiveView === VIEW_TYPES.LOGS && (
|
||||
<EntityLogs
|
||||
eventEntity={eventCategory}
|
||||
queryKey={`${queryKeyPrefix}Logs`}
|
||||
category={category}
|
||||
initialExpression={logsAndTracesInitialExpression}
|
||||
/>
|
||||
)}
|
||||
{effectiveView === VIEW_TYPES.TRACES && (
|
||||
<EntityTraces
|
||||
eventEntity={eventCategory}
|
||||
queryKey={`${queryKeyPrefix}Traces`}
|
||||
category={category}
|
||||
initialExpression={logsAndTracesInitialExpression}
|
||||
/>
|
||||
)}
|
||||
{effectiveView === VIEW_TYPES.EVENTS && tabVisibility.showEvents && (
|
||||
<EntityEvents
|
||||
eventEntity={eventCategory}
|
||||
queryKey={`${queryKeyPrefix}Events`}
|
||||
initialExpression={eventsInitialExpression}
|
||||
category={category}
|
||||
/>
|
||||
)}
|
||||
{customTabs?.map((tab) =>
|
||||
selectedView === tab.key ? (
|
||||
<Fragment key={tab.key}>
|
||||
{tab.render({
|
||||
entity,
|
||||
timeRange,
|
||||
selectedInterval,
|
||||
handleTimeChange,
|
||||
})}
|
||||
</Fragment>
|
||||
) : null,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -245,6 +245,7 @@ function K8sHeader<TData>({
|
||||
showAutoRefresh={showAutoRefresh}
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
defaultRelativeTime="30m"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -52,7 +52,6 @@ export function EntityCountsSection<T>({
|
||||
},
|
||||
};
|
||||
|
||||
// TODO(H4ad): After https://github.com/SigNoz/signoz/pull/12038, inherit custom time of drawer to list
|
||||
const urlParams = new URLSearchParams();
|
||||
urlParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.CATEGORY, targetCategory);
|
||||
urlParams.set(
|
||||
@@ -60,6 +59,44 @@ export function EntityCountsSection<T>({
|
||||
encodeURIComponent(JSON.stringify(compositeQuery)),
|
||||
);
|
||||
|
||||
const currentSearchParams = new URLSearchParams(window.location.search);
|
||||
const detailRelativeTime = currentSearchParams.get(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_RELATIVE_TIME,
|
||||
);
|
||||
const detailStartTime = currentSearchParams.get(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_START_TIME,
|
||||
);
|
||||
const detailEndTime = currentSearchParams.get(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_END_TIME,
|
||||
);
|
||||
|
||||
const listRelativeTime = currentSearchParams.get(QueryParams.relativeTime);
|
||||
const listStartTime = currentSearchParams.get(QueryParams.startTime);
|
||||
const listEndTime = currentSearchParams.get(QueryParams.endTime);
|
||||
|
||||
if (listRelativeTime) {
|
||||
urlParams.set(QueryParams.relativeTime, listRelativeTime);
|
||||
} else if (listStartTime && listEndTime) {
|
||||
urlParams.set(QueryParams.startTime, listStartTime);
|
||||
urlParams.set(QueryParams.endTime, listEndTime);
|
||||
}
|
||||
|
||||
if (detailRelativeTime) {
|
||||
urlParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_RELATIVE_TIME,
|
||||
detailRelativeTime,
|
||||
);
|
||||
} else if (detailStartTime && detailEndTime) {
|
||||
urlParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_START_TIME,
|
||||
detailStartTime,
|
||||
);
|
||||
urlParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_END_TIME,
|
||||
detailEndTime,
|
||||
);
|
||||
}
|
||||
|
||||
return `${ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES}?${urlParams.toString()}`;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
import { ReactNode } from 'react';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import { EntityCountConfig } from './components/EntityCountsSection/EntityCountsSection';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { SelectedItemParams } from '../hooks';
|
||||
|
||||
export type K8sDetailsCountConfig<T> = EntityCountConfig<T>;
|
||||
|
||||
export type K8sBaseFilters = {
|
||||
filter: {
|
||||
expression: string;
|
||||
@@ -33,3 +47,100 @@ export type K8sTableRowData<T> = T & {
|
||||
/** Metadata about which attributes were used for grouping */
|
||||
groupedByMeta?: Record<string, string>;
|
||||
};
|
||||
|
||||
export interface K8sDetailsMetadataConfig<T> {
|
||||
label: string;
|
||||
getValue: (entity: T) => string | number;
|
||||
render?: (value: string | number, entity: T) => ReactNode;
|
||||
}
|
||||
|
||||
export interface K8sDetailsFilters {
|
||||
filter: { expression: string };
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export interface K8sDetailsWidgetInfo {
|
||||
title: string;
|
||||
yAxisUnit: string;
|
||||
}
|
||||
|
||||
export type GetEntityQueryPayload<T> = (
|
||||
entity: T,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
) => GetQueryResultsProps[];
|
||||
|
||||
export interface K8sDetailsTabsConfig {
|
||||
showMetrics?: boolean;
|
||||
showLogs?: boolean;
|
||||
showTraces?: boolean;
|
||||
showEvents?: boolean;
|
||||
}
|
||||
|
||||
export interface K8sDetailsCustomTabRenderProps<T> {
|
||||
entity: T;
|
||||
/** Time range in seconds — see useEntityDetailsTime */
|
||||
timeRange: { startTime: number; endTime: number };
|
||||
selectedInterval: Time;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface K8sDetailsCustomTab<T> {
|
||||
key: string;
|
||||
label: string;
|
||||
icon: ReactNode;
|
||||
render: (props: K8sDetailsCustomTabRenderProps<T>) => ReactNode;
|
||||
}
|
||||
|
||||
export interface K8sBaseDetailsProps<T> {
|
||||
category: InfraMonitoringEntity;
|
||||
eventCategory: string;
|
||||
// Data fetching configuration
|
||||
getSelectedItemExpression: (params: SelectedItemParams) => string;
|
||||
fetchEntityData: (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<{ data: T | null; error?: APIError | null }>;
|
||||
// Entity configuration
|
||||
getEntityName: (entity: T) => string;
|
||||
getInitialLogTracesExpression: (entity: T) => string;
|
||||
getInitialEventsExpression: (entity: T) => string;
|
||||
metadataConfig: K8sDetailsMetadataConfig<T>[];
|
||||
countsConfig?: K8sDetailsCountConfig<T>[];
|
||||
getCountsFilterExpression?: (entity: T) => string;
|
||||
entityWidgetInfo: K8sDetailsWidgetInfo[];
|
||||
getEntityQueryPayload: GetEntityQueryPayload<T>;
|
||||
queryKeyPrefix: string;
|
||||
/** When true, only metrics are shown and the Metrics/Logs/Traces/Events tab bar is hidden. */
|
||||
hideDetailViewTabs?: boolean;
|
||||
tabsConfig?: K8sDetailsTabsConfig;
|
||||
customTabs?: K8sDetailsCustomTab<T>[];
|
||||
}
|
||||
|
||||
export interface K8sBaseDetailsContentProps<T> {
|
||||
entity: T;
|
||||
category: InfraMonitoringEntity;
|
||||
eventCategory: string;
|
||||
metadataConfig: K8sDetailsMetadataConfig<T>[];
|
||||
countsConfig?: K8sDetailsCountConfig<T>[];
|
||||
getCountsFilterExpression?: (entity: T) => string;
|
||||
selectedItem: string | null;
|
||||
handleClose: () => void;
|
||||
entityWidgetInfo: K8sDetailsWidgetInfo[];
|
||||
getEntityQueryPayload: GetEntityQueryPayload<T>;
|
||||
queryKeyPrefix: string;
|
||||
hideDetailViewTabs: boolean;
|
||||
tabsConfig?: K8sDetailsTabsConfig;
|
||||
customTabs?: K8sDetailsCustomTab<T>[];
|
||||
logsAndTracesInitialExpression: string;
|
||||
eventsInitialExpression: string;
|
||||
}
|
||||
|
||||
// Aliases for backward compatibility
|
||||
export type CustomTab<T> = K8sDetailsCustomTab<T>;
|
||||
export type CustomTabRenderProps<T> = K8sDetailsCustomTabRenderProps<T>;
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useCallback } from 'react';
|
||||
import { Undo } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
|
||||
import { useEntityDetailsTime } from './useEntityDetailsTime';
|
||||
|
||||
interface EntityDateTimeSelectorProps {
|
||||
eventEntity: string;
|
||||
category: InfraMonitoringEntity;
|
||||
view: string;
|
||||
}
|
||||
|
||||
function EntityDateTimeSelector({
|
||||
eventEntity,
|
||||
category,
|
||||
view,
|
||||
}: EntityDateTimeSelectorProps): JSX.Element {
|
||||
const {
|
||||
timeRange,
|
||||
selectedInterval,
|
||||
handleTimeChange,
|
||||
handleResetToParentTime,
|
||||
hasTimeChanged,
|
||||
} = useEntityDetailsTime();
|
||||
|
||||
const onTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
void logEvent(InfraMonitoringEvents.TimeUpdated, {
|
||||
entity: eventEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category,
|
||||
view,
|
||||
interval,
|
||||
});
|
||||
handleTimeChange(interval, dateTimeRange);
|
||||
},
|
||||
[category, view, eventEntity, handleTimeChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="entity-date-time-selector">
|
||||
{hasTimeChanged && (
|
||||
<TooltipSimple title="Reset to list time" side="bottom">
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={handleResetToParentTime}
|
||||
data-testid="reset-to-list-time-button"
|
||||
prefix={<Undo size={14} />}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
)}
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
isModalTimeSelection
|
||||
onTimeChange={onTimeChange}
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EntityDateTimeSelector;
|
||||
@@ -0,0 +1,95 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import {
|
||||
createCustomTimeRange,
|
||||
isCustomTimeRange,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
useGlobalTime,
|
||||
} from 'store/globalTime';
|
||||
|
||||
export interface EntityDetailsTimeRange {
|
||||
/** Unix timestamp in seconds — the unit API payload builders expect. Multiply by 1000 for ms-based UI (modals, URL params). */
|
||||
startTime: number;
|
||||
/** Unix timestamp in seconds — the unit API payload builders expect. Multiply by 1000 for ms-based UI (modals, URL params). */
|
||||
endTime: number;
|
||||
}
|
||||
|
||||
export interface UseEntityDetailsTimeResult {
|
||||
/** Time range in seconds */
|
||||
timeRange: EntityDetailsTimeRange;
|
||||
selectedInterval: Time;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
handleResetToParentTime: () => boolean;
|
||||
canResetToParent: boolean;
|
||||
hasTimeChanged: boolean;
|
||||
}
|
||||
|
||||
export function useEntityDetailsTime(): UseEntityDetailsTimeResult {
|
||||
const selectedTime = useGlobalTime((s) => s.selectedTime);
|
||||
const lastComputedMinMax = useGlobalTime((s) => s.lastComputedMinMax);
|
||||
const setSelectedTime = useGlobalTime((s) => s.setSelectedTime);
|
||||
const handleResetToParentTime = useGlobalTime((s) => s.resetToParentTime);
|
||||
const parentStore = useGlobalTime((s) => s.parentStore);
|
||||
|
||||
const canResetToParent = !!parentStore;
|
||||
|
||||
const hasTimeChanged = useMemo(() => {
|
||||
if (!parentStore) {
|
||||
return false;
|
||||
}
|
||||
return selectedTime !== parentStore.getState().selectedTime;
|
||||
}, [parentStore, selectedTime]);
|
||||
|
||||
const timeRange = useMemo<EntityDetailsTimeRange>(
|
||||
() => ({
|
||||
startTime: Math.floor(
|
||||
lastComputedMinMax.minTime / NANO_SECOND_MULTIPLIER / 1000,
|
||||
),
|
||||
endTime: Math.floor(
|
||||
lastComputedMinMax.maxTime / NANO_SECOND_MULTIPLIER / 1000,
|
||||
),
|
||||
}),
|
||||
[lastComputedMinMax],
|
||||
);
|
||||
|
||||
const selectedInterval = useMemo<Time>(
|
||||
() =>
|
||||
isCustomTimeRange(selectedTime)
|
||||
? ('custom' as Time)
|
||||
: (selectedTime as Time),
|
||||
[selectedTime],
|
||||
);
|
||||
|
||||
const handleTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
if (interval === 'custom' && dateTimeRange) {
|
||||
// DateTimeSelector delivers milliseconds; the store keys custom
|
||||
// ranges by nanoseconds.
|
||||
setSelectedTime(
|
||||
createCustomTimeRange(
|
||||
dateTimeRange[0] * NANO_SECOND_MULTIPLIER,
|
||||
dateTimeRange[1] * NANO_SECOND_MULTIPLIER,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setSelectedTime(interval as Time);
|
||||
}
|
||||
},
|
||||
[setSelectedTime],
|
||||
);
|
||||
|
||||
return {
|
||||
timeRange,
|
||||
selectedInterval,
|
||||
handleTimeChange,
|
||||
handleResetToParentTime,
|
||||
canResetToParent,
|
||||
hasTimeChanged,
|
||||
};
|
||||
}
|
||||
@@ -21,17 +21,14 @@ import Controls from 'container/Controls';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import LoadingContainer from 'container/InfraMonitoringK8sV2/LoadingContainer';
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { ChevronDown, ChevronRight } from '@signozhq/icons';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
|
||||
import { validateQuery } from 'utils/queryValidationUtils';
|
||||
|
||||
import EntityDateTimeSelector from '../EntityDateTimeSelector/EntityDateTimeSelector';
|
||||
import { useEntityDetailsTime } from '../EntityDateTimeSelector/useEntityDetailsTime';
|
||||
import EntityEmptyState from '../EntityEmptyState/EntityEmptyState';
|
||||
import EntityError from '../EntityError/EntityError';
|
||||
import { EventContents } from './EventsContent';
|
||||
@@ -53,16 +50,7 @@ interface EventDataType {
|
||||
}
|
||||
|
||||
interface Props {
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
isModalTimeSelection: boolean;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
selectedInterval: Time;
|
||||
eventEntity: string;
|
||||
queryKey: string;
|
||||
category: InfraMonitoringEntity;
|
||||
initialExpression: string;
|
||||
@@ -77,13 +65,11 @@ const handleExpandRow = (record: EventDataType): JSX.Element => (
|
||||
);
|
||||
|
||||
function EntityEventsContent({
|
||||
timeRange,
|
||||
isModalTimeSelection,
|
||||
handleTimeChange,
|
||||
selectedInterval,
|
||||
eventEntity,
|
||||
queryKey,
|
||||
category,
|
||||
}: Omit<Props, 'initialExpression'>): JSX.Element {
|
||||
const { timeRange } = useEntityDetailsTime();
|
||||
const expression = useExpression();
|
||||
const inputExpression = useInputExpression();
|
||||
const userExpression = useUserExpression();
|
||||
@@ -131,8 +117,8 @@ function EntityEventsContent({
|
||||
if (validation.isValid) {
|
||||
querySearchOnRun(newUserExpression || '');
|
||||
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
void logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: eventEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category,
|
||||
view: InfraMonitoringEvents.EventsView,
|
||||
@@ -141,7 +127,14 @@ function EntityEventsContent({
|
||||
refetch();
|
||||
}
|
||||
},
|
||||
[inputExpression, initialExpression, refetch, querySearchOnRun, category],
|
||||
[
|
||||
inputExpression,
|
||||
initialExpression,
|
||||
refetch,
|
||||
querySearchOnRun,
|
||||
category,
|
||||
eventEntity,
|
||||
],
|
||||
);
|
||||
|
||||
const queryData = useMemo(
|
||||
@@ -229,16 +222,10 @@ function EntityEventsContent({
|
||||
<div className={styles.container}>
|
||||
<div className={styles.filterContainer}>
|
||||
<div className={styles.filterContainerTime}>
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
onTimeChange={handleTimeChange}
|
||||
defaultRelativeTime="5m"
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
<EntityDateTimeSelector
|
||||
eventEntity={eventEntity}
|
||||
category={category}
|
||||
view={InfraMonitoringEvents.EventsView}
|
||||
/>
|
||||
|
||||
<RunQueryBtn
|
||||
|
||||
@@ -29,25 +29,25 @@ function verifyEntityEventsV5Request({
|
||||
expect(orderKeys).toContain('timestamp');
|
||||
}
|
||||
|
||||
jest.mock('container/TopNav/DateTimeSelectionV2/index.tsx', () => ({
|
||||
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
onTimeChange,
|
||||
}: {
|
||||
onTimeChange?: (interval: string, dateTimeRange?: [number, number]) => void;
|
||||
}): JSX.Element => (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="mock-datetime-selection"
|
||||
onClick={(): void => {
|
||||
onTimeChange?.('5m');
|
||||
}}
|
||||
>
|
||||
Select Time
|
||||
</button>
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="mock-datetime-selection">Date Time</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/useEntityDetailsTime', () => ({
|
||||
useEntityDetailsTime: (): {
|
||||
timeRange: { startTime: number; endTime: number };
|
||||
selectedInterval: string;
|
||||
handleTimeChange: jest.Mock;
|
||||
} => ({
|
||||
timeRange: { startTime: 1, endTime: 2 },
|
||||
selectedInterval: '5m',
|
||||
handleTimeChange: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('EntityEvents', () => {
|
||||
let capturedQueryRangePayloads: QueryRangePayloadV5[] = [];
|
||||
|
||||
@@ -69,10 +69,7 @@ describe('EntityEvents', () => {
|
||||
searchParams={`${K8S_ENTITY_EVENTS_EXPRESSION_KEY}=k8s.pod.name+%3D+%22x%22`}
|
||||
>
|
||||
<EntityEvents
|
||||
timeRange={{ startTime: 1, endTime: 2 }}
|
||||
isModalTimeSelection={false}
|
||||
handleTimeChange={jest.fn()}
|
||||
selectedInterval="5m"
|
||||
eventEntity="test"
|
||||
queryKey="test"
|
||||
category={InfraMonitoringEntity.PODS}
|
||||
initialExpression='k8s.pod.name = "x"'
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { mockFieldsAPIsWithEmptyResponse } from '__tests__/fields_api.util';
|
||||
import { mockQueryRangeV5WithEventsResponse } from '__tests__/query_range_v5.util';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import {
|
||||
createCustomTimeRange,
|
||||
createGlobalTimeStore,
|
||||
GlobalTimeContext,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
} from 'store/globalTime';
|
||||
import { act, render, waitFor } from 'tests/test-utils';
|
||||
import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
|
||||
|
||||
import EntityEvents from '../EntityEvents';
|
||||
import { K8S_ENTITY_EVENTS_EXPRESSION_KEY } from '../hooks';
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="mock-datetime-selection">Date Time</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const START_MS = 1705315200000; // 2024-01-15T10:40:00Z
|
||||
const END_MS = 1705318800000; // 2024-01-15T11:40:00Z
|
||||
|
||||
describe('EntityEvents time range wiring', () => {
|
||||
let capturedPayloads: QueryRangePayloadV5[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
capturedPayloads = [];
|
||||
mockQueryRangeV5WithEventsResponse({
|
||||
onReceiveRequest: async (req) => {
|
||||
const body = (await req.json()) as QueryRangePayloadV5;
|
||||
capturedPayloads.push(body);
|
||||
return {};
|
||||
},
|
||||
});
|
||||
mockFieldsAPIsWithEmptyResponse();
|
||||
});
|
||||
|
||||
it('should query the V5 API with the global time range converted to milliseconds', async () => {
|
||||
const store = createGlobalTimeStore();
|
||||
store
|
||||
.getState()
|
||||
.setSelectedTime(
|
||||
createCustomTimeRange(
|
||||
START_MS * NANO_SECOND_MULTIPLIER,
|
||||
END_MS * NANO_SECOND_MULTIPLIER,
|
||||
),
|
||||
);
|
||||
|
||||
const expression = 'k8s.pod.name = "test-pod"';
|
||||
|
||||
act(() => {
|
||||
render(
|
||||
<GlobalTimeContext.Provider value={store}>
|
||||
<NuqsTestingAdapter
|
||||
searchParams={`${K8S_ENTITY_EVENTS_EXPRESSION_KEY}=${encodeURIComponent(
|
||||
expression,
|
||||
)}`}
|
||||
>
|
||||
<EntityEvents
|
||||
eventEntity="test"
|
||||
queryKey="test"
|
||||
category={InfraMonitoringEntity.PODS}
|
||||
initialExpression={expression}
|
||||
/>
|
||||
</NuqsTestingAdapter>
|
||||
</GlobalTimeContext.Provider>,
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedPayloads).toHaveLength(1);
|
||||
});
|
||||
|
||||
expect(capturedPayloads[0].start).toBe(START_MS);
|
||||
expect(capturedPayloads[0].end).toBe(END_MS);
|
||||
});
|
||||
});
|
||||
@@ -25,11 +25,6 @@ import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants'
|
||||
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { getOldLogsOperatorFromNew } from 'hooks/logs/useActiveLog';
|
||||
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
|
||||
import useScrollToLog from 'hooks/logs/useScrollToLog';
|
||||
@@ -38,6 +33,8 @@ import { ILog } from 'types/api/logs/log';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { validateQuery } from 'utils/queryValidationUtils';
|
||||
|
||||
import EntityDateTimeSelector from '../EntityDateTimeSelector/EntityDateTimeSelector';
|
||||
import { useEntityDetailsTime } from '../EntityDateTimeSelector/useEntityDetailsTime';
|
||||
import EntityEmptyState from '../EntityEmptyState/EntityEmptyState';
|
||||
import EntityError from '../EntityError/EntityError';
|
||||
import { isKeyNotFoundError } from '../utils';
|
||||
@@ -47,29 +44,18 @@ import { getEntityLogsQueryPayload } from './utils';
|
||||
import styles from './EntityLogs.module.scss';
|
||||
|
||||
interface Props {
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
isModalTimeSelection: boolean;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
selectedInterval: Time;
|
||||
eventEntity: string;
|
||||
queryKey: string;
|
||||
category: InfraMonitoringEntity;
|
||||
initialExpression: string;
|
||||
}
|
||||
|
||||
function EntityLogsContent({
|
||||
timeRange,
|
||||
isModalTimeSelection,
|
||||
handleTimeChange,
|
||||
selectedInterval,
|
||||
eventEntity,
|
||||
queryKey,
|
||||
category,
|
||||
}: Omit<Props, 'initialExpression'>): JSX.Element {
|
||||
const { timeRange } = useEntityDetailsTime();
|
||||
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
||||
|
||||
const expression = useExpression();
|
||||
@@ -134,7 +120,7 @@ function EntityLogsContent({
|
||||
if (validation.isValid) {
|
||||
querySearchOnRun(newUserExpression);
|
||||
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
void logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category,
|
||||
@@ -239,16 +225,10 @@ function EntityLogsContent({
|
||||
<div className={styles.container}>
|
||||
<div className={styles.filterContainer}>
|
||||
<div className={styles.filterContainerTime}>
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
onTimeChange={handleTimeChange}
|
||||
defaultRelativeTime="5m"
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
<EntityDateTimeSelector
|
||||
eventEntity={eventEntity}
|
||||
category={category}
|
||||
view={InfraMonitoringEvents.LogsView}
|
||||
/>
|
||||
|
||||
<RunQueryBtn
|
||||
|
||||
@@ -50,25 +50,25 @@ jest.mock(
|
||||
},
|
||||
);
|
||||
|
||||
jest.mock('container/TopNav/DateTimeSelectionV2/index.tsx', () => ({
|
||||
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
onTimeChange,
|
||||
}: {
|
||||
onTimeChange?: (interval: string, dateTimeRange?: [number, number]) => void;
|
||||
}): JSX.Element => (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="mock-datetime-selection"
|
||||
onClick={(): void => {
|
||||
onTimeChange?.('5m');
|
||||
}}
|
||||
>
|
||||
Select Time
|
||||
</button>
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="mock-datetime-selection">Date Time</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/useEntityDetailsTime', () => ({
|
||||
useEntityDetailsTime: (): {
|
||||
timeRange: { startTime: number; endTime: number };
|
||||
selectedInterval: string;
|
||||
handleTimeChange: jest.Mock;
|
||||
} => ({
|
||||
timeRange: { startTime: 1, endTime: 2 },
|
||||
selectedInterval: '5m',
|
||||
handleTimeChange: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('EntityLogs', () => {
|
||||
let capturedQueryRangePayloads: QueryRangePayloadV5[] = [];
|
||||
const itemHeight = 100;
|
||||
@@ -94,10 +94,7 @@ describe('EntityLogs', () => {
|
||||
>
|
||||
<VirtuosoMockContext.Provider value={{ viewportHeight: 500, itemHeight }}>
|
||||
<EntityLogs
|
||||
timeRange={{ startTime: 1, endTime: 2 }}
|
||||
isModalTimeSelection={false}
|
||||
handleTimeChange={jest.fn()}
|
||||
selectedInterval="5m"
|
||||
eventEntity="test"
|
||||
queryKey="test"
|
||||
category={InfraMonitoringEntity.PODS}
|
||||
initialExpression='k8s.pod.name = "x"'
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { mockFieldsAPIsWithEmptyResponse } from '__tests__/fields_api.util';
|
||||
import { mockQueryRangeV5WithLogsResponse } from '__tests__/query_range_v5.util';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import {
|
||||
createCustomTimeRange,
|
||||
createGlobalTimeStore,
|
||||
GlobalTimeContext,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
} from 'store/globalTime';
|
||||
import { act, render, waitFor } from 'tests/test-utils';
|
||||
import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
|
||||
|
||||
import EntityLogs from '../EntityLogs';
|
||||
import { K8S_ENTITY_LOGS_EXPRESSION_KEY } from '../hooks';
|
||||
|
||||
jest.mock(
|
||||
'components/OverlayScrollbar/OverlayScrollbar',
|
||||
() =>
|
||||
function MockOverlayScrollbar({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
return <div>{children}</div>;
|
||||
},
|
||||
);
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="mock-datetime-selection">Date Time</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const START_MS = 1705315200000; // 2024-01-15T10:40:00Z
|
||||
const END_MS = 1705318800000; // 2024-01-15T11:40:00Z
|
||||
|
||||
describe('EntityLogs time range wiring', () => {
|
||||
let capturedPayloads: QueryRangePayloadV5[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
capturedPayloads = [];
|
||||
mockQueryRangeV5WithLogsResponse({
|
||||
onReceiveRequest: async (req) => {
|
||||
const body = (await req.json()) as QueryRangePayloadV5;
|
||||
capturedPayloads.push(body);
|
||||
return {};
|
||||
},
|
||||
});
|
||||
mockFieldsAPIsWithEmptyResponse();
|
||||
});
|
||||
|
||||
it('should query the V5 API with the global time range converted to milliseconds', async () => {
|
||||
const store = createGlobalTimeStore();
|
||||
store
|
||||
.getState()
|
||||
.setSelectedTime(
|
||||
createCustomTimeRange(
|
||||
START_MS * NANO_SECOND_MULTIPLIER,
|
||||
END_MS * NANO_SECOND_MULTIPLIER,
|
||||
),
|
||||
);
|
||||
|
||||
const expression = 'k8s.pod.name = "test-pod"';
|
||||
|
||||
act(() => {
|
||||
render(
|
||||
<GlobalTimeContext.Provider value={store}>
|
||||
<NuqsTestingAdapter
|
||||
searchParams={`${K8S_ENTITY_LOGS_EXPRESSION_KEY}=${encodeURIComponent(
|
||||
expression,
|
||||
)}`}
|
||||
>
|
||||
<EntityLogs
|
||||
eventEntity="test"
|
||||
queryKey="test"
|
||||
category={InfraMonitoringEntity.PODS}
|
||||
initialExpression={expression}
|
||||
/>
|
||||
</NuqsTestingAdapter>
|
||||
</GlobalTimeContext.Provider>,
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedPayloads.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
expect(capturedPayloads[0].start).toBe(START_MS);
|
||||
expect(capturedPayloads[0].end).toBe(END_MS);
|
||||
});
|
||||
});
|
||||
@@ -2,15 +2,11 @@ import { useCallback, useMemo, useRef } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { Skeleton } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import TimeSeries from 'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
@@ -24,25 +20,17 @@ import { getMetricsExplorerUrl } from 'utils/explorerUtils';
|
||||
import { buildEntityMetricsChartConfig } from './configBuilder';
|
||||
import ChartHeader from './ChartHeader';
|
||||
|
||||
import EntityDateTimeSelector from '../EntityDateTimeSelector/EntityDateTimeSelector';
|
||||
import { useEntityDetailsTime } from '../EntityDateTimeSelector/useEntityDetailsTime';
|
||||
import { useEntityMetrics } from './hooks';
|
||||
import { isKeyNotFoundError } from '../utils';
|
||||
|
||||
import styles from './EntityMetrics.module.scss';
|
||||
import { MetricsTable } from './MetricsTable';
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
|
||||
interface EntityMetricsProps<T> {
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
isModalTimeSelection: boolean;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
selectedInterval: Time;
|
||||
entity: T;
|
||||
eventEntity: string;
|
||||
entityWidgetInfo: {
|
||||
title: string;
|
||||
yAxisUnit: string;
|
||||
@@ -59,16 +47,16 @@ interface EntityMetricsProps<T> {
|
||||
}
|
||||
|
||||
function EntityMetrics<T>({
|
||||
selectedInterval,
|
||||
entity,
|
||||
timeRange,
|
||||
handleTimeChange,
|
||||
isModalTimeSelection,
|
||||
eventEntity,
|
||||
entityWidgetInfo,
|
||||
getEntityQueryPayload,
|
||||
queryKey,
|
||||
category,
|
||||
}: EntityMetricsProps<T>): JSX.Element {
|
||||
const { timeRange, selectedInterval, handleTimeChange } =
|
||||
useEntityDetailsTime();
|
||||
|
||||
const { visibilities, setElement } = useMultiIntersectionObserver(
|
||||
entityWidgetInfo.length,
|
||||
{ threshold: 0.1 },
|
||||
@@ -91,10 +79,8 @@ function EntityMetrics<T>({
|
||||
|
||||
const onDragSelect = useCallback(
|
||||
(start: number, end: number): void => {
|
||||
const startTimestamp = Math.trunc(start);
|
||||
const endTimestamp = Math.trunc(end);
|
||||
|
||||
handleTimeChange('custom', [startTimestamp, endTimestamp]);
|
||||
// UPlotConfigBuilder's setSelect hook already delivers milliseconds
|
||||
handleTimeChange('custom', [Math.trunc(start), Math.trunc(end)]);
|
||||
},
|
||||
[handleTimeChange],
|
||||
);
|
||||
@@ -188,16 +174,10 @@ function EntityMetrics<T>({
|
||||
return (
|
||||
<>
|
||||
<div className={styles.metricsHeader}>
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
onTimeChange={handleTimeChange}
|
||||
defaultRelativeTime={DEFAULT_TIME_RANGE}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
<EntityDateTimeSelector
|
||||
eventEntity={eventEntity}
|
||||
category={category}
|
||||
view={InfraMonitoringEvents.MetricsView}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.entityMetricsContainer}>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import * as appContextHooks from 'providers/App/App';
|
||||
import { LicenseEvent } from 'types/api/licensesV3/getActive';
|
||||
import uPlot from 'uplot';
|
||||
@@ -28,13 +27,28 @@ jest.mock('lib/uPlotV2/utils/dataUtils', () => ({
|
||||
hasSingleVisiblePoint: jest.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
||||
jest.mock('container/TopNav/DateTimeSelectionV2', () => ({
|
||||
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="date-time-selection">Date Time</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const mockTimeRange = { startTime: 1705315200, endTime: 1705318800 };
|
||||
let mockSelectedInterval = '5m';
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/useEntityDetailsTime', () => ({
|
||||
useEntityDetailsTime: (): {
|
||||
timeRange: { startTime: number; endTime: number };
|
||||
selectedInterval: string;
|
||||
handleTimeChange: jest.Mock;
|
||||
} => ({
|
||||
timeRange: mockTimeRange,
|
||||
selectedInterval: mockSelectedInterval,
|
||||
handleTimeChange: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries',
|
||||
() => ({
|
||||
@@ -144,13 +158,6 @@ const mockGetEntityQueryPayload = jest.fn().mockReturnValue([
|
||||
},
|
||||
]);
|
||||
|
||||
const mockTimeRange = {
|
||||
startTime: 1705315200,
|
||||
endTime: 1705318800,
|
||||
};
|
||||
|
||||
const mockHandleTimeChange = jest.fn();
|
||||
|
||||
const mockQueries = [
|
||||
{
|
||||
data: {
|
||||
@@ -283,10 +290,6 @@ const mockEmptyQueries = [
|
||||
|
||||
const renderEntityMetrics = (overrides = {}): any => {
|
||||
const defaultProps = {
|
||||
timeRange: mockTimeRange,
|
||||
isModalTimeSelection: false,
|
||||
handleTimeChange: mockHandleTimeChange,
|
||||
selectedInterval: '5m' as Time,
|
||||
entity: mockEntity,
|
||||
entityWidgetInfo: mockEntityWidgetInfo,
|
||||
getEntityQueryPayload: mockGetEntityQueryPayload,
|
||||
@@ -298,11 +301,8 @@ const renderEntityMetrics = (overrides = {}): any => {
|
||||
return render(
|
||||
<MemoryRouter>
|
||||
<EntityMetrics
|
||||
timeRange={defaultProps.timeRange}
|
||||
isModalTimeSelection={defaultProps.isModalTimeSelection}
|
||||
handleTimeChange={defaultProps.handleTimeChange}
|
||||
selectedInterval={defaultProps.selectedInterval}
|
||||
entity={defaultProps.entity}
|
||||
eventEntity="test"
|
||||
entityWidgetInfo={defaultProps.entityWidgetInfo}
|
||||
getEntityQueryPayload={defaultProps.getEntityQueryPayload}
|
||||
queryKey={defaultProps.queryKey}
|
||||
@@ -344,6 +344,7 @@ const mockQueryPayloads = [
|
||||
describe('EntityMetrics', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockSelectedInterval = '5m';
|
||||
mockUseEntityMetrics.mockReturnValue({
|
||||
queries: mockQueries as any,
|
||||
chartData: mockChartData,
|
||||
@@ -454,7 +455,8 @@ describe('EntityMetrics', () => {
|
||||
});
|
||||
|
||||
it('builds metrics explorer link with relativeTime when a relative interval is selected', () => {
|
||||
renderEntityMetrics({ selectedInterval: '5m' as Time });
|
||||
mockSelectedInterval = '5m';
|
||||
renderEntityMetrics();
|
||||
const href = screen
|
||||
.getByTestId('open-metrics-explorer-0')
|
||||
.getAttribute('href');
|
||||
@@ -464,7 +466,8 @@ describe('EntityMetrics', () => {
|
||||
});
|
||||
|
||||
it('builds metrics explorer link with absolute time range in milliseconds for custom interval', () => {
|
||||
renderEntityMetrics({ selectedInterval: 'custom' as Time });
|
||||
mockSelectedInterval = 'custom';
|
||||
renderEntityMetrics();
|
||||
const href = screen
|
||||
.getByTestId('open-metrics-explorer-0')
|
||||
.getAttribute('href');
|
||||
@@ -478,7 +481,6 @@ describe('EntityMetrics', () => {
|
||||
expect(mockUseEntityMetrics).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
queryKey: 'test-query-key',
|
||||
timeRange: mockTimeRange,
|
||||
entity: mockEntity,
|
||||
category: InfraMonitoringEntity.PODS,
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import {
|
||||
createCustomTimeRange,
|
||||
createGlobalTimeStore,
|
||||
GlobalTimeContext,
|
||||
GlobalTimeStoreApi,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
} from 'store/globalTime';
|
||||
import { act, render } from 'tests/test-utils';
|
||||
|
||||
import { buildEntityMetricsChartConfig } from '../configBuilder';
|
||||
import EntityMetrics from '../EntityMetrics';
|
||||
|
||||
jest.mock('../configBuilder', () => ({
|
||||
buildEntityMetricsChartConfig: jest.fn().mockReturnValue({
|
||||
getId: jest.fn().mockReturnValue('mock-id'),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="date-time-selection">Date Time</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const mockBuildChartConfig = buildEntityMetricsChartConfig as jest.Mock;
|
||||
|
||||
const START_MS = 1705315200000; // 2024-01-15T10:40:00Z
|
||||
const END_MS = 1705318800000; // 2024-01-15T11:40:00Z
|
||||
const START_SECONDS = 1705315200;
|
||||
const END_SECONDS = 1705318800;
|
||||
|
||||
const entity = { id: 'test-entity-1' };
|
||||
|
||||
// The jsdom IntersectionObserver polyfill never reports visibility, so the
|
||||
// queries stay disabled and no network request is issued.
|
||||
const queryPayload = {
|
||||
graphType: PANEL_TYPES.TIME_SERIES,
|
||||
} as unknown as GetQueryResultsProps;
|
||||
|
||||
function createStoreWithCustomRange(): GlobalTimeStoreApi {
|
||||
const store = createGlobalTimeStore();
|
||||
store
|
||||
.getState()
|
||||
.setSelectedTime(
|
||||
createCustomTimeRange(
|
||||
START_MS * NANO_SECOND_MULTIPLIER,
|
||||
END_MS * NANO_SECOND_MULTIPLIER,
|
||||
),
|
||||
);
|
||||
return store;
|
||||
}
|
||||
|
||||
function renderEntityMetrics(store: GlobalTimeStoreApi): jest.Mock {
|
||||
const getEntityQueryPayload = jest.fn().mockReturnValue([queryPayload]);
|
||||
render(
|
||||
<GlobalTimeContext.Provider value={store}>
|
||||
<EntityMetrics
|
||||
entity={entity}
|
||||
eventEntity="test"
|
||||
entityWidgetInfo={[{ title: 'CPU Usage', yAxisUnit: 'percentage' }]}
|
||||
getEntityQueryPayload={getEntityQueryPayload}
|
||||
queryKey="test-query-key"
|
||||
category={InfraMonitoringEntity.PODS}
|
||||
/>
|
||||
</GlobalTimeContext.Provider>,
|
||||
);
|
||||
return getEntityQueryPayload;
|
||||
}
|
||||
|
||||
describe('EntityMetrics time range wiring', () => {
|
||||
beforeEach(() => {
|
||||
mockBuildChartConfig.mockClear();
|
||||
});
|
||||
|
||||
it('should build the metrics query payloads and chart config from the global time range in seconds', () => {
|
||||
const store = createStoreWithCustomRange();
|
||||
|
||||
const getEntityQueryPayload = renderEntityMetrics(store);
|
||||
|
||||
expect(getEntityQueryPayload).toHaveBeenCalledWith(
|
||||
entity,
|
||||
START_SECONDS,
|
||||
END_SECONDS,
|
||||
false,
|
||||
);
|
||||
expect(mockBuildChartConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
minTimeScale: START_SECONDS,
|
||||
maxTimeScale: END_SECONDS,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should store a drag selection (received in milliseconds) as the equivalent custom range', () => {
|
||||
const store = createStoreWithCustomRange();
|
||||
|
||||
renderEntityMetrics(store);
|
||||
|
||||
const { onDragSelect } = mockBuildChartConfig.mock.calls[0][0];
|
||||
|
||||
// UPlotConfigBuilder's setSelect hook calls onDragSelect with milliseconds
|
||||
const dragStartMs = 1705316000000;
|
||||
const dragEndMs = 1705317000000;
|
||||
act(() => {
|
||||
onDragSelect(dragStartMs, dragEndMs);
|
||||
});
|
||||
|
||||
expect(store.getState().selectedTime).toBe(
|
||||
createCustomTimeRange(
|
||||
dragStartMs * NANO_SECOND_MULTIPLIER,
|
||||
dragEndMs * NANO_SECOND_MULTIPLIER,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -20,11 +20,6 @@ import { InfraMonitoringEvents } from 'constants/events';
|
||||
import Controls from 'container/Controls';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
|
||||
import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading';
|
||||
import { useQueryState } from 'nuqs';
|
||||
@@ -32,10 +27,11 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
|
||||
import { validateQuery } from 'utils/queryValidationUtils';
|
||||
|
||||
import EntityDateTimeSelector from '../EntityDateTimeSelector/EntityDateTimeSelector';
|
||||
import { useEntityDetailsTime } from '../EntityDateTimeSelector/useEntityDetailsTime';
|
||||
import EntityEmptyState from '../EntityEmptyState/EntityEmptyState';
|
||||
import EntityError from '../EntityError/EntityError';
|
||||
import { selectedEntityTracesColumns } from '../utils';
|
||||
import { isKeyNotFoundError } from '../utils';
|
||||
import { isKeyNotFoundError, selectedEntityTracesColumns } from '../utils';
|
||||
import { K8S_ENTITY_TRACES_EXPRESSION_KEY, useEntityTraces } from './hooks';
|
||||
import { getTraceListColumns } from './traceListColumns';
|
||||
import { getEntityTracesQueryPayload } from './utils';
|
||||
@@ -44,29 +40,18 @@ import styles from './EntityTraces.module.scss';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
|
||||
interface Props {
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
isModalTimeSelection: boolean;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
selectedInterval: Time;
|
||||
eventEntity: string;
|
||||
queryKey: string;
|
||||
category: InfraMonitoringEntity;
|
||||
initialExpression: string;
|
||||
}
|
||||
|
||||
function EntityTracesContent({
|
||||
timeRange,
|
||||
isModalTimeSelection,
|
||||
handleTimeChange,
|
||||
selectedInterval,
|
||||
eventEntity,
|
||||
queryKey,
|
||||
category,
|
||||
}: Omit<Props, 'initialExpression'>): JSX.Element {
|
||||
const { timeRange } = useEntityDetailsTime();
|
||||
const expression = useExpression();
|
||||
const inputExpression = useInputExpression();
|
||||
const userExpression = useUserExpression();
|
||||
@@ -114,8 +99,8 @@ function EntityTracesContent({
|
||||
if (validation.isValid) {
|
||||
querySearchOnRun(newUserExpression || '');
|
||||
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
void logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: eventEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category,
|
||||
view: InfraMonitoringEvents.TracesView,
|
||||
@@ -124,7 +109,14 @@ function EntityTracesContent({
|
||||
refetch();
|
||||
}
|
||||
},
|
||||
[inputExpression, initialExpression, refetch, querySearchOnRun, category],
|
||||
[
|
||||
inputExpression,
|
||||
initialExpression,
|
||||
refetch,
|
||||
querySearchOnRun,
|
||||
category,
|
||||
eventEntity,
|
||||
],
|
||||
);
|
||||
|
||||
const queryData = useMemo(
|
||||
@@ -152,7 +144,7 @@ function EntityTracesContent({
|
||||
const hasAdditionalFilters = !!userExpression?.trim();
|
||||
|
||||
const handleRowClick = useCallback(() => {
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
void logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
category,
|
||||
view: InfraMonitoringEvents.TracesView,
|
||||
@@ -169,16 +161,10 @@ function EntityTracesContent({
|
||||
<div className={styles.container}>
|
||||
<div className={styles.filterContainer}>
|
||||
<div className={styles.filterContainerTime}>
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
onTimeChange={handleTimeChange}
|
||||
defaultRelativeTime="5m"
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
<EntityDateTimeSelector
|
||||
eventEntity={eventEntity}
|
||||
category={category}
|
||||
view={InfraMonitoringEvents.TracesView}
|
||||
/>
|
||||
|
||||
<RunQueryBtn
|
||||
|
||||
@@ -34,10 +34,8 @@ describe('EntityTraces - Default Behavior', () => {
|
||||
});
|
||||
|
||||
it('should pass time range to API (converted to milliseconds)', async () => {
|
||||
const timeRange = { startTime: 1000, endTime: 2000 };
|
||||
|
||||
act(() => {
|
||||
renderEntityTraces({ timeRange });
|
||||
renderEntityTraces();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -47,8 +45,8 @@ describe('EntityTraces - Default Behavior', () => {
|
||||
verifyQueryPayload({
|
||||
payload: capturedPayloads[0],
|
||||
expectedTimeRange: {
|
||||
start: timeRange.startTime * 1000,
|
||||
end: timeRange.endTime * 1000,
|
||||
start: 1 * 1000,
|
||||
end: 2 * 1000,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { mockFieldsAPIsWithEmptyResponse } from '__tests__/fields_api.util';
|
||||
import { mockQueryRangeV5WithTracesResponse } from '__tests__/query_range_v5.util';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import {
|
||||
createCustomTimeRange,
|
||||
createGlobalTimeStore,
|
||||
GlobalTimeContext,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
} from 'store/globalTime';
|
||||
import { act, render, waitFor } from 'tests/test-utils';
|
||||
import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
|
||||
|
||||
import EntityTraces from '../EntityTraces';
|
||||
import { K8S_ENTITY_TRACES_EXPRESSION_KEY } from '../hooks';
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="mock-datetime-selection">Date Time</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const START_MS = 1705315200000; // 2024-01-15T10:40:00Z
|
||||
const END_MS = 1705318800000; // 2024-01-15T11:40:00Z
|
||||
|
||||
describe('EntityTraces time range wiring', () => {
|
||||
let capturedPayloads: QueryRangePayloadV5[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
capturedPayloads = [];
|
||||
mockQueryRangeV5WithTracesResponse({
|
||||
onReceiveRequest: async (req) => {
|
||||
const body = (await req.json()) as QueryRangePayloadV5;
|
||||
capturedPayloads.push(body);
|
||||
return {};
|
||||
},
|
||||
});
|
||||
mockFieldsAPIsWithEmptyResponse();
|
||||
});
|
||||
|
||||
it('should query the V5 API with the global time range converted to milliseconds', async () => {
|
||||
const store = createGlobalTimeStore();
|
||||
store
|
||||
.getState()
|
||||
.setSelectedTime(
|
||||
createCustomTimeRange(
|
||||
START_MS * NANO_SECOND_MULTIPLIER,
|
||||
END_MS * NANO_SECOND_MULTIPLIER,
|
||||
),
|
||||
);
|
||||
|
||||
const expression = 'k8s.pod.name = "test-pod"';
|
||||
|
||||
act(() => {
|
||||
render(
|
||||
<GlobalTimeContext.Provider value={store}>
|
||||
<NuqsTestingAdapter
|
||||
searchParams={`${K8S_ENTITY_TRACES_EXPRESSION_KEY}=${encodeURIComponent(
|
||||
expression,
|
||||
)}`}
|
||||
>
|
||||
<EntityTraces
|
||||
eventEntity="test"
|
||||
queryKey="test"
|
||||
category={InfraMonitoringEntity.PODS}
|
||||
initialExpression={expression}
|
||||
/>
|
||||
</NuqsTestingAdapter>
|
||||
</GlobalTimeContext.Provider>,
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedPayloads).toHaveLength(1);
|
||||
});
|
||||
|
||||
expect(capturedPayloads[0].start).toBe(START_MS);
|
||||
expect(capturedPayloads[0].end).toBe(END_MS);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,5 @@
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { mockFieldsAPIsWithEmptyResponse } from '__tests__/fields_api.util';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { render, RenderResult } from 'tests/test-utils';
|
||||
import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
|
||||
@@ -9,39 +7,19 @@ import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
|
||||
import EntityTraces from '../EntityTraces';
|
||||
import { K8S_ENTITY_TRACES_EXPRESSION_KEY } from '../hooks';
|
||||
|
||||
// QuerySearch fires autocomplete requests on mount; without handlers MSW
|
||||
// passes them through to the real network and the resulting AxiosError fails
|
||||
// whichever test happens to be running.
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/keys`, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ status: 'success', data: { keys: {}, complete: true } }),
|
||||
),
|
||||
),
|
||||
rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/values`, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ status: 'success', data: { values: {}, complete: true } }),
|
||||
),
|
||||
),
|
||||
);
|
||||
mockFieldsAPIsWithEmptyResponse();
|
||||
});
|
||||
|
||||
export interface RenderEntityTracesOptions {
|
||||
expression?: string;
|
||||
timeRange?: { startTime: number; endTime: number };
|
||||
category?: InfraMonitoringEntity;
|
||||
selectedInterval?: '5m' | '15m' | '30m' | '1h';
|
||||
pagination?: { offset: number; limit: number };
|
||||
}
|
||||
|
||||
export function renderEntityTraces({
|
||||
expression = 'k8s.pod.name = "test-pod"',
|
||||
timeRange = { startTime: 1, endTime: 2 },
|
||||
category = InfraMonitoringEntity.PODS,
|
||||
selectedInterval = '5m',
|
||||
pagination,
|
||||
}: RenderEntityTracesOptions = {}): RenderResult {
|
||||
const encodedExpression = encodeURIComponent(expression);
|
||||
@@ -55,10 +33,7 @@ export function renderEntityTraces({
|
||||
return render(
|
||||
<NuqsTestingAdapter searchParams={searchParams}>
|
||||
<EntityTraces
|
||||
timeRange={timeRange}
|
||||
isModalTimeSelection={false}
|
||||
handleTimeChange={jest.fn()}
|
||||
selectedInterval={selectedInterval}
|
||||
eventEntity="test"
|
||||
queryKey="test"
|
||||
category={category}
|
||||
initialExpression={expression}
|
||||
@@ -101,21 +76,21 @@ export function verifyQueryPayload({
|
||||
expect(orderKeys).toContain('timestamp');
|
||||
}
|
||||
|
||||
jest.mock('container/TopNav/DateTimeSelectionV2/index.tsx', () => ({
|
||||
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
onTimeChange,
|
||||
}: {
|
||||
onTimeChange?: (interval: string, dateTimeRange?: [number, number]) => void;
|
||||
}): JSX.Element => (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="mock-datetime-selection"
|
||||
onClick={(): void => {
|
||||
onTimeChange?.('5m');
|
||||
}}
|
||||
>
|
||||
Select Time
|
||||
</button>
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="mock-datetime-selection">Date Time</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/useEntityDetailsTime', () => ({
|
||||
useEntityDetailsTime: (): {
|
||||
timeRange: { startTime: number; endTime: number };
|
||||
selectedInterval: string;
|
||||
handleTimeChange: jest.Mock;
|
||||
} => ({
|
||||
timeRange: { startTime: 1, endTime: 2 },
|
||||
selectedInterval: '5m',
|
||||
handleTimeChange: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Container } from '@signozhq/icons';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
|
||||
import { CustomTab } from '../Base/K8sBaseDetails';
|
||||
@@ -10,6 +11,20 @@ import {
|
||||
|
||||
import EntityMetrics from './EntityMetrics';
|
||||
|
||||
const categoryToEventEntity: Record<InfraMonitoringEntity, string> = {
|
||||
[InfraMonitoringEntity.DAEMONSETS]: InfraMonitoringEvents.DaemonSet,
|
||||
[InfraMonitoringEntity.DEPLOYMENTS]: InfraMonitoringEvents.Deployment,
|
||||
[InfraMonitoringEntity.JOBS]: InfraMonitoringEvents.Job,
|
||||
[InfraMonitoringEntity.NAMESPACES]: InfraMonitoringEvents.Namespace,
|
||||
[InfraMonitoringEntity.STATEFULSETS]: InfraMonitoringEvents.StatefulSet,
|
||||
[InfraMonitoringEntity.PODS]: InfraMonitoringEvents.Pod,
|
||||
[InfraMonitoringEntity.NODES]: InfraMonitoringEvents.Node,
|
||||
[InfraMonitoringEntity.CLUSTERS]: InfraMonitoringEvents.Cluster,
|
||||
[InfraMonitoringEntity.VOLUMES]: InfraMonitoringEvents.Volume,
|
||||
[InfraMonitoringEntity.HOSTS]: InfraMonitoringEvents.HostEntity,
|
||||
[InfraMonitoringEntity.CONTAINERS]: 'container',
|
||||
};
|
||||
|
||||
interface CreatePodMetricsTabParams<T> {
|
||||
getQueryPayload: (
|
||||
entity: T,
|
||||
@@ -17,30 +32,29 @@ interface CreatePodMetricsTabParams<T> {
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
) => GetQueryResultsProps[];
|
||||
category: InfraMonitoringEntity;
|
||||
queryKey: string;
|
||||
category: InfraMonitoringEntity;
|
||||
}
|
||||
|
||||
export function createPodMetricsTab<T>({
|
||||
getQueryPayload,
|
||||
category,
|
||||
queryKey,
|
||||
category,
|
||||
}: CreatePodMetricsTabParams<T>): CustomTab<T> {
|
||||
const eventEntity = categoryToEventEntity[category];
|
||||
|
||||
return {
|
||||
key: VIEW_TYPES.POD_METRICS,
|
||||
label: 'Pod Metrics',
|
||||
icon: <Container size={14} />,
|
||||
render: ({ entity, timeRange, selectedInterval, handleTimeChange }) => (
|
||||
render: ({ entity }) => (
|
||||
<EntityMetrics
|
||||
entity={entity}
|
||||
selectedInterval={selectedInterval}
|
||||
timeRange={timeRange}
|
||||
handleTimeChange={handleTimeChange}
|
||||
isModalTimeSelection
|
||||
eventEntity={eventEntity}
|
||||
entityWidgetInfo={podUtilizationByPodWidgetInfo}
|
||||
getEntityQueryPayload={getQueryPayload}
|
||||
category={category}
|
||||
queryKey={queryKey}
|
||||
category={category}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -454,3 +454,9 @@
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.entity-date-time-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@@ -905,6 +905,9 @@ export const INFRA_MONITORING_K8S_PARAMS_KEYS = {
|
||||
SELECTED_ITEM: 'selectedItem',
|
||||
SELECTED_ITEM_CLUSTER_NAME: 'selectedItemClusterName',
|
||||
SELECTED_ITEM_NAMESPACE_NAME: 'selectedItemNamespaceName',
|
||||
DETAIL_RELATIVE_TIME: 'detailRelativeTime',
|
||||
DETAIL_START_TIME: 'detailStartTime',
|
||||
DETAIL_END_TIME: 'detailEndTime',
|
||||
};
|
||||
|
||||
/** Metric namespace prefixes for /fields/keys and /fields/values APIs */
|
||||
|
||||
@@ -7,6 +7,7 @@ import AppRoutes from 'AppRoutes';
|
||||
import { AxiosError } from 'axios';
|
||||
import { GlobalTimeStoreAdapter } from 'components/GlobalTimeStoreAdapter/GlobalTimeStoreAdapter';
|
||||
import { ThemeProvider } from 'hooks/useDarkMode';
|
||||
import { configureOverlayScrollbars } from 'lib/configureOverlayScrollbars';
|
||||
import { NuqsAdapter } from 'nuqs/adapters/react';
|
||||
import { AppProvider } from 'providers/App/App';
|
||||
import TimezoneProvider from 'providers/Timezone';
|
||||
@@ -17,6 +18,8 @@ import './ReactI18';
|
||||
|
||||
import 'styles.scss';
|
||||
|
||||
configureOverlayScrollbars();
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
|
||||
22
frontend/src/lib/configureOverlayScrollbars.ts
Normal file
22
frontend/src/lib/configureOverlayScrollbars.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { OverlayScrollbars } from 'overlayscrollbars';
|
||||
|
||||
/**
|
||||
* Disables the content `elementEvents` option (default: `[['img', 'load']]`)
|
||||
* for every OverlayScrollbars instance.
|
||||
*
|
||||
* The per-element event cleanups OverlayScrollbars stores in its internal
|
||||
* WeakMap close over the MutationObserver callback scope, which holds arrays
|
||||
* with every node added/removed in that mutation batch. A single long-lived
|
||||
* reference to one of those elements (e.g. CodeMirror's EditContext or its
|
||||
* module-level scratch Range pinning a detached editor) then retains entire
|
||||
* unmounted subtrees as detached DOM — ~1.3k nodes per InfraMonitoring
|
||||
* category switch.
|
||||
*
|
||||
* Content size changes from loading images are still handled by the
|
||||
* scrollbars' size observer, so scrollbar geometry stays correct.
|
||||
*/
|
||||
export function configureOverlayScrollbars(): void {
|
||||
OverlayScrollbars.env().setDefaultOptions({
|
||||
update: { elementEvents: null },
|
||||
});
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import type {
|
||||
Querybuildertypesv5QueryWarnDataDTO as WarningDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type { AxiosError } from 'axios';
|
||||
import type { Querybuildertypesv5QueryWarnDataDTO as WarningDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { StatusCodes } from 'http-status-codes';
|
||||
|
||||
import { panelStatusFromError, panelStatusFromWarning } from '../utils';
|
||||
@@ -46,7 +44,7 @@ describe('panelStatusFromError', () => {
|
||||
|
||||
it('falls back to the error message when there is no structured body', () => {
|
||||
expect(panelStatusFromError(new Error('boom'))).toStrictEqual({
|
||||
code: 'UPSTREAM_UNAVAILABLE',
|
||||
code: 'unknown_error',
|
||||
message: 'boom',
|
||||
docsUrl: undefined,
|
||||
messages: [],
|
||||
|
||||
@@ -55,6 +55,7 @@ export function GlobalTimeProvider({
|
||||
name,
|
||||
selectedTime: resolveInitialTime(),
|
||||
refreshInterval: initialRefreshInterval ?? 0,
|
||||
parentStore: inheritGlobalTime ? globalStore : undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -38,6 +38,36 @@ const createWrapper = (
|
||||
};
|
||||
};
|
||||
|
||||
const createNestedWrapper = (
|
||||
parentProps: GlobalTimeProviderOptions,
|
||||
childProps: GlobalTimeProviderOptions,
|
||||
nuqsProps?: {
|
||||
searchParams?: string;
|
||||
onUrlUpdate?: (event: { queryString: string }) => void;
|
||||
},
|
||||
) => {
|
||||
const queryClient = createTestQueryClient();
|
||||
|
||||
return function NestedWrapper({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NuqsTestingAdapter
|
||||
searchParams={nuqsProps?.searchParams}
|
||||
onUrlUpdate={nuqsProps?.onUrlUpdate}
|
||||
>
|
||||
<GlobalTimeProvider {...parentProps}>
|
||||
<GlobalTimeProvider {...childProps}>{children}</GlobalTimeProvider>
|
||||
</GlobalTimeProvider>
|
||||
</NuqsTestingAdapter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
describe('GlobalTimeProvider', () => {
|
||||
describe('name prop', () => {
|
||||
it('should pass name to store when provided', () => {
|
||||
@@ -82,138 +112,73 @@ describe('GlobalTimeProvider', () => {
|
||||
|
||||
describe('inheritGlobalTime', () => {
|
||||
it('should inherit time from parent store when inheritGlobalTime is true', () => {
|
||||
const queryClient = createTestQueryClient();
|
||||
|
||||
const NestedWrapper = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NuqsTestingAdapter>
|
||||
<GlobalTimeProvider initialTime="6h">
|
||||
<GlobalTimeProvider inheritGlobalTime>{children}</GlobalTimeProvider>
|
||||
</GlobalTimeProvider>
|
||||
</NuqsTestingAdapter>
|
||||
</QueryClientProvider>
|
||||
const wrapper = createNestedWrapper(
|
||||
{ initialTime: '6h' },
|
||||
{ inheritGlobalTime: true },
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper: NestedWrapper,
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// Should inherit '6h' from parent provider
|
||||
expect(result.current).toBe('6h');
|
||||
});
|
||||
|
||||
it('should use initialTime when inheritGlobalTime is false', () => {
|
||||
const queryClient = createTestQueryClient();
|
||||
|
||||
const NestedWrapper = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NuqsTestingAdapter>
|
||||
<GlobalTimeProvider initialTime="6h">
|
||||
<GlobalTimeProvider inheritGlobalTime={false} initialTime="15m">
|
||||
{children}
|
||||
</GlobalTimeProvider>
|
||||
</GlobalTimeProvider>
|
||||
</NuqsTestingAdapter>
|
||||
</QueryClientProvider>
|
||||
const wrapper = createNestedWrapper(
|
||||
{ initialTime: '6h' },
|
||||
{ inheritGlobalTime: false, initialTime: '15m' },
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper: NestedWrapper,
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// Should use its own initialTime, not parent's
|
||||
expect(result.current).toBe('15m');
|
||||
});
|
||||
|
||||
it('should prefer URL params over inheritGlobalTime when both are present', async () => {
|
||||
const queryClient = createTestQueryClient();
|
||||
|
||||
const NestedWrapper = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NuqsTestingAdapter searchParams="?relativeTime=1h">
|
||||
<GlobalTimeProvider initialTime="6h">
|
||||
<GlobalTimeProvider inheritGlobalTime enableUrlParams>
|
||||
{children}
|
||||
</GlobalTimeProvider>
|
||||
</GlobalTimeProvider>
|
||||
</NuqsTestingAdapter>
|
||||
</QueryClientProvider>
|
||||
const wrapper = createNestedWrapper(
|
||||
{ initialTime: '6h' },
|
||||
{ inheritGlobalTime: true, enableUrlParams: true },
|
||||
{ searchParams: '?relativeTime=1h' },
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper: NestedWrapper,
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// inheritGlobalTime sets initial value to '6h', but URL sync updates it to '1h'
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBe('1h');
|
||||
});
|
||||
});
|
||||
|
||||
it('should use inherited time when URL params are empty', async () => {
|
||||
const queryClient = createTestQueryClient();
|
||||
|
||||
const NestedWrapper = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NuqsTestingAdapter searchParams="">
|
||||
<GlobalTimeProvider initialTime="6h">
|
||||
<GlobalTimeProvider inheritGlobalTime enableUrlParams>
|
||||
{children}
|
||||
</GlobalTimeProvider>
|
||||
</GlobalTimeProvider>
|
||||
</NuqsTestingAdapter>
|
||||
</QueryClientProvider>
|
||||
const wrapper = createNestedWrapper(
|
||||
{ initialTime: '6h' },
|
||||
{ inheritGlobalTime: true, enableUrlParams: true },
|
||||
{ searchParams: '' },
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper: NestedWrapper,
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// No URL params, should keep inherited value
|
||||
expect(result.current).toBe('6h');
|
||||
});
|
||||
|
||||
it('should prefer custom time URL params over inheritGlobalTime', async () => {
|
||||
const queryClient = createTestQueryClient();
|
||||
const startTime = 1700000000000;
|
||||
const endTime = 1700003600000;
|
||||
|
||||
const NestedWrapper = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NuqsTestingAdapter
|
||||
searchParams={`?startTime=${startTime}&endTime=${endTime}`}
|
||||
>
|
||||
<GlobalTimeProvider initialTime="6h">
|
||||
<GlobalTimeProvider inheritGlobalTime enableUrlParams>
|
||||
{children}
|
||||
</GlobalTimeProvider>
|
||||
</GlobalTimeProvider>
|
||||
</NuqsTestingAdapter>
|
||||
</QueryClientProvider>
|
||||
const wrapper = createNestedWrapper(
|
||||
{ initialTime: '6h' },
|
||||
{ inheritGlobalTime: true, enableUrlParams: true },
|
||||
{ searchParams: `?startTime=${startTime}&endTime=${endTime}` },
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime(), {
|
||||
wrapper: NestedWrapper,
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// URL custom time params should override inherited time
|
||||
@@ -690,4 +655,151 @@ describe('GlobalTimeProvider', () => {
|
||||
expect(result.current.isRefreshEnabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetToParentTime', () => {
|
||||
it('should return false when no parent store exists', () => {
|
||||
const wrapper = createWrapper({ initialTime: '1h' });
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
expect(result.current.resetToParentTime()).toBe(false);
|
||||
expect(result.current.parentStore).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should have parentStore when inheritGlobalTime is true', () => {
|
||||
const wrapper = createNestedWrapper(
|
||||
{ initialTime: '6h' },
|
||||
{ inheritGlobalTime: true },
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
expect(result.current.parentStore).toBeDefined();
|
||||
});
|
||||
|
||||
it('should reset to parent time when called', () => {
|
||||
const wrapper = createNestedWrapper(
|
||||
{ initialTime: '6h' },
|
||||
{ inheritGlobalTime: true },
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
expect(result.current.selectedTime).toBe('6h');
|
||||
const initialMinMax = result.current.lastComputedMinMax;
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m');
|
||||
});
|
||||
|
||||
expect(result.current.selectedTime).toBe('15m');
|
||||
const changedMinMax = result.current.lastComputedMinMax;
|
||||
expect(changedMinMax.maxTime - changedMinMax.minTime).toBeLessThan(
|
||||
initialMinMax.maxTime - initialMinMax.minTime,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
const success = result.current.resetToParentTime();
|
||||
expect(success).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.selectedTime).toBe('6h');
|
||||
const resetMinMax = result.current.lastComputedMinMax;
|
||||
expect(resetMinMax.maxTime - resetMinMax.minTime).toBeGreaterThan(
|
||||
changedMinMax.maxTime - changedMinMax.minTime,
|
||||
);
|
||||
});
|
||||
|
||||
it('should set shouldClearUrlParams flag when reset', () => {
|
||||
const wrapper = createNestedWrapper(
|
||||
{ initialTime: '6h' },
|
||||
{ inheritGlobalTime: true },
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m');
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.resetToParentTime();
|
||||
});
|
||||
|
||||
expect(result.current.shouldClearUrlParams).toBe(true);
|
||||
|
||||
act(() => {
|
||||
result.current.clearUrlParamsFlag();
|
||||
});
|
||||
|
||||
expect(result.current.shouldClearUrlParams).toBe(false);
|
||||
});
|
||||
|
||||
it('should clear URL params when resetToParentTime is called with enableUrlParams', async () => {
|
||||
let currentQueryString = 'relativeTime=15m';
|
||||
|
||||
const wrapper = createNestedWrapper(
|
||||
{ initialTime: '6h' },
|
||||
{ inheritGlobalTime: true, enableUrlParams: true },
|
||||
{
|
||||
searchParams: currentQueryString,
|
||||
onUrlUpdate: (event): void => {
|
||||
currentQueryString = event.queryString;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.selectedTime).toBe('15m');
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.resetToParentTime();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(currentQueryString).not.toContain('relativeTime');
|
||||
expect(currentQueryString).not.toContain('startTime');
|
||||
expect(currentQueryString).not.toContain('endTime');
|
||||
});
|
||||
|
||||
expect(result.current.selectedTime).toBe('6h');
|
||||
});
|
||||
|
||||
it('should clear custom time URL params when resetToParentTime is called', async () => {
|
||||
const startTime = 1700000000000;
|
||||
const endTime = 1700003600000;
|
||||
let currentQueryString = `startTime=${startTime}&endTime=${endTime}`;
|
||||
|
||||
const wrapper = createNestedWrapper(
|
||||
{ initialTime: '6h' },
|
||||
{ inheritGlobalTime: true, enableUrlParams: true },
|
||||
{
|
||||
searchParams: currentQueryString,
|
||||
onUrlUpdate: (event): void => {
|
||||
currentQueryString = event.queryString;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.selectedTime).toContain('||_||');
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.resetToParentTime();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(currentQueryString).not.toContain('startTime');
|
||||
expect(currentQueryString).not.toContain('endTime');
|
||||
});
|
||||
|
||||
expect(result.current.selectedTime).toBe('6h');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constan
|
||||
import {
|
||||
GlobalTimeSelectedTime,
|
||||
GlobalTimeState,
|
||||
GlobalTimeStoreApiRef,
|
||||
GlobalTimeStore,
|
||||
ParsedTimeRange,
|
||||
} from './types';
|
||||
@@ -18,6 +19,10 @@ import {
|
||||
export type GlobalTimeStoreApi = StoreApi<GlobalTimeStore>;
|
||||
export type IGlobalTimeStore = GlobalTimeStore;
|
||||
|
||||
export interface CreateGlobalTimeStoreOptions extends Partial<GlobalTimeState> {
|
||||
parentStore?: GlobalTimeStoreApiRef;
|
||||
}
|
||||
|
||||
function computeIsRefreshEnabled(
|
||||
selectedTime: GlobalTimeSelectedTime,
|
||||
refreshInterval: number,
|
||||
@@ -29,11 +34,12 @@ function computeIsRefreshEnabled(
|
||||
}
|
||||
|
||||
export function createGlobalTimeStore(
|
||||
initialState?: Partial<GlobalTimeState>,
|
||||
options?: CreateGlobalTimeStoreOptions,
|
||||
): GlobalTimeStoreApi {
|
||||
const selectedTime = initialState?.selectedTime ?? DEFAULT_TIME_RANGE;
|
||||
const refreshInterval = initialState?.refreshInterval ?? 0;
|
||||
const name = initialState?.name;
|
||||
const selectedTime = options?.selectedTime ?? DEFAULT_TIME_RANGE;
|
||||
const refreshInterval = options?.refreshInterval ?? 0;
|
||||
const name = options?.name;
|
||||
const parentStore = options?.parentStore;
|
||||
|
||||
return createStore<GlobalTimeStore>((set, get) => ({
|
||||
name,
|
||||
@@ -42,6 +48,8 @@ export function createGlobalTimeStore(
|
||||
isRefreshEnabled: computeIsRefreshEnabled(selectedTime, refreshInterval),
|
||||
lastRefreshTimestamp: 0,
|
||||
lastComputedMinMax: safeParseSelectedTime(selectedTime),
|
||||
parentStore,
|
||||
shouldClearUrlParams: false,
|
||||
|
||||
setSelectedTime: (
|
||||
time: GlobalTimeSelectedTime,
|
||||
@@ -130,6 +138,29 @@ export function createGlobalTimeStore(
|
||||
}
|
||||
return [REACT_QUERY_KEY.AUTO_REFRESH_QUERY, ...queryParts, selectedTime];
|
||||
},
|
||||
|
||||
resetToParentTime: (): boolean => {
|
||||
const state = get();
|
||||
if (!state.parentStore) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parentSelectedTime = state.parentStore.getState().selectedTime;
|
||||
const computedMinMax = parseSelectedTime(parentSelectedTime);
|
||||
|
||||
set({
|
||||
selectedTime: parentSelectedTime,
|
||||
lastComputedMinMax: computedMinMax,
|
||||
lastRefreshTimestamp: Date.now(),
|
||||
shouldClearUrlParams: true,
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
clearUrlParamsFlag: (): void => {
|
||||
set({ shouldClearUrlParams: false });
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -206,6 +206,7 @@
|
||||
* | `getMinMaxTime(time?)` | Get min/max (fresh if auto-refresh enabled, cached otherwise) |
|
||||
* | `computeAndStoreMinMax()` | Compute fresh values and cache them |
|
||||
* | `getAutoRefreshQueryKey(time, ...parts)` | Build scoped query key for this store instance |
|
||||
* | `resetToParentTime()` | Reset to parent store's time (only if `inheritGlobalTime` was used). Clears URL params. Returns `true` on success |
|
||||
*
|
||||
* ### Utilities
|
||||
*
|
||||
@@ -235,7 +236,7 @@
|
||||
* | Option | Type | Description |
|
||||
* |--------|------|-------------|
|
||||
* | `name` | `string` | Scope query keys to this store (enables isolated invalidation) |
|
||||
* | `inheritGlobalTime` | `boolean` | Initialize with parent/global time value |
|
||||
* | `inheritGlobalTime` | `boolean` | Initialize with parent/global time value. Enables `resetToParentTime()` |
|
||||
* | `initialTime` | `string` | Initial time if not inheriting |
|
||||
* | `enableUrlParams` | `boolean \| object` | Sync time to URL query params |
|
||||
* | `removeQueryParamsOnUnmount` | `boolean` | Clean URL params on unmount |
|
||||
@@ -341,7 +342,39 @@
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ### Example 3: Nested Contexts
|
||||
* ### Example 3: Reset to Parent Time
|
||||
*
|
||||
* When using `inheritGlobalTime`, you can reset the child store back to the parent's time:
|
||||
*
|
||||
* ```tsx
|
||||
* function DrawerHeader({ onClose }) {
|
||||
* const selectedTime = useGlobalTime((s) => s.selectedTime);
|
||||
* const resetToParentTime = useGlobalTime((s) => s.resetToParentTime);
|
||||
* const parentStore = useGlobalTime((s) => s.parentStore);
|
||||
*
|
||||
* const handleReset = () => {
|
||||
* // Returns true if reset succeeded, false if no parent store
|
||||
* const success = resetToParentTime();
|
||||
* if (success) {
|
||||
* // URL params are automatically cleared
|
||||
* console.log('Reset to parent time');
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* <DateTimeSelectionV3 />
|
||||
* {parentStore && (
|
||||
* <Button onClick={handleReset}>
|
||||
* Reset to Global Time
|
||||
* </Button>
|
||||
* )}
|
||||
* </div>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ### Example 4: Nested Contexts
|
||||
*
|
||||
* Contexts can be nested - each level creates isolation:
|
||||
*
|
||||
@@ -379,9 +412,10 @@
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ### Example 4: URL Sync for Shareable Links
|
||||
* ### Example 5: URL Sync for Shareable Links
|
||||
*
|
||||
* Persist time selection to URL for shareable links:
|
||||
* Persist time selection to URL for shareable links. When using `resetToParentTime()`,
|
||||
* URL params are automatically cleared:
|
||||
*
|
||||
* ```tsx
|
||||
* function TracesExplorer() {
|
||||
@@ -400,7 +434,7 @@
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ### Example 5: localStorage Persistence
|
||||
* ### Example 6: localStorage Persistence
|
||||
*
|
||||
* Remember user's last selected time across sessions:
|
||||
*
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { StoreApi } from 'zustand';
|
||||
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
|
||||
export type CustomTimeRangeSeparator = '||_||';
|
||||
export type CustomTimeRange = `${number}${CustomTimeRangeSeparator}${number}`;
|
||||
export type GlobalTimeSelectedTime = Time | CustomTimeRange;
|
||||
|
||||
// Forward declaration to avoid circular dependency
|
||||
export type GlobalTimeStoreApiRef = StoreApi<GlobalTimeStore>;
|
||||
|
||||
export interface IGlobalTimeStoreState {
|
||||
/**
|
||||
* The selected time range, can be:
|
||||
@@ -88,6 +92,16 @@ export interface GlobalTimeState {
|
||||
isRefreshEnabled: boolean;
|
||||
lastRefreshTimestamp: number;
|
||||
lastComputedMinMax: ParsedTimeRange;
|
||||
/**
|
||||
* Reference to parent store when inheritGlobalTime was used.
|
||||
* Enables resetToParentTime() functionality.
|
||||
*/
|
||||
parentStore?: GlobalTimeStoreApiRef;
|
||||
/**
|
||||
* Flag indicating URL params should be cleared (not synced).
|
||||
* Set by resetToParentTime(), consumed by useUrlSync.
|
||||
*/
|
||||
shouldClearUrlParams: boolean;
|
||||
}
|
||||
|
||||
export interface GlobalTimeActions {
|
||||
@@ -118,6 +132,16 @@ export interface GlobalTimeActions {
|
||||
selectedTime: GlobalTimeSelectedTime,
|
||||
...queryParts: unknown[]
|
||||
) => unknown[];
|
||||
/**
|
||||
* Reset to parent store's time. Only works if inheritGlobalTime was used.
|
||||
* Clears URL params and sets selectedTime to parent's value.
|
||||
* @returns true if reset succeeded, false if no parent store
|
||||
*/
|
||||
resetToParentTime: () => boolean;
|
||||
/**
|
||||
* Clear the shouldClearUrlParams flag after URL sync has processed it.
|
||||
*/
|
||||
clearUrlParamsFlag: () => void;
|
||||
}
|
||||
|
||||
export type GlobalTimeStore = GlobalTimeState & GlobalTimeActions;
|
||||
|
||||
@@ -91,6 +91,17 @@ export function useUrlSync(
|
||||
let previousSelectedTime = store.getState().selectedTime;
|
||||
|
||||
return store.subscribe((state) => {
|
||||
if (state.shouldClearUrlParams) {
|
||||
previousSelectedTime = state.selectedTime;
|
||||
void setUrlState({
|
||||
[keys.relativeTimeKey]: null,
|
||||
[keys.startTimeKey]: null,
|
||||
[keys.endTimeKey]: null,
|
||||
});
|
||||
store.getState().clearUrlParamsFlag();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.selectedTime === previousSelectedTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -42,12 +42,7 @@ export function toAPIError(
|
||||
try {
|
||||
ErrorResponseHandlerForGeneratedAPIs(error);
|
||||
} catch (apiError) {
|
||||
// UPSTREAM_UNAVAILABLE means the handler couldn't find a backend error code
|
||||
// (non-envelope body); prefer the caller's context-specific defaultMessage.
|
||||
if (
|
||||
apiError instanceof APIError &&
|
||||
apiError.getErrorCode() !== 'UPSTREAM_UNAVAILABLE'
|
||||
) {
|
||||
if (apiError instanceof APIError) {
|
||||
return apiError;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func buildClusterRecords(
|
||||
ClusterCPUAllocatable: -1,
|
||||
ClusterMemory: -1,
|
||||
ClusterMemoryAllocatable: -1,
|
||||
Meta: map[string]string{},
|
||||
Meta: inframonitoringtypes.NewClusterMeta(nil),
|
||||
}
|
||||
|
||||
if metrics, ok := metricsMap[compositeKey]; ok {
|
||||
@@ -85,9 +85,7 @@ func buildClusterRecords(
|
||||
}
|
||||
|
||||
if attrs, ok := metadataMap[compositeKey]; ok {
|
||||
for k, v := range attrs {
|
||||
record.Meta[k] = v
|
||||
}
|
||||
record.Meta = inframonitoringtypes.NewClusterMeta(attrs)
|
||||
}
|
||||
|
||||
records = append(records, record)
|
||||
@@ -149,7 +147,7 @@ func (m *module) getTopClusterGroups(
|
||||
|
||||
func (m *module) getClustersTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableClusters) (map[string]map[string]string, error) {
|
||||
var nonGroupByAttrs []string
|
||||
for _, key := range clusterAttrKeysForMetadata {
|
||||
for _, key := range inframonitoringtypes.ClusterMetaKeys {
|
||||
if !isKeyInGroupByAttrs(req.GroupBy, key) {
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
|
||||
@@ -31,10 +31,6 @@ var clustersTableMetricNamesList = []string{
|
||||
"k8s.container.status.reason",
|
||||
}
|
||||
|
||||
var clusterAttrKeysForMetadata = []string{
|
||||
"k8s.cluster.name",
|
||||
}
|
||||
|
||||
// clusterCountAttrKeys are the resource attributes whose distinct values are
|
||||
// counted per cluster. Node name is read from the node metric universe, while
|
||||
// namespace + workload names come from the pod metric universe — both unioned
|
||||
|
||||
@@ -48,7 +48,7 @@ func buildContainerRecords(
|
||||
Memory: -1,
|
||||
MemoryRequestUtilization: -1,
|
||||
MemoryLimitUtilization: -1,
|
||||
Meta: map[string]string{},
|
||||
Meta: inframonitoringtypes.NewContainerMeta(nil),
|
||||
}
|
||||
|
||||
if metrics, ok := metricsMap[compositeKey]; ok {
|
||||
@@ -128,9 +128,7 @@ func buildContainerRecords(
|
||||
}
|
||||
|
||||
if attrs, ok := metadataMap[compositeKey]; ok {
|
||||
for k, v := range attrs {
|
||||
record.Meta[k] = v
|
||||
}
|
||||
record.Meta = inframonitoringtypes.NewContainerMeta(attrs)
|
||||
}
|
||||
|
||||
records = append(records, record)
|
||||
@@ -192,7 +190,7 @@ func (m *module) getTopContainerGroups(
|
||||
|
||||
func (m *module) getContainersTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableContainers) (map[string]map[string]string, error) {
|
||||
var nonGroupByAttrs []string
|
||||
for _, key := range containerAttrKeysForMetadata {
|
||||
for _, key := range inframonitoringtypes.ContainerMetaKeys {
|
||||
if !isKeyInGroupByAttrs(req.GroupBy, key) {
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
|
||||
@@ -58,22 +58,6 @@ var containersTableMetricNamesList = []string{
|
||||
"k8s.container.status.state",
|
||||
}
|
||||
|
||||
var containerAttrKeysForMetadata = []string{
|
||||
"k8s.pod.uid",
|
||||
"k8s.container.name",
|
||||
"k8s.pod.name",
|
||||
"container.image.name",
|
||||
"container.image.tag",
|
||||
"k8s.namespace.name",
|
||||
"k8s.node.name",
|
||||
"k8s.deployment.name",
|
||||
"k8s.statefulset.name",
|
||||
"k8s.daemonset.name",
|
||||
"k8s.job.name",
|
||||
"k8s.cronjob.name",
|
||||
"k8s.cluster.name",
|
||||
}
|
||||
|
||||
var orderByToContainersQueryNames = map[string][]string{
|
||||
inframonitoringtypes.ContainersOrderByCPU: {"A"},
|
||||
inframonitoringtypes.ContainersOrderByCPURequest: {"B"},
|
||||
|
||||
@@ -40,7 +40,7 @@ func buildDaemonSetRecords(
|
||||
CurrentNodes: -1,
|
||||
ReadyNodes: -1,
|
||||
MisscheduledNodes: -1,
|
||||
Meta: map[string]string{},
|
||||
Meta: inframonitoringtypes.NewDaemonSetMeta(nil),
|
||||
}
|
||||
|
||||
if metrics, ok := metricsMap[compositeKey]; ok {
|
||||
@@ -91,9 +91,7 @@ func buildDaemonSetRecords(
|
||||
}
|
||||
|
||||
if attrs, ok := metadataMap[compositeKey]; ok {
|
||||
for k, v := range attrs {
|
||||
record.Meta[k] = v
|
||||
}
|
||||
record.Meta = inframonitoringtypes.NewDaemonSetMeta(attrs)
|
||||
}
|
||||
|
||||
records = append(records, record)
|
||||
@@ -155,7 +153,7 @@ func (m *module) getTopDaemonSetGroups(
|
||||
|
||||
func (m *module) getDaemonSetsTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableDaemonSets) (map[string]map[string]string, error) {
|
||||
var nonGroupByAttrs []string
|
||||
for _, key := range daemonSetAttrKeysForMetadata {
|
||||
for _, key := range inframonitoringtypes.DaemonSetMetaKeys {
|
||||
if !isKeyInGroupByAttrs(req.GroupBy, key) {
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
|
||||
@@ -39,11 +39,6 @@ var daemonSetsTableMetricNamesList = []string{
|
||||
|
||||
// Carried forward from v1 daemonSetAttrsToEnrich
|
||||
// (pkg/query-service/app/inframetrics/daemonsets.go:29-33).
|
||||
var daemonSetAttrKeysForMetadata = []string{
|
||||
"k8s.daemonset.name",
|
||||
"k8s.namespace.name",
|
||||
"k8s.cluster.name",
|
||||
}
|
||||
|
||||
// orderByToDaemonSetsQueryNames maps the orderBy column to the query name
|
||||
// used for ranking daemonset groups. v2 B/C/E/F are direct metrics, no
|
||||
|
||||
@@ -38,7 +38,7 @@ func buildDeploymentRecords(
|
||||
DeploymentMemoryLimit: -1,
|
||||
DesiredPods: -1,
|
||||
AvailablePods: -1,
|
||||
Meta: map[string]string{},
|
||||
Meta: inframonitoringtypes.NewDeploymentMeta(nil),
|
||||
}
|
||||
|
||||
if metrics, ok := metricsMap[compositeKey]; ok {
|
||||
@@ -83,9 +83,7 @@ func buildDeploymentRecords(
|
||||
}
|
||||
|
||||
if attrs, ok := metadataMap[compositeKey]; ok {
|
||||
for k, v := range attrs {
|
||||
record.Meta[k] = v
|
||||
}
|
||||
record.Meta = inframonitoringtypes.NewDeploymentMeta(attrs)
|
||||
}
|
||||
|
||||
records = append(records, record)
|
||||
@@ -147,7 +145,7 @@ func (m *module) getTopDeploymentGroups(
|
||||
|
||||
func (m *module) getDeploymentsTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableDeployments) (map[string]map[string]string, error) {
|
||||
var nonGroupByAttrs []string
|
||||
for _, key := range deploymentAttrKeysForMetadata {
|
||||
for _, key := range inframonitoringtypes.DeploymentMetaKeys {
|
||||
if !isKeyInGroupByAttrs(req.GroupBy, key) {
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
|
||||
@@ -37,11 +37,6 @@ var deploymentsTableMetricNamesList = []string{
|
||||
|
||||
// Carried forward from v1 deploymentAttrsToEnrich
|
||||
// (pkg/query-service/app/inframetrics/deployments.go:29-33).
|
||||
var deploymentAttrKeysForMetadata = []string{
|
||||
"k8s.deployment.name",
|
||||
"k8s.namespace.name",
|
||||
"k8s.cluster.name",
|
||||
}
|
||||
|
||||
// orderByToDeploymentsQueryNames maps the orderBy column to the query name
|
||||
// used for ranking deployment groups. v2 B/C/E/F are direct metrics, no
|
||||
|
||||
@@ -216,7 +216,7 @@ func buildHostRecords(
|
||||
Wait: -1,
|
||||
Load15: -1,
|
||||
DiskUsage: -1,
|
||||
Meta: map[string]string{},
|
||||
Meta: inframonitoringtypes.NewHostMeta(nil),
|
||||
}
|
||||
|
||||
if metrics, ok := metricsMap[compositeKey]; ok {
|
||||
@@ -238,9 +238,7 @@ func buildHostRecords(
|
||||
}
|
||||
|
||||
if attrs, ok := metadataMap[compositeKey]; ok {
|
||||
for k, v := range attrs {
|
||||
record.Meta[k] = v
|
||||
}
|
||||
record.Meta = inframonitoringtypes.NewHostMeta(attrs)
|
||||
}
|
||||
|
||||
records = append(records, record)
|
||||
@@ -333,7 +331,7 @@ func (m *module) applyHostsActiveStatusFilter(req *inframonitoringtypes.Postable
|
||||
|
||||
func (m *module) getHostsTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableHosts) (map[string]map[string]string, error) {
|
||||
var nonGroupByAttrs []string
|
||||
for _, key := range hostAttrKeysForMetadata {
|
||||
for _, key := range inframonitoringtypes.HostMetaKeys {
|
||||
if !isKeyInGroupByAttrs(req.GroupBy, key) {
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
|
||||
@@ -23,10 +23,6 @@ var hostsTableMetricNamesList = []string{
|
||||
"system.filesystem.usage",
|
||||
}
|
||||
|
||||
var hostAttrKeysForMetadata = []string{
|
||||
"os.type",
|
||||
}
|
||||
|
||||
// orderByToHostsQueryNames maps the orderBy column to the query/formula names
|
||||
// from HostsTableListQuery used for ranking host groups.
|
||||
var orderByToHostsQueryNames = map[string][]string{
|
||||
|
||||
@@ -40,7 +40,7 @@ func buildJobRecords(
|
||||
ActivePods: -1,
|
||||
FailedPods: -1,
|
||||
SuccessfulPods: -1,
|
||||
Meta: map[string]string{},
|
||||
Meta: inframonitoringtypes.NewJobMeta(nil),
|
||||
}
|
||||
|
||||
if metrics, ok := metricsMap[compositeKey]; ok {
|
||||
@@ -91,9 +91,7 @@ func buildJobRecords(
|
||||
}
|
||||
|
||||
if attrs, ok := metadataMap[compositeKey]; ok {
|
||||
for k, v := range attrs {
|
||||
record.Meta[k] = v
|
||||
}
|
||||
record.Meta = inframonitoringtypes.NewJobMeta(attrs)
|
||||
}
|
||||
|
||||
records = append(records, record)
|
||||
@@ -155,7 +153,7 @@ func (m *module) getTopJobGroups(
|
||||
|
||||
func (m *module) getJobsTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableJobs) (map[string]map[string]string, error) {
|
||||
var nonGroupByAttrs []string
|
||||
for _, key := range jobAttrKeysForMetadata {
|
||||
for _, key := range inframonitoringtypes.JobMetaKeys {
|
||||
if !isKeyInGroupByAttrs(req.GroupBy, key) {
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
|
||||
@@ -39,11 +39,6 @@ var jobsTableMetricNamesList = []string{
|
||||
|
||||
// Carried forward from v1 jobAttrsToEnrich
|
||||
// (pkg/query-service/app/inframetrics/jobs.go:31-35).
|
||||
var jobAttrKeysForMetadata = []string{
|
||||
"k8s.job.name",
|
||||
"k8s.namespace.name",
|
||||
"k8s.cluster.name",
|
||||
}
|
||||
|
||||
// orderByToJobsQueryNames maps the orderBy column to the query name
|
||||
// used for ranking job groups. v2 B/C/E/F are direct metrics, no
|
||||
|
||||
@@ -32,7 +32,7 @@ func buildNamespaceRecords(
|
||||
NamespaceName: namespaceName,
|
||||
NamespaceCPU: -1,
|
||||
NamespaceMemory: -1,
|
||||
Meta: map[string]string{},
|
||||
Meta: inframonitoringtypes.NewNamespaceMeta(nil),
|
||||
}
|
||||
|
||||
if metrics, ok := metricsMap[compositeKey]; ok {
|
||||
@@ -66,9 +66,7 @@ func buildNamespaceRecords(
|
||||
}
|
||||
|
||||
if attrs, ok := metadataMap[compositeKey]; ok {
|
||||
for k, v := range attrs {
|
||||
record.Meta[k] = v
|
||||
}
|
||||
record.Meta = inframonitoringtypes.NewNamespaceMeta(attrs)
|
||||
}
|
||||
|
||||
records = append(records, record)
|
||||
@@ -130,7 +128,7 @@ func (m *module) getTopNamespaceGroups(
|
||||
|
||||
func (m *module) getNamespacesTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableNamespaces) (map[string]map[string]string, error) {
|
||||
var nonGroupByAttrs []string
|
||||
for _, key := range namespaceAttrKeysForMetadata {
|
||||
for _, key := range inframonitoringtypes.NamespaceMetaKeys {
|
||||
if !isKeyInGroupByAttrs(req.GroupBy, key) {
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
|
||||
@@ -59,11 +59,6 @@ var namespacesMetricNamesListForCounts = []string{
|
||||
"k8s.pod.status_reason",
|
||||
}
|
||||
|
||||
var namespaceAttrKeysForMetadata = []string{
|
||||
"k8s.namespace.name",
|
||||
"k8s.cluster.name",
|
||||
}
|
||||
|
||||
// namespaceCountAttrKeys are the workload resource attributes whose distinct
|
||||
// values are counted per namespace. They are read from the pod metric universe,
|
||||
// which carries the owner workload names for each pod series.
|
||||
|
||||
@@ -42,7 +42,7 @@ func buildNodeRecords(
|
||||
NodeCPUAllocatable: -1,
|
||||
NodeMemory: -1,
|
||||
NodeMemoryAllocatable: -1,
|
||||
Meta: map[string]string{},
|
||||
Meta: inframonitoringtypes.NewNodeMeta(nil),
|
||||
}
|
||||
|
||||
if metrics, ok := metricsMap[compositeKey]; ok {
|
||||
@@ -92,9 +92,7 @@ func buildNodeRecords(
|
||||
}
|
||||
|
||||
if attrs, ok := metadataMap[compositeKey]; ok {
|
||||
for k, v := range attrs {
|
||||
record.Meta[k] = v
|
||||
}
|
||||
record.Meta = inframonitoringtypes.NewNodeMeta(attrs)
|
||||
}
|
||||
|
||||
records = append(records, record)
|
||||
@@ -156,7 +154,7 @@ func (m *module) getTopNodeGroups(
|
||||
|
||||
func (m *module) getNodesTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableNodes) (map[string]map[string]string, error) {
|
||||
var nonGroupByAttrs []string
|
||||
for _, key := range nodeAttrKeysForMetadata {
|
||||
for _, key := range inframonitoringtypes.NodeMetaKeys {
|
||||
if !isKeyInGroupByAttrs(req.GroupBy, key) {
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
|
||||
@@ -30,11 +30,6 @@ var nodesTableMetricNamesList = []string{
|
||||
"k8s.container.status.reason",
|
||||
}
|
||||
|
||||
var nodeAttrKeysForMetadata = []string{
|
||||
"k8s.node.uid",
|
||||
"k8s.cluster.name",
|
||||
}
|
||||
|
||||
var orderByToNodesQueryNames = map[string][]string{
|
||||
inframonitoringtypes.NodesOrderByCPU: {"A"},
|
||||
inframonitoringtypes.NodesOrderByCPUAllocatable: {"B"},
|
||||
|
||||
@@ -49,7 +49,7 @@ func buildPodRecords(
|
||||
PodMemoryRequest: -1,
|
||||
PodMemoryLimit: -1,
|
||||
PodAge: -1,
|
||||
Meta: map[string]string{},
|
||||
Meta: inframonitoringtypes.NewPodMeta(nil),
|
||||
}
|
||||
|
||||
if metrics, ok := metricsMap[compositeKey]; ok {
|
||||
@@ -163,9 +163,7 @@ func buildPodRecords(
|
||||
}
|
||||
}
|
||||
}
|
||||
for k, v := range attrs {
|
||||
record.Meta[k] = v
|
||||
}
|
||||
record.Meta = inframonitoringtypes.NewPodMeta(attrs)
|
||||
}
|
||||
|
||||
records = append(records, record)
|
||||
@@ -227,7 +225,7 @@ func (m *module) getTopPodGroups(
|
||||
|
||||
func (m *module) getPodsTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostablePods) (map[string]map[string]string, error) {
|
||||
var nonGroupByAttrs []string
|
||||
for _, key := range podAttrKeysForMetadata {
|
||||
for _, key := range inframonitoringtypes.PodMetaKeys {
|
||||
if !isKeyInGroupByAttrs(req.GroupBy, key) {
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
|
||||
@@ -45,20 +45,6 @@ var podsTableMetricNamesList = []string{
|
||||
// "k8s.container.restarts",
|
||||
}
|
||||
|
||||
var podAttrKeysForMetadata = []string{
|
||||
"k8s.pod.uid",
|
||||
"k8s.pod.name",
|
||||
"k8s.namespace.name",
|
||||
"k8s.node.name",
|
||||
"k8s.deployment.name",
|
||||
"k8s.statefulset.name",
|
||||
"k8s.daemonset.name",
|
||||
"k8s.job.name",
|
||||
"k8s.cronjob.name",
|
||||
"k8s.cluster.name",
|
||||
"k8s.pod.start_time",
|
||||
}
|
||||
|
||||
var orderByToPodsQueryNames = map[string][]string{
|
||||
inframonitoringtypes.PodsOrderByCPU: {"A"},
|
||||
inframonitoringtypes.PodsOrderByCPURequest: {"B"},
|
||||
|
||||
@@ -38,7 +38,7 @@ func buildStatefulSetRecords(
|
||||
StatefulSetMemoryLimit: -1,
|
||||
DesiredPods: -1,
|
||||
CurrentPods: -1,
|
||||
Meta: map[string]string{},
|
||||
Meta: inframonitoringtypes.NewStatefulSetMeta(nil),
|
||||
}
|
||||
|
||||
if metrics, ok := metricsMap[compositeKey]; ok {
|
||||
@@ -83,9 +83,7 @@ func buildStatefulSetRecords(
|
||||
}
|
||||
|
||||
if attrs, ok := metadataMap[compositeKey]; ok {
|
||||
for k, v := range attrs {
|
||||
record.Meta[k] = v
|
||||
}
|
||||
record.Meta = inframonitoringtypes.NewStatefulSetMeta(attrs)
|
||||
}
|
||||
|
||||
records = append(records, record)
|
||||
@@ -147,7 +145,7 @@ func (m *module) getTopStatefulSetGroups(
|
||||
|
||||
func (m *module) getStatefulSetsTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableStatefulSets) (map[string]map[string]string, error) {
|
||||
var nonGroupByAttrs []string
|
||||
for _, key := range statefulSetAttrKeysForMetadata {
|
||||
for _, key := range inframonitoringtypes.StatefulSetMetaKeys {
|
||||
if !isKeyInGroupByAttrs(req.GroupBy, key) {
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
|
||||
@@ -37,11 +37,6 @@ var statefulSetsTableMetricNamesList = []string{
|
||||
|
||||
// Carried forward from v1 statefulSetAttrsToEnrich
|
||||
// (pkg/query-service/app/inframetrics/statefulsets.go:29-33).
|
||||
var statefulSetAttrKeysForMetadata = []string{
|
||||
"k8s.statefulset.name",
|
||||
"k8s.namespace.name",
|
||||
"k8s.cluster.name",
|
||||
}
|
||||
|
||||
// orderByToStatefulSetsQueryNames maps the orderBy column to the query name
|
||||
// used for ranking statefulset groups. v2 B/C/E/F are direct metrics, no
|
||||
|
||||
@@ -33,7 +33,7 @@ func buildVolumeRecords(
|
||||
VolumeInodes: -1,
|
||||
VolumeInodesFree: -1,
|
||||
VolumeInodesUsed: -1,
|
||||
Meta: map[string]string{},
|
||||
Meta: inframonitoringtypes.NewVolumeMeta(nil),
|
||||
}
|
||||
|
||||
if metrics, ok := metricsMap[compositeKey]; ok {
|
||||
@@ -58,9 +58,7 @@ func buildVolumeRecords(
|
||||
}
|
||||
|
||||
if attrs, ok := metadataMap[compositeKey]; ok {
|
||||
for k, v := range attrs {
|
||||
record.Meta[k] = v
|
||||
}
|
||||
record.Meta = inframonitoringtypes.NewVolumeMeta(attrs)
|
||||
}
|
||||
|
||||
records = append(records, record)
|
||||
@@ -122,7 +120,7 @@ func (m *module) getTopVolumeGroups(
|
||||
|
||||
func (m *module) getVolumesTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableVolumes) (map[string]map[string]string, error) {
|
||||
var nonGroupByAttrs []string
|
||||
for _, key := range volumeAttrKeysForMetadata {
|
||||
for _, key := range inframonitoringtypes.VolumeMetaKeys {
|
||||
if !isKeyInGroupByAttrs(req.GroupBy, key) {
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
|
||||
@@ -28,15 +28,6 @@ var volumesTableMetricNamesList = []string{
|
||||
|
||||
// Carried forward from v1 volumeAttrsToEnrich
|
||||
// (pkg/query-service/app/inframetrics/pvcs.go:23-31).
|
||||
var volumeAttrKeysForMetadata = []string{
|
||||
"k8s.persistentvolumeclaim.name",
|
||||
"k8s.pod.uid",
|
||||
"k8s.pod.name",
|
||||
"k8s.namespace.name",
|
||||
"k8s.node.name",
|
||||
"k8s.statefulset.name",
|
||||
"k8s.cluster.name",
|
||||
}
|
||||
|
||||
// orderByToVolumesQueryNames maps the orderBy column to the query/formula names
|
||||
// from newVolumesTableListQuery used for ranking volume groups. For "usage",
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
)
|
||||
|
||||
type Clusters struct {
|
||||
@@ -36,7 +38,7 @@ type ClusterRecord struct {
|
||||
Jobs int64 `json:"jobs" required:"true"`
|
||||
StatefulSets int64 `json:"statefulSets" required:"true"`
|
||||
} `json:"counts" required:"true"`
|
||||
Meta map[string]string `json:"meta" required:"true"`
|
||||
Meta ClusterMeta `json:"meta" required:"true"`
|
||||
}
|
||||
|
||||
// PostableClusters is the request body for the v2 clusters list API.
|
||||
@@ -114,3 +116,36 @@ func (req *PostableClusters) UnmarshalJSON(data []byte) error {
|
||||
*req = PostableClusters(decoded)
|
||||
return req.Validate()
|
||||
}
|
||||
|
||||
// ClusterMeta carries the guaranteed cluster metadata keys as typed fields plus
|
||||
// any dynamic group-by keys in Extra.
|
||||
type ClusterMeta struct {
|
||||
ClusterName string `json:"k8s.cluster.name" required:"true"`
|
||||
Extra map[string]string `json:"-"`
|
||||
}
|
||||
|
||||
var ClusterMetaKeys = getMetaKeys(&ClusterMeta{})
|
||||
|
||||
func NewClusterMeta(attrs map[string]string) ClusterMeta {
|
||||
var m ClusterMeta
|
||||
m.Extra = populateMeta(&m, attrs)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m ClusterMeta) MarshalJSON() ([]byte, error) {
|
||||
return marshalMeta(&m, m.Extra)
|
||||
}
|
||||
|
||||
func (m *ClusterMeta) UnmarshalJSON(data []byte) error {
|
||||
raw, err := splitMeta(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Extra = populateMeta(m, raw)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ClusterMeta) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
addStringAdditionalProps(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
)
|
||||
|
||||
type Containers struct {
|
||||
@@ -68,7 +70,7 @@ type ContainerRecord struct {
|
||||
MemoryRequestUtilization float64 `json:"memoryRequestUtilization" required:"true"` // k8s.container.memory_request_utilization
|
||||
MemoryLimitUtilization float64 `json:"memoryLimitUtilization" required:"true"` // k8s.container.memory_limit_utilization
|
||||
|
||||
Meta map[string]string `json:"meta" required:"true"`
|
||||
Meta ContainerMeta `json:"meta" required:"true"`
|
||||
}
|
||||
|
||||
// PostableContainers is the request body for the v2 containers list API.
|
||||
@@ -146,3 +148,48 @@ func (req *PostableContainers) UnmarshalJSON(data []byte) error {
|
||||
*req = PostableContainers(decoded)
|
||||
return req.Validate()
|
||||
}
|
||||
|
||||
// ContainerMeta carries the guaranteed container metadata keys as typed fields
|
||||
// plus any dynamic group-by keys in Extra.
|
||||
type ContainerMeta struct {
|
||||
PodUID string `json:"k8s.pod.uid" required:"true"`
|
||||
ContainerName string `json:"k8s.container.name" required:"true"`
|
||||
PodName string `json:"k8s.pod.name" required:"true"`
|
||||
ImageName string `json:"container.image.name" required:"true"`
|
||||
ImageTag string `json:"container.image.tag" required:"true"`
|
||||
NamespaceName string `json:"k8s.namespace.name" required:"true"`
|
||||
NodeName string `json:"k8s.node.name" required:"true"`
|
||||
DeploymentName string `json:"k8s.deployment.name" required:"true"`
|
||||
StatefulSetName string `json:"k8s.statefulset.name" required:"true"`
|
||||
DaemonSetName string `json:"k8s.daemonset.name" required:"true"`
|
||||
JobName string `json:"k8s.job.name" required:"true"`
|
||||
CronJobName string `json:"k8s.cronjob.name" required:"true"`
|
||||
ClusterName string `json:"k8s.cluster.name" required:"true"`
|
||||
Extra map[string]string `json:"-"`
|
||||
}
|
||||
|
||||
var ContainerMetaKeys = getMetaKeys(&ContainerMeta{})
|
||||
|
||||
func NewContainerMeta(attrs map[string]string) ContainerMeta {
|
||||
var m ContainerMeta
|
||||
m.Extra = populateMeta(&m, attrs)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m ContainerMeta) MarshalJSON() ([]byte, error) {
|
||||
return marshalMeta(&m, m.Extra)
|
||||
}
|
||||
|
||||
func (m *ContainerMeta) UnmarshalJSON(data []byte) error {
|
||||
raw, err := splitMeta(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Extra = populateMeta(m, raw)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ContainerMeta) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
addStringAdditionalProps(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
)
|
||||
|
||||
type DaemonSets struct {
|
||||
@@ -30,7 +32,7 @@ type DaemonSetRecord struct {
|
||||
MisscheduledNodes int `json:"misscheduledNodes" required:"true"`
|
||||
PodCountsByPhase PodCountsByPhase `json:"podCountsByPhase" required:"true"`
|
||||
PodCountsByStatus PodCountsByStatus `json:"podCountsByStatus" required:"true"`
|
||||
Meta map[string]string `json:"meta" required:"true"`
|
||||
Meta DaemonSetMeta `json:"meta" required:"true"`
|
||||
}
|
||||
|
||||
// PostableDaemonSets is the request body for the v2 daemonsets list API.
|
||||
@@ -108,3 +110,38 @@ func (req *PostableDaemonSets) UnmarshalJSON(data []byte) error {
|
||||
*req = PostableDaemonSets(decoded)
|
||||
return req.Validate()
|
||||
}
|
||||
|
||||
// DaemonSetMeta carries the guaranteed daemonset metadata keys as typed fields
|
||||
// plus any dynamic group-by keys in Extra.
|
||||
type DaemonSetMeta struct {
|
||||
DaemonSetName string `json:"k8s.daemonset.name" required:"true"`
|
||||
NamespaceName string `json:"k8s.namespace.name" required:"true"`
|
||||
ClusterName string `json:"k8s.cluster.name" required:"true"`
|
||||
Extra map[string]string `json:"-"`
|
||||
}
|
||||
|
||||
var DaemonSetMetaKeys = getMetaKeys(&DaemonSetMeta{})
|
||||
|
||||
func NewDaemonSetMeta(attrs map[string]string) DaemonSetMeta {
|
||||
var m DaemonSetMeta
|
||||
m.Extra = populateMeta(&m, attrs)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m DaemonSetMeta) MarshalJSON() ([]byte, error) {
|
||||
return marshalMeta(&m, m.Extra)
|
||||
}
|
||||
|
||||
func (m *DaemonSetMeta) UnmarshalJSON(data []byte) error {
|
||||
raw, err := splitMeta(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Extra = populateMeta(m, raw)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (DaemonSetMeta) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
addStringAdditionalProps(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
)
|
||||
|
||||
type Deployments struct {
|
||||
@@ -28,7 +30,7 @@ type DeploymentRecord struct {
|
||||
AvailablePods int `json:"availablePods" required:"true"`
|
||||
PodCountsByPhase PodCountsByPhase `json:"podCountsByPhase" required:"true"`
|
||||
PodCountsByStatus PodCountsByStatus `json:"podCountsByStatus" required:"true"`
|
||||
Meta map[string]string `json:"meta" required:"true"`
|
||||
Meta DeploymentMeta `json:"meta" required:"true"`
|
||||
}
|
||||
|
||||
// PostableDeployments is the request body for the v2 deployments list API.
|
||||
@@ -106,3 +108,38 @@ func (req *PostableDeployments) UnmarshalJSON(data []byte) error {
|
||||
*req = PostableDeployments(decoded)
|
||||
return req.Validate()
|
||||
}
|
||||
|
||||
// DeploymentMeta carries the guaranteed deployment metadata keys as typed fields
|
||||
// plus any dynamic group-by keys in Extra.
|
||||
type DeploymentMeta struct {
|
||||
DeploymentName string `json:"k8s.deployment.name" required:"true"`
|
||||
NamespaceName string `json:"k8s.namespace.name" required:"true"`
|
||||
ClusterName string `json:"k8s.cluster.name" required:"true"`
|
||||
Extra map[string]string `json:"-"`
|
||||
}
|
||||
|
||||
var DeploymentMetaKeys = getMetaKeys(&DeploymentMeta{})
|
||||
|
||||
func NewDeploymentMeta(attrs map[string]string) DeploymentMeta {
|
||||
var m DeploymentMeta
|
||||
m.Extra = populateMeta(&m, attrs)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m DeploymentMeta) MarshalJSON() ([]byte, error) {
|
||||
return marshalMeta(&m, m.Extra)
|
||||
}
|
||||
|
||||
func (m *DeploymentMeta) UnmarshalJSON(data []byte) error {
|
||||
raw, err := splitMeta(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Extra = populateMeta(m, raw)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (DeploymentMeta) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
addStringAdditionalProps(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
)
|
||||
|
||||
type Hosts struct {
|
||||
@@ -17,16 +19,16 @@ type Hosts struct {
|
||||
}
|
||||
|
||||
type HostRecord struct {
|
||||
HostName string `json:"hostName" required:"true"`
|
||||
Status HostStatus `json:"status" required:"true"`
|
||||
ActiveHostCount int `json:"activeHostCount" required:"true"`
|
||||
InactiveHostCount int `json:"inactiveHostCount" required:"true"`
|
||||
CPU float64 `json:"cpu" required:"true"`
|
||||
Memory float64 `json:"memory" required:"true"`
|
||||
Wait float64 `json:"wait" required:"true"`
|
||||
Load15 float64 `json:"load15" required:"true"`
|
||||
DiskUsage float64 `json:"diskUsage" required:"true"`
|
||||
Meta map[string]string `json:"meta" required:"true"`
|
||||
HostName string `json:"hostName" required:"true"`
|
||||
Status HostStatus `json:"status" required:"true"`
|
||||
ActiveHostCount int `json:"activeHostCount" required:"true"`
|
||||
InactiveHostCount int `json:"inactiveHostCount" required:"true"`
|
||||
CPU float64 `json:"cpu" required:"true"`
|
||||
Memory float64 `json:"memory" required:"true"`
|
||||
Wait float64 `json:"wait" required:"true"`
|
||||
Load15 float64 `json:"load15" required:"true"`
|
||||
DiskUsage float64 `json:"diskUsage" required:"true"`
|
||||
Meta HostMeta `json:"meta" required:"true"`
|
||||
}
|
||||
|
||||
type PostableHosts struct {
|
||||
@@ -113,3 +115,37 @@ func (req *PostableHosts) UnmarshalJSON(data []byte) error {
|
||||
*req = PostableHosts(decoded)
|
||||
return req.Validate()
|
||||
}
|
||||
|
||||
// HostMeta carries the guaranteed host metadata keys as typed fields plus any
|
||||
// dynamic group-by keys in Extra.
|
||||
type HostMeta struct {
|
||||
OSType string `json:"os.type" required:"true"`
|
||||
HostName string `json:"host.name" required:"true"`
|
||||
Extra map[string]string `json:"-"`
|
||||
}
|
||||
|
||||
var HostMetaKeys = getMetaKeys(&HostMeta{})
|
||||
|
||||
func NewHostMeta(attrs map[string]string) HostMeta {
|
||||
var m HostMeta
|
||||
m.Extra = populateMeta(&m, attrs)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m HostMeta) MarshalJSON() ([]byte, error) {
|
||||
return marshalMeta(&m, m.Extra)
|
||||
}
|
||||
|
||||
func (m *HostMeta) UnmarshalJSON(data []byte) error {
|
||||
raw, err := splitMeta(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Extra = populateMeta(m, raw)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (HostMeta) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
addStringAdditionalProps(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
)
|
||||
|
||||
type Jobs struct {
|
||||
@@ -30,7 +32,7 @@ type JobRecord struct {
|
||||
SuccessfulPods int `json:"successfulPods" required:"true"`
|
||||
PodCountsByPhase PodCountsByPhase `json:"podCountsByPhase" required:"true"`
|
||||
PodCountsByStatus PodCountsByStatus `json:"podCountsByStatus" required:"true"`
|
||||
Meta map[string]string `json:"meta" required:"true"`
|
||||
Meta JobMeta `json:"meta" required:"true"`
|
||||
}
|
||||
|
||||
// PostableJobs is the request body for the v2 jobs list API.
|
||||
@@ -108,3 +110,38 @@ func (req *PostableJobs) UnmarshalJSON(data []byte) error {
|
||||
*req = PostableJobs(decoded)
|
||||
return req.Validate()
|
||||
}
|
||||
|
||||
// JobMeta carries the guaranteed job metadata keys as typed fields plus any
|
||||
// dynamic group-by keys in Extra.
|
||||
type JobMeta struct {
|
||||
JobName string `json:"k8s.job.name" required:"true"`
|
||||
NamespaceName string `json:"k8s.namespace.name" required:"true"`
|
||||
ClusterName string `json:"k8s.cluster.name" required:"true"`
|
||||
Extra map[string]string `json:"-"`
|
||||
}
|
||||
|
||||
var JobMetaKeys = getMetaKeys(&JobMeta{})
|
||||
|
||||
func NewJobMeta(attrs map[string]string) JobMeta {
|
||||
var m JobMeta
|
||||
m.Extra = populateMeta(&m, attrs)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m JobMeta) MarshalJSON() ([]byte, error) {
|
||||
return marshalMeta(&m, m.Extra)
|
||||
}
|
||||
|
||||
func (m *JobMeta) UnmarshalJSON(data []byte) error {
|
||||
raw, err := splitMeta(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Extra = populateMeta(m, raw)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (JobMeta) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
addStringAdditionalProps(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
112
pkg/types/inframonitoringtypes/meta.go
Normal file
112
pkg/types/inframonitoringtypes/meta.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"maps"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
)
|
||||
|
||||
// The meta field on every infra-monitoring v2 record carries a fixed set of
|
||||
// guaranteed keys (surfaced as strongly-typed struct fields) plus any dynamic
|
||||
// keys the caller introduces via group-by (kept in Extra). Each entity defines
|
||||
// its own XMeta type; the helpers below give them a shared, wire-stable
|
||||
// marshaling and schema contract:
|
||||
//
|
||||
// - MarshalJSON -> flattenMeta: known + Extra collapse into one flat object.
|
||||
// - UnmarshalJSON -> splitMeta: decode the flat object, route known keys to
|
||||
// fields (via the entity's NewXMeta), the rest to Extra.
|
||||
// - PrepareJSONSchema -> addStringAdditionalProps: keep the reflected known
|
||||
// properties and mark the object as an open map of string values.
|
||||
//
|
||||
// flattenMeta merges the guaranteed known keys and the open Extra map into a
|
||||
// single flat JSON object, preserving the historical wire shape. known wins on
|
||||
// collision. The output map is always non-nil, so an empty meta serializes as
|
||||
// {} rather than null.
|
||||
func flattenMeta(extra map[string]string, known map[string]string) ([]byte, error) {
|
||||
out := make(map[string]string, len(extra)+len(known))
|
||||
maps.Copy(out, extra)
|
||||
maps.Copy(out, known)
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
// splitMeta decodes a flat meta object into its raw string map. Callers pull
|
||||
// their known keys out of the returned map; whatever remains becomes Extra.
|
||||
func splitMeta(data []byte) (map[string]string, error) {
|
||||
raw := map[string]string{}
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
// metaField is one guaranteed meta key discovered from an XMeta struct's json
|
||||
// tags (json:"-" fields like Extra are skipped).
|
||||
type metaField struct {
|
||||
index int
|
||||
key string
|
||||
}
|
||||
|
||||
// metaFieldsOf reflects the string fields of an XMeta struct (skipping json:"-"
|
||||
// fields like Extra).
|
||||
func metaFieldsOf(t reflect.Type) []metaField {
|
||||
fields := make([]metaField, 0, t.NumField())
|
||||
for i := range t.NumField() {
|
||||
f := t.Field(i)
|
||||
key, _, _ := strings.Cut(f.Tag.Get("json"), ",")
|
||||
if key == "" || key == "-" {
|
||||
continue
|
||||
}
|
||||
fields = append(fields, metaField{index: i, key: key})
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
// populateMeta routes src into the guaranteed fields of the XMeta pointed to by
|
||||
// m (empty string when a key is absent) and returns the leftover keys (dynamic
|
||||
// group-by keys) as Extra.
|
||||
func populateMeta(m any, src map[string]string) map[string]string {
|
||||
extra := map[string]string{}
|
||||
maps.Copy(extra, src)
|
||||
rv := reflect.ValueOf(m).Elem()
|
||||
for _, f := range metaFieldsOf(rv.Type()) {
|
||||
rv.Field(f.index).SetString(src[f.key])
|
||||
delete(extra, f.key)
|
||||
}
|
||||
return extra
|
||||
}
|
||||
|
||||
// marshalMeta flattens the guaranteed fields of the XMeta pointed to by m plus
|
||||
// its Extra map into a single flat JSON object.
|
||||
func marshalMeta(m any, extra map[string]string) ([]byte, error) {
|
||||
rv := reflect.ValueOf(m).Elem()
|
||||
known := make(map[string]string)
|
||||
for _, f := range metaFieldsOf(rv.Type()) {
|
||||
known[f.key] = rv.Field(f.index).String()
|
||||
}
|
||||
return flattenMeta(extra, known)
|
||||
}
|
||||
|
||||
// getMetaKeys returns the guaranteed keys of an XMeta, in struct-field order. Used
|
||||
// by implinframonitoring as the metadata fetch column list, keeping the key set
|
||||
// defined once (on the struct).
|
||||
func getMetaKeys(m any) []string {
|
||||
rv := reflect.ValueOf(m).Elem()
|
||||
fields := metaFieldsOf(rv.Type())
|
||||
keys := make([]string, 0, len(fields))
|
||||
for _, f := range fields {
|
||||
keys = append(keys, f.key)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// addStringAdditionalProps marks an already-reflected meta object schema as an
|
||||
// open map of string values. The known properties and their required list come
|
||||
// from struct-tag reflection (this runs after, via the Preparer hook); this
|
||||
// only attaches additionalProperties:{type:string} so callers may add arbitrary
|
||||
// group-by keys.
|
||||
func addStringAdditionalProps(s *jsonschema.Schema) {
|
||||
s.WithAdditionalProperties(jsonschema.String.ToSchemaOrBool())
|
||||
}
|
||||
150
pkg/types/inframonitoringtypes/meta_test.go
Normal file
150
pkg/types/inframonitoringtypes/meta_test.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
)
|
||||
|
||||
// fullPodAttrs is a metadata map with every guaranteed pod key plus a dynamic
|
||||
// group-by key, used across the round-trip cases.
|
||||
func fullPodAttrs() map[string]string {
|
||||
return map[string]string{
|
||||
"k8s.pod.uid": "uid-1",
|
||||
"k8s.pod.name": "pod-1",
|
||||
"k8s.namespace.name": "ns-1",
|
||||
"k8s.node.name": "node-1",
|
||||
"k8s.deployment.name": "dep-1",
|
||||
"k8s.statefulset.name": "",
|
||||
"k8s.daemonset.name": "",
|
||||
"k8s.job.name": "",
|
||||
"k8s.cronjob.name": "",
|
||||
"k8s.cluster.name": "cluster-1",
|
||||
"k8s.pod.start_time": "2024-01-01T00:00:00Z",
|
||||
"custom.groupby": "gv",
|
||||
}
|
||||
}
|
||||
|
||||
func TestPodMeta_RoundTripAndRouting(t *testing.T) {
|
||||
attrs := fullPodAttrs()
|
||||
m := NewPodMeta(attrs)
|
||||
|
||||
// known keys route to typed fields
|
||||
require.Equal(t, "uid-1", m.PodUID)
|
||||
require.Equal(t, "cluster-1", m.ClusterName)
|
||||
require.Equal(t, "2024-01-01T00:00:00Z", m.PodStartTime)
|
||||
// dynamic key routes to Extra, guaranteed keys do not
|
||||
require.Equal(t, map[string]string{"custom.groupby": "gv"}, m.Extra)
|
||||
|
||||
// marshals to a single flat object equal to the input map
|
||||
b, err := json.Marshal(m)
|
||||
require.NoError(t, err)
|
||||
var flat map[string]string
|
||||
require.NoError(t, json.Unmarshal(b, &flat))
|
||||
require.Equal(t, attrs, flat)
|
||||
require.NotContains(t, string(b), "Extra")
|
||||
|
||||
// round-trip back into the struct is stable
|
||||
var back PodMeta
|
||||
require.NoError(t, json.Unmarshal(b, &back))
|
||||
require.Equal(t, m, back)
|
||||
}
|
||||
|
||||
func TestMeta_EmptyMarshalsToObjectNotNull(t *testing.T) {
|
||||
b, err := json.Marshal(NewClusterMeta(nil))
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `{"k8s.cluster.name":""}`, string(b))
|
||||
|
||||
// a nil-Extra zero value still marshals its guaranteed keys, never null
|
||||
b, err = json.Marshal(ClusterMeta{})
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `{"k8s.cluster.name":""}`, string(b))
|
||||
}
|
||||
|
||||
func TestMeta_AbsentGuaranteedKeyEmitsEmptyString(t *testing.T) {
|
||||
// os.type absent from attrs -> still present as "" (accepted behavior)
|
||||
m := NewHostMeta(map[string]string{"host.name": "h1"})
|
||||
require.Equal(t, "", m.OSType)
|
||||
b, err := json.Marshal(m)
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `{"host.name":"h1","os.type":""}`, string(b))
|
||||
}
|
||||
|
||||
func TestMeta_DynamicKeyDoesNotDuplicateKnownKey(t *testing.T) {
|
||||
// a dynamic key colliding with a known key must not double-emit; the typed
|
||||
// field wins and there is exactly one occurrence on the wire.
|
||||
m := NewClusterMeta(map[string]string{"k8s.cluster.name": "c1"})
|
||||
require.Empty(t, m.Extra)
|
||||
require.Equal(t, "c1", m.ClusterName)
|
||||
b, err := json.Marshal(m)
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `{"k8s.cluster.name":"c1"}`, string(b))
|
||||
}
|
||||
|
||||
func TestMetaKeys_ExpectedSetAndOrder(t *testing.T) {
|
||||
require.Equal(t, []string{"os.type", "host.name"}, HostMetaKeys)
|
||||
require.Equal(t, []string{"k8s.node.uid", "k8s.cluster.name", "k8s.node.name"}, NodeMetaKeys)
|
||||
require.Equal(t, []string{
|
||||
"k8s.pod.uid", "k8s.pod.name", "k8s.namespace.name", "k8s.node.name",
|
||||
"k8s.deployment.name", "k8s.statefulset.name", "k8s.daemonset.name",
|
||||
"k8s.job.name", "k8s.cronjob.name", "k8s.cluster.name", "k8s.pod.start_time",
|
||||
}, PodMetaKeys)
|
||||
}
|
||||
|
||||
// TestMetaKeys_MatchStructTags guards against a field rename/reorder silently
|
||||
// changing the fetch column list: every key must correspond to a json tag on
|
||||
// the struct, and the two must be in the same order.
|
||||
func TestMetaKeys_MatchStructTags(t *testing.T) {
|
||||
cases := []struct {
|
||||
keys []string
|
||||
typ reflect.Type
|
||||
}{
|
||||
{HostMetaKeys, reflect.TypeOf(HostMeta{})},
|
||||
{PodMetaKeys, reflect.TypeOf(PodMeta{})},
|
||||
{ContainerMetaKeys, reflect.TypeOf(ContainerMeta{})},
|
||||
{NodeMetaKeys, reflect.TypeOf(NodeMeta{})},
|
||||
{NamespaceMetaKeys, reflect.TypeOf(NamespaceMeta{})},
|
||||
{ClusterMetaKeys, reflect.TypeOf(ClusterMeta{})},
|
||||
{DeploymentMetaKeys, reflect.TypeOf(DeploymentMeta{})},
|
||||
{DaemonSetMetaKeys, reflect.TypeOf(DaemonSetMeta{})},
|
||||
{StatefulSetMetaKeys, reflect.TypeOf(StatefulSetMeta{})},
|
||||
{JobMetaKeys, reflect.TypeOf(JobMeta{})},
|
||||
{VolumeMetaKeys, reflect.TypeOf(VolumeMeta{})},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.typ.Name(), func(t *testing.T) {
|
||||
var tagKeys []string
|
||||
for i := range c.typ.NumField() {
|
||||
tag := c.typ.Field(i).Tag.Get("json")
|
||||
if tag == "" || tag == "-" {
|
||||
continue
|
||||
}
|
||||
tagKeys = append(tagKeys, tag)
|
||||
}
|
||||
require.Equal(t, tagKeys, c.keys)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMeta_SchemaShape locks the OpenAPI contract the frontend depends on: known
|
||||
// keys as properties + all required + additionalProperties:{type:string}.
|
||||
func TestMeta_SchemaShape(t *testing.T) {
|
||||
r := jsonschema.Reflector{}
|
||||
s, err := r.Reflect(PodMeta{})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, PodMetaKeys, s.Required)
|
||||
require.Len(t, s.Properties, len(PodMetaKeys))
|
||||
for _, k := range PodMetaKeys {
|
||||
prop, ok := s.Properties[k]
|
||||
require.True(t, ok, "missing property %s", k)
|
||||
require.NotNil(t, prop.TypeObject)
|
||||
require.Equal(t, jsonschema.String.Type(), *prop.TypeObject.Type)
|
||||
}
|
||||
require.NotNil(t, s.AdditionalProperties)
|
||||
require.NotNil(t, s.AdditionalProperties.TypeObject)
|
||||
require.Equal(t, jsonschema.String.Type(), *s.AdditionalProperties.TypeObject.Type)
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
)
|
||||
|
||||
type Namespaces struct {
|
||||
@@ -28,7 +30,7 @@ type NamespaceRecord struct {
|
||||
Jobs int64 `json:"jobs" required:"true"`
|
||||
StatefulSets int64 `json:"statefulSets" required:"true"`
|
||||
} `json:"counts" required:"true"`
|
||||
Meta map[string]string `json:"meta" required:"true"`
|
||||
Meta NamespaceMeta `json:"meta" required:"true"`
|
||||
}
|
||||
|
||||
// PostableNamespaces is the request body for the v2 namespaces list API.
|
||||
@@ -106,3 +108,37 @@ func (req *PostableNamespaces) UnmarshalJSON(data []byte) error {
|
||||
*req = PostableNamespaces(decoded)
|
||||
return req.Validate()
|
||||
}
|
||||
|
||||
// NamespaceMeta carries the guaranteed namespace metadata keys as typed fields
|
||||
// plus any dynamic group-by keys in Extra.
|
||||
type NamespaceMeta struct {
|
||||
NamespaceName string `json:"k8s.namespace.name" required:"true"`
|
||||
ClusterName string `json:"k8s.cluster.name" required:"true"`
|
||||
Extra map[string]string `json:"-"`
|
||||
}
|
||||
|
||||
var NamespaceMetaKeys = getMetaKeys(&NamespaceMeta{})
|
||||
|
||||
func NewNamespaceMeta(attrs map[string]string) NamespaceMeta {
|
||||
var m NamespaceMeta
|
||||
m.Extra = populateMeta(&m, attrs)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m NamespaceMeta) MarshalJSON() ([]byte, error) {
|
||||
return marshalMeta(&m, m.Extra)
|
||||
}
|
||||
|
||||
func (m *NamespaceMeta) UnmarshalJSON(data []byte) error {
|
||||
raw, err := splitMeta(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Extra = populateMeta(m, raw)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (NamespaceMeta) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
addStringAdditionalProps(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
)
|
||||
|
||||
type Nodes struct {
|
||||
@@ -33,7 +35,7 @@ type NodeRecord struct {
|
||||
NodeCPUAllocatable float64 `json:"nodeCPUAllocatable" required:"true"`
|
||||
NodeMemory float64 `json:"nodeMemory" required:"true"`
|
||||
NodeMemoryAllocatable float64 `json:"nodeMemoryAllocatable" required:"true"`
|
||||
Meta map[string]string `json:"meta" required:"true"`
|
||||
Meta NodeMeta `json:"meta" required:"true"`
|
||||
}
|
||||
|
||||
// PostableNodes is the request body for the v2 nodes list API.
|
||||
@@ -111,3 +113,38 @@ func (req *PostableNodes) UnmarshalJSON(data []byte) error {
|
||||
*req = PostableNodes(decoded)
|
||||
return req.Validate()
|
||||
}
|
||||
|
||||
// NodeMeta carries the guaranteed node metadata keys as typed fields plus any
|
||||
// dynamic group-by keys in Extra.
|
||||
type NodeMeta struct {
|
||||
NodeUID string `json:"k8s.node.uid" required:"true"`
|
||||
ClusterName string `json:"k8s.cluster.name" required:"true"`
|
||||
NodeName string `json:"k8s.node.name" required:"true"`
|
||||
Extra map[string]string `json:"-"`
|
||||
}
|
||||
|
||||
var NodeMetaKeys = getMetaKeys(&NodeMeta{})
|
||||
|
||||
func NewNodeMeta(attrs map[string]string) NodeMeta {
|
||||
var m NodeMeta
|
||||
m.Extra = populateMeta(&m, attrs)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m NodeMeta) MarshalJSON() ([]byte, error) {
|
||||
return marshalMeta(&m, m.Extra)
|
||||
}
|
||||
|
||||
func (m *NodeMeta) UnmarshalJSON(data []byte) error {
|
||||
raw, err := splitMeta(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Extra = populateMeta(m, raw)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (NodeMeta) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
addStringAdditionalProps(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
)
|
||||
|
||||
type Pods struct {
|
||||
@@ -68,7 +70,7 @@ type PodRecord struct {
|
||||
PodCountsByStatus PodCountsByStatus `json:"podCountsByStatus" required:"true"`
|
||||
PodRestarts int64 `json:"podRestarts" required:"true"`
|
||||
PodAge int64 `json:"podAge" required:"true"`
|
||||
Meta map[string]string `json:"meta" required:"true"`
|
||||
Meta PodMeta `json:"meta" required:"true"`
|
||||
}
|
||||
|
||||
// PostablePods is the request body for the v2 pods list API.
|
||||
@@ -146,3 +148,46 @@ func (req *PostablePods) UnmarshalJSON(data []byte) error {
|
||||
*req = PostablePods(decoded)
|
||||
return req.Validate()
|
||||
}
|
||||
|
||||
// PodMeta carries the guaranteed pod metadata keys as typed fields plus any
|
||||
// dynamic group-by keys in Extra.
|
||||
type PodMeta struct {
|
||||
PodUID string `json:"k8s.pod.uid" required:"true"`
|
||||
PodName string `json:"k8s.pod.name" required:"true"`
|
||||
NamespaceName string `json:"k8s.namespace.name" required:"true"`
|
||||
NodeName string `json:"k8s.node.name" required:"true"`
|
||||
DeploymentName string `json:"k8s.deployment.name" required:"true"`
|
||||
StatefulSetName string `json:"k8s.statefulset.name" required:"true"`
|
||||
DaemonSetName string `json:"k8s.daemonset.name" required:"true"`
|
||||
JobName string `json:"k8s.job.name" required:"true"`
|
||||
CronJobName string `json:"k8s.cronjob.name" required:"true"`
|
||||
ClusterName string `json:"k8s.cluster.name" required:"true"`
|
||||
PodStartTime string `json:"k8s.pod.start_time" required:"true"`
|
||||
Extra map[string]string `json:"-"`
|
||||
}
|
||||
|
||||
var PodMetaKeys = getMetaKeys(&PodMeta{})
|
||||
|
||||
func NewPodMeta(attrs map[string]string) PodMeta {
|
||||
var m PodMeta
|
||||
m.Extra = populateMeta(&m, attrs)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m PodMeta) MarshalJSON() ([]byte, error) {
|
||||
return marshalMeta(&m, m.Extra)
|
||||
}
|
||||
|
||||
func (m *PodMeta) UnmarshalJSON(data []byte) error {
|
||||
raw, err := splitMeta(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Extra = populateMeta(m, raw)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (PodMeta) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
addStringAdditionalProps(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
)
|
||||
|
||||
type StatefulSets struct {
|
||||
@@ -28,7 +30,7 @@ type StatefulSetRecord struct {
|
||||
CurrentPods int `json:"currentPods" required:"true"`
|
||||
PodCountsByPhase PodCountsByPhase `json:"podCountsByPhase" required:"true"`
|
||||
PodCountsByStatus PodCountsByStatus `json:"podCountsByStatus" required:"true"`
|
||||
Meta map[string]string `json:"meta" required:"true"`
|
||||
Meta StatefulSetMeta `json:"meta" required:"true"`
|
||||
}
|
||||
|
||||
// PostableStatefulSets is the request body for the v2 statefulsets list API.
|
||||
@@ -106,3 +108,38 @@ func (req *PostableStatefulSets) UnmarshalJSON(data []byte) error {
|
||||
*req = PostableStatefulSets(decoded)
|
||||
return req.Validate()
|
||||
}
|
||||
|
||||
// StatefulSetMeta carries the guaranteed statefulset metadata keys as typed
|
||||
// fields plus any dynamic group-by keys in Extra.
|
||||
type StatefulSetMeta struct {
|
||||
StatefulSetName string `json:"k8s.statefulset.name" required:"true"`
|
||||
NamespaceName string `json:"k8s.namespace.name" required:"true"`
|
||||
ClusterName string `json:"k8s.cluster.name" required:"true"`
|
||||
Extra map[string]string `json:"-"`
|
||||
}
|
||||
|
||||
var StatefulSetMetaKeys = getMetaKeys(&StatefulSetMeta{})
|
||||
|
||||
func NewStatefulSetMeta(attrs map[string]string) StatefulSetMeta {
|
||||
var m StatefulSetMeta
|
||||
m.Extra = populateMeta(&m, attrs)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m StatefulSetMeta) MarshalJSON() ([]byte, error) {
|
||||
return marshalMeta(&m, m.Extra)
|
||||
}
|
||||
|
||||
func (m *StatefulSetMeta) UnmarshalJSON(data []byte) error {
|
||||
raw, err := splitMeta(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Extra = populateMeta(m, raw)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (StatefulSetMeta) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
addStringAdditionalProps(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
)
|
||||
|
||||
type Volumes struct {
|
||||
@@ -17,14 +19,14 @@ type Volumes struct {
|
||||
}
|
||||
|
||||
type VolumeRecord struct {
|
||||
PersistentVolumeClaimName string `json:"persistentVolumeClaimName" required:"true"`
|
||||
VolumeAvailable float64 `json:"volumeAvailable" required:"true"`
|
||||
VolumeCapacity float64 `json:"volumeCapacity" required:"true"`
|
||||
VolumeUsage float64 `json:"volumeUsage" required:"true"`
|
||||
VolumeInodes float64 `json:"volumeInodes" required:"true"`
|
||||
VolumeInodesFree float64 `json:"volumeInodesFree" required:"true"`
|
||||
VolumeInodesUsed float64 `json:"volumeInodesUsed" required:"true"`
|
||||
Meta map[string]string `json:"meta" required:"true"`
|
||||
PersistentVolumeClaimName string `json:"persistentVolumeClaimName" required:"true"`
|
||||
VolumeAvailable float64 `json:"volumeAvailable" required:"true"`
|
||||
VolumeCapacity float64 `json:"volumeCapacity" required:"true"`
|
||||
VolumeUsage float64 `json:"volumeUsage" required:"true"`
|
||||
VolumeInodes float64 `json:"volumeInodes" required:"true"`
|
||||
VolumeInodesFree float64 `json:"volumeInodesFree" required:"true"`
|
||||
VolumeInodesUsed float64 `json:"volumeInodesUsed" required:"true"`
|
||||
Meta VolumeMeta `json:"meta" required:"true"`
|
||||
}
|
||||
|
||||
// PostableVolumes is the request body for the v2 volumes (PVCs) list API.
|
||||
@@ -102,3 +104,42 @@ func (req *PostableVolumes) UnmarshalJSON(data []byte) error {
|
||||
*req = PostableVolumes(decoded)
|
||||
return req.Validate()
|
||||
}
|
||||
|
||||
// VolumeMeta carries the guaranteed volume (PVC) metadata keys as typed fields
|
||||
// plus any dynamic group-by keys in Extra.
|
||||
type VolumeMeta struct {
|
||||
PVCName string `json:"k8s.persistentvolumeclaim.name" required:"true"`
|
||||
PodUID string `json:"k8s.pod.uid" required:"true"`
|
||||
PodName string `json:"k8s.pod.name" required:"true"`
|
||||
NamespaceName string `json:"k8s.namespace.name" required:"true"`
|
||||
NodeName string `json:"k8s.node.name" required:"true"`
|
||||
StatefulSetName string `json:"k8s.statefulset.name" required:"true"`
|
||||
ClusterName string `json:"k8s.cluster.name" required:"true"`
|
||||
Extra map[string]string `json:"-"`
|
||||
}
|
||||
|
||||
var VolumeMetaKeys = getMetaKeys(&VolumeMeta{})
|
||||
|
||||
func NewVolumeMeta(attrs map[string]string) VolumeMeta {
|
||||
var m VolumeMeta
|
||||
m.Extra = populateMeta(&m, attrs)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m VolumeMeta) MarshalJSON() ([]byte, error) {
|
||||
return marshalMeta(&m, m.Extra)
|
||||
}
|
||||
|
||||
func (m *VolumeMeta) UnmarshalJSON(data []byte) error {
|
||||
raw, err := splitMeta(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Extra = populateMeta(m, raw)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (VolumeMeta) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
addStringAdditionalProps(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
7
tests/fixtures/clickhouse.py
vendored
7
tests/fixtures/clickhouse.py
vendored
@@ -203,9 +203,9 @@ def create_clickhouse( # pylint: disable=too-many-arguments,too-many-positional
|
||||
pytestconfig: pytest.Config,
|
||||
cache_key: str = "clickhouse",
|
||||
) -> types.TestContainerClickhouse:
|
||||
coordinator = next(iter(keeper.container_configs.values()))
|
||||
|
||||
def create() -> types.TestContainerClickhouse:
|
||||
# Lazy: the keeper fixture is empty under --teardown (never created).
|
||||
coordinator = next(iter(keeper.container_configs.values()))
|
||||
clickhouse_version = request.config.getoption("--clickhouse-version")
|
||||
|
||||
container = ClickHouseContainer(
|
||||
@@ -381,9 +381,10 @@ def create_clickhouse_cluster( # pylint: disable=too-many-arguments,too-many-po
|
||||
migration goes through. Per-node containers are exposed via `nodes` so
|
||||
tests can assert shard-local state.
|
||||
"""
|
||||
coordinator = next(iter(keeper.container_configs.values()))
|
||||
|
||||
def create() -> types.TestContainerClickhouse:
|
||||
# Lazy: the keeper fixture is empty under --teardown (never created).
|
||||
coordinator = next(iter(keeper.container_configs.values()))
|
||||
clickhouse_version = request.config.getoption("--clickhouse-version")
|
||||
|
||||
# Unique aliases per creation: docker allows duplicate network aliases
|
||||
|
||||
29
tests/fixtures/network.py
vendored
29
tests/fixtures/network.py
vendored
@@ -1,3 +1,5 @@
|
||||
import time
|
||||
|
||||
import docker
|
||||
import docker.errors
|
||||
import pytest
|
||||
@@ -23,12 +25,37 @@ def network(request: pytest.FixtureRequest, pytestconfig: pytest.Config) -> type
|
||||
def delete(nw: types.Network):
|
||||
client = docker.from_env()
|
||||
try:
|
||||
client.networks.get(network_id=nw.id).remove()
|
||||
network = client.networks.get(network_id=nw.id)
|
||||
except docker.errors.NotFound:
|
||||
logger.info(
|
||||
"Skipping removal of Network, Network(%s) not found. Maybe it was manually removed?",
|
||||
{"name": nw.name, "id": nw.id},
|
||||
)
|
||||
return
|
||||
|
||||
# Docker detaches endpoints asynchronously, so the network can briefly
|
||||
# report "has active endpoints" after its containers are gone. Retry,
|
||||
# force-disconnecting any stragglers.
|
||||
last_err: docker.errors.APIError | None = None
|
||||
for _ in range(10):
|
||||
try:
|
||||
network.remove()
|
||||
return
|
||||
except docker.errors.NotFound:
|
||||
return
|
||||
except docker.errors.APIError as err:
|
||||
if "has active endpoints" not in str(err):
|
||||
raise
|
||||
last_err = err
|
||||
network.reload()
|
||||
for container_id in network.attrs.get("Containers") or {}:
|
||||
try:
|
||||
network.disconnect(container_id, force=True)
|
||||
except docker.errors.APIError:
|
||||
pass
|
||||
time.sleep(1)
|
||||
|
||||
raise last_err
|
||||
|
||||
def restore(existing: dict) -> types.Network:
|
||||
client = docker.from_env()
|
||||
|
||||
Reference in New Issue
Block a user