mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-09 14:40:38 +01:00
Compare commits
23 Commits
feat/alert
...
ns/saved-v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfade5b070 | ||
|
|
377eb89f85 | ||
|
|
228a944956 | ||
|
|
ad15653789 | ||
|
|
450685ba3e | ||
|
|
6372af75a6 | ||
|
|
dc86f88bf7 | ||
|
|
c4eff4edcd | ||
|
|
b9069afb4d | ||
|
|
40a036f2b0 | ||
|
|
d752452d40 | ||
|
|
4a8a1efb03 | ||
|
|
7d2afa5a54 | ||
|
|
b579190140 | ||
|
|
6222204af7 | ||
|
|
0fcd6aeada | ||
|
|
32af113d0c | ||
|
|
61588b9303 | ||
|
|
dbc40efe24 | ||
|
|
33a428496b | ||
|
|
85bf5ce644 | ||
|
|
a654f648ee | ||
|
|
a81c7d3f97 |
1
.github/workflows/integrationci.yaml
vendored
1
.github/workflows/integrationci.yaml
vendored
@@ -61,6 +61,7 @@ jobs:
|
||||
- querierauthz
|
||||
- role
|
||||
- rootuser
|
||||
- savedview
|
||||
- serviceaccount
|
||||
- spanmapper
|
||||
- querier_json_body
|
||||
|
||||
@@ -4212,6 +4212,21 @@ components:
|
||||
- missingOptionalMetrics
|
||||
- missingRequiredAttributes
|
||||
type: object
|
||||
InframonitoringtypesClusterFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByNodeReadiness:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesNodeCondition'
|
||||
nullable: true
|
||||
type: array
|
||||
filterByPodStatus:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
InframonitoringtypesClusterRecord:
|
||||
properties:
|
||||
clusterCPU:
|
||||
@@ -4349,6 +4364,16 @@ components:
|
||||
- containerCannotRun
|
||||
- unknown
|
||||
type: object
|
||||
InframonitoringtypesContainerFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByContainerStatus:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesContainerStatus'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
InframonitoringtypesContainerReady:
|
||||
enum:
|
||||
- ready
|
||||
@@ -4448,6 +4473,16 @@ components:
|
||||
- total
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesDaemonSetFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByPodStatus:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
InframonitoringtypesDaemonSetRecord:
|
||||
properties:
|
||||
currentNodes:
|
||||
@@ -4520,6 +4555,16 @@ components:
|
||||
- total
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesDeploymentFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByPodStatus:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
InframonitoringtypesDeploymentRecord:
|
||||
properties:
|
||||
availablePods:
|
||||
@@ -4661,6 +4706,16 @@ components:
|
||||
- total
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesJobFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByPodStatus:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
InframonitoringtypesJobRecord:
|
||||
properties:
|
||||
activePods:
|
||||
@@ -4784,6 +4839,16 @@ components:
|
||||
- message
|
||||
- documentationLink
|
||||
type: object
|
||||
InframonitoringtypesNamespaceFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByPodStatus:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
InframonitoringtypesNamespaceRecord:
|
||||
properties:
|
||||
counts:
|
||||
@@ -4865,6 +4930,21 @@ components:
|
||||
- ready
|
||||
- notReady
|
||||
type: object
|
||||
InframonitoringtypesNodeFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByNodeReadiness:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesNodeCondition'
|
||||
nullable: true
|
||||
type: array
|
||||
filterByPodStatus:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
InframonitoringtypesNodeRecord:
|
||||
properties:
|
||||
condition:
|
||||
@@ -4981,6 +5061,16 @@ components:
|
||||
- shutdown
|
||||
- unexpectedAdmissionError
|
||||
type: object
|
||||
InframonitoringtypesPodFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByPodStatus:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
InframonitoringtypesPodRecord:
|
||||
properties:
|
||||
meta:
|
||||
@@ -5080,7 +5170,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesClusterFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5106,7 +5196,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesContainerFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5132,7 +5222,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesDaemonSetFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5158,7 +5248,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesDeploymentFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5210,7 +5300,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesJobFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5236,7 +5326,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesNamespaceFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5262,7 +5352,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesNodeFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5288,7 +5378,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5314,7 +5404,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesStatefulSetFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5365,6 +5455,16 @@ components:
|
||||
- list
|
||||
- grouped_list
|
||||
type: string
|
||||
InframonitoringtypesStatefulSetFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByPodStatus:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
InframonitoringtypesStatefulSetRecord:
|
||||
properties:
|
||||
currentPods:
|
||||
@@ -7780,17 +7880,20 @@ components:
|
||||
type: string
|
||||
SavedviewtypesPostableSavedView:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewData'
|
||||
generateName:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
schemaVersion:
|
||||
$ref: '#/components/schemas/SavedviewtypesSchemaVersion'
|
||||
source:
|
||||
$ref: '#/components/schemas/SavedviewtypesSource'
|
||||
spec:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
|
||||
required:
|
||||
- source
|
||||
- data
|
||||
- schemaVersion
|
||||
- spec
|
||||
type: object
|
||||
SavedviewtypesSavedView:
|
||||
properties:
|
||||
@@ -7799,14 +7902,16 @@ components:
|
||||
type: string
|
||||
createdBy:
|
||||
type: string
|
||||
data:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewData'
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
schemaVersion:
|
||||
$ref: '#/components/schemas/SavedviewtypesSchemaVersion'
|
||||
source:
|
||||
$ref: '#/components/schemas/SavedviewtypesSource'
|
||||
spec:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
@@ -7814,14 +7919,6 @@ components:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
SavedviewtypesSavedViewData:
|
||||
properties:
|
||||
schemaVersion:
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
|
||||
required:
|
||||
- schemaVersion
|
||||
- spec
|
||||
type: object
|
||||
@@ -7836,6 +7933,7 @@ components:
|
||||
queries:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryEnvelope'
|
||||
minItems: 1
|
||||
type: array
|
||||
selectedFields:
|
||||
items:
|
||||
@@ -7845,9 +7943,11 @@ components:
|
||||
- displayName
|
||||
- panelType
|
||||
- queries
|
||||
- selectedFields
|
||||
- display
|
||||
type: object
|
||||
SavedviewtypesSchemaVersion:
|
||||
enum:
|
||||
- v2
|
||||
type: string
|
||||
SavedviewtypesSource:
|
||||
enum:
|
||||
- traces
|
||||
@@ -7857,13 +7957,16 @@ components:
|
||||
type: string
|
||||
SavedviewtypesUpdatableSavedView:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewData'
|
||||
schemaVersion:
|
||||
$ref: '#/components/schemas/SavedviewtypesSchemaVersion'
|
||||
source:
|
||||
$ref: '#/components/schemas/SavedviewtypesSource'
|
||||
spec:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
|
||||
required:
|
||||
- source
|
||||
- data
|
||||
- schemaVersion
|
||||
- spec
|
||||
type: object
|
||||
ServiceaccounttypesDeprecatedPostableServiceAccountRole:
|
||||
properties:
|
||||
@@ -22872,6 +22975,12 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"409":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Conflict
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
|
||||
@@ -5648,6 +5648,47 @@ export interface InframonitoringtypesChecksDTO {
|
||||
type: InframonitoringtypesCheckTypeDTO;
|
||||
}
|
||||
|
||||
export enum InframonitoringtypesNodeConditionDTO {
|
||||
ready = 'ready',
|
||||
not_ready = 'not_ready',
|
||||
no_data = 'no_data',
|
||||
}
|
||||
export enum InframonitoringtypesPodStatusDTO {
|
||||
pending = 'pending',
|
||||
running = 'running',
|
||||
failed = 'failed',
|
||||
unknown = 'unknown',
|
||||
crashloopbackoff = 'crashloopbackoff',
|
||||
imagepullbackoff = 'imagepullbackoff',
|
||||
errimagepull = 'errimagepull',
|
||||
createcontainerconfigerror = 'createcontainerconfigerror',
|
||||
containercreating = 'containercreating',
|
||||
oomkilled = 'oomkilled',
|
||||
completed = 'completed',
|
||||
error = 'error',
|
||||
containercannotrun = 'containercannotrun',
|
||||
evicted = 'evicted',
|
||||
nodeaffinity = 'nodeaffinity',
|
||||
nodelost = 'nodelost',
|
||||
shutdown = 'shutdown',
|
||||
unexpectedadmissionerror = 'unexpectedadmissionerror',
|
||||
no_data = 'no_data',
|
||||
}
|
||||
export interface InframonitoringtypesClusterFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByNodeReadiness?: InframonitoringtypesNodeConditionDTO[] | null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesClusterRecordDTOCounts = {
|
||||
/**
|
||||
* @type integer
|
||||
@@ -5923,21 +5964,6 @@ export interface InframonitoringtypesContainerCountsByStatusDTO {
|
||||
waiting: number;
|
||||
}
|
||||
|
||||
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',
|
||||
@@ -5954,6 +5980,32 @@ export enum InframonitoringtypesContainerStatusDTO {
|
||||
unknown = 'unknown',
|
||||
no_data = 'no_data',
|
||||
}
|
||||
export interface InframonitoringtypesContainerFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByContainerStatus?: InframonitoringtypesContainerStatusDTO[] | null;
|
||||
}
|
||||
|
||||
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 interface InframonitoringtypesContainerRecordDTO {
|
||||
containerCountsByReady: InframonitoringtypesContainerCountsByReadyDTO;
|
||||
containerCountsByStatus: InframonitoringtypesContainerCountsByStatusDTO;
|
||||
@@ -6025,6 +6077,17 @@ export interface InframonitoringtypesContainersDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesDaemonSetFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesDaemonSetRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -6110,6 +6173,17 @@ export interface InframonitoringtypesDaemonSetsDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesDeploymentFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesDeploymentRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -6272,6 +6346,17 @@ export interface InframonitoringtypesHostsDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesJobFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesJobRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -6357,6 +6442,17 @@ export interface InframonitoringtypesJobsDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesNamespaceFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesNamespaceRecordDTOCounts = {
|
||||
/**
|
||||
* @type integer
|
||||
@@ -6433,11 +6529,21 @@ export interface InframonitoringtypesNamespacesDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export enum InframonitoringtypesNodeConditionDTO {
|
||||
ready = 'ready',
|
||||
not_ready = 'not_ready',
|
||||
no_data = 'no_data',
|
||||
export interface InframonitoringtypesNodeFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByNodeReadiness?: InframonitoringtypesNodeConditionDTO[] | null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesNodeRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -6499,6 +6605,17 @@ export interface InframonitoringtypesNodesDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesPodFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesPodRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -6509,27 +6626,6 @@ export type InframonitoringtypesPodRecordDTOMetaAnyOf = {
|
||||
export type InframonitoringtypesPodRecordDTOMeta =
|
||||
InframonitoringtypesPodRecordDTOMetaAnyOf | null;
|
||||
|
||||
export enum InframonitoringtypesPodStatusDTO {
|
||||
pending = 'pending',
|
||||
running = 'running',
|
||||
failed = 'failed',
|
||||
unknown = 'unknown',
|
||||
crashloopbackoff = 'crashloopbackoff',
|
||||
imagepullbackoff = 'imagepullbackoff',
|
||||
errimagepull = 'errimagepull',
|
||||
createcontainerconfigerror = 'createcontainerconfigerror',
|
||||
containercreating = 'containercreating',
|
||||
oomkilled = 'oomkilled',
|
||||
completed = 'completed',
|
||||
error = 'error',
|
||||
containercannotrun = 'containercannotrun',
|
||||
evicted = 'evicted',
|
||||
nodeaffinity = 'nodeaffinity',
|
||||
nodelost = 'nodelost',
|
||||
shutdown = 'shutdown',
|
||||
unexpectedadmissionerror = 'unexpectedadmissionerror',
|
||||
no_data = 'no_data',
|
||||
}
|
||||
export interface InframonitoringtypesPodRecordDTO {
|
||||
/**
|
||||
* @type object,null
|
||||
@@ -6606,7 +6702,7 @@ export interface InframonitoringtypesPostableClustersDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesClusterFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6633,7 +6729,7 @@ export interface InframonitoringtypesPostableContainersDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesContainerFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6660,7 +6756,7 @@ export interface InframonitoringtypesPostableDaemonSetsDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesDaemonSetFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6687,7 +6783,7 @@ export interface InframonitoringtypesPostableDeploymentsDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesDeploymentFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6741,7 +6837,7 @@ export interface InframonitoringtypesPostableJobsDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesJobFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6768,7 +6864,7 @@ export interface InframonitoringtypesPostableNamespacesDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesNamespaceFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6795,7 +6891,7 @@ export interface InframonitoringtypesPostableNodesDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesNodeFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6822,7 +6918,7 @@ export interface InframonitoringtypesPostablePodsDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesPodFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6843,13 +6939,24 @@ export interface InframonitoringtypesPostablePodsDTO {
|
||||
start: number;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesStatefulSetFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesPostableStatefulSetsDTO {
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesStatefulSetFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -8884,8 +8991,17 @@ export enum SavedviewtypesPanelTypeDTO {
|
||||
list = 'list',
|
||||
trace = 'trace',
|
||||
}
|
||||
export enum SavedviewtypesSchemaVersionDTO {
|
||||
v2 = 'v2',
|
||||
}
|
||||
export enum SavedviewtypesSourceDTO {
|
||||
traces = 'traces',
|
||||
logs = 'logs',
|
||||
metrics = 'metrics',
|
||||
meter = 'meter',
|
||||
}
|
||||
export interface SavedviewtypesSavedViewSpecDTO {
|
||||
display: SavedviewtypesDisplayDTO;
|
||||
display?: SavedviewtypesDisplayDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -8898,25 +9014,10 @@ export interface SavedviewtypesSavedViewSpecDTO {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
selectedFields: TelemetrytypesTelemetryFieldKeyDTO[];
|
||||
selectedFields?: TelemetrytypesTelemetryFieldKeyDTO[];
|
||||
}
|
||||
|
||||
export interface SavedviewtypesSavedViewDataDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
schemaVersion: string;
|
||||
spec: SavedviewtypesSavedViewSpecDTO;
|
||||
}
|
||||
|
||||
export enum SavedviewtypesSourceDTO {
|
||||
traces = 'traces',
|
||||
logs = 'logs',
|
||||
metrics = 'metrics',
|
||||
meter = 'meter',
|
||||
}
|
||||
export interface SavedviewtypesPostableSavedViewDTO {
|
||||
data: SavedviewtypesSavedViewDataDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
@@ -8925,7 +9026,9 @@ export interface SavedviewtypesPostableSavedViewDTO {
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
schemaVersion: SavedviewtypesSchemaVersionDTO;
|
||||
source: SavedviewtypesSourceDTO;
|
||||
spec: SavedviewtypesSavedViewSpecDTO;
|
||||
}
|
||||
|
||||
export interface SavedviewtypesSavedViewDTO {
|
||||
@@ -8938,7 +9041,6 @@ export interface SavedviewtypesSavedViewDTO {
|
||||
* @type string
|
||||
*/
|
||||
createdBy?: string;
|
||||
data?: SavedviewtypesSavedViewDataDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -8947,7 +9049,9 @@ export interface SavedviewtypesSavedViewDTO {
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
schemaVersion: SavedviewtypesSchemaVersionDTO;
|
||||
source?: SavedviewtypesSourceDTO;
|
||||
spec: SavedviewtypesSavedViewSpecDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
@@ -8960,8 +9064,9 @@ export interface SavedviewtypesSavedViewDTO {
|
||||
}
|
||||
|
||||
export interface SavedviewtypesUpdatableSavedViewDTO {
|
||||
data: SavedviewtypesSavedViewDataDTO;
|
||||
schemaVersion: SavedviewtypesSchemaVersionDTO;
|
||||
source: SavedviewtypesSourceDTO;
|
||||
spec: SavedviewtypesSavedViewSpecDTO;
|
||||
}
|
||||
|
||||
export interface ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO {
|
||||
|
||||
@@ -51,7 +51,7 @@ func (provider *provider) addSavedViewRoutes(router *mux.Router) error {
|
||||
Response: new(types.Identifiable),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceSavedView.Scope(coretypes.VerbCreate)}),
|
||||
},
|
||||
|
||||
@@ -84,20 +84,38 @@ func buildClusterRecords(
|
||||
return records
|
||||
}
|
||||
|
||||
// getTopClusterGroupsAndMetadata concurrently fetches metadata + the ordering-metric
|
||||
// ranking (plus the full-scope pod-status / node-readiness keysets when filtering,
|
||||
// to intersect all).
|
||||
func (m *module) getTopClusterGroupsAndMetadata(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableClusters,
|
||||
) ([]map[string]string, map[string]map[string]string, error) {
|
||||
) ([]map[string]string, map[string]map[string]string, map[string]podStatusCounts, *qbtypes.QueryWarnData, map[string]nodeConditionCounts, error) {
|
||||
|
||||
var (
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
statusCounts map[string]podStatusCounts
|
||||
statusWarning *qbtypes.QueryWarnData
|
||||
nodeConditionCounts map[string]nodeConditionCounts
|
||||
filter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
filterByNodeReadiness []inframonitoringtypes.NodeCondition
|
||||
)
|
||||
|
||||
orderByKey = req.OrderBy.Key.Name
|
||||
|
||||
// When filtering by pod status / node readiness, resolve the full-scope
|
||||
// keyset(s) concurrently (pageGroups=nil spans all groups under the user
|
||||
// filter) to intersect metadata + ranked groups below. Filters compose as AND.
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
filterByNodeReadiness = req.Filter.FilterByNodeReadiness
|
||||
}
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -106,12 +124,37 @@ func (m *module) getTopClusterGroupsAndMetadata(
|
||||
return err
|
||||
})
|
||||
|
||||
if len(filterByPodStatus) != 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByPodStatus)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if len(filterByNodeReadiness) != 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
nodeConditionCounts, err = m.getPerGroupNodeConditionCounts(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByNodeReadiness)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if orderByKey == inframonitoringtypes.ClusterNameAttrKey {
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
// Secondary filter: keep only status/readiness-matching groups. A missing
|
||||
// metric yields an empty statusCounts, so this correctly empties the result
|
||||
// (the caller also surfaces the warning). Filters compose as AND.
|
||||
if len(filterByPodStatus) != 0 {
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
if len(filterByNodeReadiness) != 0 {
|
||||
metadataMap = intersectMap(metadataMap, nodeConditionCounts)
|
||||
}
|
||||
pageGroups := inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.ClusterNameAttrKey)
|
||||
return pageGroups, metadataMap, nil
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nodeConditionCounts, nil
|
||||
}
|
||||
|
||||
queryNamesForOrderBy := orderByToClustersQueryNames[orderByKey]
|
||||
@@ -157,10 +200,23 @@ func (m *module) getTopClusterGroupsAndMetadata(
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), metadataMap, nil
|
||||
// Secondary filter: intersect ranked groups + metadata with the status/readiness
|
||||
// keyset. A missing metric yields an empty keyset, correctly emptying the result
|
||||
// (the caller also surfaces the warning). Filters compose as AND.
|
||||
if len(filterByPodStatus) != 0 {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, statusCounts)
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
if len(filterByNodeReadiness) != 0 {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, nodeConditionCounts)
|
||||
metadataMap = intersectMap(metadataMap, nodeConditionCounts)
|
||||
}
|
||||
|
||||
pageGroups := paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit)
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nodeConditionCounts, nil
|
||||
}
|
||||
|
||||
func (m *module) getClustersTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableClusters) (map[string]map[string]string, error) {
|
||||
@@ -170,5 +226,9 @@ func (m *module) getClustersTableMetadata(ctx context.Context, orgID valuer.UUID
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, clustersTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
|
||||
var filter *qbtypes.Filter
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, clustersTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
|
||||
}
|
||||
|
||||
@@ -139,20 +139,34 @@ func buildContainerRecords(
|
||||
return records
|
||||
}
|
||||
|
||||
// getTopContainerGroupsAndMetadata concurrently fetches metadata + the ordering-metric
|
||||
// ranking (plus the full-scope container-status keyset when filtering, to intersect both).
|
||||
func (m *module) getTopContainerGroupsAndMetadata(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableContainers,
|
||||
) ([]map[string]string, map[string]map[string]string, error) {
|
||||
) ([]map[string]string, map[string]map[string]string, map[string]containerStatusCounts, *qbtypes.QueryWarnData, error) {
|
||||
|
||||
var (
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
statusCounts map[string]containerStatusCounts
|
||||
statusWarning *qbtypes.QueryWarnData
|
||||
filter *qbtypes.Filter
|
||||
filterByContainerStatus []inframonitoringtypes.ContainerStatus
|
||||
)
|
||||
|
||||
orderByKey = req.OrderBy.Key.Name
|
||||
|
||||
// When filtering by container status, resolve the full-scope status keyset
|
||||
// concurrently (pageGroups=nil spans all groups under the user filter) so it
|
||||
// can intersect metadata + ranked groups below.
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
filterByContainerStatus = req.Filter.FilterByContainerStatus
|
||||
}
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -161,12 +175,26 @@ func (m *module) getTopContainerGroupsAndMetadata(
|
||||
return err
|
||||
})
|
||||
|
||||
if len(filterByContainerStatus) != 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
statusCounts, statusWarning, err = m.getPerGroupContainerStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByContainerStatus)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if orderByKey == inframonitoringtypes.ContainerNameAttrKey {
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
// Secondary filter: keep only status-matching groups. A missing metric
|
||||
// yields an empty statusCounts, so this correctly empties the result
|
||||
// (the caller also surfaces the warning).
|
||||
if len(filterByContainerStatus) != 0 {
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
pageGroups := inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.ContainerNameAttrKey)
|
||||
return pageGroups, metadataMap, nil
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nil
|
||||
}
|
||||
|
||||
queryNamesForOrderBy := orderByToContainersQueryNames[orderByKey]
|
||||
@@ -212,10 +240,19 @@ func (m *module) getTopContainerGroupsAndMetadata(
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), metadataMap, nil
|
||||
// Secondary filter: intersect ranked groups + metadata with the status keyset.
|
||||
// A missing metric yields an empty statusCounts, correctly emptying the result
|
||||
// (the caller also surfaces the warning).
|
||||
if len(filterByContainerStatus) != 0 {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, statusCounts)
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
|
||||
pageGroups := paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit)
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nil
|
||||
}
|
||||
|
||||
func (m *module) getContainersTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableContainers) (map[string]map[string]string, error) {
|
||||
@@ -225,7 +262,11 @@ func (m *module) getContainersTableMetadata(ctx context.Context, orgID valuer.UU
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, containersTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
|
||||
var filter *qbtypes.Filter
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, containersTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
|
||||
}
|
||||
|
||||
// getPerGroupContainerStatusCountsWithReqMetricChecks gates
|
||||
@@ -241,6 +282,7 @@ func (m *module) getPerGroupContainerStatusCountsWithReqMetricChecks(
|
||||
filter *qbtypes.Filter,
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
pageGroups []map[string]string,
|
||||
filterByContainerStatus []inframonitoringtypes.ContainerStatus,
|
||||
) (map[string]containerStatusCounts, *qbtypes.QueryWarnData, error) {
|
||||
present, err := m.getMetricsExistence(ctx, containerStatusMetricNamesList)
|
||||
if err != nil {
|
||||
@@ -266,13 +308,28 @@ func (m *module) getPerGroupContainerStatusCountsWithReqMetricChecks(
|
||||
return map[string]containerStatusCounts{}, warning, nil
|
||||
}
|
||||
|
||||
counts, err := m.getPerGroupContainerStatusCounts(ctx, orgID, start, end, filter, groupBy, pageGroups)
|
||||
counts, err := m.getPerGroupContainerStatusCounts(ctx, orgID, start, end, filter, groupBy, pageGroups, filterByContainerStatus)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return counts, nil, nil
|
||||
}
|
||||
|
||||
// applyContainerStatusFilter adds the display-status push-down (lower(display_status)
|
||||
// IN (...)) to the outer count builder. valuer lowercases the wire value while
|
||||
// display_status is kubectl-cased, so we compare lower() on both. No-op when the
|
||||
// requested set is empty.
|
||||
func applyContainerStatusFilter(cb *sqlbuilder.SelectBuilder, filterByContainerStatus []inframonitoringtypes.ContainerStatus) {
|
||||
if len(filterByContainerStatus) == 0 {
|
||||
return
|
||||
}
|
||||
vals := make([]string, len(filterByContainerStatus))
|
||||
for i, c := range filterByContainerStatus {
|
||||
vals[i] = c.StringValue()
|
||||
}
|
||||
cb.Where(cb.In("lower(display_status)", sqlbuilder.List(vals)))
|
||||
}
|
||||
|
||||
// getPerGroupContainerStatusCounts computes per-group counts of distinct
|
||||
// containers bucketed by their latest kubectl-style display status in window.
|
||||
// Caller must ensure the required metrics exist
|
||||
@@ -297,8 +354,11 @@ func (m *module) getPerGroupContainerStatusCounts(
|
||||
filter *qbtypes.Filter,
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
pageGroups []map[string]string,
|
||||
filterByContainerStatus []inframonitoringtypes.ContainerStatus,
|
||||
) (map[string]containerStatusCounts, error) {
|
||||
if len(pageGroups) == 0 || len(groupBy) == 0 {
|
||||
// Empty pageGroups means "span all under user filter", allowed only in
|
||||
// full-scope mode (filtering by status). Otherwise it's an empty page.
|
||||
if len(groupBy) == 0 || (len(pageGroups) == 0 && len(filterByContainerStatus) == 0) {
|
||||
return map[string]containerStatusCounts{}, nil
|
||||
}
|
||||
|
||||
@@ -482,11 +542,15 @@ func (m *module) getPerGroupContainerStatusCounts(
|
||||
countGroupBy = append(countGroupBy, col)
|
||||
}
|
||||
countSelectCols = append(countSelectCols, statusCountCols...)
|
||||
countSQL := fmt.Sprintf(
|
||||
"SELECT %s FROM container_status GROUP BY %s",
|
||||
strings.Join(countSelectCols, ", "),
|
||||
strings.Join(countGroupBy, ", "),
|
||||
)
|
||||
|
||||
// Outer count query. Built with sqlbuilder so the status push-down uses a
|
||||
// proper IN (keep only containers whose display status is in the requested set).
|
||||
countBuilder := sqlbuilder.NewSelectBuilder()
|
||||
countBuilder.Select(countSelectCols...)
|
||||
countBuilder.From("container_status")
|
||||
applyContainerStatusFilter(countBuilder, filterByContainerStatus)
|
||||
countBuilder.GroupBy(countGroupBy...)
|
||||
countSQL, countArgs := countBuilder.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
// Combine CTEs + outer. Arg order mirrors CTE declaration order.
|
||||
cteFragments := []string{
|
||||
@@ -499,7 +563,7 @@ func (m *module) getPerGroupContainerStatusCounts(
|
||||
finalSQL := querybuilder.CombineCTEs(cteFragments) + countSQL
|
||||
finalArgs := querybuilder.PrependArgs([][]any{
|
||||
stateFpsArgs, containerStateArgs, reasonFpsArgs, reasonInnerArgs,
|
||||
}, nil)
|
||||
}, countArgs)
|
||||
|
||||
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, finalSQL, finalArgs...)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestApplyContainerStatusFilter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
statuses []inframonitoringtypes.ContainerStatus
|
||||
wantWhere bool
|
||||
wantArgs []any
|
||||
}{
|
||||
{
|
||||
name: "empty set yields no clause",
|
||||
statuses: nil,
|
||||
wantWhere: false,
|
||||
wantArgs: nil,
|
||||
},
|
||||
{
|
||||
name: "single status pushes lowercased arg via IN",
|
||||
statuses: []inframonitoringtypes.ContainerStatus{inframonitoringtypes.ContainerStatusRunning},
|
||||
wantWhere: true,
|
||||
wantArgs: []any{"running"},
|
||||
},
|
||||
{
|
||||
name: "multiple statuses push lowercased args via IN",
|
||||
statuses: []inframonitoringtypes.ContainerStatus{
|
||||
inframonitoringtypes.ContainerStatusRunning,
|
||||
inframonitoringtypes.ContainerStatusCrashLoopBackOff,
|
||||
},
|
||||
wantWhere: true,
|
||||
wantArgs: []any{"running", "crashloopbackoff"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cb := sqlbuilder.NewSelectBuilder()
|
||||
cb.Select("pod_uid")
|
||||
cb.From("container_status")
|
||||
applyContainerStatusFilter(cb, tt.statuses)
|
||||
sql, args := cb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
hasWhere := strings.Contains(sql, "lower(display_status) IN (")
|
||||
assert.Equal(t, tt.wantWhere, hasWhere)
|
||||
if len(tt.wantArgs) == 0 {
|
||||
assert.Empty(t, args)
|
||||
} else {
|
||||
assert.Equal(t, tt.wantArgs, args)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -90,20 +90,34 @@ func buildDaemonSetRecords(
|
||||
return records
|
||||
}
|
||||
|
||||
// getTopDaemonSetGroupsAndMetadata concurrently fetches metadata + the ordering-metric
|
||||
// ranking (plus the full-scope pod-status keyset when filtering, to intersect both).
|
||||
func (m *module) getTopDaemonSetGroupsAndMetadata(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableDaemonSets,
|
||||
) ([]map[string]string, map[string]map[string]string, error) {
|
||||
) ([]map[string]string, map[string]map[string]string, map[string]podStatusCounts, *qbtypes.QueryWarnData, error) {
|
||||
|
||||
var (
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
statusCounts map[string]podStatusCounts
|
||||
statusWarning *qbtypes.QueryWarnData
|
||||
filter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
)
|
||||
|
||||
orderByKey = req.OrderBy.Key.Name
|
||||
|
||||
// When filtering by pod status, resolve the full-scope status keyset
|
||||
// concurrently (pageGroups=nil spans all groups under the user filter) so it
|
||||
// can intersect metadata + ranked groups below.
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
}
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -112,12 +126,26 @@ func (m *module) getTopDaemonSetGroupsAndMetadata(
|
||||
return err
|
||||
})
|
||||
|
||||
if len(filterByPodStatus) != 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByPodStatus)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if orderByKey == inframonitoringtypes.DaemonSetNameAttrKey {
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
// Secondary filter: keep only status-matching groups. A missing metric
|
||||
// yields an empty statusCounts, so this correctly empties the result
|
||||
// (the caller also surfaces the warning).
|
||||
if len(filterByPodStatus) != 0 {
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
pageGroups := inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.DaemonSetNameAttrKey)
|
||||
return pageGroups, metadataMap, nil
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nil
|
||||
}
|
||||
|
||||
queryNamesForOrderBy := orderByToDaemonSetsQueryNames[orderByKey]
|
||||
@@ -163,10 +191,19 @@ func (m *module) getTopDaemonSetGroupsAndMetadata(
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), metadataMap, nil
|
||||
// Secondary filter: intersect ranked groups + metadata with the status keyset.
|
||||
// A missing metric yields an empty statusCounts, correctly emptying the result
|
||||
// (the caller also surfaces the warning).
|
||||
if len(filterByPodStatus) != 0 {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, statusCounts)
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
|
||||
pageGroups := paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit)
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nil
|
||||
}
|
||||
|
||||
func (m *module) getDaemonSetsTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableDaemonSets) (map[string]map[string]string, error) {
|
||||
@@ -176,5 +213,9 @@ func (m *module) getDaemonSetsTableMetadata(ctx context.Context, orgID valuer.UU
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, daemonSetsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
|
||||
var filter *qbtypes.Filter
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, daemonSetsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
|
||||
}
|
||||
|
||||
@@ -82,20 +82,34 @@ func buildDeploymentRecords(
|
||||
return records
|
||||
}
|
||||
|
||||
// getTopDeploymentGroupsAndMetadata concurrently fetches metadata + the ordering-metric
|
||||
// ranking (plus the full-scope pod-status keyset when filtering, to intersect both).
|
||||
func (m *module) getTopDeploymentGroupsAndMetadata(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableDeployments,
|
||||
) ([]map[string]string, map[string]map[string]string, error) {
|
||||
) ([]map[string]string, map[string]map[string]string, map[string]podStatusCounts, *qbtypes.QueryWarnData, error) {
|
||||
|
||||
var (
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
statusCounts map[string]podStatusCounts
|
||||
statusWarning *qbtypes.QueryWarnData
|
||||
filter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
)
|
||||
|
||||
orderByKey = req.OrderBy.Key.Name
|
||||
|
||||
// When filtering by pod status, resolve the full-scope status keyset
|
||||
// concurrently (pageGroups=nil spans all groups under the user filter) so it
|
||||
// can intersect metadata + ranked groups below.
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
}
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -104,12 +118,26 @@ func (m *module) getTopDeploymentGroupsAndMetadata(
|
||||
return err
|
||||
})
|
||||
|
||||
if len(filterByPodStatus) != 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByPodStatus)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if orderByKey == inframonitoringtypes.DeploymentNameAttrKey {
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
// Secondary filter: keep only status-matching groups. A missing metric
|
||||
// yields an empty statusCounts, so this correctly empties the result
|
||||
// (the caller also surfaces the warning).
|
||||
if len(filterByPodStatus) != 0 {
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
pageGroups := inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.DeploymentNameAttrKey)
|
||||
return pageGroups, metadataMap, nil
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nil
|
||||
}
|
||||
|
||||
queryNamesForOrderBy := orderByToDeploymentsQueryNames[orderByKey]
|
||||
@@ -155,10 +183,19 @@ func (m *module) getTopDeploymentGroupsAndMetadata(
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), metadataMap, nil
|
||||
// Secondary filter: intersect ranked groups + metadata with the status keyset.
|
||||
// A missing metric yields an empty statusCounts, correctly emptying the result
|
||||
// (the caller also surfaces the warning).
|
||||
if len(filterByPodStatus) != 0 {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, statusCounts)
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
|
||||
pageGroups := paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit)
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nil
|
||||
}
|
||||
|
||||
func (m *module) getDeploymentsTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableDeployments) (map[string]map[string]string, error) {
|
||||
@@ -168,5 +205,9 @@ func (m *module) getDeploymentsTableMetadata(ctx context.Context, orgID valuer.U
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, deploymentsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
|
||||
var filter *qbtypes.Filter
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, deploymentsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
|
||||
}
|
||||
|
||||
@@ -63,6 +63,33 @@ func compositeKeyFromLabels(labels map[string]string, groupBy []qbtypes.GroupByK
|
||||
return compositeKeyFromList(parts)
|
||||
}
|
||||
|
||||
// intersectMap returns the entries of m whose key is present in keep (a new
|
||||
// map). keep's value type is irrelevant — only its keys are read — so a
|
||||
// per-group counts map (already filtered by the SQL push-down) can be passed
|
||||
// directly. Used to trim metadataMap to the status-matching groups.
|
||||
func intersectMap[V any, K any](m map[string]V, keep map[string]K) map[string]V {
|
||||
out := make(map[string]V, len(m))
|
||||
for k, v := range m {
|
||||
if _, ok := keep[k]; ok {
|
||||
out[k] = v
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// intersectRankedGroups returns the ranked groups whose compositeKey is present
|
||||
// in keep, preserving order. Keeps status-unmatched groups out of the ranked
|
||||
// page slots.
|
||||
func intersectRankedGroups[K any](groups []rankedGroup, keep map[string]K) []rankedGroup {
|
||||
out := make([]rankedGroup, 0, len(groups))
|
||||
for _, g := range groups {
|
||||
if _, ok := keep[g.compositeKey]; ok {
|
||||
out = append(out, g)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// parseAndSortGroups extracts group label maps from a ScalarData response and
|
||||
// sorts them by the ranking query's aggregation value.
|
||||
func parseAndSortGroups(
|
||||
@@ -850,8 +877,10 @@ func (m *module) getPerGroupDistinctCounts(
|
||||
valueExpr = fmt.Sprintf("(%s)", strings.Join(parts, ", "))
|
||||
}
|
||||
|
||||
// Prefix the alias so it never collides with a groupBy col alias
|
||||
// (e.g. clusters grouped by k8s.node.name, which is also counted).
|
||||
selectCols = append(selectCols,
|
||||
fmt.Sprintf("uniqExactIf(%s, %s != '') AS %s", valueExpr, extract, quoteIdentifier(attr)),
|
||||
fmt.Sprintf("uniqExactIf(%s, %s != '') AS %s", valueExpr, extract, quoteIdentifier(fmt.Sprintf("__count_%s", attr))),
|
||||
)
|
||||
}
|
||||
sb.Select(selectCols...)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func groupByKey(name string) qbtypes.GroupByKey {
|
||||
@@ -88,10 +89,7 @@ func TestIsKeyInGroupByAttrs(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := isKeyInGroupByAttrs(tt.groupByAttrs, tt.key)
|
||||
if got != tt.expectedFound {
|
||||
t.Errorf("isKeyInGroupByAttrs(%v, %q) = %v, want %v",
|
||||
tt.groupByAttrs, tt.key, got, tt.expectedFound)
|
||||
}
|
||||
assert.Equal(t, tt.expectedFound, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -156,10 +154,7 @@ func TestMergeFilterExpressions(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := mergeFilterExpressions(tt.queryFilterExpr, tt.reqFilterExpr)
|
||||
if got != tt.expected {
|
||||
t.Errorf("mergeFilterExpressions(%q, %q) = %q, want %q",
|
||||
tt.queryFilterExpr, tt.reqFilterExpr, got, tt.expected)
|
||||
}
|
||||
assert.Equal(t, tt.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -205,10 +200,7 @@ func TestCompositeKeyFromList(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := compositeKeyFromList(tt.parts)
|
||||
if got != tt.expected {
|
||||
t.Errorf("compositeKeyFromList(%v) = %q, want %q",
|
||||
tt.parts, got, tt.expected)
|
||||
}
|
||||
assert.Equal(t, tt.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -376,10 +368,81 @@ func TestCompositeKeyFromLabels(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := compositeKeyFromLabels(tt.labels, tt.groupBy)
|
||||
if got != tt.expected {
|
||||
t.Errorf("compositeKeyFromLabels(%v, %v) = %q, want %q",
|
||||
tt.labels, tt.groupBy, got, tt.expected)
|
||||
}
|
||||
assert.Equal(t, tt.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntersectMap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
m map[string]int
|
||||
keep map[string]podStatusCounts
|
||||
expected map[string]int
|
||||
}{
|
||||
{
|
||||
name: "keep subset",
|
||||
m: map[string]int{"a": 1, "b": 2, "c": 3},
|
||||
keep: map[string]podStatusCounts{"a": {}, "c": {}},
|
||||
expected: map[string]int{"a": 1, "c": 3},
|
||||
},
|
||||
{
|
||||
name: "empty keep drops everything",
|
||||
m: map[string]int{"a": 1, "b": 2},
|
||||
keep: map[string]podStatusCounts{},
|
||||
expected: map[string]int{},
|
||||
},
|
||||
{
|
||||
name: "keep key absent from m is ignored",
|
||||
m: map[string]int{"a": 1},
|
||||
keep: map[string]podStatusCounts{"a": {}, "z": {}},
|
||||
expected: map[string]int{"a": 1},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := intersectMap(tt.m, tt.keep)
|
||||
assert.Equal(t, tt.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntersectRankedGroups(t *testing.T) {
|
||||
groups := []rankedGroup{
|
||||
{compositeKey: "a", value: 3},
|
||||
{compositeKey: "b", value: 2},
|
||||
{compositeKey: "c", value: 1},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
groups []rankedGroup
|
||||
keep map[string]podStatusCounts
|
||||
expected []string // compositeKeys in order
|
||||
}{
|
||||
{
|
||||
name: "preserves order, drops non-matching",
|
||||
groups: groups,
|
||||
keep: map[string]podStatusCounts{"a": {}, "c": {}},
|
||||
expected: []string{"a", "c"},
|
||||
},
|
||||
{
|
||||
name: "empty keep drops all",
|
||||
groups: groups,
|
||||
keep: map[string]podStatusCounts{},
|
||||
expected: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := intersectRankedGroups(tt.groups, tt.keep)
|
||||
gotKeys := make([]string, 0, len(got))
|
||||
for _, g := range got {
|
||||
gotKeys = append(gotKeys, g.compositeKey)
|
||||
}
|
||||
assert.Equal(t, tt.expected, gotKeys)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,20 +90,34 @@ func buildJobRecords(
|
||||
return records
|
||||
}
|
||||
|
||||
// getTopJobGroupsAndMetadata concurrently fetches metadata + the ordering-metric
|
||||
// ranking (plus the full-scope pod-status keyset when filtering, to intersect both).
|
||||
func (m *module) getTopJobGroupsAndMetadata(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableJobs,
|
||||
) ([]map[string]string, map[string]map[string]string, error) {
|
||||
) ([]map[string]string, map[string]map[string]string, map[string]podStatusCounts, *qbtypes.QueryWarnData, error) {
|
||||
|
||||
var (
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
statusCounts map[string]podStatusCounts
|
||||
statusWarning *qbtypes.QueryWarnData
|
||||
filter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
)
|
||||
|
||||
orderByKey = req.OrderBy.Key.Name
|
||||
|
||||
// When filtering by pod status, resolve the full-scope status keyset
|
||||
// concurrently (pageGroups=nil spans all groups under the user filter) so it
|
||||
// can intersect metadata + ranked groups below.
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
}
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -112,12 +126,26 @@ func (m *module) getTopJobGroupsAndMetadata(
|
||||
return err
|
||||
})
|
||||
|
||||
if len(filterByPodStatus) != 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByPodStatus)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if orderByKey == inframonitoringtypes.JobNameAttrKey {
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
// Secondary filter: keep only status-matching groups. A missing metric
|
||||
// yields an empty statusCounts, so this correctly empties the result
|
||||
// (the caller also surfaces the warning).
|
||||
if len(filterByPodStatus) != 0 {
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
pageGroups := inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.JobNameAttrKey)
|
||||
return pageGroups, metadataMap, nil
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nil
|
||||
}
|
||||
|
||||
queryNamesForOrderBy := orderByToJobsQueryNames[orderByKey]
|
||||
@@ -163,10 +191,19 @@ func (m *module) getTopJobGroupsAndMetadata(
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), metadataMap, nil
|
||||
// Secondary filter: intersect ranked groups + metadata with the status keyset.
|
||||
// A missing metric yields an empty statusCounts, correctly emptying the result
|
||||
// (the caller also surfaces the warning).
|
||||
if len(filterByPodStatus) != 0 {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, statusCounts)
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
|
||||
pageGroups := paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit)
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nil
|
||||
}
|
||||
|
||||
func (m *module) getJobsTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableJobs) (map[string]map[string]string, error) {
|
||||
@@ -176,5 +213,9 @@ func (m *module) getJobsTableMetadata(ctx context.Context, orgID valuer.UUID, re
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, jobsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
|
||||
var filter *qbtypes.Filter
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, jobsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
|
||||
}
|
||||
|
||||
@@ -286,11 +286,36 @@ func (m *module) ListPods(ctx context.Context, orgID valuer.UUID, req *inframoni
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
pageGroups, metadataMap, err := m.getTopPodGroupsAndMetadata(ctx, orgID, req)
|
||||
var (
|
||||
filterExpr string
|
||||
podFilter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
restartCounts map[string]int64
|
||||
)
|
||||
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
podFilter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
}
|
||||
|
||||
// getTopPodGroupsAndMetadata fetches metadata + ranking (+ full-scope pod
|
||||
// status when filtering) concurrently, intersecting metadata/ranked groups
|
||||
// against the status keyset. It returns the keyset + its warning.
|
||||
pageGroups, metadataMap, statusCounts, statusWarning, err := m.getTopPodGroupsAndMetadata(ctx, orgID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Required metric missing while filtering: surface the warning + empty result.
|
||||
if len(filterByPodStatus) != 0 && statusWarning != nil {
|
||||
resp.Warning = statusWarning
|
||||
resp.Records = []inframonitoringtypes.PodRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.Total = len(metadataMap)
|
||||
|
||||
if len(pageGroups) == 0 {
|
||||
@@ -298,20 +323,8 @@ func (m *module) ListPods(ctx context.Context, orgID valuer.UUID, req *inframoni
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
filterExpr := ""
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newPodsTableListQuery())
|
||||
|
||||
var (
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
statusCounts map[string]podStatusCounts
|
||||
statusWarning *qbtypes.QueryWarnData
|
||||
restartCounts map[string]int64
|
||||
)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -321,14 +334,18 @@ func (m *module) ListPods(ctx context.Context, orgID valuer.UUID, req *inframoni
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
restartCounts, err = m.getPerGroupPodRestartCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
restartCounts, err = m.getPerGroupPodRestartCounts(gCtx, orgID, req.Start, req.End, podFilter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
// When filtering, statusCounts already holds the full-scope map (a superset
|
||||
// of the page); otherwise compute it page-scoped here.
|
||||
if len(filterByPodStatus) == 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, podFilter, req.GroupBy, pageGroups, nil)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
@@ -379,11 +396,37 @@ func (m *module) ListContainers(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
pageGroups, metadataMap, err := m.getTopContainerGroupsAndMetadata(ctx, orgID, req)
|
||||
var (
|
||||
filterExpr string
|
||||
containerFilter *qbtypes.Filter
|
||||
filterByContainerStatus []inframonitoringtypes.ContainerStatus
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
restartCounts map[string]int64
|
||||
readyCounts map[string]containerReadyCounts
|
||||
)
|
||||
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
containerFilter = &req.Filter.Filter
|
||||
filterByContainerStatus = req.Filter.FilterByContainerStatus
|
||||
}
|
||||
|
||||
// getTopContainerGroupsAndMetadata fetches metadata + ranking (+ full-scope
|
||||
// container status when filtering) concurrently, intersecting metadata/ranked
|
||||
// groups against the status keyset. It returns the keyset + its warning.
|
||||
pageGroups, metadataMap, statusCounts, statusWarning, err := m.getTopContainerGroupsAndMetadata(ctx, orgID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Required metric missing while filtering: surface the warning + empty result.
|
||||
if len(filterByContainerStatus) != 0 && statusWarning != nil {
|
||||
resp.Warning = statusWarning
|
||||
resp.Records = []inframonitoringtypes.ContainerRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.Total = len(metadataMap)
|
||||
|
||||
if len(pageGroups) == 0 {
|
||||
@@ -391,21 +434,8 @@ func (m *module) ListContainers(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
filterExpr := ""
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newContainersTableListQuery())
|
||||
|
||||
var (
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
statusCounts map[string]containerStatusCounts
|
||||
statusWarning *qbtypes.QueryWarnData
|
||||
restartCounts map[string]int64
|
||||
readyCounts map[string]containerReadyCounts
|
||||
)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -415,19 +445,23 @@ func (m *module) ListContainers(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
statusCounts, statusWarning, err = m.getPerGroupContainerStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
restartCounts, err = m.getPerGroupContainerRestartCounts(gCtx, orgID, req.Start, req.End, containerFilter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
restartCounts, err = m.getPerGroupContainerRestartCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
readyCounts, err = m.getPerGroupContainerReadyCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
readyCounts, err = m.getPerGroupContainerReadyCounts(gCtx, orgID, req.Start, req.End, containerFilter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
// When filtering, statusCounts already holds the full-scope map (a superset
|
||||
// of the page); otherwise compute it page-scoped here.
|
||||
if len(filterByContainerStatus) == 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
statusCounts, statusWarning, err = m.getPerGroupContainerStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, containerFilter, req.GroupBy, pageGroups, nil)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
@@ -478,11 +512,37 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
pageGroups, metadataMap, err := m.getTopNodeGroupsAndMetadata(ctx, orgID, req)
|
||||
var (
|
||||
filterExpr string
|
||||
nodeFilter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
filterByNodeReadiness []inframonitoringtypes.NodeCondition
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
)
|
||||
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
nodeFilter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
filterByNodeReadiness = req.Filter.FilterByNodeReadiness
|
||||
}
|
||||
|
||||
// getTopNodeGroupsAndMetadata fetches metadata + ranking (+ full-scope pod
|
||||
// status / node readiness when filtering) concurrently, intersecting
|
||||
// metadata/ranked groups against the keysets. It returns the keysets + warning.
|
||||
pageGroups, metadataMap, podStatusCounts, podStatusWarning, nodeConditionCounts, err := m.getTopNodeGroupsAndMetadata(ctx, orgID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Required metric missing while filtering: surface the warning + empty result.
|
||||
if len(filterByPodStatus) != 0 && podStatusWarning != nil {
|
||||
resp.Warning = podStatusWarning
|
||||
resp.Records = []inframonitoringtypes.NodeRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.Total = len(metadataMap)
|
||||
|
||||
if len(pageGroups) == 0 {
|
||||
@@ -490,20 +550,8 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
filterExpr := ""
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newNodesTableListQuery())
|
||||
|
||||
var (
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
nodeConditionCounts map[string]nodeConditionCounts
|
||||
podStatusCounts map[string]podStatusCounts
|
||||
podStatusWarning *qbtypes.QueryWarnData
|
||||
)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -511,16 +559,24 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
nodeConditionCounts, err = m.getPerGroupNodeConditionCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
// When filtering by readiness, nodeConditionCounts already holds the full-scope
|
||||
// map (a superset of the page); otherwise compute it page-scoped here.
|
||||
if len(filterByNodeReadiness) == 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
nodeConditionCounts, err = m.getPerGroupNodeConditionCounts(gCtx, orgID, req.Start, req.End, nodeFilter, req.GroupBy, pageGroups, nil)
|
||||
return err
|
||||
})
|
||||
}
|
||||
// When filtering by pod status, podStatusCounts already holds the full-scope
|
||||
// map; otherwise compute it page-scoped here.
|
||||
if len(filterByPodStatus) == 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, nodeFilter, req.GroupBy, pageGroups, nil)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
@@ -571,11 +627,36 @@ func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
pageGroups, metadataMap, err := m.getTopNamespaceGroupsAndMetadata(ctx, orgID, req)
|
||||
var (
|
||||
filterExpr string
|
||||
namespaceFilter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
resourceCounts map[string]map[string]int64
|
||||
)
|
||||
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
namespaceFilter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
}
|
||||
|
||||
// getTopNamespaceGroupsAndMetadata fetches metadata + ranking (+ full-scope pod
|
||||
// status when filtering) concurrently, intersecting metadata/ranked groups
|
||||
// against the status keyset. It returns the keyset + its warning.
|
||||
pageGroups, metadataMap, podStatusCounts, podStatusWarning, err := m.getTopNamespaceGroupsAndMetadata(ctx, orgID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Required metric missing while filtering: surface the warning + empty result.
|
||||
if len(filterByPodStatus) != 0 && podStatusWarning != nil {
|
||||
resp.Warning = podStatusWarning
|
||||
resp.Records = []inframonitoringtypes.NamespaceRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.Total = len(metadataMap)
|
||||
|
||||
if len(pageGroups) == 0 {
|
||||
@@ -583,20 +664,8 @@ func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
filterExpr := ""
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newNamespacesTableListQuery())
|
||||
|
||||
var (
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
podStatusCounts map[string]podStatusCounts
|
||||
podStatusWarning *qbtypes.QueryWarnData
|
||||
resourceCounts map[string]map[string]int64
|
||||
)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -606,14 +675,18 @@ func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
resourceCounts, err = m.getPerGroupDistinctCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups, namespaceCountAttrKeys, namespacesMetricNamesListForCounts)
|
||||
resourceCounts, err = m.getPerGroupDistinctCounts(gCtx, orgID, req.Start, req.End, namespaceFilter, req.GroupBy, pageGroups, namespaceCountAttrKeys, namespacesMetricNamesListForCounts)
|
||||
return err
|
||||
})
|
||||
// When filtering, podStatusCounts already holds the full-scope map (a superset
|
||||
// of the page); otherwise compute it page-scoped here.
|
||||
if len(filterByPodStatus) == 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, namespaceFilter, req.GroupBy, pageGroups, nil)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
@@ -663,11 +736,39 @@ func (m *module) ListClusters(ctx context.Context, orgID valuer.UUID, req *infra
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
pageGroups, metadataMap, err := m.getTopClusterGroupsAndMetadata(ctx, orgID, req)
|
||||
var (
|
||||
filterExpr string
|
||||
clusterFilter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
filterByNodeReadiness []inframonitoringtypes.NodeCondition
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
nodeConditionCountsMap map[string]nodeConditionCounts
|
||||
resourceCounts map[string]map[string]int64
|
||||
)
|
||||
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
clusterFilter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
filterByNodeReadiness = req.Filter.FilterByNodeReadiness
|
||||
}
|
||||
|
||||
// getTopClusterGroupsAndMetadata fetches metadata + ranking (+ full-scope pod
|
||||
// status / node readiness when filtering) concurrently, intersecting
|
||||
// metadata/ranked groups against the keysets. It returns the keysets + warning.
|
||||
pageGroups, metadataMap, podStatusCounts, podStatusWarning, nodeConditionCountsMap, err := m.getTopClusterGroupsAndMetadata(ctx, orgID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Required metric missing while filtering: surface the warning + empty result.
|
||||
if len(filterByPodStatus) != 0 && podStatusWarning != nil {
|
||||
resp.Warning = podStatusWarning
|
||||
resp.Records = []inframonitoringtypes.ClusterRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.Total = len(metadataMap)
|
||||
|
||||
if len(pageGroups) == 0 {
|
||||
@@ -675,23 +776,8 @@ func (m *module) ListClusters(ctx context.Context, orgID valuer.UUID, req *infra
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
filterExpr := ""
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newClustersTableListQuery())
|
||||
|
||||
// With default groupBy [k8s.cluster.name], counts are bucketed per cluster;
|
||||
// with a custom groupBy, they aggregate across clusters in that group.
|
||||
var (
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
nodeConditionCountsMap map[string]nodeConditionCounts
|
||||
podStatusCounts map[string]podStatusCounts
|
||||
podStatusWarning *qbtypes.QueryWarnData
|
||||
resourceCounts map[string]map[string]int64
|
||||
)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -699,21 +785,29 @@ func (m *module) ListClusters(ctx context.Context, orgID valuer.UUID, req *infra
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
// When filtering by readiness, nodeConditionCountsMap already holds the
|
||||
// full-scope map (a superset of the page); otherwise compute it page-scoped here.
|
||||
if len(filterByNodeReadiness) == 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
nodeConditionCountsMap, err = m.getPerGroupNodeConditionCounts(gCtx, orgID, req.Start, req.End, clusterFilter, req.GroupBy, pageGroups, nil)
|
||||
return err
|
||||
})
|
||||
}
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
nodeConditionCountsMap, err = m.getPerGroupNodeConditionCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
resourceCounts, err = m.getPerGroupDistinctCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups, clusterCountAttrKeys, clusterMetricNamesListForCounts)
|
||||
resourceCounts, err = m.getPerGroupDistinctCounts(gCtx, orgID, req.Start, req.End, clusterFilter, req.GroupBy, pageGroups, clusterCountAttrKeys, clusterMetricNamesListForCounts)
|
||||
return err
|
||||
})
|
||||
// When filtering by pod status, podStatusCounts already holds the full-scope
|
||||
// map; otherwise compute it page-scoped here.
|
||||
if len(filterByPodStatus) == 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, clusterFilter, req.GroupBy, pageGroups, nil)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
@@ -827,7 +921,7 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
|
||||
|
||||
// Bake the deployments base filter into req.Filter so all downstream helpers pick it up.
|
||||
if req.Filter == nil {
|
||||
req.Filter = &qbtypes.Filter{}
|
||||
req.Filter = &inframonitoringtypes.DeploymentFilter{}
|
||||
}
|
||||
req.Filter.Expression = mergeFilterExpressions(deploymentsBaseFilterExpr, req.Filter.Expression)
|
||||
|
||||
@@ -842,11 +936,35 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
pageGroups, metadataMap, err := m.getTopDeploymentGroupsAndMetadata(ctx, orgID, req)
|
||||
var (
|
||||
filterExpr string
|
||||
deploymentFilter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
)
|
||||
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
deploymentFilter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
}
|
||||
|
||||
// getTopDeploymentGroupsAndMetadata fetches metadata + ranking (+ full-scope pod
|
||||
// status when filtering) concurrently, intersecting metadata/ranked groups
|
||||
// against the status keyset. It returns the keyset + its warning.
|
||||
pageGroups, metadataMap, podStatusCounts, podStatusWarning, err := m.getTopDeploymentGroupsAndMetadata(ctx, orgID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Required metric missing while filtering: surface the warning + empty result.
|
||||
if len(filterByPodStatus) != 0 && podStatusWarning != nil {
|
||||
resp.Warning = podStatusWarning
|
||||
resp.Records = []inframonitoringtypes.DeploymentRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.Total = len(metadataMap)
|
||||
|
||||
if len(pageGroups) == 0 {
|
||||
@@ -854,19 +972,8 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
filterExpr := ""
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newDeploymentsTableListQuery())
|
||||
|
||||
var (
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
podStatusCounts map[string]podStatusCounts
|
||||
podStatusWarning *qbtypes.QueryWarnData
|
||||
)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -874,11 +981,15 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
// When filtering, podStatusCounts already holds the full-scope map (a superset
|
||||
// of the page); otherwise compute it page-scoped here.
|
||||
if len(filterByPodStatus) == 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, deploymentFilter, req.GroupBy, pageGroups, nil)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
@@ -919,7 +1030,7 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
|
||||
|
||||
// Bake the workload base filter into req.Filter so all downstream helpers pick it up.
|
||||
if req.Filter == nil {
|
||||
req.Filter = &qbtypes.Filter{}
|
||||
req.Filter = &inframonitoringtypes.StatefulSetFilter{}
|
||||
}
|
||||
req.Filter.Expression = mergeFilterExpressions(statefulSetsBaseFilterExpr, req.Filter.Expression)
|
||||
|
||||
@@ -934,11 +1045,35 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
pageGroups, metadataMap, err := m.getTopStatefulSetGroupsAndMetadata(ctx, orgID, req)
|
||||
var (
|
||||
filterExpr string
|
||||
statefulSetFilter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
)
|
||||
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
statefulSetFilter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
}
|
||||
|
||||
// getTopStatefulSetGroupsAndMetadata fetches metadata + ranking (+ full-scope pod
|
||||
// status when filtering) concurrently, intersecting metadata/ranked groups
|
||||
// against the status keyset. It returns the keyset + its warning.
|
||||
pageGroups, metadataMap, podStatusCounts, podStatusWarning, err := m.getTopStatefulSetGroupsAndMetadata(ctx, orgID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Required metric missing while filtering: surface the warning + empty result.
|
||||
if len(filterByPodStatus) != 0 && podStatusWarning != nil {
|
||||
resp.Warning = podStatusWarning
|
||||
resp.Records = []inframonitoringtypes.StatefulSetRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.Total = len(metadataMap)
|
||||
|
||||
if len(pageGroups) == 0 {
|
||||
@@ -946,21 +1081,8 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
filterExpr := ""
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newStatefulSetsTableListQuery())
|
||||
|
||||
// Pods owned by a StatefulSet carry k8s.statefulset.name as a resource attribute,
|
||||
// so default-groupBy gives per-statefulset status counts automatically.
|
||||
var (
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
podStatusCounts map[string]podStatusCounts
|
||||
podStatusWarning *qbtypes.QueryWarnData
|
||||
)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -968,11 +1090,15 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
// When filtering, podStatusCounts already holds the full-scope map (a superset
|
||||
// of the page); otherwise compute it page-scoped here.
|
||||
if len(filterByPodStatus) == 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, statefulSetFilter, req.GroupBy, pageGroups, nil)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
@@ -1013,7 +1139,7 @@ func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframoni
|
||||
|
||||
// Bake the jobs base filter into req.Filter so all downstream helpers pick it up.
|
||||
if req.Filter == nil {
|
||||
req.Filter = &qbtypes.Filter{}
|
||||
req.Filter = &inframonitoringtypes.JobFilter{}
|
||||
}
|
||||
req.Filter.Expression = mergeFilterExpressions(jobsBaseFilterExpr, req.Filter.Expression)
|
||||
|
||||
@@ -1028,11 +1154,35 @@ func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframoni
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
pageGroups, metadataMap, err := m.getTopJobGroupsAndMetadata(ctx, orgID, req)
|
||||
var (
|
||||
filterExpr string
|
||||
jobFilter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
)
|
||||
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
jobFilter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
}
|
||||
|
||||
// getTopJobGroupsAndMetadata fetches metadata + ranking (+ full-scope pod
|
||||
// status when filtering) concurrently, intersecting metadata/ranked groups
|
||||
// against the status keyset. It returns the keyset + its warning.
|
||||
pageGroups, metadataMap, podStatusCounts, podStatusWarning, err := m.getTopJobGroupsAndMetadata(ctx, orgID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Required metric missing while filtering: surface the warning + empty result.
|
||||
if len(filterByPodStatus) != 0 && podStatusWarning != nil {
|
||||
resp.Warning = podStatusWarning
|
||||
resp.Records = []inframonitoringtypes.JobRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.Total = len(metadataMap)
|
||||
|
||||
if len(pageGroups) == 0 {
|
||||
@@ -1040,21 +1190,8 @@ func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframoni
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
filterExpr := ""
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newJobsTableListQuery())
|
||||
|
||||
// Pods owned by a Job carry k8s.job.name as a resource attribute, so default-groupBy
|
||||
// gives per-job status counts automatically.
|
||||
var (
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
podStatusCounts map[string]podStatusCounts
|
||||
podStatusWarning *qbtypes.QueryWarnData
|
||||
)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -1062,11 +1199,15 @@ func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframoni
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
// When filtering, podStatusCounts already holds the full-scope map (a superset
|
||||
// of the page); otherwise compute it page-scoped here.
|
||||
if len(filterByPodStatus) == 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, jobFilter, req.GroupBy, pageGroups, nil)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
@@ -1107,7 +1248,7 @@ func (m *module) ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
|
||||
// Bake the workload base filter into req.Filter so all downstream helpers pick it up.
|
||||
if req.Filter == nil {
|
||||
req.Filter = &qbtypes.Filter{}
|
||||
req.Filter = &inframonitoringtypes.DaemonSetFilter{}
|
||||
}
|
||||
req.Filter.Expression = mergeFilterExpressions(daemonSetsBaseFilterExpr, req.Filter.Expression)
|
||||
|
||||
@@ -1122,11 +1263,35 @@ func (m *module) ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
pageGroups, metadataMap, err := m.getTopDaemonSetGroupsAndMetadata(ctx, orgID, req)
|
||||
var (
|
||||
filterExpr string
|
||||
daemonSetFilter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
)
|
||||
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
daemonSetFilter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
}
|
||||
|
||||
// getTopDaemonSetGroupsAndMetadata fetches metadata + ranking (+ full-scope pod
|
||||
// status when filtering) concurrently, intersecting metadata/ranked groups
|
||||
// against the status keyset. It returns the keyset + its warning.
|
||||
pageGroups, metadataMap, podStatusCounts, podStatusWarning, err := m.getTopDaemonSetGroupsAndMetadata(ctx, orgID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Required metric missing while filtering: surface the warning + empty result.
|
||||
if len(filterByPodStatus) != 0 && podStatusWarning != nil {
|
||||
resp.Warning = podStatusWarning
|
||||
resp.Records = []inframonitoringtypes.DaemonSetRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.Total = len(metadataMap)
|
||||
|
||||
if len(pageGroups) == 0 {
|
||||
@@ -1134,21 +1299,8 @@ func (m *module) ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
filterExpr := ""
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newDaemonSetsTableListQuery())
|
||||
|
||||
// Pods owned by a DaemonSet carry k8s.daemonset.name as a resource attribute,
|
||||
// so default-groupBy gives per-daemonset status counts automatically.
|
||||
var (
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
podStatusCounts map[string]podStatusCounts
|
||||
podStatusWarning *qbtypes.QueryWarnData
|
||||
)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -1156,11 +1308,15 @@ func (m *module) ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
// When filtering, podStatusCounts already holds the full-scope map (a superset
|
||||
// of the page); otherwise compute it page-scoped here.
|
||||
if len(filterByPodStatus) == 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, daemonSetFilter, req.GroupBy, pageGroups, nil)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -65,20 +65,34 @@ func buildNamespaceRecords(
|
||||
return records
|
||||
}
|
||||
|
||||
// getTopNamespaceGroupsAndMetadata concurrently fetches metadata + the ordering-metric
|
||||
// ranking (plus the full-scope pod-status keyset when filtering, to intersect both).
|
||||
func (m *module) getTopNamespaceGroupsAndMetadata(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableNamespaces,
|
||||
) ([]map[string]string, map[string]map[string]string, error) {
|
||||
) ([]map[string]string, map[string]map[string]string, map[string]podStatusCounts, *qbtypes.QueryWarnData, error) {
|
||||
|
||||
var (
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
statusCounts map[string]podStatusCounts
|
||||
statusWarning *qbtypes.QueryWarnData
|
||||
filter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
)
|
||||
|
||||
orderByKey = req.OrderBy.Key.Name
|
||||
|
||||
// When filtering by pod status, resolve the full-scope status keyset
|
||||
// concurrently (pageGroups=nil spans all groups under the user filter) so it
|
||||
// can intersect metadata + ranked groups below.
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
}
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -87,12 +101,26 @@ func (m *module) getTopNamespaceGroupsAndMetadata(
|
||||
return err
|
||||
})
|
||||
|
||||
if len(filterByPodStatus) != 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByPodStatus)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if orderByKey == inframonitoringtypes.NamespaceNameAttrKey {
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
// Secondary filter: keep only status-matching groups. A missing metric
|
||||
// yields an empty statusCounts, so this correctly empties the result
|
||||
// (the caller also surfaces the warning).
|
||||
if len(filterByPodStatus) != 0 {
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
pageGroups := inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.NamespaceNameAttrKey)
|
||||
return pageGroups, metadataMap, nil
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nil
|
||||
}
|
||||
|
||||
queryNamesForOrderBy := orderByToNamespacesQueryNames[orderByKey]
|
||||
@@ -138,10 +166,19 @@ func (m *module) getTopNamespaceGroupsAndMetadata(
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), metadataMap, nil
|
||||
// Secondary filter: intersect ranked groups + metadata with the status keyset.
|
||||
// A missing metric yields an empty statusCounts, correctly emptying the result
|
||||
// (the caller also surfaces the warning).
|
||||
if len(filterByPodStatus) != 0 {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, statusCounts)
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
|
||||
pageGroups := paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit)
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nil
|
||||
}
|
||||
|
||||
func (m *module) getNamespacesTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableNamespaces) (map[string]map[string]string, error) {
|
||||
@@ -151,5 +188,9 @@ func (m *module) getNamespacesTableMetadata(ctx context.Context, orgID valuer.UU
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, namespacesTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
|
||||
var filter *qbtypes.Filter
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, namespacesTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/telemetryschema/metricstelemetryschema"
|
||||
@@ -92,20 +91,38 @@ func buildNodeRecords(
|
||||
return records
|
||||
}
|
||||
|
||||
// getTopNodeGroupsAndMetadata concurrently fetches metadata + the ordering-metric
|
||||
// ranking (plus the full-scope pod-status / node-readiness keysets when filtering,
|
||||
// to intersect all).
|
||||
func (m *module) getTopNodeGroupsAndMetadata(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableNodes,
|
||||
) ([]map[string]string, map[string]map[string]string, error) {
|
||||
) ([]map[string]string, map[string]map[string]string, map[string]podStatusCounts, *qbtypes.QueryWarnData, map[string]nodeConditionCounts, error) {
|
||||
|
||||
var (
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
statusCounts map[string]podStatusCounts
|
||||
statusWarning *qbtypes.QueryWarnData
|
||||
nodeConditionCounts map[string]nodeConditionCounts
|
||||
filter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
filterByNodeReadiness []inframonitoringtypes.NodeCondition
|
||||
)
|
||||
|
||||
orderByKey = req.OrderBy.Key.Name
|
||||
|
||||
// When filtering by pod status / node readiness, resolve the full-scope
|
||||
// keyset(s) concurrently (pageGroups=nil spans all groups under the user
|
||||
// filter) to intersect metadata + ranked groups below. Filters compose as AND.
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
filterByNodeReadiness = req.Filter.FilterByNodeReadiness
|
||||
}
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -114,12 +131,37 @@ func (m *module) getTopNodeGroupsAndMetadata(
|
||||
return err
|
||||
})
|
||||
|
||||
if len(filterByPodStatus) != 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByPodStatus)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if len(filterByNodeReadiness) != 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
nodeConditionCounts, err = m.getPerGroupNodeConditionCounts(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByNodeReadiness)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if orderByKey == inframonitoringtypes.NodeNameAttrKey {
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
// Secondary filter: keep only status/readiness-matching groups. A missing
|
||||
// metric yields an empty statusCounts, so this correctly empties the result
|
||||
// (the caller also surfaces the warning). Filters compose as AND.
|
||||
if len(filterByPodStatus) != 0 {
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
if len(filterByNodeReadiness) != 0 {
|
||||
metadataMap = intersectMap(metadataMap, nodeConditionCounts)
|
||||
}
|
||||
pageGroups := inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.NodeNameAttrKey)
|
||||
return pageGroups, metadataMap, nil
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nodeConditionCounts, nil
|
||||
}
|
||||
|
||||
queryNamesForOrderBy := orderByToNodesQueryNames[orderByKey]
|
||||
@@ -165,10 +207,23 @@ func (m *module) getTopNodeGroupsAndMetadata(
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), metadataMap, nil
|
||||
// Secondary filter: intersect ranked groups + metadata with the status/readiness
|
||||
// keyset. A missing metric yields an empty keyset, correctly emptying the result
|
||||
// (the caller also surfaces the warning). Filters compose as AND.
|
||||
if len(filterByPodStatus) != 0 {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, statusCounts)
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
if len(filterByNodeReadiness) != 0 {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, nodeConditionCounts)
|
||||
metadataMap = intersectMap(metadataMap, nodeConditionCounts)
|
||||
}
|
||||
|
||||
pageGroups := paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit)
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nodeConditionCounts, nil
|
||||
}
|
||||
|
||||
func (m *module) getNodesTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableNodes) (map[string]map[string]string, error) {
|
||||
@@ -178,7 +233,11 @@ func (m *module) getNodesTableMetadata(ctx context.Context, orgID valuer.UUID, r
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, nodesTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
|
||||
var filter *qbtypes.Filter
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, nodesTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
|
||||
}
|
||||
|
||||
// getPerGroupNodeConditionCounts computes per-group node counts bucketed by each
|
||||
@@ -192,6 +251,24 @@ func (m *module) getNodesTableMetadata(ctx context.Context, orgID valuer.UUID, r
|
||||
// countNodesPerCondition: per-group uniqExactIf into ready/not_ready buckets.
|
||||
//
|
||||
// Groups absent from the result map have implicit zero counts (caller default).
|
||||
// applyNodeReadinessFilter adds the readiness push-down (condition_value IN (...))
|
||||
// to the outer count builder. condition_value is numeric (1=Ready, 0=NotReady), so
|
||||
// we map each requested enum to its int. No-op when the requested set is empty.
|
||||
func applyNodeReadinessFilter(cb *sqlbuilder.SelectBuilder, filterByNodeReadiness []inframonitoringtypes.NodeCondition) {
|
||||
if len(filterByNodeReadiness) == 0 {
|
||||
return
|
||||
}
|
||||
nums := make([]int, len(filterByNodeReadiness))
|
||||
for i, c := range filterByNodeReadiness {
|
||||
v := inframonitoringtypes.NodeConditionNumNotReady
|
||||
if c == inframonitoringtypes.NodeConditionReady {
|
||||
v = inframonitoringtypes.NodeConditionNumReady
|
||||
}
|
||||
nums[i] = v
|
||||
}
|
||||
cb.Where(cb.In("condition_value", sqlbuilder.List(nums)))
|
||||
}
|
||||
|
||||
func (m *module) getPerGroupNodeConditionCounts(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
@@ -199,8 +276,11 @@ func (m *module) getPerGroupNodeConditionCounts(
|
||||
filter *qbtypes.Filter,
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
pageGroups []map[string]string,
|
||||
filterByNodeReadiness []inframonitoringtypes.NodeCondition,
|
||||
) (map[string]nodeConditionCounts, error) {
|
||||
if len(pageGroups) == 0 || len(groupBy) == 0 {
|
||||
// Empty pageGroups means "span all under user filter", allowed only in
|
||||
// full-scope mode (filtering by readiness). Otherwise it's an empty page.
|
||||
if len(groupBy) == 0 || (len(pageGroups) == 0 && len(filterByNodeReadiness) == 0) {
|
||||
return map[string]nodeConditionCounts{}, nil
|
||||
}
|
||||
|
||||
@@ -288,11 +368,14 @@ func (m *module) getPerGroupNodeConditionCounts(
|
||||
fmt.Sprintf("uniqExactIf(node_name, condition_value = %d) AS ready_count", inframonitoringtypes.NodeConditionNumReady),
|
||||
fmt.Sprintf("uniqExactIf(node_name, condition_value = %d) AS not_ready_count", inframonitoringtypes.NodeConditionNumNotReady),
|
||||
)
|
||||
countNodesPerConditionSQL := fmt.Sprintf(
|
||||
"SELECT %s FROM latest_condition_per_node GROUP BY %s",
|
||||
strings.Join(countNodesPerConditionSelectCols, ", "),
|
||||
strings.Join(countNodesPerConditionGroupBy, ", "),
|
||||
)
|
||||
// Outer count query. Built with sqlbuilder so the readiness push-down uses a
|
||||
// proper IN (keep only nodes whose readiness is in the requested set).
|
||||
countBuilder := sqlbuilder.NewSelectBuilder()
|
||||
countBuilder.Select(countNodesPerConditionSelectCols...)
|
||||
countBuilder.From("latest_condition_per_node")
|
||||
applyNodeReadinessFilter(countBuilder, filterByNodeReadiness)
|
||||
countBuilder.GroupBy(countNodesPerConditionGroupBy...)
|
||||
countNodesPerConditionSQL, countArgs := countBuilder.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
// Combine CTEs + outer.
|
||||
cteFragments := []string{
|
||||
@@ -300,7 +383,7 @@ func (m *module) getPerGroupNodeConditionCounts(
|
||||
fmt.Sprintf("latest_condition_per_node AS (%s)", latestConditionPerNodeSQL),
|
||||
}
|
||||
finalSQL := querybuilder.CombineCTEs(cteFragments) + countNodesPerConditionSQL
|
||||
finalArgs := querybuilder.PrependArgs([][]any{timeSeriesFPsArgs, latestConditionPerNodeArgs}, nil)
|
||||
finalArgs := querybuilder.PrependArgs([][]any{timeSeriesFPsArgs, latestConditionPerNodeArgs}, countArgs)
|
||||
|
||||
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, finalSQL, finalArgs...)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestApplyNodeReadinessFilter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
readiness []inframonitoringtypes.NodeCondition
|
||||
wantWhere bool
|
||||
wantArgs []any
|
||||
}{
|
||||
{
|
||||
name: "empty set yields no clause",
|
||||
readiness: nil,
|
||||
wantWhere: false,
|
||||
wantArgs: nil,
|
||||
},
|
||||
{
|
||||
name: "ready maps to 1 via IN",
|
||||
readiness: []inframonitoringtypes.NodeCondition{inframonitoringtypes.NodeConditionReady},
|
||||
wantWhere: true,
|
||||
wantArgs: []any{inframonitoringtypes.NodeConditionNumReady},
|
||||
},
|
||||
{
|
||||
name: "not_ready maps to 0 via IN",
|
||||
readiness: []inframonitoringtypes.NodeCondition{inframonitoringtypes.NodeConditionNotReady},
|
||||
wantWhere: true,
|
||||
wantArgs: []any{inframonitoringtypes.NodeConditionNumNotReady},
|
||||
},
|
||||
{
|
||||
name: "multiple conditions map to their ints via IN",
|
||||
readiness: []inframonitoringtypes.NodeCondition{
|
||||
inframonitoringtypes.NodeConditionReady,
|
||||
inframonitoringtypes.NodeConditionNotReady,
|
||||
},
|
||||
wantWhere: true,
|
||||
wantArgs: []any{inframonitoringtypes.NodeConditionNumReady, inframonitoringtypes.NodeConditionNumNotReady},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cb := sqlbuilder.NewSelectBuilder()
|
||||
cb.Select("node_name")
|
||||
cb.From("latest_condition_per_node")
|
||||
applyNodeReadinessFilter(cb, tt.readiness)
|
||||
sql, args := cb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
hasWhere := strings.Contains(sql, "condition_value IN (")
|
||||
assert.Equal(t, tt.wantWhere, hasWhere)
|
||||
if len(tt.wantArgs) == 0 {
|
||||
assert.Empty(t, args)
|
||||
} else {
|
||||
assert.Equal(t, tt.wantArgs, args)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -146,24 +146,34 @@ func buildPodRecords(
|
||||
return records
|
||||
}
|
||||
|
||||
// getTopPodGroupsAndMetadata fetches the group metadata and the ordering-metric
|
||||
// ranking concurrently, then pages the ranked groups, backfilling from metadata
|
||||
// when the page extends past the metric-ranked groups. Returns the page of
|
||||
// groups and the metadata map (needed by the caller for Total and records).
|
||||
// getTopPodGroupsAndMetadata concurrently fetches metadata + the ordering-metric
|
||||
// ranking (plus the full-scope pod-status keyset when filtering, to intersect both).
|
||||
func (m *module) getTopPodGroupsAndMetadata(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostablePods,
|
||||
) ([]map[string]string, map[string]map[string]string, error) {
|
||||
) ([]map[string]string, map[string]map[string]string, map[string]podStatusCounts, *qbtypes.QueryWarnData, error) {
|
||||
|
||||
var (
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
statusCounts map[string]podStatusCounts
|
||||
statusWarning *qbtypes.QueryWarnData
|
||||
filter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
)
|
||||
|
||||
orderByKey = req.OrderBy.Key.Name
|
||||
|
||||
// When filtering by pod status, resolve the full-scope status keyset
|
||||
// concurrently (pageGroups=nil spans all groups under the user filter) so it
|
||||
// can intersect metadata + ranked groups below.
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
}
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -172,12 +182,26 @@ func (m *module) getTopPodGroupsAndMetadata(
|
||||
return err
|
||||
})
|
||||
|
||||
if len(filterByPodStatus) != 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByPodStatus)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if orderByKey == inframonitoringtypes.PodNameAttrKey {
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
// Secondary filter: keep only status-matching groups. A missing metric
|
||||
// yields an empty statusCounts, so this correctly empties the result
|
||||
// (the caller also surfaces the warning).
|
||||
if len(filterByPodStatus) != 0 {
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
pageGroups := inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.PodNameAttrKey)
|
||||
return pageGroups, metadataMap, nil
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nil
|
||||
}
|
||||
|
||||
queryNamesForOrderBy := orderByToPodsQueryNames[orderByKey]
|
||||
@@ -223,10 +247,19 @@ func (m *module) getTopPodGroupsAndMetadata(
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), metadataMap, nil
|
||||
// Secondary filter: intersect ranked groups + metadata with the status keyset.
|
||||
// A missing metric yields an empty statusCounts, correctly emptying the result
|
||||
// (the caller also surfaces the warning).
|
||||
if len(filterByPodStatus) != 0 {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, statusCounts)
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
|
||||
pageGroups := paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit)
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nil
|
||||
}
|
||||
|
||||
func (m *module) getPodsTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostablePods) (map[string]map[string]string, error) {
|
||||
@@ -236,7 +269,11 @@ func (m *module) getPodsTableMetadata(ctx context.Context, orgID valuer.UUID, re
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, podsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
|
||||
var filter *qbtypes.Filter
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, podsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
|
||||
}
|
||||
|
||||
// getPerGroupPodStatusCountsWithReqMetricChecks gates getPerGroupPodStatusCounts
|
||||
@@ -251,6 +288,7 @@ func (m *module) getPerGroupPodStatusCountsWithReqMetricChecks(
|
||||
filter *qbtypes.Filter,
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
pageGroups []map[string]string,
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus,
|
||||
) (map[string]podStatusCounts, *qbtypes.QueryWarnData, error) {
|
||||
present, err := m.getMetricsExistence(ctx, podStatusMetricNamesList)
|
||||
if err != nil {
|
||||
@@ -276,13 +314,28 @@ func (m *module) getPerGroupPodStatusCountsWithReqMetricChecks(
|
||||
return map[string]podStatusCounts{}, warning, nil
|
||||
}
|
||||
|
||||
counts, err := m.getPerGroupPodStatusCounts(ctx, orgID, start, end, filter, groupBy, pageGroups)
|
||||
counts, err := m.getPerGroupPodStatusCounts(ctx, orgID, start, end, filter, groupBy, pageGroups, filterByPodStatus)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return counts, nil, nil
|
||||
}
|
||||
|
||||
// applyPodStatusFilter adds the display-status push-down (lower(display_status)
|
||||
// IN (...)) to the outer count builder. valuer lowercases the wire value while
|
||||
// display_status is kubectl-cased, so we compare lower() on both. No-op when the
|
||||
// requested set is empty.
|
||||
func applyPodStatusFilter(cb *sqlbuilder.SelectBuilder, filterByPodStatus []inframonitoringtypes.PodStatus) {
|
||||
if len(filterByPodStatus) == 0 {
|
||||
return
|
||||
}
|
||||
vals := make([]string, len(filterByPodStatus))
|
||||
for i, s := range filterByPodStatus {
|
||||
vals[i] = s.StringValue()
|
||||
}
|
||||
cb.Where(cb.In("lower(display_status)", sqlbuilder.List(vals)))
|
||||
}
|
||||
|
||||
// getPerGroupPodStatusCounts computes per-group pod counts bucketed by each
|
||||
// pod's latest kubectl-style display status in the requested window. Caller
|
||||
// must ensure the required metrics exist (getPerGroupPodStatusCountsWithReqMetricChecks).
|
||||
@@ -303,13 +356,20 @@ func (m *module) getPerGroupPodStatusCounts(
|
||||
filter *qbtypes.Filter,
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
pageGroups []map[string]string,
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus,
|
||||
) (map[string]podStatusCounts, error) {
|
||||
if len(pageGroups) == 0 || len(groupBy) == 0 {
|
||||
// return early if no group by or (no pagegroups provided plus no filterBystatus given for a full scan)
|
||||
if len(groupBy) == 0 || (len(pageGroups) == 0 && len(filterByPodStatus) == 0) {
|
||||
return map[string]podStatusCounts{}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
filterClause *sqlbuilder.WhereClause
|
||||
err error
|
||||
userFilterExpr string
|
||||
)
|
||||
|
||||
// Merge user filter with page-groups IN clauses.
|
||||
userFilterExpr := ""
|
||||
if filter != nil {
|
||||
userFilterExpr = filter.Expression
|
||||
}
|
||||
@@ -322,10 +382,7 @@ func (m *module) getPerGroupPodStatusCounts(
|
||||
// CTEs, and buildFilterClause hits the metadata store + parses the
|
||||
// expression, so we don't want to repeat it per CTE. AddWhereClause only
|
||||
// reads the clause, so the same instance is safe to attach to each builder.
|
||||
var (
|
||||
filterClause *sqlbuilder.WhereClause
|
||||
err error
|
||||
)
|
||||
|
||||
if mergedFilterExpr != "" {
|
||||
filterClause, err = m.buildFilterClause(ctx, orgID, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
|
||||
if err != nil {
|
||||
@@ -540,11 +597,15 @@ func (m *module) getPerGroupPodStatusCounts(
|
||||
countGroupBy = append(countGroupBy, col)
|
||||
}
|
||||
countSelectCols = append(countSelectCols, statusCountCols...)
|
||||
countSQL := fmt.Sprintf(
|
||||
"SELECT %s FROM pod_status GROUP BY %s",
|
||||
strings.Join(countSelectCols, ", "),
|
||||
strings.Join(countGroupBy, ", "),
|
||||
)
|
||||
|
||||
// Outer count query. Built with sqlbuilder so the status push-down uses a
|
||||
// proper IN (keep only pods whose display status is in the requested set).
|
||||
countBuilder := sqlbuilder.NewSelectBuilder()
|
||||
countBuilder.Select(countSelectCols...)
|
||||
countBuilder.From("pod_status")
|
||||
applyPodStatusFilter(countBuilder, filterByPodStatus)
|
||||
countBuilder.GroupBy(countGroupBy...)
|
||||
countSQL, countArgs := countBuilder.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
// Combine CTEs + outer. Arg order mirrors CTE declaration order.
|
||||
cteFragments := []string{
|
||||
@@ -561,7 +622,7 @@ func (m *module) getPerGroupPodStatusCounts(
|
||||
phaseFpsArgs, phasePerPodArgs,
|
||||
podReasonFpsArgs, podReasonPerPodArgs,
|
||||
containerReasonFpsArgs, containerInnerArgs,
|
||||
}, nil)
|
||||
}, countArgs)
|
||||
|
||||
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, finalSQL, finalArgs...)
|
||||
if err != nil {
|
||||
|
||||
59
pkg/modules/inframonitoring/implinframonitoring/pods_test.go
Normal file
59
pkg/modules/inframonitoring/implinframonitoring/pods_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestApplyPodStatusFilter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
statuses []inframonitoringtypes.PodStatus
|
||||
wantWhere bool
|
||||
wantArgs []any
|
||||
}{
|
||||
{
|
||||
name: "empty set yields no clause",
|
||||
statuses: nil,
|
||||
wantWhere: false,
|
||||
wantArgs: nil,
|
||||
},
|
||||
{
|
||||
name: "single status pushes lowercased arg via IN",
|
||||
statuses: []inframonitoringtypes.PodStatus{inframonitoringtypes.PodStatusRunning},
|
||||
wantWhere: true,
|
||||
wantArgs: []any{"running"},
|
||||
},
|
||||
{
|
||||
name: "multiple statuses push lowercased args via IN",
|
||||
statuses: []inframonitoringtypes.PodStatus{
|
||||
inframonitoringtypes.PodStatusRunning,
|
||||
inframonitoringtypes.PodStatusCrashLoopBackOff,
|
||||
},
|
||||
wantWhere: true,
|
||||
wantArgs: []any{"running", "crashloopbackoff"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cb := sqlbuilder.NewSelectBuilder()
|
||||
cb.Select("pod_uid")
|
||||
cb.From("pod_status")
|
||||
applyPodStatusFilter(cb, tt.statuses)
|
||||
sql, args := cb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
hasWhere := strings.Contains(sql, "lower(display_status) IN (")
|
||||
assert.Equal(t, tt.wantWhere, hasWhere)
|
||||
if len(tt.wantArgs) == 0 {
|
||||
assert.Empty(t, args)
|
||||
} else {
|
||||
assert.Equal(t, tt.wantArgs, args)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -82,20 +82,34 @@ func buildStatefulSetRecords(
|
||||
return records
|
||||
}
|
||||
|
||||
// getTopStatefulSetGroupsAndMetadata concurrently fetches metadata + the ordering-metric
|
||||
// ranking (plus the full-scope pod-status keyset when filtering, to intersect both).
|
||||
func (m *module) getTopStatefulSetGroupsAndMetadata(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableStatefulSets,
|
||||
) ([]map[string]string, map[string]map[string]string, error) {
|
||||
) ([]map[string]string, map[string]map[string]string, map[string]podStatusCounts, *qbtypes.QueryWarnData, error) {
|
||||
|
||||
var (
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
statusCounts map[string]podStatusCounts
|
||||
statusWarning *qbtypes.QueryWarnData
|
||||
filter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
)
|
||||
|
||||
orderByKey = req.OrderBy.Key.Name
|
||||
|
||||
// When filtering by pod status, resolve the full-scope status keyset
|
||||
// concurrently (pageGroups=nil spans all groups under the user filter) so it
|
||||
// can intersect metadata + ranked groups below.
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
}
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -104,12 +118,26 @@ func (m *module) getTopStatefulSetGroupsAndMetadata(
|
||||
return err
|
||||
})
|
||||
|
||||
if len(filterByPodStatus) != 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByPodStatus)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if orderByKey == inframonitoringtypes.StatefulSetNameAttrKey {
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
// Secondary filter: keep only status-matching groups. A missing metric
|
||||
// yields an empty statusCounts, so this correctly empties the result
|
||||
// (the caller also surfaces the warning).
|
||||
if len(filterByPodStatus) != 0 {
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
pageGroups := inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.StatefulSetNameAttrKey)
|
||||
return pageGroups, metadataMap, nil
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nil
|
||||
}
|
||||
|
||||
queryNamesForOrderBy := orderByToStatefulSetsQueryNames[orderByKey]
|
||||
@@ -155,10 +183,19 @@ func (m *module) getTopStatefulSetGroupsAndMetadata(
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), metadataMap, nil
|
||||
// Secondary filter: intersect ranked groups + metadata with the status keyset.
|
||||
// A missing metric yields an empty statusCounts, correctly emptying the result
|
||||
// (the caller also surfaces the warning).
|
||||
if len(filterByPodStatus) != 0 {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, statusCounts)
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
|
||||
pageGroups := paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit)
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nil
|
||||
}
|
||||
|
||||
func (m *module) getStatefulSetsTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableStatefulSets) (map[string]map[string]string, error) {
|
||||
@@ -168,5 +205,9 @@ func (m *module) getStatefulSetsTableMetadata(ctx context.Context, orgID valuer.
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, statefulSetsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
|
||||
var filter *qbtypes.Filter
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, statefulSetsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
|
||||
}
|
||||
|
||||
@@ -39,26 +39,24 @@ type legacyExtraData struct {
|
||||
func newPostableSavedViewFromLegacyView(v *v3.SavedView) savedviewtypes.PostableSavedView {
|
||||
var legacy legacyExtraData
|
||||
if v.ExtraData != "" {
|
||||
// Best-effort: malformed/older extraData shapes never fail the request
|
||||
// Best-effort: malformed/older extraData shapes never fail the request.
|
||||
_ = json.Unmarshal([]byte(v.ExtraData), &legacy)
|
||||
}
|
||||
|
||||
return savedviewtypes.PostableSavedView{
|
||||
GenerateName: true,
|
||||
Source: savedviewtypes.Source{String: valuer.NewString(v.SourcePage)},
|
||||
Data: savedviewtypes.SavedViewData{
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: v.Name,
|
||||
PanelType: savedviewtypes.PanelType{String: valuer.NewString(string(v.CompositeQuery.PanelType))},
|
||||
Queries: v.CompositeQuery.Queries,
|
||||
SelectedFields: legacy.SelectColumns,
|
||||
Display: savedviewtypes.Display{
|
||||
MaxLines: legacy.MaxLines,
|
||||
FontSize: legacy.FontSize,
|
||||
Format: legacy.Format,
|
||||
Color: legacy.Color,
|
||||
},
|
||||
GenerateName: true,
|
||||
Source: savedviewtypes.Source{String: valuer.NewString(v.SourcePage)},
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: v.Name,
|
||||
PanelType: savedviewtypes.PanelType{String: valuer.NewString(string(v.CompositeQuery.PanelType))},
|
||||
Queries: v.CompositeQuery.Queries,
|
||||
SelectedFields: legacy.SelectColumns,
|
||||
Display: savedviewtypes.Display{
|
||||
MaxLines: legacy.MaxLines,
|
||||
FontSize: legacy.FontSize,
|
||||
Format: legacy.Format,
|
||||
Color: legacy.Color,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -68,25 +66,23 @@ func newPostableSavedViewFromLegacyView(v *v3.SavedView) savedviewtypes.Postable
|
||||
func newUpdatableSavedViewFromLegacyView(v *v3.SavedView) savedviewtypes.UpdatableSavedView {
|
||||
var legacy legacyExtraData
|
||||
if v.ExtraData != "" {
|
||||
// Best-effort: malformed/older extraData shapes never fail the request
|
||||
// Best-effort: malformed/older extraData shapes never fail the request.
|
||||
_ = json.Unmarshal([]byte(v.ExtraData), &legacy)
|
||||
}
|
||||
|
||||
return savedviewtypes.UpdatableSavedView{
|
||||
Source: savedviewtypes.Source{String: valuer.NewString(v.SourcePage)},
|
||||
Data: savedviewtypes.SavedViewData{
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: v.Name,
|
||||
PanelType: savedviewtypes.PanelType{String: valuer.NewString(string(v.CompositeQuery.PanelType))},
|
||||
Queries: v.CompositeQuery.Queries,
|
||||
SelectedFields: legacy.SelectColumns,
|
||||
Display: savedviewtypes.Display{
|
||||
MaxLines: legacy.MaxLines,
|
||||
FontSize: legacy.FontSize,
|
||||
Format: legacy.Format,
|
||||
Color: legacy.Color,
|
||||
},
|
||||
Source: savedviewtypes.Source{String: valuer.NewString(v.SourcePage)},
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: v.Name,
|
||||
PanelType: savedviewtypes.PanelType{String: valuer.NewString(string(v.CompositeQuery.PanelType))},
|
||||
Queries: v.CompositeQuery.Queries,
|
||||
SelectedFields: legacy.SelectColumns,
|
||||
Display: savedviewtypes.Display{
|
||||
MaxLines: legacy.MaxLines,
|
||||
FontSize: legacy.FontSize,
|
||||
Format: legacy.Format,
|
||||
Color: legacy.Color,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -95,11 +91,11 @@ func newUpdatableSavedViewFromLegacyView(v *v3.SavedView) savedviewtypes.Updatab
|
||||
// newLegacyViewFromSavedView renders a v2 SavedView back into the v1 shape.
|
||||
func newLegacyViewFromSavedView(v *savedviewtypes.SavedView) (*v3.SavedView, error) {
|
||||
extraData, err := json.Marshal(legacyExtraData{
|
||||
Color: v.Data.Spec.Display.Color,
|
||||
SelectColumns: v.Data.Spec.SelectedFields,
|
||||
Format: v.Data.Spec.Display.Format,
|
||||
MaxLines: v.Data.Spec.Display.MaxLines,
|
||||
FontSize: v.Data.Spec.Display.FontSize,
|
||||
Color: v.Spec.Display.Color,
|
||||
SelectColumns: v.Spec.SelectedFields,
|
||||
Format: v.Spec.Display.Format,
|
||||
MaxLines: v.Spec.Display.MaxLines,
|
||||
FontSize: v.Spec.Display.FontSize,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error in marshalling extra data")
|
||||
@@ -107,17 +103,17 @@ func newLegacyViewFromSavedView(v *savedviewtypes.SavedView) (*v3.SavedView, err
|
||||
|
||||
return &v3.SavedView{
|
||||
ID: v.ID,
|
||||
Name: v.Data.Spec.DisplayName,
|
||||
Name: v.Spec.DisplayName,
|
||||
CreatedAt: v.CreatedAt,
|
||||
CreatedBy: v.CreatedBy,
|
||||
UpdatedAt: v.UpdatedAt,
|
||||
UpdatedBy: v.UpdatedBy,
|
||||
SourcePage: v.Source.StringValue(),
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
PanelType: v3.PanelType(v.Data.Spec.PanelType.StringValue()),
|
||||
PanelType: v3.PanelType(v.Spec.PanelType.StringValue()),
|
||||
// Saved views are only ever created from the explorer's builder mode.
|
||||
QueryType: v3.QueryTypeBuilder,
|
||||
Queries: v.Data.Spec.Queries,
|
||||
Queries: v.Spec.Queries,
|
||||
},
|
||||
ExtraData: string(extraData),
|
||||
}, nil
|
||||
@@ -156,7 +152,14 @@ func (handler *handler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
uuid, err := handler.module.CreateView(ctx, claims.OrgID, newPostableSavedViewFromLegacyView(&view))
|
||||
postable := newPostableSavedViewFromLegacyView(&view)
|
||||
|
||||
if err := postable.Validate(); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
uuid, err := handler.module.CreateView(ctx, claims.OrgID, postable)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
@@ -224,8 +227,14 @@ func (handler *handler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.module.UpdateView(ctx, claims.OrgID, viewUUID, newUpdatableSavedViewFromLegacyView(&view))
|
||||
if err != nil {
|
||||
updatable := newUpdatableSavedViewFromLegacyView(&view)
|
||||
|
||||
if err := updatable.Validate(); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := handler.module.UpdateView(ctx, claims.OrgID, viewUUID, updatable); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -42,13 +42,13 @@ func TestNewPostableSavedViewFromLegacyView(t *testing.T) {
|
||||
|
||||
assert.Empty(t, postable.Name, "v1 has no slug concept -- name must always be generated")
|
||||
assert.True(t, postable.GenerateName, "v1 has no slug concept -- name must always be generated")
|
||||
assert.Equal(t, "my view", postable.Data.Spec.DisplayName)
|
||||
assert.Equal(t, "my view", postable.Spec.DisplayName)
|
||||
assert.Equal(t, savedviewtypes.SourceLogs, postable.Source)
|
||||
assert.Equal(t, savedviewtypes.SavedViewSchemaVersion, postable.Data.SchemaVersion)
|
||||
assert.Equal(t, savedviewtypes.PanelTypeGraph, postable.Data.Spec.PanelType)
|
||||
assert.Equal(t, legacy.CompositeQuery.Queries, postable.Data.Spec.Queries)
|
||||
assert.Equal(t, []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}}, postable.Data.Spec.SelectedFields)
|
||||
assert.Equal(t, savedviewtypes.Display{MaxLines: 10, FontSize: "large", Format: "table", Color: "blue"}, postable.Data.Spec.Display)
|
||||
assert.Equal(t, savedviewtypes.SavedViewSchemaVersion, postable.SchemaVersion)
|
||||
assert.Equal(t, savedviewtypes.PanelTypeGraph, postable.Spec.PanelType)
|
||||
assert.Equal(t, legacy.CompositeQuery.Queries, postable.Spec.Queries)
|
||||
assert.Equal(t, []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}}, postable.Spec.SelectedFields)
|
||||
assert.Equal(t, savedviewtypes.Display{MaxLines: 10, FontSize: "large", Format: "table", Color: "blue"}, postable.Spec.Display)
|
||||
})
|
||||
|
||||
t.Run("empty extra data leaves display and selected fields zero-valued", func(t *testing.T) {
|
||||
@@ -64,8 +64,8 @@ func TestNewPostableSavedViewFromLegacyView(t *testing.T) {
|
||||
|
||||
postable := newPostableSavedViewFromLegacyView(legacy)
|
||||
|
||||
assert.Equal(t, savedviewtypes.Display{}, postable.Data.Spec.Display)
|
||||
assert.Nil(t, postable.Data.Spec.SelectedFields)
|
||||
assert.Equal(t, savedviewtypes.Display{}, postable.Spec.Display)
|
||||
assert.Nil(t, postable.Spec.SelectedFields)
|
||||
})
|
||||
|
||||
t.Run("malformed extra data is ignored, not an error", func(t *testing.T) {
|
||||
@@ -81,8 +81,25 @@ func TestNewPostableSavedViewFromLegacyView(t *testing.T) {
|
||||
|
||||
postable := newPostableSavedViewFromLegacyView(legacy)
|
||||
|
||||
assert.Equal(t, "malformed extra data", postable.Data.Spec.DisplayName)
|
||||
assert.Equal(t, savedviewtypes.Display{}, postable.Data.Spec.Display)
|
||||
assert.Equal(t, "malformed extra data", postable.Spec.DisplayName)
|
||||
assert.Equal(t, savedviewtypes.Display{}, postable.Spec.Display)
|
||||
})
|
||||
|
||||
t.Run("legacy validation gap: empty builderQueries map with no queries", func(t *testing.T) {
|
||||
legacy := &v3.SavedView{
|
||||
Name: "no real queries",
|
||||
SourcePage: "logs",
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
PanelType: v3.PanelTypeGraph,
|
||||
QueryType: v3.QueryTypeBuilder,
|
||||
BuilderQueries: map[string]*v3.BuilderQuery{},
|
||||
},
|
||||
}
|
||||
|
||||
require.NoError(t, legacy.Validate(), "the legacy CompositeQuery check is expected to miss this")
|
||||
|
||||
postable := newPostableSavedViewFromLegacyView(legacy)
|
||||
assert.Error(t, postable.Validate(), "the converted postable must catch what the legacy check missed")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -99,24 +116,22 @@ func TestNewUpdatableSavedViewFromLegacyView(t *testing.T) {
|
||||
|
||||
updatable := newUpdatableSavedViewFromLegacyView(legacy)
|
||||
|
||||
assert.Equal(t, "renamed view", updatable.Data.Spec.DisplayName)
|
||||
assert.Equal(t, "renamed view", updatable.Spec.DisplayName)
|
||||
assert.Equal(t, savedviewtypes.SourceTraces, updatable.Source)
|
||||
}
|
||||
|
||||
func TestNewLegacyViewFromSavedView(t *testing.T) {
|
||||
now := time.Now()
|
||||
savedView := &savedviewtypes.SavedView{
|
||||
Name: "my-view-abc123ef",
|
||||
Source: savedviewtypes.SourceLogs,
|
||||
Data: savedviewtypes.SavedViewData{
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: "my view",
|
||||
PanelType: savedviewtypes.PanelTypeGraph,
|
||||
Queries: testQueries(),
|
||||
SelectedFields: []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}},
|
||||
Display: savedviewtypes.Display{MaxLines: 10, FontSize: "large", Format: "table", Color: "blue"},
|
||||
},
|
||||
Name: "my-view-abc123ef",
|
||||
Source: savedviewtypes.SourceLogs,
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: "my view",
|
||||
PanelType: savedviewtypes.PanelTypeGraph,
|
||||
Queries: testQueries(),
|
||||
SelectedFields: []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}},
|
||||
Display: savedviewtypes.Display{MaxLines: 10, FontSize: "large", Format: "table", Color: "blue"},
|
||||
},
|
||||
}
|
||||
savedView.ID = valuer.GenerateUUID()
|
||||
@@ -129,7 +144,7 @@ func TestNewLegacyViewFromSavedView(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, savedView.ID, legacy.ID)
|
||||
assert.Equal(t, savedView.Data.Spec.DisplayName, legacy.Name)
|
||||
assert.Equal(t, savedView.Spec.DisplayName, legacy.Name)
|
||||
assert.Equal(t, savedView.CreatedAt, legacy.CreatedAt)
|
||||
assert.Equal(t, savedView.CreatedBy, legacy.CreatedBy)
|
||||
assert.Equal(t, savedView.UpdatedAt, legacy.UpdatedAt)
|
||||
@@ -137,20 +152,20 @@ func TestNewLegacyViewFromSavedView(t *testing.T) {
|
||||
assert.Equal(t, "logs", legacy.SourcePage)
|
||||
assert.Equal(t, v3.PanelTypeGraph, legacy.CompositeQuery.PanelType)
|
||||
assert.Equal(t, v3.QueryTypeBuilder, legacy.CompositeQuery.QueryType)
|
||||
assert.Equal(t, savedView.Data.Spec.Queries, legacy.CompositeQuery.Queries)
|
||||
assert.Equal(t, savedView.Spec.Queries, legacy.CompositeQuery.Queries)
|
||||
|
||||
var extra legacyExtraData
|
||||
require.NoError(t, json.Unmarshal([]byte(legacy.ExtraData), &extra))
|
||||
assert.Equal(t, "blue", extra.Color)
|
||||
assert.Equal(t, savedView.Data.Spec.SelectedFields, extra.SelectColumns)
|
||||
assert.Equal(t, savedView.Spec.SelectedFields, extra.SelectColumns)
|
||||
assert.Equal(t, "table", extra.Format)
|
||||
assert.Equal(t, 10, extra.MaxLines)
|
||||
assert.Equal(t, "large", extra.FontSize)
|
||||
}
|
||||
|
||||
func TestNewLegacyViewsFromSavedViews(t *testing.T) {
|
||||
a := &savedviewtypes.SavedView{Name: "a-slug", Source: savedviewtypes.SourceLogs, Data: savedviewtypes.SavedViewData{Spec: savedviewtypes.SavedViewSpec{DisplayName: "a", PanelType: savedviewtypes.PanelTypeGraph, Queries: testQueries()}}}
|
||||
b := &savedviewtypes.SavedView{Name: "b-slug", Source: savedviewtypes.SourceTraces, Data: savedviewtypes.SavedViewData{Spec: savedviewtypes.SavedViewSpec{DisplayName: "b", PanelType: savedviewtypes.PanelTypeTable, Queries: testQueries()}}}
|
||||
a := &savedviewtypes.SavedView{Name: "a-slug", Source: savedviewtypes.SourceLogs, Spec: savedviewtypes.SavedViewSpec{DisplayName: "a", PanelType: savedviewtypes.PanelTypeGraph, Queries: testQueries()}}
|
||||
b := &savedviewtypes.SavedView{Name: "b-slug", Source: savedviewtypes.SourceTraces, Spec: savedviewtypes.SavedViewSpec{DisplayName: "b", PanelType: savedviewtypes.PanelTypeTable, Queries: testQueries()}}
|
||||
|
||||
legacyViews, err := newLegacyViewsFromSavedViews([]*savedviewtypes.SavedView{a, b})
|
||||
require.NoError(t, err)
|
||||
@@ -167,17 +182,15 @@ func TestNewLegacyViewsFromSavedViews(t *testing.T) {
|
||||
// slug (Name) is deliberately NOT part of this contract -- v1 never sees it.
|
||||
func TestLegacyViewRoundTrip(t *testing.T) {
|
||||
original := &savedviewtypes.SavedView{
|
||||
Name: "round-trip-abc123ef",
|
||||
Source: savedviewtypes.SourceMetrics,
|
||||
Data: savedviewtypes.SavedViewData{
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: "round trip",
|
||||
PanelType: savedviewtypes.PanelTypeTable,
|
||||
Queries: testQueries(),
|
||||
SelectedFields: []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}},
|
||||
Display: savedviewtypes.Display{MaxLines: 5, FontSize: "small", Format: "list", Color: "red"},
|
||||
},
|
||||
Name: "round-trip-abc123ef",
|
||||
Source: savedviewtypes.SourceMetrics,
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: "round trip",
|
||||
PanelType: savedviewtypes.PanelTypeTable,
|
||||
Queries: testQueries(),
|
||||
SelectedFields: []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}},
|
||||
Display: savedviewtypes.Display{MaxLines: 5, FontSize: "small", Format: "list", Color: "red"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -188,10 +201,37 @@ func TestLegacyViewRoundTrip(t *testing.T) {
|
||||
|
||||
assert.Empty(t, roundTripped.Name)
|
||||
assert.True(t, roundTripped.GenerateName)
|
||||
assert.Equal(t, original.Data.Spec.DisplayName, roundTripped.Data.Spec.DisplayName)
|
||||
assert.Equal(t, original.Spec.DisplayName, roundTripped.Spec.DisplayName)
|
||||
assert.Equal(t, original.Source, roundTripped.Source)
|
||||
assert.Equal(t, original.Data.Spec.PanelType, roundTripped.Data.Spec.PanelType)
|
||||
assert.Equal(t, original.Data.Spec.Queries, roundTripped.Data.Spec.Queries)
|
||||
assert.Equal(t, original.Data.Spec.SelectedFields, roundTripped.Data.Spec.SelectedFields)
|
||||
assert.Equal(t, original.Data.Spec.Display, roundTripped.Data.Spec.Display)
|
||||
assert.Equal(t, original.Spec.PanelType, roundTripped.Spec.PanelType)
|
||||
assert.Equal(t, original.Spec.Queries, roundTripped.Spec.Queries)
|
||||
assert.Equal(t, original.Spec.SelectedFields, roundTripped.Spec.SelectedFields)
|
||||
assert.Equal(t, original.Spec.Display, roundTripped.Spec.Display)
|
||||
}
|
||||
|
||||
func TestLegacyViewRoundTrip_EmptySelectedFieldsAndDisplay(t *testing.T) {
|
||||
original := &savedviewtypes.SavedView{
|
||||
Name: "round-trip-empty-abc123ef",
|
||||
Source: savedviewtypes.SourceMetrics,
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: "round trip empty",
|
||||
PanelType: savedviewtypes.PanelTypeTable,
|
||||
Queries: testQueries(),
|
||||
SelectedFields: []telemetrytypes.TelemetryFieldKey{},
|
||||
Display: savedviewtypes.Display{},
|
||||
},
|
||||
}
|
||||
|
||||
legacy, err := newLegacyViewFromSavedView(original)
|
||||
require.NoError(t, err)
|
||||
|
||||
var extra legacyExtraData
|
||||
require.NoError(t, json.Unmarshal([]byte(legacy.ExtraData), &extra))
|
||||
assert.Nil(t, extra.SelectColumns, "omitempty drops an empty selectColumns from extraData entirely")
|
||||
|
||||
roundTripped := newPostableSavedViewFromLegacyView(legacy)
|
||||
|
||||
assert.Empty(t, roundTripped.Spec.SelectedFields, "empty, not necessarily non-nil, on this leg of the round trip")
|
||||
assert.Equal(t, savedviewtypes.Display{}, roundTripped.Spec.Display)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,11 @@ func NewModule(store savedviewtypes.Store) savedview.Module {
|
||||
}
|
||||
|
||||
func (module *module) GetViewsForFilters(ctx context.Context, orgID string, source savedviewtypes.Source, name string) ([]*savedviewtypes.SavedView, error) {
|
||||
return module.store.List(ctx, orgID, source, name)
|
||||
storables, err := module.store.List(ctx, orgID, source, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return savedviewtypes.NewSavedViewsFromStorableSavedViews(storables), nil
|
||||
}
|
||||
|
||||
func (module *module) CreateView(ctx context.Context, orgID string, view savedviewtypes.PostableSavedView) (valuer.UUID, error) {
|
||||
@@ -30,14 +34,19 @@ func (module *module) CreateView(ctx context.Context, orgID string, view savedvi
|
||||
|
||||
dbView := view.ToSavedView(orgID, claims.Email)
|
||||
|
||||
if err := module.store.Create(ctx, dbView); err != nil {
|
||||
if err := module.store.Create(ctx, savedviewtypes.NewStorableSavedView(dbView)); err != nil {
|
||||
return valuer.UUID{}, err
|
||||
}
|
||||
return dbView.ID, nil
|
||||
}
|
||||
|
||||
func (module *module) GetView(ctx context.Context, orgID string, uuid valuer.UUID) (*savedviewtypes.SavedView, error) {
|
||||
return module.store.Get(ctx, orgID, uuid)
|
||||
storable, err := module.store.Get(ctx, orgID, uuid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return storable.ToSavedView(), nil
|
||||
}
|
||||
|
||||
func (module *module) UpdateView(ctx context.Context, orgID string, uuid valuer.UUID, view savedviewtypes.UpdatableSavedView) error {
|
||||
@@ -46,7 +55,8 @@ func (module *module) UpdateView(ctx context.Context, orgID string, uuid valuer.
|
||||
return errors.NewInternalf(errors.CodeInternal, "error in getting email from context")
|
||||
}
|
||||
|
||||
return module.store.Update(ctx, view.ToSavedView(uuid, orgID, claims.Email))
|
||||
dbView := view.ToSavedView(uuid, orgID, claims.Email)
|
||||
return module.store.Update(ctx, savedviewtypes.NewStorableSavedView(dbView))
|
||||
}
|
||||
|
||||
func (module *module) DeleteView(ctx context.Context, orgID string, uuid valuer.UUID) error {
|
||||
@@ -54,10 +64,10 @@ func (module *module) DeleteView(ctx context.Context, orgID string, uuid valuer.
|
||||
}
|
||||
|
||||
func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
|
||||
savedViews, err := module.store.List(ctx, orgID.StringValue(), savedviewtypes.Source{}, "")
|
||||
storables, err := module.store.List(ctx, orgID.StringValue(), savedviewtypes.Source{}, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return savedviewtypes.NewStatsFromSavedViews(savedViews), nil
|
||||
return savedviewtypes.NewStatsFromStorableSavedViews(storables), nil
|
||||
}
|
||||
|
||||
@@ -28,24 +28,22 @@ func newTestStore() (savedview.Module, *savedviewtypestest.StoreTest) {
|
||||
|
||||
func testPostableSavedView(name string, source savedviewtypes.Source) savedviewtypes.PostableSavedView {
|
||||
return savedviewtypes.PostableSavedView{
|
||||
Name: name,
|
||||
Source: source,
|
||||
Data: savedviewtypes.SavedViewData{
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: name,
|
||||
PanelType: savedviewtypes.PanelTypeGraph,
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Aggregations: []qbtypes.LogAggregation{{Expression: "count()"}},
|
||||
},
|
||||
Name: name,
|
||||
Source: source,
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: name,
|
||||
PanelType: savedviewtypes.PanelTypeGraph,
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Aggregations: []qbtypes.LogAggregation{{Expression: "count()"}},
|
||||
},
|
||||
},
|
||||
SelectedFields: []telemetrytypes.TelemetryFieldKey{},
|
||||
},
|
||||
SelectedFields: []telemetrytypes.TelemetryFieldKey{},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -53,8 +51,9 @@ func testPostableSavedView(name string, source savedviewtypes.Source) savedviewt
|
||||
func testUpdatableSavedView(displayName string, source savedviewtypes.Source) savedviewtypes.UpdatableSavedView {
|
||||
postable := testPostableSavedView(displayName, source)
|
||||
return savedviewtypes.UpdatableSavedView{
|
||||
Source: postable.Source,
|
||||
Data: postable.Data,
|
||||
Source: postable.Source,
|
||||
SchemaVersion: postable.SchemaVersion,
|
||||
Spec: postable.Spec,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +92,7 @@ func TestModule_CreateAndGetView(t *testing.T) {
|
||||
assert.Equal(t, savedviewtypes.SourceLogs, got.Source)
|
||||
assert.Equal(t, "creator@signoz.io", got.CreatedBy)
|
||||
assert.Equal(t, "creator@signoz.io", got.UpdatedBy)
|
||||
assert.Equal(t, savedviewtypes.PanelTypeGraph, got.Data.Spec.PanelType)
|
||||
assert.Equal(t, savedviewtypes.PanelTypeGraph, got.Spec.PanelType)
|
||||
|
||||
require.NoError(t, st.AssertExpectations())
|
||||
}
|
||||
@@ -138,21 +137,21 @@ func TestModule_UpdateView(t *testing.T) {
|
||||
existingName := existing.Name
|
||||
|
||||
updated := testUpdatableSavedView("renamed", savedviewtypes.SourceTraces)
|
||||
updated.Data.Spec.PanelType = savedviewtypes.PanelTypeTable
|
||||
updated.Spec.PanelType = savedviewtypes.PanelTypeTable
|
||||
|
||||
st.ExpectUpdate(orgID, id, 1)
|
||||
require.NoError(t, m.UpdateView(contextWithClaims(orgID, "updater@signoz.io"), orgID, id, updated))
|
||||
|
||||
stored := testSavedView(orgID, id, "updater@signoz.io", testPostableSavedView("renamed", savedviewtypes.SourceTraces))
|
||||
stored.Name = existingName
|
||||
stored.Data.Spec.PanelType = savedviewtypes.PanelTypeTable
|
||||
stored.Spec.PanelType = savedviewtypes.PanelTypeTable
|
||||
st.ExpectGet(orgID, id, stored)
|
||||
got, err := m.GetView(contextWithClaims(orgID, "creator@signoz.io"), orgID, id)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, existingName, got.Name, "name must not change on update")
|
||||
assert.Equal(t, "renamed", got.Data.Spec.DisplayName)
|
||||
assert.Equal(t, "renamed", got.Spec.DisplayName)
|
||||
assert.Equal(t, savedviewtypes.SourceTraces, got.Source)
|
||||
assert.Equal(t, savedviewtypes.PanelTypeTable, got.Data.Spec.PanelType)
|
||||
assert.Equal(t, savedviewtypes.PanelTypeTable, got.Spec.PanelType)
|
||||
assert.Equal(t, "updater@signoz.io", got.UpdatedBy)
|
||||
|
||||
require.NoError(t, st.AssertExpectations())
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/savedviewtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
@@ -18,32 +17,31 @@ func NewStore(sqlstore sqlstore.SQLStore) savedviewtypes.Store {
|
||||
return &store{sqlstore: sqlstore}
|
||||
}
|
||||
|
||||
func (store *store) Create(ctx context.Context, view *savedviewtypes.SavedView) error {
|
||||
_, err := store.sqlstore.BunDB().NewInsert().Model(view).Exec(ctx)
|
||||
func (store *store) Create(ctx context.Context, storable *savedviewtypes.StorableSavedView) error {
|
||||
_, err := store.sqlstore.BunDB().NewInsert().Model(storable).Exec(ctx)
|
||||
if err != nil {
|
||||
return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "saved view with name %s already exists", view.Name)
|
||||
return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "saved view with name %s already exists", storable.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) Get(ctx context.Context, orgID string, id valuer.UUID) (*savedviewtypes.SavedView, error) {
|
||||
var view savedviewtypes.SavedView
|
||||
err := store.sqlstore.BunDB().NewSelect().Model(&view).Where("org_id = ? AND id = ?", orgID, id.StringValue()).Scan(ctx)
|
||||
func (store *store) Get(ctx context.Context, orgID string, id valuer.UUID) (*savedviewtypes.StorableSavedView, error) {
|
||||
var storable savedviewtypes.StorableSavedView
|
||||
err := store.sqlstore.BunDB().NewSelect().Model(&storable).Where("org_id = ? AND id = ?", orgID, id.StringValue()).Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, store.sqlstore.WrapNotFoundErrf(err, savedviewtypes.ErrCodeSavedViewNotFound, "saved view %s not found", id.StringValue())
|
||||
}
|
||||
|
||||
normalizeSelectedFields(&view)
|
||||
return &view, nil
|
||||
return &storable, nil
|
||||
}
|
||||
|
||||
func (store *store) Update(ctx context.Context, view *savedviewtypes.SavedView) error {
|
||||
func (store *store) Update(ctx context.Context, storable *savedviewtypes.StorableSavedView) error {
|
||||
res, err := store.sqlstore.BunDB().NewUpdate().
|
||||
Model(&savedviewtypes.SavedView{}).
|
||||
Model((*savedviewtypes.StorableSavedView)(nil)).
|
||||
Set("updated_at = ?, updated_by = ?, source = ?, data = ?",
|
||||
view.UpdatedAt, view.UpdatedBy, view.Source, view.Data).
|
||||
Where("id = ?", view.ID.StringValue()).
|
||||
Where("org_id = ?", view.OrgID).
|
||||
storable.UpdatedAt, storable.UpdatedBy, storable.Source, storable.Data).
|
||||
Where("id = ?", storable.ID.StringValue()).
|
||||
Where("org_id = ?", storable.OrgID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return errors.WrapInternalf(err, errors.CodeInternal, "error in updating saved view")
|
||||
@@ -54,7 +52,7 @@ func (store *store) Update(ctx context.Context, view *savedviewtypes.SavedView)
|
||||
return errors.WrapInternalf(err, errors.CodeInternal, "error in verifying the updated saved view")
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
return errors.NewNotFoundf(savedviewtypes.ErrCodeSavedViewNotFound, "saved view %s not found", view.ID.StringValue())
|
||||
return errors.NewNotFoundf(savedviewtypes.ErrCodeSavedViewNotFound, "saved view %s not found", storable.ID.StringValue())
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -62,7 +60,7 @@ func (store *store) Update(ctx context.Context, view *savedviewtypes.SavedView)
|
||||
|
||||
func (store *store) Delete(ctx context.Context, orgID string, id valuer.UUID) error {
|
||||
res, err := store.sqlstore.BunDB().NewDelete().
|
||||
Model(&savedviewtypes.SavedView{}).
|
||||
Model((*savedviewtypes.StorableSavedView)(nil)).
|
||||
Where("id = ?", id.StringValue()).
|
||||
Where("org_id = ?", orgID).
|
||||
Exec(ctx)
|
||||
@@ -81,9 +79,9 @@ func (store *store) Delete(ctx context.Context, orgID string, id valuer.UUID) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) List(ctx context.Context, orgID string, source savedviewtypes.Source, name string) ([]*savedviewtypes.SavedView, error) {
|
||||
var views []*savedviewtypes.SavedView
|
||||
q := store.sqlstore.BunDB().NewSelect().Model(&views).
|
||||
func (store *store) List(ctx context.Context, orgID string, source savedviewtypes.Source, name string) ([]*savedviewtypes.StorableSavedView, error) {
|
||||
var storables []*savedviewtypes.StorableSavedView
|
||||
q := store.sqlstore.BunDB().NewSelect().Model(&storables).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("name LIKE ?", "%"+name+"%")
|
||||
if !source.IsZero() {
|
||||
@@ -94,16 +92,5 @@ func (store *store) List(ctx context.Context, orgID string, source savedviewtype
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error in getting saved views")
|
||||
}
|
||||
|
||||
for _, view := range views {
|
||||
normalizeSelectedFields(view)
|
||||
}
|
||||
|
||||
return views, nil
|
||||
}
|
||||
|
||||
// normalizeSelectedFields fixes up a scanned row's nil SelectedFields.
|
||||
func normalizeSelectedFields(view *savedviewtypes.SavedView) {
|
||||
if view.Data.Spec.SelectedFields == nil {
|
||||
view.Data.Spec.SelectedFields = []telemetrytypes.TelemetryFieldKey{}
|
||||
}
|
||||
return storables, nil
|
||||
}
|
||||
|
||||
111
pkg/modules/savedview/implsavedview/store_test.go
Normal file
111
pkg/modules/savedview/implsavedview/store_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package implsavedview
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory/factorytest"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore/sqlitesqlstore"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/savedviewtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// newRealSQLiteStore spins up a real, file-backed sqlite database (not
|
||||
// sqlmock) with the actual saved_view table + the same UNIQUE(org_id, name)
|
||||
// index migration 109 creates in production, so tests here exercise the
|
||||
// genuine constraint violation and sqlitesqlstore.WrapAlreadyExistsErrf's
|
||||
// real error classification -- something a mocked "return this canned
|
||||
// error" test can't verify.
|
||||
func newRealSQLiteStore(t *testing.T) *store {
|
||||
t.Helper()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "saved_view_test.db")
|
||||
sqlStore, err := sqlitesqlstore.New(context.Background(), factorytest.NewSettings(), sqlstore.Config{
|
||||
Provider: "sqlite",
|
||||
Connection: sqlstore.ConnectionConfig{
|
||||
MaxOpenConns: 1,
|
||||
MaxConnLifetime: 0,
|
||||
},
|
||||
Sqlite: sqlstore.SqliteConfig{
|
||||
Path: dbPath,
|
||||
Mode: "wal",
|
||||
BusyTimeout: 5 * time.Second,
|
||||
TransactionMode: "deferred",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = sqlStore.BunDB().NewCreateTable().
|
||||
Model((*savedviewtypes.StorableSavedView)(nil)).
|
||||
IfNotExists().
|
||||
Exec(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = sqlStore.BunDB().Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_saved_view_org_id_name ON saved_view (org_id, name)`)
|
||||
require.NoError(t, err)
|
||||
|
||||
return &store{sqlstore: sqlStore}
|
||||
}
|
||||
|
||||
func testStorableSavedView(orgID string) *savedviewtypes.StorableSavedView {
|
||||
const name = "same-name"
|
||||
view := &savedviewtypes.SavedView{
|
||||
Name: name,
|
||||
Source: savedviewtypes.SourceLogs,
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: name,
|
||||
PanelType: savedviewtypes.PanelTypeGraph,
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Aggregations: []qbtypes.LogAggregation{{Expression: "count()"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
view.ID = valuer.GenerateUUID()
|
||||
view.OrgID = orgID
|
||||
view.CreatedBy = "creator@signoz.io"
|
||||
view.UpdatedBy = "creator@signoz.io"
|
||||
now := time.Now()
|
||||
view.CreatedAt = now
|
||||
view.UpdatedAt = now
|
||||
return savedviewtypes.NewStorableSavedView(view)
|
||||
}
|
||||
|
||||
// TestStore_Create_DuplicateNameIsConflict proves the full, real path: a
|
||||
// genuine sqlite UNIQUE(org_id, name) violation is classified as
|
||||
// errors.TypeAlreadyExists, not left as an opaque internal error -- this is
|
||||
// what makes the 409 declared on CreateSavedView actually true, not just
|
||||
// documented in the OpenAPI schema.
|
||||
func TestStore_Create_DuplicateNameIsConflict(t *testing.T) {
|
||||
s := newRealSQLiteStore(t)
|
||||
orgID := valuer.GenerateUUID().StringValue()
|
||||
|
||||
require.NoError(t, s.Create(context.Background(), testStorableSavedView(orgID)))
|
||||
|
||||
err := s.Create(context.Background(), testStorableSavedView(orgID))
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Ast(err, errors.TypeAlreadyExists), "expected an already-exists error, got %v", err)
|
||||
}
|
||||
|
||||
// TestStore_Create_SameNameDifferentOrgSucceeds guards against an
|
||||
// overly-broad fix: uniqueness is scoped to (org_id, name), so the same name
|
||||
// under a different org must succeed.
|
||||
func TestStore_Create_SameNameDifferentOrgSucceeds(t *testing.T) {
|
||||
s := newRealSQLiteStore(t)
|
||||
|
||||
require.NoError(t, s.Create(context.Background(), testStorableSavedView(valuer.GenerateUUID().StringValue())))
|
||||
require.NoError(t, s.Create(context.Background(), testStorableSavedView(valuer.GenerateUUID().StringValue())))
|
||||
}
|
||||
@@ -237,6 +237,7 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewAddDashboardTuplesFactory(sqlstore),
|
||||
sqlmigration.NewRestructureSavedViewSpecFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewAddSavedViewTuplesFactory(sqlstore),
|
||||
sqlmigration.NewFixSavedViewSelectedFieldsFactory(sqlstore),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
type restructureSavedViewSpec struct {
|
||||
@@ -43,11 +45,11 @@ type legacySavedViewCompositeQuery struct {
|
||||
|
||||
// legacySavedViewExtraData mirrors the frontend defined extraData JSON shape.
|
||||
type legacySavedViewExtraData struct {
|
||||
Color string `json:"color,omitempty"`
|
||||
SelectColumns json.RawMessage `json:"selectColumns,omitempty"`
|
||||
Format string `json:"format,omitempty"`
|
||||
MaxLines int `json:"maxLines,omitempty"`
|
||||
FontSize string `json:"fontSize,omitempty"`
|
||||
Color string `json:"color,omitempty"`
|
||||
SelectColumns []telemetrytypes.TelemetryFieldKey `json:"selectColumns,omitempty"`
|
||||
Format string `json:"format,omitempty"`
|
||||
MaxLines int `json:"maxLines,omitempty"`
|
||||
FontSize string `json:"fontSize,omitempty"`
|
||||
}
|
||||
|
||||
type savedViewDisplay struct {
|
||||
@@ -58,11 +60,11 @@ type savedViewDisplay struct {
|
||||
}
|
||||
|
||||
type savedViewSpec struct {
|
||||
DisplayName string `json:"displayName"`
|
||||
PanelType string `json:"panelType"`
|
||||
Queries json.RawMessage `json:"queries"`
|
||||
SelectedFields json.RawMessage `json:"selectedFields"`
|
||||
Display savedViewDisplay `json:"display"`
|
||||
DisplayName string `json:"displayName"`
|
||||
PanelType string `json:"panelType"`
|
||||
Queries json.RawMessage `json:"queries"`
|
||||
SelectedFields []telemetrytypes.TelemetryFieldKey `json:"selectedFields"`
|
||||
Display savedViewDisplay `json:"display"`
|
||||
}
|
||||
|
||||
type savedViewData struct {
|
||||
@@ -164,6 +166,15 @@ func (migration *restructureSavedViewSpec) Up(ctx context.Context, db *bun.DB) e
|
||||
return err
|
||||
}
|
||||
|
||||
var orgIDs []string
|
||||
if err := tx.NewSelect().Model((*types.Organization)(nil)).Column("id").Scan(ctx, &orgIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
validOrgIDs := make(map[string]struct{}, len(orgIDs))
|
||||
for _, id := range orgIDs {
|
||||
validOrgIDs[id] = struct{}{}
|
||||
}
|
||||
|
||||
// drop table `saved_views`
|
||||
for _, sql := range migration.sqlschema.Operator().DropTable(savedViewsTable) {
|
||||
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
|
||||
@@ -210,6 +221,13 @@ func (migration *restructureSavedViewSpec) Up(ctx context.Context, db *bun.DB) e
|
||||
continue // orphaned row from a pre-existing org_id backfill gap; nothing sane to attach it to
|
||||
}
|
||||
|
||||
// to avoid foreign key constraint issues
|
||||
if _, ok := validOrgIDs[old.OrgID]; !ok {
|
||||
skipped++
|
||||
migration.settings.Logger.WarnContext(ctx, "saved view references an org that no longer exists, skipping", slog.String("org_id", old.OrgID), slog.String("saved_view_id", old.ID))
|
||||
continue
|
||||
}
|
||||
|
||||
var compositeQuery legacySavedViewCompositeQuery
|
||||
if err := json.Unmarshal([]byte(old.Data), &compositeQuery); err != nil {
|
||||
failed++
|
||||
@@ -221,7 +239,9 @@ func (migration *restructureSavedViewSpec) Up(ctx context.Context, db *bun.DB) e
|
||||
if old.ExtraData != "" {
|
||||
// best-effort: malformed/older extraData shapes never fail the migration,
|
||||
// they just leave selectedFields/display empty.
|
||||
_ = json.Unmarshal([]byte(old.ExtraData), &extraData)
|
||||
if err := json.Unmarshal([]byte(old.ExtraData), &extraData); err != nil {
|
||||
migration.settings.Logger.WarnContext(ctx, "failed to unmarshal saved view extra data, continuing with empty selectedFields/display", slog.String("org_id", old.OrgID), slog.String("saved_view_id", old.ID), slog.Any("error", err))
|
||||
}
|
||||
}
|
||||
|
||||
dataJSON, err := json.Marshal(savedViewData{
|
||||
|
||||
185
pkg/sqlmigration/111_fix_saved_view_selected_fields.go
Normal file
185
pkg/sqlmigration/111_fix_saved_view_selected_fields.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/savedviewtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
type fixSavedViewSelectedFields struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
settings factory.ProviderSettings
|
||||
}
|
||||
|
||||
func NewFixSavedViewSelectedFieldsFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("fix_saved_view_selected_fields"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &fixSavedViewSelectedFields{sqlstore: sqlstore, settings: ps}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (migration *fixSavedViewSelectedFields) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
// storableSavedViewData is the shape of the `saved_view` table this migration repairs.
|
||||
type storableSavedViewData struct {
|
||||
bun.BaseModel `bun:"table:saved_view"`
|
||||
|
||||
ID string `bun:"id,pk,type:text"`
|
||||
Data string `bun:"data,type:text"`
|
||||
}
|
||||
|
||||
// specFieldUnmarshalsCleanly reports whether value can be unmarshalled into
|
||||
// the real type of the given savedviewtypes.SavedViewSpec JSON key.
|
||||
func specFieldUnmarshalsCleanly(key string, value json.RawMessage) bool {
|
||||
switch key {
|
||||
case "displayName", "panelType":
|
||||
var s string
|
||||
return json.Unmarshal(value, &s) == nil
|
||||
case "queries":
|
||||
var q []qbtypes.QueryEnvelope
|
||||
return json.Unmarshal(value, &q) == nil
|
||||
case "selectedFields":
|
||||
var f []telemetrytypes.TelemetryFieldKey
|
||||
return json.Unmarshal(value, &f) == nil
|
||||
case "display":
|
||||
var d savedviewtypes.Display
|
||||
return json.Unmarshal(value, &d) == nil
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// specFieldZeroValueJSON is the JSON to substitute for a spec key that fails
|
||||
// to unmarshal into its real type.
|
||||
var specFieldZeroValueJSON = map[string]string{
|
||||
"displayName": `""`,
|
||||
"panelType": `""`,
|
||||
"queries": `[]`,
|
||||
"selectedFields": `[]`,
|
||||
"display": `{}`,
|
||||
}
|
||||
|
||||
// repairSavedViewData tries to make data unmarshal cleanly into
|
||||
// savedviewtypes.SavedViewData by blanking, one key at a time, whichever
|
||||
// top-level spec fields fail to unmarshal into their real type -- e.g. a
|
||||
// selectedFields shape the 109 migration forwarded verbatim from a
|
||||
// pre-telemetrytypes.TelemetryFieldKey install, or a queries shape that
|
||||
// predates the current discriminated-union QueryEnvelope. Every other key is
|
||||
// left byte-for-byte untouched. Returns ok=false if data/spec aren't even
|
||||
// JSON objects, or the result still doesn't unmarshal cleanly afterward.
|
||||
func repairSavedViewData(data string) (fixed string, blanked []string, ok bool) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal([]byte(data), &raw); err != nil {
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
var spec map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw["spec"], &spec); err != nil {
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
for key, value := range spec {
|
||||
if specFieldUnmarshalsCleanly(key, value) {
|
||||
continue
|
||||
}
|
||||
zero, known := specFieldZeroValueJSON[key]
|
||||
if !known {
|
||||
continue
|
||||
}
|
||||
spec[key] = json.RawMessage(zero)
|
||||
blanked = append(blanked, key)
|
||||
}
|
||||
|
||||
fixedSpec, err := json.Marshal(spec)
|
||||
if err != nil {
|
||||
return "", nil, false
|
||||
}
|
||||
raw["spec"] = fixedSpec
|
||||
|
||||
fixedData, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
// verify the fix actually round-trips through the real type before writing it.
|
||||
if err := json.Unmarshal(fixedData, new(savedviewtypes.SavedViewData)); err != nil {
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
return string(fixedData), blanked, true
|
||||
}
|
||||
|
||||
// placeholderSavedViewData is substituted whole when a row can't be repaired
|
||||
// field-by-field (data/spec aren't JSON objects at all, or repair still
|
||||
// doesn't unmarshal cleanly). It must itself always unmarshal cleanly, since
|
||||
// every Get/List reads saved_view.data straight into savedviewtypes.SavedViewData
|
||||
// -- leaving genuinely-unrepairable data in place would 500 on every future read.
|
||||
func placeholderSavedViewData(id string) string {
|
||||
data, err := json.Marshal(savedviewtypes.SavedViewData{
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion.StringValue(),
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: fmt.Sprintf("corrupted saved view %s", id),
|
||||
PanelType: savedviewtypes.PanelTypeTable,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
// marshalling a static, well-formed literal cannot fail.
|
||||
panic(err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func (migration *fixSavedViewSelectedFields) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
var rows []*storableSavedViewData
|
||||
if err := tx.NewSelect().Model(&rows).Scan(ctx); err != nil && err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
|
||||
var repaired, replaced int
|
||||
for _, row := range rows {
|
||||
// already scans cleanly as-is -- nothing to repair.
|
||||
if err := json.Unmarshal([]byte(row.Data), new(savedviewtypes.SavedViewData)); err == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
fixedData, blanked, ok := repairSavedViewData(row.Data)
|
||||
if !ok {
|
||||
fixedData = placeholderSavedViewData(row.ID)
|
||||
replaced++
|
||||
migration.settings.Logger.WarnContext(ctx, "saved view data could not be repaired field-by-field, replacing with a placeholder view", slog.String("saved_view_id", row.ID))
|
||||
} else {
|
||||
repaired++
|
||||
migration.settings.Logger.WarnContext(ctx, "repaired saved view data by blanking fields that failed to unmarshal", slog.String("saved_view_id", row.ID), slog.Any("fields_blanked", blanked))
|
||||
}
|
||||
|
||||
if _, err := tx.NewUpdate().Model((*storableSavedViewData)(nil)).Set("data = ?", fixedData).Where("id = ?", row.ID).Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
migration.settings.Logger.InfoContext(ctx, "checked saved views for unreadable data", slog.Int("total", len(rows)), slog.Int("repaired", repaired), slog.Int("replaced", replaced))
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *fixSavedViewSelectedFields) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
174
pkg/sqlmigration/111_fix_saved_view_selected_fields_test.go
Normal file
174
pkg/sqlmigration/111_fix_saved_view_selected_fields_test.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/savedviewtypes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// healthyData is a well-formed row: every spec key unmarshals cleanly.
|
||||
const healthyData = `{"schemaVersion":"v2","spec":{"displayName":"My View","panelType":"table","queries":[{"type":"builder_query","spec":{"name":"A","signal":"logs","aggregations":[{"expression":"count()"}]}}],"selectedFields":[{"name":"service.name"}],"display":{"maxLines":0,"fontSize":"","format":"","color":""}}}`
|
||||
|
||||
func TestRepairSavedViewData(t *testing.T) {
|
||||
t.Run("healthy data is a no-op", func(t *testing.T) {
|
||||
fixed, blanked, ok := repairSavedViewData(healthyData)
|
||||
require.True(t, ok)
|
||||
assert.Empty(t, blanked)
|
||||
|
||||
var got savedviewtypes.SavedViewData
|
||||
require.NoError(t, json.Unmarshal([]byte(fixed), &got))
|
||||
assert.Equal(t, "My View", got.Spec.DisplayName)
|
||||
assert.Equal(t, []string{"service.name"}, telemetryFieldKeyName(t, got))
|
||||
})
|
||||
|
||||
t.Run("selectedFields in the pre-TelemetryFieldKey bare-string shape is blanked, other fields untouched", func(t *testing.T) {
|
||||
// this is the actual real-world corruption: migration 109 forwarded the
|
||||
// legacy frontend's extraData.selectColumns verbatim before it was typed
|
||||
// as []telemetrytypes.TelemetryFieldKey -- a bare list of strings like
|
||||
// this fails to unmarshal into a list of objects.
|
||||
data := `{"schemaVersion":"v2","spec":{"displayName":"Corrupted Fields","panelType":"table","queries":[{"type":"builder_query","spec":{"name":"A","signal":"logs","aggregations":[{"expression":"count()"}]}}],"selectedFields":["service.name","http.method"],"display":{"maxLines":0,"fontSize":"","format":"","color":""}}}`
|
||||
|
||||
fixed, blanked, ok := repairSavedViewData(data)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, []string{"selectedFields"}, blanked)
|
||||
|
||||
var got savedviewtypes.SavedViewData
|
||||
require.NoError(t, json.Unmarshal([]byte(fixed), &got))
|
||||
assert.Equal(t, "Corrupted Fields", got.Spec.DisplayName, "unrelated fields must survive untouched")
|
||||
assert.Equal(t, "table", got.Spec.PanelType.StringValue())
|
||||
assert.Len(t, got.Spec.Queries, 1, "unrelated fields must survive untouched")
|
||||
assert.Empty(t, got.Spec.SelectedFields, "the corrupted field is blanked to an empty list, not left broken")
|
||||
})
|
||||
|
||||
t.Run("queries predating the discriminated-union QueryEnvelope shape is blanked, other fields untouched", func(t *testing.T) {
|
||||
data := `{"schemaVersion":"v2","spec":{"displayName":"Old Queries","panelType":"table","queries":[{"query":"select 1"}],"selectedFields":[{"name":"service.name"}],"display":{"maxLines":0,"fontSize":"","format":"","color":""}}}`
|
||||
|
||||
fixed, blanked, ok := repairSavedViewData(data)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, []string{"queries"}, blanked)
|
||||
|
||||
var got savedviewtypes.SavedViewData
|
||||
require.NoError(t, json.Unmarshal([]byte(fixed), &got))
|
||||
assert.Equal(t, "Old Queries", got.Spec.DisplayName)
|
||||
assert.Empty(t, got.Spec.Queries)
|
||||
assert.Len(t, got.Spec.SelectedFields, 1, "unrelated fields must survive untouched")
|
||||
})
|
||||
|
||||
t.Run("multiple corrupted fields are each blanked independently", func(t *testing.T) {
|
||||
data := `{"schemaVersion":"v2","spec":{"displayName":"Double Trouble","panelType":"table","queries":[{"query":"select 1"}],"selectedFields":["service.name"],"display":{"maxLines":0,"fontSize":"","format":"","color":""}}}`
|
||||
|
||||
fixed, blanked, ok := repairSavedViewData(data)
|
||||
require.True(t, ok)
|
||||
assert.ElementsMatch(t, []string{"queries", "selectedFields"}, blanked)
|
||||
|
||||
var got savedviewtypes.SavedViewData
|
||||
require.NoError(t, json.Unmarshal([]byte(fixed), &got))
|
||||
assert.Equal(t, "Double Trouble", got.Spec.DisplayName)
|
||||
assert.Empty(t, got.Spec.Queries)
|
||||
assert.Empty(t, got.Spec.SelectedFields)
|
||||
})
|
||||
|
||||
t.Run("data is not a JSON object at all", func(t *testing.T) {
|
||||
_, _, ok := repairSavedViewData(`not json`)
|
||||
assert.False(t, ok)
|
||||
})
|
||||
|
||||
t.Run("data is valid JSON but not an object", func(t *testing.T) {
|
||||
_, _, ok := repairSavedViewData(`[1,2,3]`)
|
||||
assert.False(t, ok)
|
||||
})
|
||||
|
||||
t.Run("spec is missing entirely", func(t *testing.T) {
|
||||
_, _, ok := repairSavedViewData(`{"schemaVersion":"v2"}`)
|
||||
assert.False(t, ok)
|
||||
})
|
||||
|
||||
t.Run("spec is present but not an object", func(t *testing.T) {
|
||||
_, _, ok := repairSavedViewData(`{"schemaVersion":"v2","spec":"garbage"}`)
|
||||
assert.False(t, ok)
|
||||
})
|
||||
|
||||
t.Run("an unknown spec key is left untouched regardless of its shape", func(t *testing.T) {
|
||||
data := `{"schemaVersion":"v2","spec":{"displayName":"Has Extra Key","panelType":"table","queries":[{"type":"builder_query","spec":{"name":"A","signal":"logs","aggregations":[{"expression":"count()"}]}}],"selectedFields":[],"display":{},"someFutureField":{"whatever":123}}}`
|
||||
|
||||
fixed, blanked, ok := repairSavedViewData(data)
|
||||
require.True(t, ok)
|
||||
assert.Empty(t, blanked)
|
||||
assert.Contains(t, fixed, "someFutureField")
|
||||
})
|
||||
}
|
||||
|
||||
func TestSpecFieldUnmarshalsCleanly(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
key string
|
||||
value string
|
||||
want bool
|
||||
}{
|
||||
{name: "valid displayName", key: "displayName", value: `"My View"`, want: true},
|
||||
{name: "displayName as a number fails", key: "displayName", value: `123`, want: false},
|
||||
{name: "valid panelType", key: "panelType", value: `"table"`, want: true},
|
||||
{name: "valid queries", key: "queries", value: `[{"type":"builder_query","spec":{"name":"A","signal":"logs","aggregations":[{"expression":"count()"}]}}]`, want: true},
|
||||
{name: "queries missing the type discriminator fails", key: "queries", value: `[{"query":"select 1"}]`, want: false},
|
||||
{name: "valid selectedFields", key: "selectedFields", value: `[{"name":"service.name"}]`, want: true},
|
||||
{name: "selectedFields as bare strings fails", key: "selectedFields", value: `["service.name"]`, want: false},
|
||||
{name: "valid display", key: "display", value: `{"maxLines":0,"fontSize":"","format":"","color":""}`, want: true},
|
||||
{name: "display as a bare string fails", key: "display", value: `"blue"`, want: false},
|
||||
{name: "unknown key always reports clean", key: "someFutureField", value: `{"anything":"goes"}`, want: true},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got := specFieldUnmarshalsCleanly(c.key, json.RawMessage(c.value))
|
||||
assert.Equal(t, c.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaceholderSavedViewData(t *testing.T) {
|
||||
placeholder := placeholderSavedViewData("019fe515-d981-7de7-a8c3-6137ae1200c2")
|
||||
|
||||
var got savedviewtypes.SavedViewData
|
||||
require.NoError(t, json.Unmarshal([]byte(placeholder), &got), "the placeholder itself must always unmarshal cleanly")
|
||||
|
||||
assert.Equal(t, savedviewtypes.SavedViewSchemaVersion.StringValue(), got.SchemaVersion)
|
||||
assert.Contains(t, got.Spec.DisplayName, "019fe515-d981-7de7-a8c3-6137ae1200c2")
|
||||
assert.Equal(t, savedviewtypes.PanelTypeTable, got.Spec.PanelType)
|
||||
|
||||
// verify the full read path a Get/List would take doesn't panic or error.
|
||||
storable := &savedviewtypes.StorableSavedView{Data: got}
|
||||
view := storable.ToSavedView()
|
||||
assert.NotNil(t, view.Spec.SelectedFields)
|
||||
}
|
||||
|
||||
// TestUnrepairableDataGetsReplacedNotLeftBroken exercises the exact decision
|
||||
// Up() makes per row: an unrepairable row must never be left in a state that
|
||||
// fails to unmarshal, since every future Get/List reads saved_view.data
|
||||
// straight into savedviewtypes.SavedViewData.
|
||||
func TestUnrepairableDataGetsReplacedNotLeftBroken(t *testing.T) {
|
||||
cases := []string{
|
||||
`not json at all`,
|
||||
`{"schemaVersion":"v2","spec":"garbage"}`,
|
||||
`{"schemaVersion":"v2"}`,
|
||||
}
|
||||
|
||||
for _, data := range cases {
|
||||
_, _, ok := repairSavedViewData(data)
|
||||
require.False(t, ok, "expected %q to be unrepairable", data)
|
||||
|
||||
fixed := placeholderSavedViewData("some-id")
|
||||
require.NoError(t, json.Unmarshal([]byte(fixed), new(savedviewtypes.SavedViewData)))
|
||||
}
|
||||
}
|
||||
|
||||
func telemetryFieldKeyName(t *testing.T, data savedviewtypes.SavedViewData) []string {
|
||||
t.Helper()
|
||||
names := make([]string, 0, len(data.Spec.SelectedFields))
|
||||
for _, f := range data.Spec.SelectedFields {
|
||||
names = append(names, f.Name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
@@ -42,13 +42,22 @@ type ClusterRecord struct {
|
||||
type PostableClusters struct {
|
||||
Start int64 `json:"start" required:"true"`
|
||||
End int64 `json:"end" required:"true"`
|
||||
Filter *qbtypes.Filter `json:"filter"`
|
||||
Filter *ClusterFilter `json:"filter"`
|
||||
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
|
||||
OrderBy *qbtypes.OrderBy `json:"orderBy"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit" required:"true"`
|
||||
}
|
||||
|
||||
// ClusterFilter is the attribute filter plus optional secondary filters on the
|
||||
// derived pod display status(es) (see PodStatus; matches any listed, OR) and node
|
||||
// readiness (see NodeCondition; matches any listed, OR). Empty FilterByPodStatus / FilterByNodeReadiness = off.
|
||||
type ClusterFilter struct {
|
||||
qbtypes.Filter `json:",inline"`
|
||||
FilterByPodStatus []PodStatus `json:"filterByPodStatus"`
|
||||
FilterByNodeReadiness []NodeCondition `json:"filterByNodeReadiness"`
|
||||
}
|
||||
|
||||
// Validate ensures PostableClusters contains acceptable values.
|
||||
func (req *PostableClusters) Validate() error {
|
||||
if req == nil {
|
||||
@@ -88,6 +97,19 @@ func (req *PostableClusters) Validate() error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.Filter != nil {
|
||||
for _, s := range req.Filter.FilterByPodStatus {
|
||||
if !s.IsFilterable() {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", s)
|
||||
}
|
||||
}
|
||||
for _, c := range req.Filter.FilterByNodeReadiness {
|
||||
if !c.IsFilterable() {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by node readiness: %s", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.OrderBy != nil {
|
||||
if !slices.Contains(ClustersValidOrderByKeys, req.OrderBy.Key.Name) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)
|
||||
|
||||
@@ -75,13 +75,21 @@ type ContainerRecord struct {
|
||||
type PostableContainers struct {
|
||||
Start int64 `json:"start" required:"true"`
|
||||
End int64 `json:"end" required:"true"`
|
||||
Filter *qbtypes.Filter `json:"filter"`
|
||||
Filter *ContainerFilter `json:"filter"`
|
||||
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
|
||||
OrderBy *qbtypes.OrderBy `json:"orderBy"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit" required:"true"`
|
||||
}
|
||||
|
||||
// ContainerFilter is the attribute filter plus an optional secondary filter on
|
||||
// the derived container display status(es) (see ContainerStatus; matches any
|
||||
// listed, OR). Empty FilterByContainerStatus = off.
|
||||
type ContainerFilter struct {
|
||||
qbtypes.Filter `json:",inline"`
|
||||
FilterByContainerStatus []ContainerStatus `json:"filterByContainerStatus"`
|
||||
}
|
||||
|
||||
// Validate ensures PostableContainers contains acceptable values.
|
||||
func (req *PostableContainers) Validate() error {
|
||||
if req == nil {
|
||||
@@ -121,6 +129,14 @@ func (req *PostableContainers) Validate() error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.Filter != nil {
|
||||
for _, c := range req.Filter.FilterByContainerStatus {
|
||||
if !c.IsFilterable() {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by container status: %s", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.OrderBy != nil {
|
||||
if !slices.Contains(ContainersValidOrderByKeys, req.OrderBy.Key.Name) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/valuer"
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// ContainerStatus is the kubectl-style display status of a container, derived
|
||||
// from k8s.container.status.state (base) + k8s.container.status.reason (overlay).
|
||||
@@ -31,6 +35,12 @@ var (
|
||||
ContainerStatusNoData = ContainerStatus{valuer.NewString("no_data")}
|
||||
)
|
||||
|
||||
// IsFilterable reports whether c is a concrete, user-filterable
|
||||
// container status: any Enum() member except the no_data sentinel.
|
||||
func (c ContainerStatus) IsFilterable() bool {
|
||||
return c != ContainerStatusNoData && slices.Contains((ContainerStatus{}).Enum(), any(c))
|
||||
}
|
||||
|
||||
func (ContainerStatus) Enum() []any {
|
||||
return []any{
|
||||
ContainerStatusRunning,
|
||||
|
||||
@@ -36,13 +36,20 @@ type DaemonSetRecord struct {
|
||||
type PostableDaemonSets struct {
|
||||
Start int64 `json:"start" required:"true"`
|
||||
End int64 `json:"end" required:"true"`
|
||||
Filter *qbtypes.Filter `json:"filter"`
|
||||
Filter *DaemonSetFilter `json:"filter"`
|
||||
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
|
||||
OrderBy *qbtypes.OrderBy `json:"orderBy"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit" required:"true"`
|
||||
}
|
||||
|
||||
// DaemonSetFilter is the attribute filter plus an optional secondary filter on the
|
||||
// derived pod display status(es) (see PodStatus); matches any listed (OR). Empty = off.
|
||||
type DaemonSetFilter struct {
|
||||
qbtypes.Filter `json:",inline"`
|
||||
FilterByPodStatus []PodStatus `json:"filterByPodStatus"`
|
||||
}
|
||||
|
||||
// Validate ensures PostableDaemonSets contains acceptable values.
|
||||
func (req *PostableDaemonSets) Validate() error {
|
||||
if req == nil {
|
||||
@@ -82,6 +89,14 @@ func (req *PostableDaemonSets) Validate() error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.Filter != nil {
|
||||
for _, s := range req.Filter.FilterByPodStatus {
|
||||
if !s.IsFilterable() {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.OrderBy != nil {
|
||||
if !slices.Contains(DaemonSetsValidOrderByKeys, req.OrderBy.Key.Name) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)
|
||||
|
||||
@@ -34,13 +34,20 @@ type DeploymentRecord struct {
|
||||
type PostableDeployments struct {
|
||||
Start int64 `json:"start" required:"true"`
|
||||
End int64 `json:"end" required:"true"`
|
||||
Filter *qbtypes.Filter `json:"filter"`
|
||||
Filter *DeploymentFilter `json:"filter"`
|
||||
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
|
||||
OrderBy *qbtypes.OrderBy `json:"orderBy"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit" required:"true"`
|
||||
}
|
||||
|
||||
// DeploymentFilter is the attribute filter plus an optional secondary filter on the
|
||||
// derived pod display status(es) (see PodStatus); matches any listed (OR). Empty = off.
|
||||
type DeploymentFilter struct {
|
||||
qbtypes.Filter `json:",inline"`
|
||||
FilterByPodStatus []PodStatus `json:"filterByPodStatus"`
|
||||
}
|
||||
|
||||
// Validate ensures PostableDeployments contains acceptable values.
|
||||
func (req *PostableDeployments) Validate() error {
|
||||
if req == nil {
|
||||
@@ -80,6 +87,14 @@ func (req *PostableDeployments) Validate() error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.Filter != nil {
|
||||
for _, s := range req.Filter.FilterByPodStatus {
|
||||
if !s.IsFilterable() {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.OrderBy != nil {
|
||||
if !slices.Contains(DeploymentsValidOrderByKeys, req.OrderBy.Key.Name) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)
|
||||
|
||||
@@ -36,13 +36,20 @@ type JobRecord struct {
|
||||
type PostableJobs struct {
|
||||
Start int64 `json:"start" required:"true"`
|
||||
End int64 `json:"end" required:"true"`
|
||||
Filter *qbtypes.Filter `json:"filter"`
|
||||
Filter *JobFilter `json:"filter"`
|
||||
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
|
||||
OrderBy *qbtypes.OrderBy `json:"orderBy"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit" required:"true"`
|
||||
}
|
||||
|
||||
// JobFilter is the attribute filter plus an optional secondary filter on the
|
||||
// derived pod display status(es) (see PodStatus); matches any listed (OR). Empty = off.
|
||||
type JobFilter struct {
|
||||
qbtypes.Filter `json:",inline"`
|
||||
FilterByPodStatus []PodStatus `json:"filterByPodStatus"`
|
||||
}
|
||||
|
||||
// Validate ensures PostableJobs contains acceptable values.
|
||||
func (req *PostableJobs) Validate() error {
|
||||
if req == nil {
|
||||
@@ -82,6 +89,14 @@ func (req *PostableJobs) Validate() error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.Filter != nil {
|
||||
for _, s := range req.Filter.FilterByPodStatus {
|
||||
if !s.IsFilterable() {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.OrderBy != nil {
|
||||
if !slices.Contains(JobsValidOrderByKeys, req.OrderBy.Key.Name) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)
|
||||
|
||||
@@ -34,13 +34,20 @@ type NamespaceRecord struct {
|
||||
type PostableNamespaces struct {
|
||||
Start int64 `json:"start" required:"true"`
|
||||
End int64 `json:"end" required:"true"`
|
||||
Filter *qbtypes.Filter `json:"filter"`
|
||||
Filter *NamespaceFilter `json:"filter"`
|
||||
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
|
||||
OrderBy *qbtypes.OrderBy `json:"orderBy"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit" required:"true"`
|
||||
}
|
||||
|
||||
// NamespaceFilter is the attribute filter plus an optional secondary filter on the
|
||||
// derived pod display status(es) (see PodStatus); matches any listed (OR). Empty = off.
|
||||
type NamespaceFilter struct {
|
||||
qbtypes.Filter `json:",inline"`
|
||||
FilterByPodStatus []PodStatus `json:"filterByPodStatus"`
|
||||
}
|
||||
|
||||
// Validate ensures PostableNamespaces contains acceptable values.
|
||||
func (req *PostableNamespaces) Validate() error {
|
||||
if req == nil {
|
||||
@@ -80,6 +87,14 @@ func (req *PostableNamespaces) Validate() error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.Filter != nil {
|
||||
for _, s := range req.Filter.FilterByPodStatus {
|
||||
if !s.IsFilterable() {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.OrderBy != nil {
|
||||
if !slices.Contains(NamespacesValidOrderByKeys, req.OrderBy.Key.Name) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)
|
||||
|
||||
@@ -39,13 +39,22 @@ type NodeRecord struct {
|
||||
type PostableNodes struct {
|
||||
Start int64 `json:"start" required:"true"`
|
||||
End int64 `json:"end" required:"true"`
|
||||
Filter *qbtypes.Filter `json:"filter"`
|
||||
Filter *NodeFilter `json:"filter"`
|
||||
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
|
||||
OrderBy *qbtypes.OrderBy `json:"orderBy"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit" required:"true"`
|
||||
}
|
||||
|
||||
// NodeFilter is the attribute filter plus an optional secondary filter on the
|
||||
// derived pod display status(es) (see PodStatus; matches any listed, OR) and node
|
||||
// readiness (see NodeCondition; matches any listed, OR). Empty FilterByPodStatus / FilterByNodeReadiness = off.
|
||||
type NodeFilter struct {
|
||||
qbtypes.Filter `json:",inline"`
|
||||
FilterByPodStatus []PodStatus `json:"filterByPodStatus"`
|
||||
FilterByNodeReadiness []NodeCondition `json:"filterByNodeReadiness"`
|
||||
}
|
||||
|
||||
// Validate ensures PostableNodes contains acceptable values.
|
||||
func (req *PostableNodes) Validate() error {
|
||||
if req == nil {
|
||||
@@ -85,6 +94,19 @@ func (req *PostableNodes) Validate() error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.Filter != nil {
|
||||
for _, s := range req.Filter.FilterByPodStatus {
|
||||
if !s.IsFilterable() {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", s)
|
||||
}
|
||||
}
|
||||
for _, c := range req.Filter.FilterByNodeReadiness {
|
||||
if !c.IsFilterable() {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by node readiness: %s", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.OrderBy != nil {
|
||||
if !slices.Contains(NodesValidOrderByKeys, req.OrderBy.Key.Name) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/valuer"
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type NodeCondition struct {
|
||||
valuer.String
|
||||
@@ -20,6 +24,12 @@ func (NodeCondition) Enum() []any {
|
||||
}
|
||||
}
|
||||
|
||||
// IsFilterable reports whether c is a concrete, user-filterable
|
||||
// node readiness: any Enum() member except the no_data sentinel.
|
||||
func (c NodeCondition) IsFilterable() bool {
|
||||
return c != NodeConditionNoData && slices.Contains((NodeCondition{}).Enum(), any(c))
|
||||
}
|
||||
|
||||
// Numeric values emitted by the k8s.node.condition_ready metric
|
||||
// (source: OTel kubeletstats receiver).
|
||||
const (
|
||||
|
||||
@@ -63,13 +63,20 @@ type PodRecord struct {
|
||||
type PostablePods struct {
|
||||
Start int64 `json:"start" required:"true"`
|
||||
End int64 `json:"end" required:"true"`
|
||||
Filter *qbtypes.Filter `json:"filter"`
|
||||
Filter *PodFilter `json:"filter"`
|
||||
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
|
||||
OrderBy *qbtypes.OrderBy `json:"orderBy"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit" required:"true"`
|
||||
}
|
||||
|
||||
// PodFilter is the attribute filter plus an optional secondary filter on the
|
||||
// derived pod display status(es) (see PodStatus); matches any listed (OR). Empty = off.
|
||||
type PodFilter struct {
|
||||
qbtypes.Filter `json:",inline"`
|
||||
FilterByPodStatus []PodStatus `json:"filterByPodStatus"`
|
||||
}
|
||||
|
||||
// Validate ensures PostablePods contains acceptable values.
|
||||
func (req *PostablePods) Validate() error {
|
||||
if req == nil {
|
||||
@@ -109,6 +116,14 @@ func (req *PostablePods) Validate() error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.Filter != nil {
|
||||
for _, s := range req.Filter.FilterByPodStatus {
|
||||
if !s.IsFilterable() {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.OrderBy != nil {
|
||||
if !slices.Contains(PodsValidOrderByKeys, req.OrderBy.Key.Name) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/valuer"
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// PodStatus is the kubectl-style pod display status, derived from
|
||||
// k8s.pod.phase + k8s.pod.status_reason + k8s.container.status.reason
|
||||
@@ -66,6 +70,12 @@ func (PodStatus) Enum() []any {
|
||||
}
|
||||
}
|
||||
|
||||
// IsFilterable reports whether s is a concrete, user-filterable pod
|
||||
// status: any Enum() member except the no_data sentinel.
|
||||
func (s PodStatus) IsFilterable() bool {
|
||||
return s != PodStatusNoData && slices.Contains((PodStatus{}).Enum(), any(s))
|
||||
}
|
||||
|
||||
const PodNameAttrKey = "k8s.pod.name"
|
||||
|
||||
const (
|
||||
|
||||
@@ -34,13 +34,20 @@ type StatefulSetRecord struct {
|
||||
type PostableStatefulSets struct {
|
||||
Start int64 `json:"start" required:"true"`
|
||||
End int64 `json:"end" required:"true"`
|
||||
Filter *qbtypes.Filter `json:"filter"`
|
||||
Filter *StatefulSetFilter `json:"filter"`
|
||||
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
|
||||
OrderBy *qbtypes.OrderBy `json:"orderBy"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit" required:"true"`
|
||||
}
|
||||
|
||||
// StatefulSetFilter is the attribute filter plus an optional secondary filter on the
|
||||
// derived pod display status(es) (see PodStatus); matches any listed (OR). Empty = off.
|
||||
type StatefulSetFilter struct {
|
||||
qbtypes.Filter `json:",inline"`
|
||||
FilterByPodStatus []PodStatus `json:"filterByPodStatus"`
|
||||
}
|
||||
|
||||
// Validate ensures PostableStatefulSets contains acceptable values.
|
||||
func (req *PostableStatefulSets) Validate() error {
|
||||
if req == nil {
|
||||
@@ -80,6 +87,14 @@ func (req *PostableStatefulSets) Validate() error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.Filter != nil {
|
||||
for _, s := range req.Filter.FilterByPodStatus {
|
||||
if !s.IsFilterable() {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.OrderBy != nil {
|
||||
if !slices.Contains(StatefulSetsValidOrderByKeys, req.OrderBy.Key.Name) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
@@ -28,27 +29,73 @@ var (
|
||||
)
|
||||
|
||||
type SavedView struct {
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
OrgID string `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Source Source `json:"source"`
|
||||
SchemaVersion SchemaVersion `json:"schemaVersion" required:"true"`
|
||||
Spec SavedViewSpec `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
type StorableSavedView struct {
|
||||
bun.BaseModel `bun:"table:saved_view"`
|
||||
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
OrgID string `json:"-" bun:"org_id,notnull"`
|
||||
Name string `json:"name" bun:"name,type:text,notnull"`
|
||||
Source Source `json:"source" bun:"source,type:text,notnull"`
|
||||
Data SavedViewData `json:"data" bun:"data,type:text,notnull"`
|
||||
OrgID string `bun:"org_id,notnull"`
|
||||
Name string `bun:"name,type:text,notnull"`
|
||||
Source Source `bun:"source,type:text,notnull"`
|
||||
Data SavedViewData `bun:"data,type:text,notnull"`
|
||||
}
|
||||
|
||||
func (s *StorableSavedView) ToSavedView() *SavedView {
|
||||
spec := s.Data.Spec
|
||||
if spec.SelectedFields == nil {
|
||||
spec.SelectedFields = []telemetrytypes.TelemetryFieldKey{}
|
||||
}
|
||||
|
||||
return &SavedView{
|
||||
Identifiable: s.Identifiable,
|
||||
TimeAuditable: s.TimeAuditable,
|
||||
UserAuditable: s.UserAuditable,
|
||||
OrgID: s.OrgID,
|
||||
Name: s.Name,
|
||||
Source: s.Source,
|
||||
SchemaVersion: SchemaVersion{valuer.NewString(s.Data.SchemaVersion)},
|
||||
Spec: spec,
|
||||
}
|
||||
}
|
||||
|
||||
func NewStorableSavedView(view *SavedView) *StorableSavedView {
|
||||
return &StorableSavedView{
|
||||
Identifiable: view.Identifiable,
|
||||
TimeAuditable: view.TimeAuditable,
|
||||
UserAuditable: view.UserAuditable,
|
||||
OrgID: view.OrgID,
|
||||
Name: view.Name,
|
||||
Source: view.Source,
|
||||
Data: SavedViewData{
|
||||
SchemaVersion: view.SchemaVersion.StringValue(),
|
||||
Spec: view.Spec,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type PostableSavedView struct {
|
||||
Name string `json:"name"`
|
||||
GenerateName bool `json:"generateName"`
|
||||
Source Source `json:"source" required:"true"`
|
||||
Data SavedViewData `json:"data" required:"true"`
|
||||
Name string `json:"name"`
|
||||
GenerateName bool `json:"generateName"`
|
||||
Source Source `json:"source" required:"true"`
|
||||
SchemaVersion SchemaVersion `json:"schemaVersion" required:"true"`
|
||||
Spec SavedViewSpec `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
type UpdatableSavedView struct {
|
||||
Source Source `json:"source" required:"true"`
|
||||
Data SavedViewData `json:"data" required:"true"`
|
||||
Source Source `json:"source" required:"true"`
|
||||
SchemaVersion SchemaVersion `json:"schemaVersion" required:"true"`
|
||||
Spec SavedViewSpec `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
type ListSavedViewsParams struct {
|
||||
@@ -83,7 +130,7 @@ func (postable PostableSavedView) ToSavedView(orgID string, createdBy string) *S
|
||||
|
||||
name := postable.Name
|
||||
if postable.GenerateName {
|
||||
name = generateSavedViewName(postable.Data.Spec.DisplayName)
|
||||
name = generateSavedViewName(postable.Spec.DisplayName)
|
||||
}
|
||||
|
||||
return &SavedView{
|
||||
@@ -93,7 +140,8 @@ func (postable PostableSavedView) ToSavedView(orgID string, createdBy string) *S
|
||||
OrgID: orgID,
|
||||
Name: name,
|
||||
Source: postable.Source,
|
||||
Data: postable.Data,
|
||||
SchemaVersion: postable.SchemaVersion,
|
||||
Spec: postable.Spec,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +154,8 @@ func (updatable UpdatableSavedView) ToSavedView(id valuer.UUID, orgID string, up
|
||||
UserAuditable: types.UserAuditable{UpdatedBy: updatedBy},
|
||||
OrgID: orgID,
|
||||
Source: updatable.Source,
|
||||
Data: updatable.Data,
|
||||
SchemaVersion: updatable.SchemaVersion,
|
||||
Spec: updatable.Spec,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,8 +166,11 @@ func (p *PostableSavedView) Validate() error {
|
||||
if err := p.Source.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.SchemaVersion.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.Data.Validate()
|
||||
return p.Spec.Validate()
|
||||
}
|
||||
|
||||
func (p *PostableSavedView) validateName() error {
|
||||
@@ -135,8 +187,11 @@ func (u *UpdatableSavedView) Validate() error {
|
||||
if err := u.Source.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := u.SchemaVersion.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return u.Data.Validate()
|
||||
return u.Spec.Validate()
|
||||
}
|
||||
|
||||
func (p *ListSavedViewsParams) Validate() error {
|
||||
@@ -147,7 +202,17 @@ func (p *ListSavedViewsParams) Validate() error {
|
||||
return p.Source.Validate()
|
||||
}
|
||||
|
||||
func NewStatsFromSavedViews(savedViews []*SavedView) map[string]any {
|
||||
// NewSavedViewsFromStorableSavedViews converts scanned rows to their domain shape.
|
||||
func NewSavedViewsFromStorableSavedViews(storableSavedViews []*StorableSavedView) []*SavedView {
|
||||
savedViews := make([]*SavedView, len(storableSavedViews))
|
||||
for idx, storableSavedView := range storableSavedViews {
|
||||
savedViews[idx] = storableSavedView.ToSavedView()
|
||||
}
|
||||
|
||||
return savedViews
|
||||
}
|
||||
|
||||
func NewStatsFromStorableSavedViews(savedViews []*StorableSavedView) map[string]any {
|
||||
stats := make(map[string]any)
|
||||
for _, savedView := range savedViews {
|
||||
key := "savedview.source." + strings.ToLower(savedView.Source.StringValue()) + ".count"
|
||||
|
||||
@@ -4,29 +4,27 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
)
|
||||
|
||||
func validPostableSavedView() PostableSavedView {
|
||||
return PostableSavedView{
|
||||
Name: "my-view",
|
||||
Source: SourceLogs,
|
||||
Data: SavedViewData{
|
||||
SchemaVersion: SavedViewSchemaVersion,
|
||||
Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()},
|
||||
},
|
||||
Name: "my-view",
|
||||
Source: SourceLogs,
|
||||
SchemaVersion: SavedViewSchemaVersion,
|
||||
Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()},
|
||||
}
|
||||
}
|
||||
|
||||
func validUpdatableSavedView() UpdatableSavedView {
|
||||
return UpdatableSavedView{
|
||||
Source: SourceLogs,
|
||||
Data: SavedViewData{
|
||||
SchemaVersion: SavedViewSchemaVersion,
|
||||
Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()},
|
||||
},
|
||||
Source: SourceLogs,
|
||||
SchemaVersion: SavedViewSchemaVersion,
|
||||
Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +67,7 @@ func TestPostableSavedViewValidate(t *testing.T) {
|
||||
|
||||
t.Run("invalid saved view data is rejected", func(t *testing.T) {
|
||||
view := validPostableSavedView()
|
||||
view.Data.SchemaVersion = "v1"
|
||||
view.SchemaVersion = SchemaVersion{valuer.NewString("v1")}
|
||||
assert.Error(t, view.Validate())
|
||||
})
|
||||
|
||||
@@ -100,7 +98,7 @@ func TestPostableSavedViewValidate(t *testing.T) {
|
||||
|
||||
t.Run("empty displayName is rejected", func(t *testing.T) {
|
||||
view := validPostableSavedView()
|
||||
view.Data.Spec.DisplayName = ""
|
||||
view.Spec.DisplayName = ""
|
||||
assert.ErrorContains(t, view.Validate(), "displayName is required")
|
||||
})
|
||||
}
|
||||
@@ -119,7 +117,7 @@ func TestUpdatableSavedViewValidate(t *testing.T) {
|
||||
|
||||
t.Run("empty displayName is rejected", func(t *testing.T) {
|
||||
view := validUpdatableSavedView()
|
||||
view.Data.Spec.DisplayName = ""
|
||||
view.Spec.DisplayName = ""
|
||||
assert.ErrorContains(t, view.Validate(), "displayName is required")
|
||||
})
|
||||
}
|
||||
@@ -153,7 +151,8 @@ func TestNewSavedView(t *testing.T) {
|
||||
assert.Equal(t, "creator@signoz.io", savedView.UpdatedBy)
|
||||
assert.Equal(t, view.Name, savedView.Name)
|
||||
assert.Equal(t, view.Source, savedView.Source)
|
||||
assert.Equal(t, view.Data, savedView.Data)
|
||||
assert.Equal(t, view.SchemaVersion, savedView.SchemaVersion)
|
||||
assert.Equal(t, view.Spec, savedView.Spec)
|
||||
assert.False(t, savedView.CreatedAt.IsZero())
|
||||
assert.Equal(t, savedView.CreatedAt, savedView.UpdatedAt)
|
||||
}
|
||||
@@ -163,14 +162,14 @@ func TestNewSavedView_GeneratesNameWhenEmpty(t *testing.T) {
|
||||
view := validPostableSavedView()
|
||||
view.Name = ""
|
||||
view.GenerateName = true
|
||||
view.Data.Spec.DisplayName = "My View!"
|
||||
view.Spec.DisplayName = "My View!"
|
||||
|
||||
savedView := view.ToSavedView(orgID, "creator@signoz.io")
|
||||
|
||||
assert.NotEmpty(t, savedView.Name)
|
||||
assert.Empty(t, validation.IsDNS1123Label(savedView.Name), "generated name must be a valid DNS-1123 label")
|
||||
assert.True(t, strings.HasPrefix(savedView.Name, "my-view-"))
|
||||
assert.Equal(t, "My View!", savedView.Data.Spec.DisplayName)
|
||||
assert.Equal(t, "My View!", savedView.Spec.DisplayName)
|
||||
}
|
||||
|
||||
func TestGenerateSavedViewName(t *testing.T) {
|
||||
@@ -212,17 +211,76 @@ func TestGenerateSavedViewName(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewStatsFromSavedViews(t *testing.T) {
|
||||
views := []*SavedView{
|
||||
func TestStorableSavedView_ToSavedView(t *testing.T) {
|
||||
t.Run("round trip preserves populated fields", func(t *testing.T) {
|
||||
view := &SavedView{
|
||||
Name: "my-view",
|
||||
Source: SourceLogs,
|
||||
SchemaVersion: SavedViewSchemaVersion,
|
||||
Spec: SavedViewSpec{
|
||||
DisplayName: "My View",
|
||||
PanelType: PanelTypeGraph,
|
||||
Queries: validQueries(),
|
||||
SelectedFields: []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}},
|
||||
},
|
||||
}
|
||||
view.OrgID = valuer.GenerateUUID().StringValue()
|
||||
|
||||
roundTripped := NewStorableSavedView(view).ToSavedView()
|
||||
|
||||
assert.Equal(t, view.OrgID, roundTripped.OrgID)
|
||||
assert.Equal(t, view.Name, roundTripped.Name)
|
||||
assert.Equal(t, view.Source, roundTripped.Source)
|
||||
assert.Equal(t, view.SchemaVersion, roundTripped.SchemaVersion)
|
||||
assert.Equal(t, view.Spec, roundTripped.Spec)
|
||||
})
|
||||
|
||||
t.Run("nil selectedFields normalizes to an empty slice, not nil", func(t *testing.T) {
|
||||
storable := &StorableSavedView{
|
||||
Data: SavedViewData{
|
||||
SchemaVersion: SavedViewSchemaVersion.StringValue(),
|
||||
Spec: SavedViewSpec{
|
||||
DisplayName: "My View",
|
||||
PanelType: PanelTypeGraph,
|
||||
Queries: validQueries(),
|
||||
SelectedFields: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
view := storable.ToSavedView()
|
||||
|
||||
assert.NotNil(t, view.Spec.SelectedFields)
|
||||
assert.Empty(t, view.Spec.SelectedFields)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewStatsFromStorableSavedViews(t *testing.T) {
|
||||
storables := []*StorableSavedView{
|
||||
{Source: SourceLogs},
|
||||
{Source: SourceLogs},
|
||||
{Source: SourceTraces},
|
||||
}
|
||||
|
||||
stats := NewStatsFromSavedViews(views)
|
||||
stats := NewStatsFromStorableSavedViews(storables)
|
||||
|
||||
assert.Equal(t, int64(3), stats["savedview.count"])
|
||||
assert.Equal(t, int64(2), stats["savedview.source.logs.count"])
|
||||
assert.Equal(t, int64(1), stats["savedview.source.traces.count"])
|
||||
assert.NotContains(t, stats, "savedview.source.metrics.count")
|
||||
}
|
||||
|
||||
func TestNewSavedViewsFromStorableSavedViews(t *testing.T) {
|
||||
storables := []*StorableSavedView{
|
||||
{Name: "a", Source: SourceLogs, Data: SavedViewData{SchemaVersion: SavedViewSchemaVersion.StringValue(), Spec: SavedViewSpec{DisplayName: "a", PanelType: PanelTypeGraph, Queries: validQueries()}}},
|
||||
{Name: "b", Source: SourceTraces, Data: SavedViewData{SchemaVersion: SavedViewSchemaVersion.StringValue(), Spec: SavedViewSpec{DisplayName: "b", PanelType: PanelTypeTable, Queries: validQueries()}}},
|
||||
}
|
||||
|
||||
views := NewSavedViewsFromStorableSavedViews(storables)
|
||||
|
||||
require.Len(t, views, 2)
|
||||
assert.Equal(t, "a", views[0].Name)
|
||||
assert.Equal(t, SourceLogs, views[0].Source)
|
||||
assert.Equal(t, "b", views[1].Name)
|
||||
assert.Equal(t, SourceTraces, views[1].Source)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func (t *StoreTest) Store() savedviewtypes.Store { return t.store }
|
||||
func (t *StoreTest) Mock() sqlmock.Sqlmock { return t.mock }
|
||||
|
||||
func savedViewRow(view *savedviewtypes.SavedView) []driver.Value {
|
||||
data, _ := json.Marshal(view.Data)
|
||||
data, _ := json.Marshal(savedviewtypes.NewStorableSavedView(view).Data)
|
||||
return []driver.Value{
|
||||
view.ID.StringValue(),
|
||||
view.CreatedAt,
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
// SavedViewSchemaVersion is the only schemaVersion currently.
|
||||
const SavedViewSchemaVersion = "v2"
|
||||
var SavedViewSchemaVersion = SchemaVersion{valuer.NewString("v2")}
|
||||
|
||||
var (
|
||||
PanelTypeValue = PanelType{valuer.NewString("value")}
|
||||
@@ -30,9 +30,9 @@ type Display struct {
|
||||
type SavedViewSpec struct {
|
||||
DisplayName string `json:"displayName" required:"true"`
|
||||
PanelType PanelType `json:"panelType" required:"true"`
|
||||
Queries []qbtypes.QueryEnvelope `json:"queries" required:"true" nullable:"false"`
|
||||
SelectedFields []telemetrytypes.TelemetryFieldKey `json:"selectedFields" required:"true" nullable:"false"`
|
||||
Display Display `json:"display" required:"true"`
|
||||
Queries []qbtypes.QueryEnvelope `json:"queries" required:"true" nullable:"false" minItems:"1"`
|
||||
SelectedFields []telemetrytypes.TelemetryFieldKey `json:"selectedFields" nullable:"false"`
|
||||
Display Display `json:"display"`
|
||||
}
|
||||
|
||||
// SavedViewData is what's persisted as saved view data.
|
||||
@@ -41,6 +41,11 @@ type SavedViewData struct {
|
||||
Spec SavedViewSpec `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
// SchemaVersion has v2 as the only value currently.
|
||||
type SchemaVersion struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
// PanelType is the explore-page panel a saved view renders as.
|
||||
type PanelType struct {
|
||||
valuer.String
|
||||
@@ -65,6 +70,17 @@ func (p PanelType) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
func (SchemaVersion) Enum() []any {
|
||||
return []any{SavedViewSchemaVersion}
|
||||
}
|
||||
|
||||
func (s SchemaVersion) Validate() error {
|
||||
if s != SavedViewSchemaVersion {
|
||||
return errors.NewInvalidInputf(ErrCodeSavedViewInvalidInput, "schemaVersion must be %q, got %q", SavedViewSchemaVersion.StringValue(), s.StringValue())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SavedViewSpec) Validate() error {
|
||||
if s.DisplayName == "" {
|
||||
return errors.NewInvalidInputf(ErrCodeSavedViewInvalidInput, "displayName is required")
|
||||
@@ -75,11 +91,3 @@ func (s *SavedViewSpec) Validate() error {
|
||||
|
||||
return (&qbtypes.CompositeQuery{Queries: s.Queries}).Validate()
|
||||
}
|
||||
|
||||
func (d *SavedViewData) Validate() error {
|
||||
if d.SchemaVersion != SavedViewSchemaVersion {
|
||||
return errors.NewInvalidInputf(ErrCodeSavedViewInvalidInput, "schemaVersion must be %q, got %q", SavedViewSchemaVersion, d.SchemaVersion)
|
||||
}
|
||||
|
||||
return d.Spec.Validate()
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package savedviewtypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func validQueries() []qbtypes.QueryEnvelope {
|
||||
@@ -75,7 +78,7 @@ func TestSavedViewSpecValidate(t *testing.T) {
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "selected fields and display are not required",
|
||||
name: "selectedFields and display populated is still valid",
|
||||
spec: SavedViewSpec{
|
||||
DisplayName: "My View",
|
||||
PanelType: PanelTypeTable,
|
||||
@@ -85,6 +88,36 @@ func TestSavedViewSpecValidate(t *testing.T) {
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "nil selectedFields is valid -- neither field is actually required",
|
||||
spec: SavedViewSpec{
|
||||
DisplayName: "My View",
|
||||
PanelType: PanelTypeTable,
|
||||
Queries: validQueries(),
|
||||
SelectedFields: nil,
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "empty (non-nil) selectedFields is valid",
|
||||
spec: SavedViewSpec{
|
||||
DisplayName: "My View",
|
||||
PanelType: PanelTypeTable,
|
||||
Queries: validQueries(),
|
||||
SelectedFields: []telemetrytypes.TelemetryFieldKey{},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "zero-value display is valid",
|
||||
spec: SavedViewSpec{
|
||||
DisplayName: "My View",
|
||||
PanelType: PanelTypeTable,
|
||||
Queries: validQueries(),
|
||||
Display: Display{},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
@@ -99,37 +132,44 @@ func TestSavedViewSpecValidate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSavedViewDataValidate(t *testing.T) {
|
||||
func TestSavedViewSpecJSONUnmarshal_OptionalFields(t *testing.T) {
|
||||
base := `"displayName":"My View","panelType":"table","queries":[{"type":"builder_query","spec":{"signal":"logs","aggregations":[{"expression":"count()"}]}}]`
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
data SavedViewData
|
||||
expectError bool
|
||||
name string
|
||||
json string
|
||||
}{
|
||||
{
|
||||
name: "valid data",
|
||||
data: SavedViewData{SchemaVersion: SavedViewSchemaVersion, Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()}},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "wrong schema version is rejected",
|
||||
data: SavedViewData{SchemaVersion: "v1", Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()}},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "empty schema version is rejected",
|
||||
data: SavedViewData{Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()}},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "invalid spec is rejected",
|
||||
data: SavedViewData{SchemaVersion: SavedViewSchemaVersion, Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph}},
|
||||
expectError: true,
|
||||
},
|
||||
{name: "selectedFields and display omitted entirely", json: `{` + base + `}`},
|
||||
{name: "selectedFields and display explicitly null", json: `{` + base + `,"selectedFields":null,"display":null}`},
|
||||
{name: "selectedFields empty array, display empty object", json: `{` + base + `,"selectedFields":[],"display":{}}`},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
err := c.data.Validate()
|
||||
var spec SavedViewSpec
|
||||
err := json.Unmarshal([]byte(c.json), &spec)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, spec.Validate())
|
||||
assert.Empty(t, spec.SelectedFields)
|
||||
assert.Equal(t, Display{}, spec.Display)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchemaVersionValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
schemaVersion SchemaVersion
|
||||
expectError bool
|
||||
}{
|
||||
{name: "valid schema version", schemaVersion: SavedViewSchemaVersion, expectError: false},
|
||||
{name: "wrong schema version is rejected", schemaVersion: SchemaVersion{valuer.NewString("v1")}, expectError: true},
|
||||
{name: "empty schema version is rejected", schemaVersion: SchemaVersion{}, expectError: true},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
err := c.schemaVersion.Validate()
|
||||
if c.expectError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
Create(ctx context.Context, view *SavedView) error
|
||||
Get(ctx context.Context, orgID string, id valuer.UUID) (*SavedView, error)
|
||||
Update(ctx context.Context, view *SavedView) error
|
||||
Create(ctx context.Context, view *StorableSavedView) error
|
||||
Get(ctx context.Context, orgID string, id valuer.UUID) (*StorableSavedView, error)
|
||||
Update(ctx context.Context, view *StorableSavedView) error
|
||||
Delete(ctx context.Context, orgID string, id valuer.UUID) error
|
||||
List(ctx context.Context, orgID string, source Source, name string) ([]*SavedView, error)
|
||||
List(ctx context.Context, orgID string, source Source, name string) ([]*StorableSavedView, error)
|
||||
}
|
||||
|
||||
19
tests/fixtures/inframonitoring.py
vendored
19
tests/fixtures/inframonitoring.py
vendored
@@ -48,3 +48,22 @@ def expected_status_counts(**nonzero: int) -> dict:
|
||||
counts = {bucket: 0 for bucket in STATUS_BUCKETS}
|
||||
counts.update(nonzero)
|
||||
return counts
|
||||
|
||||
|
||||
# All buckets of the clusters-API per-group resource counts (camelCase, matches
|
||||
# inframonitoringtypes ClusterRecord.Counts / the API response).
|
||||
RESOURCE_COUNT_BUCKETS = (
|
||||
"nodes",
|
||||
"namespaces",
|
||||
"deployments",
|
||||
"daemonSets",
|
||||
"jobs",
|
||||
"statefulSets",
|
||||
)
|
||||
|
||||
|
||||
def expected_resource_counts(**nonzero: int) -> dict:
|
||||
"""Full resource-counts dict with the given buckets set, rest 0."""
|
||||
counts = {bucket: 0 for bucket in RESOURCE_COUNT_BUCKETS}
|
||||
counts.update(nonzero)
|
||||
return counts
|
||||
|
||||
16
tests/fixtures/savedview.py
vendored
16
tests/fixtures/savedview.py
vendored
@@ -13,15 +13,13 @@ def _body(name: str, source: str = "logs") -> dict:
|
||||
return {
|
||||
"name": name,
|
||||
"source": source,
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": name,
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": name,
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -88,3 +88,15 @@
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"nd-ss-uid","k8s.pod.name":"nd-ss-pod","k8s.statefulset.name":"nd-ss","k8s.namespace.name":"ns-nd","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"nd-ss-uid","k8s.pod.name":"nd-ss-pod","k8s.statefulset.name":"nd-ss","k8s.namespace.name":"ns-nd","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"nd-ss-uid","k8s.pod.name":"nd-ss-pod","k8s.statefulset.name":"nd-ss","k8s.namespace.name":"ns-nd","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nd-ds-p1-uid", "k8s.pod.name": "nd-ds-p1", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "nd-ds-p1-clbo-uid", "k8s.pod.name": "nd-ds-p1-clbo", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nd-ds-p1-clbo-uid", "k8s.pod.name": "nd-ds-p1-clbo", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "nd-ds-p1-clbo-uid", "k8s.pod.name": "nd-ds-p1-clbo", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nd-ds-p1-uid", "k8s.pod.name": "nd-ds-p1", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "nd-ds-p1-clbo-uid", "k8s.pod.name": "nd-ds-p1-clbo", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nd-ds-p1-clbo-uid", "k8s.pod.name": "nd-ds-p1-clbo", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "nd-ds-p1-clbo-uid", "k8s.pod.name": "nd-ds-p1-clbo", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nd-ds-p1-uid", "k8s.pod.name": "nd-ds-p1", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "nd-ds-p1-clbo-uid", "k8s.pod.name": "nd-ds-p1-clbo", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nd-ds-p1-clbo-uid", "k8s.pod.name": "nd-ds-p1-clbo", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "nd-ds-p1-clbo-uid", "k8s.pod.name": "nd-ds-p1-clbo", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
|
||||
@@ -94,3 +94,15 @@
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"nj-ss-uid","k8s.pod.name":"nj-ss-pod","k8s.statefulset.name":"nj-ss","k8s.namespace.name":"ns-nj","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"nj-ss-uid","k8s.pod.name":"nj-ss-pod","k8s.statefulset.name":"nj-ss","k8s.namespace.name":"ns-nj","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"nj-ss-uid","k8s.pod.name":"nj-ss-pod","k8s.statefulset.name":"nj-ss","k8s.namespace.name":"ns-nj","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nj-job-p1-uid", "k8s.pod.name": "nj-job-p1", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "nj-job-p1-clbo-uid", "k8s.pod.name": "nj-job-p1-clbo", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nj-job-p1-clbo-uid", "k8s.pod.name": "nj-job-p1-clbo", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "nj-job-p1-clbo-uid", "k8s.pod.name": "nj-job-p1-clbo", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "k8s.cluster.name": "cluster-x", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nj-job-p1-uid", "k8s.pod.name": "nj-job-p1", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "nj-job-p1-clbo-uid", "k8s.pod.name": "nj-job-p1-clbo", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nj-job-p1-clbo-uid", "k8s.pod.name": "nj-job-p1-clbo", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "nj-job-p1-clbo-uid", "k8s.pod.name": "nj-job-p1-clbo", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "k8s.cluster.name": "cluster-x", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nj-job-p1-uid", "k8s.pod.name": "nj-job-p1", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "nj-job-p1-clbo-uid", "k8s.pod.name": "nj-job-p1-clbo", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nj-job-p1-clbo-uid", "k8s.pod.name": "nj-job-p1-clbo", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "nj-job-p1-clbo-uid", "k8s.pod.name": "nj-job-p1-clbo", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "k8s.cluster.name": "cluster-x", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:00:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:02:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:00:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:02:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:00:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:02:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:00:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:02:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:00:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:02:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:00:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:02:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.deployment.name":"gb-dep-b3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.deployment.name":"gb-dep-b3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.deployment.name":"gb-dep-b3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.deployment.name":"gb-dep-b3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:00:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.deployment.name":"gb-dep-b3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:02:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.deployment.name":"gb-dep-b3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.deployment.name":"gb-dep-b3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.deployment.name":"gb-dep-b3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.deployment.name":"gb-dep-b3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.deployment.name":"gb-dep-b4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.deployment.name":"gb-dep-b4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.deployment.name":"gb-dep-b4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.deployment.name":"gb-dep-b4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:00:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.deployment.name":"gb-dep-b4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:02:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.deployment.name":"gb-dep-b4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.deployment.name":"gb-dep-b4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.deployment.name":"gb-dep-b4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.deployment.name":"gb-dep-b4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
|
||||
@@ -31,3 +31,48 @@
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"carrier-pod-uid","k8s.pod.name":"carrier-pod","k8s.namespace.name":"carrier-ns","k8s.node.name":"carrier-phantom-host","k8s.cluster.name":"carrier-cluster"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"carrier-pod-uid","k8s.pod.name":"carrier-pod","k8s.namespace.name":"carrier-ns","k8s.node.name":"carrier-phantom-host","k8s.cluster.name":"carrier-cluster"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"carrier-pod-uid","k8s.pod.name":"carrier-pod","k8s.namespace.name":"carrier-ns","k8s.node.name":"carrier-phantom-host","k8s.cluster.name":"carrier-cluster"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name": "k8s.node.cpu.usage", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.8, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.cpu.usage", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.8, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.cpu.usage", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.8, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_cpu", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 4.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_cpu", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 4.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_cpu", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 4.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.memory.working_set", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.memory.working_set", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.memory.working_set", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_memory", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 8000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_memory", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 8000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_memory", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 8000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.condition_ready", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.condition_ready", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.condition_ready", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.cpu.usage", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.3, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.cpu.usage", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.3, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.cpu.usage", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.3, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_cpu", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 4.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_cpu", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 4.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_cpu", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 4.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.memory.working_set", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.memory.working_set", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.memory.working_set", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_memory", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 8000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_memory", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 8000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_memory", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 8000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.condition_ready", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.condition_ready", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.condition_ready", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.cpu.usage", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.cpu.usage", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.cpu.usage", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_cpu", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 4.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_cpu", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 4.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_cpu", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 4.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.memory.working_set", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.memory.working_set", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.memory.working_set", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_memory", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 8000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_memory", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 8000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_memory", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 8000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.condition_ready", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.condition_ready", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.condition_ready", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
|
||||
@@ -145,3 +145,63 @@
|
||||
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "unk-p-uid", "k8s.pod.name": "unk-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "c-clbo"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 8, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "unk-p-uid", "k8s.pod.name": "unk-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "c-clbo"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 8, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "unk-p-uid", "k8s.pod.name": "unk-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "c-clbo"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 8, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu.usage", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.7, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu.usage", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.7, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu.usage", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.7, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu_request_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu_request_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu_request_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu_limit_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu_limit_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu_limit_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory.working_set", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 100000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory.working_set", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 100000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory.working_set", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 100000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory_request_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory_request_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory_request_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory_limit_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory_limit_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory_limit_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu.usage", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.3, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu.usage", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.3, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu.usage", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.3, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu_request_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu_request_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu_request_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu_limit_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu_limit_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu_limit_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory.working_set", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 100000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory.working_set", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 100000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory.working_set", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 100000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory_request_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory_request_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory_request_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory_limit_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory_limit_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory_limit_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
|
||||
@@ -67,3 +67,15 @@
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"nd-dep-uid","k8s.pod.name":"nd-dep-pod","k8s.deployment.name":"nd-dep","k8s.namespace.name":"ns-nd","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"nd-dep-uid","k8s.pod.name":"nd-dep-pod","k8s.deployment.name":"nd-dep","k8s.namespace.name":"ns-nd","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"nd-dep-uid","k8s.pod.name":"nd-dep-pod","k8s.deployment.name":"nd-dep","k8s.namespace.name":"ns-nd","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "ns-ss-p1-uid", "k8s.pod.name": "ns-ss-p1", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "ns-ss-p1-clbo-uid", "k8s.pod.name": "ns-ss-p1-clbo", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "ns-ss-p1-clbo-uid", "k8s.pod.name": "ns-ss-p1-clbo", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "ns-ss-p1-clbo-uid", "k8s.pod.name": "ns-ss-p1-clbo", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "ns-ss-p1-uid", "k8s.pod.name": "ns-ss-p1", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "ns-ss-p1-clbo-uid", "k8s.pod.name": "ns-ss-p1-clbo", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "ns-ss-p1-clbo-uid", "k8s.pod.name": "ns-ss-p1-clbo", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "ns-ss-p1-clbo-uid", "k8s.pod.name": "ns-ss-p1-clbo", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "ns-ss-p1-uid", "k8s.pod.name": "ns-ss-p1", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "ns-ss-p1-clbo-uid", "k8s.pod.name": "ns-ss-p1-clbo", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "ns-ss-p1-clbo-uid", "k8s.pod.name": "ns-ss-p1-clbo", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "ns-ss-p1-clbo-uid", "k8s.pod.name": "ns-ss-p1-clbo", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
|
||||
@@ -8,7 +8,11 @@ import requests
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.fs import get_testdata_file_path
|
||||
from fixtures.inframonitoring import STATUS_BUCKETS, STATUS_TO_BUCKET
|
||||
from fixtures.inframonitoring import (
|
||||
STATUS_BUCKETS,
|
||||
STATUS_TO_BUCKET,
|
||||
expected_status_counts,
|
||||
)
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.querier import compare_values, get_all_warnings
|
||||
|
||||
@@ -451,6 +455,95 @@ def test_pods_pagination(
|
||||
assert set(seen_pods) == {f"page-p{i}" for i in range(1, K + 1)}
|
||||
|
||||
|
||||
def test_pods_filter_pagination_and_ordering(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token,
|
||||
insert_metrics,
|
||||
) -> None:
|
||||
"""filterByPodStatus composed with pagination + name ordering. The full-scope
|
||||
status keyset is resolved before slicing, so total reflects the full matched
|
||||
set and pages stay disjoint + complete on both the metric-ordering branch
|
||||
(paginateWithBackfill) and the name-ordering branch (PaginateMetadataByName).
|
||||
crashloopbackoff matches 4 pods in pods_phases.jsonl: clbo-a, clbo-b, run-p, unk-p."""
|
||||
now = datetime.now(tz=UTC).replace(microsecond=0)
|
||||
insert_metrics(
|
||||
_load_pods_metrics(
|
||||
"inframonitoring/pods_phases.jsonl",
|
||||
base_time=now - timedelta(minutes=4),
|
||||
)
|
||||
)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
start = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end = int(now.timestamp() * 1000)
|
||||
matched = {"clbo-a", "clbo-b", "run-p", "unk-p"}
|
||||
|
||||
# Metric-ordering branch (default cpu order): total is invariant across a paged
|
||||
# walk and the pages are disjoint + cover the full matched set.
|
||||
seen: list[str] = []
|
||||
totals: set[int] = set()
|
||||
for offset in (0, 2, 4):
|
||||
resp = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": start,
|
||||
"end": end,
|
||||
"limit": 2,
|
||||
"offset": offset,
|
||||
"filter": {"filterByPodStatus": ["crashloopbackoff"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.OK, resp.text
|
||||
data = resp.json()["data"]
|
||||
totals.add(data["total"])
|
||||
assert len(data["records"]) == max(0, min(2, 4 - offset)), f"offset={offset}: {data['records']!r}"
|
||||
seen.extend(r["meta"]["k8s.pod.name"] for r in data["records"])
|
||||
assert totals == {4}, f"total not invariant under filter+pagination: {totals}"
|
||||
assert len(seen) == 4, f"pages overlapped: {seen}"
|
||||
assert set(seen) == matched
|
||||
|
||||
# Name-ordering branch (orderBy k8s.pod.name asc, groupBy empty): the filtered
|
||||
# set is returned in name order; total is the full matched count.
|
||||
ordered = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": start,
|
||||
"end": end,
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": ["crashloopbackoff"]},
|
||||
"orderBy": {"key": {"name": "k8s.pod.name"}, "direction": "asc"},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert ordered.status_code == HTTPStatus.OK, ordered.text
|
||||
odata = ordered.json()["data"]
|
||||
assert odata["total"] == 4
|
||||
assert [r["meta"]["k8s.pod.name"] for r in odata["records"]] == ["clbo-a", "clbo-b", "run-p", "unk-p"]
|
||||
|
||||
# Second page of the name branch: PaginateMetadataByName slices the filtered set
|
||||
# correctly (offset past the first 2 matched -> the last 2), total unchanged.
|
||||
page2 = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": start,
|
||||
"end": end,
|
||||
"limit": 2,
|
||||
"offset": 2,
|
||||
"filter": {"filterByPodStatus": ["crashloopbackoff"]},
|
||||
"orderBy": {"key": {"name": "k8s.pod.name"}, "direction": "asc"},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert page2.status_code == HTTPStatus.OK, page2.text
|
||||
p2 = page2.json()["data"]
|
||||
assert p2["total"] == 4
|
||||
assert [r["meta"]["k8s.pod.name"] for r in p2["records"]] == ["run-p", "unk-p"]
|
||||
|
||||
|
||||
# orderBy keys per pods_constants.go:42-48 (snake_case request keys, camelCase
|
||||
# response fields). k8s.pod.name sorts via the metadata-name branch
|
||||
# (PaginateMetadataByName) and is only allowed when groupBy is empty.
|
||||
@@ -553,6 +646,16 @@ def test_pods_orderby( # pylint: disable=too-many-arguments,too-many-positional
|
||||
"is only allowed when groupBy is empty",
|
||||
id="orderby_podname_with_groupby",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByPodStatus": ["Bogus"]}},
|
||||
"invalid filter by pod status",
|
||||
id="filter_by_pod_status_invalid",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByPodStatus": ["running", "Bogus"]}},
|
||||
"invalid filter by pod status",
|
||||
id="filter_by_pod_status_invalid_member",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_pods_validation_errors(
|
||||
@@ -658,6 +761,44 @@ def test_pods_status_list_mode(
|
||||
if other != bucket:
|
||||
assert rec["podCountsByStatus"][other] == 0, f"expected {other}=0 when status={expected_status}, got {rec['podCountsByStatus']}"
|
||||
|
||||
# filterByPodStatus (secondary filter, applied after status is assigned):
|
||||
# a set containing the pod's status keeps it; a set without it filters it
|
||||
# out. Wire value is case-insensitive (valuer lowercases). Multi-select is
|
||||
# OR: [own_status, other] still keeps the pod.
|
||||
for variant in ([expected_status], [expected_status.upper()], [expected_status, "running"]):
|
||||
matched = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": f"k8s.pod.name = '{pod_name}'", "filterByPodStatus": variant},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert matched.status_code == HTTPStatus.OK, matched.text
|
||||
mdata = matched.json()["data"]
|
||||
assert mdata["total"] == 1, f"filterByPodStatus={variant!r} should keep {pod_name}"
|
||||
assert mdata["records"][0]["podCountsByStatus"][bucket] == 1
|
||||
|
||||
# None of the seeded pods resolves to Running or OOMKilled -> reliable
|
||||
# mismatches, as a single value and as an all-absent multi-select set.
|
||||
for fbps in (["running"], ["running", "oomKilled"]):
|
||||
filtered_out = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": f"k8s.pod.name = '{pod_name}'", "filterByPodStatus": fbps},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert filtered_out.status_code == HTTPStatus.OK, filtered_out.text
|
||||
assert filtered_out.json()["data"]["total"] == 0, f"{pod_name} must be filtered out by filterByPodStatus={fbps!r}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"pod_name,expected_restarts",
|
||||
@@ -838,6 +979,61 @@ def test_pods_status_grouped_mode(
|
||||
)
|
||||
assert rec["podCountsByStatus"] == expected_counts
|
||||
|
||||
# filterByPodStatus in grouped mode: the group is kept because >=1 pod
|
||||
# matches, and only the filtered buckets are populated (others zeroed).
|
||||
running = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"groupBy": [{"name": "k8s.namespace.name", "fieldDataType": "string", "fieldContext": "resource"}],
|
||||
"filter": {"filterByPodStatus": ["running"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert running.status_code == HTTPStatus.OK, running.text
|
||||
rdata = running.json()["data"]
|
||||
assert rdata["total"] == 1
|
||||
assert rdata["records"][0]["podCountsByStatus"] == expected_status_counts(running=2)
|
||||
|
||||
# Multi-select is OR: both requested buckets populated (union), others zeroed.
|
||||
multi = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"groupBy": [{"name": "k8s.namespace.name", "fieldDataType": "string", "fieldContext": "resource"}],
|
||||
"filter": {"filterByPodStatus": ["running", "crashLoopBackOff"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert multi.status_code == HTTPStatus.OK, multi.text
|
||||
mdata = multi.json()["data"]
|
||||
assert mdata["total"] == 1
|
||||
assert mdata["records"][0]["podCountsByStatus"] == expected_status_counts(running=2, crashLoopBackOff=1)
|
||||
|
||||
# A set fully absent from the group -> group dropped, empty page (single
|
||||
# and multi-select both).
|
||||
for fbps in (["oomKilled"], ["oomKilled", "unknown"]):
|
||||
absent = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"groupBy": [{"name": "k8s.namespace.name", "fieldDataType": "string", "fieldContext": "resource"}],
|
||||
"filter": {"filterByPodStatus": fbps},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert absent.status_code == HTTPStatus.OK, absent.text
|
||||
assert absent.json()["data"]["total"] == 0, f"group must be dropped by filterByPodStatus={fbps!r}"
|
||||
|
||||
|
||||
def test_pods_restarts_grouped_mode(
|
||||
signoz: types.SigNoz,
|
||||
@@ -927,3 +1123,25 @@ def test_pods_status_missing_metric_warning(
|
||||
for bucket in STATUS_BUCKETS:
|
||||
assert rec["podCountsByStatus"][bucket] == 0, f"expected {bucket}=0 when gated off, got {rec['podCountsByStatus']}"
|
||||
assert rec["podRestarts"] == -1
|
||||
|
||||
# filterByPodStatus + missing status metric: the up-front gate surfaces the
|
||||
# warning and returns an empty page (Total 0) instead of silently filtering
|
||||
# everything out.
|
||||
filtered = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": "k8s.pod.name = 'miss-p1'", "filterByPodStatus": ["running"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert filtered.status_code == HTTPStatus.OK, filtered.text
|
||||
fdata = filtered.json()["data"]
|
||||
assert fdata["total"] == 0
|
||||
assert fdata["records"] == []
|
||||
fwarn = fdata.get("warning") or {}
|
||||
fmsgs = ([fwarn["message"]] if fwarn.get("message") else []) + [w["message"] for w in fwarn.get("warnings", [])]
|
||||
assert any("Pod status could not be computed" in m for m in fmsgs), f"gate warning missing on filtered call: {fmsgs!r}"
|
||||
|
||||
@@ -8,6 +8,7 @@ import requests
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.fs import get_testdata_file_path
|
||||
from fixtures.inframonitoring import expected_status_counts
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.querier import compare_values, get_all_warnings
|
||||
|
||||
@@ -310,6 +311,115 @@ def test_nodes_filter_invalid(
|
||||
assert any(err_substr in e["message"] for e in body["error"]["errors"]), f"{err_substr!r} not surfaced: {body['error']['errors']!r}"
|
||||
|
||||
|
||||
def test_nodes_filter_by_pod_status(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token,
|
||||
insert_metrics,
|
||||
) -> None:
|
||||
"""filterByPodStatus on nodes: a node is kept when >=1 of its pods matches
|
||||
the requested display status, and podCountsByStatus reflects only that
|
||||
status (others 0); an absent status yields an empty page. Reuses
|
||||
clusters_pod_phases.jsonl (carries k8s.node.name + full status metrics):
|
||||
pp-node has running=3, crashLoopBackOff=1, error=1, evicted=1, pending=1."""
|
||||
now = datetime.now(tz=UTC).replace(microsecond=0)
|
||||
insert_metrics(
|
||||
Metrics.load_from_file(
|
||||
get_testdata_file_path("inframonitoring/clusters_pod_phases.jsonl"),
|
||||
base_time=now - timedelta(minutes=4),
|
||||
)
|
||||
)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Multi-select is OR -> the union of requested buckets (others zeroed).
|
||||
for fbps, expected in (
|
||||
(["running"], expected_status_counts(running=3)),
|
||||
(["CrashLoopBackOff"], expected_status_counts(crashLoopBackOff=1)),
|
||||
(["running", "CrashLoopBackOff"], expected_status_counts(running=3, crashLoopBackOff=1)),
|
||||
):
|
||||
resp = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": fbps},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.OK, resp.text
|
||||
data = resp.json()["data"]
|
||||
assert data["total"] == 1
|
||||
rec = data["records"][0]
|
||||
assert rec["nodeName"] == "pp-node"
|
||||
assert rec["podCountsByStatus"] == expected
|
||||
|
||||
# A set fully absent from the node -> empty page (single and multi).
|
||||
for fbps in (["completed"], ["completed", "oomKilled"]):
|
||||
absent = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": fbps},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert absent.status_code == HTTPStatus.OK, absent.text
|
||||
assert absent.json()["data"]["total"] == 0, f"node must be dropped by filterByPodStatus={fbps!r}"
|
||||
|
||||
# Combined filterByPodStatus + filterByNodeReadiness = AND. pp-node is Ready
|
||||
# and runs pods -> kept when both match; dropped if either side has no match.
|
||||
both_match = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": ["running"], "filterByNodeReadiness": ["ready"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert both_match.status_code == HTTPStatus.OK, both_match.text
|
||||
bdata = both_match.json()["data"]
|
||||
assert bdata["total"] == 1
|
||||
assert bdata["records"][0]["nodeName"] == "pp-node"
|
||||
|
||||
# readiness side fails (pp-node is not not_ready) -> dropped.
|
||||
readiness_fails = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": ["running"], "filterByNodeReadiness": ["not_ready"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert readiness_fails.status_code == HTTPStatus.OK, readiness_fails.text
|
||||
assert readiness_fails.json()["data"]["total"] == 0
|
||||
|
||||
# pod-status side fails (no completed pod) -> dropped.
|
||||
status_fails = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": ["completed"], "filterByNodeReadiness": ["ready"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert status_fails.status_code == HTTPStatus.OK, status_fails.text
|
||||
assert status_fails.json()["data"]["total"] == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"node_name,expected_condition",
|
||||
[
|
||||
@@ -358,6 +468,56 @@ def test_nodes_condition_list_mode(
|
||||
else:
|
||||
assert rec["nodeCountsByReadiness"] == {"ready": 0, "notReady": 1}
|
||||
|
||||
# filterByNodeReadiness (secondary filter): matching readiness keeps the node,
|
||||
# the opposite readiness filters it out.
|
||||
matched = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": f"k8s.node.name = '{node_name}'", "filterByNodeReadiness": [expected_condition]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert matched.status_code == HTTPStatus.OK, matched.text
|
||||
mdata = matched.json()["data"]
|
||||
assert mdata["total"] == 1
|
||||
assert mdata["records"][0]["nodeName"] == node_name
|
||||
|
||||
opposite = "not_ready" if expected_condition == "ready" else "ready"
|
||||
|
||||
# Multi-select is OR: a set containing the node's condition keeps it even
|
||||
# alongside the opposite readiness.
|
||||
or_keep = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": f"k8s.node.name = '{node_name}'", "filterByNodeReadiness": [expected_condition, opposite]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert or_keep.status_code == HTTPStatus.OK, or_keep.text
|
||||
assert or_keep.json()["data"]["total"] == 1, f"multi-select should keep {node_name}"
|
||||
|
||||
dropped = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": f"k8s.node.name = '{node_name}'", "filterByNodeReadiness": [opposite]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert dropped.status_code == HTTPStatus.OK, dropped.text
|
||||
assert dropped.json()["data"]["total"] == 0
|
||||
|
||||
|
||||
def test_nodes_condition_latest_wins(
|
||||
signoz: types.SigNoz,
|
||||
@@ -449,6 +609,60 @@ def test_nodes_condition_grouped_mode(
|
||||
# meta surfaces the groupBy key.
|
||||
assert rec["meta"].get("k8s.cluster.name") == "cluster-mixed"
|
||||
|
||||
# filterByNodeReadiness in grouped mode: the group is kept (>=1 matching
|
||||
# node) and only the filtered bucket is populated.
|
||||
ready = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"groupBy": [{"name": "k8s.cluster.name", "fieldDataType": "string", "fieldContext": "resource"}],
|
||||
"filter": {"filterByNodeReadiness": ["ready"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert ready.status_code == HTTPStatus.OK, ready.text
|
||||
rdata = ready.json()["data"]
|
||||
assert rdata["total"] == 1
|
||||
assert rdata["records"][0]["nodeCountsByReadiness"] == {"ready": 2, "notReady": 0}
|
||||
|
||||
not_ready = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"groupBy": [{"name": "k8s.cluster.name", "fieldDataType": "string", "fieldContext": "resource"}],
|
||||
"filter": {"filterByNodeReadiness": ["not_ready"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert not_ready.status_code == HTTPStatus.OK, not_ready.text
|
||||
ndata = not_ready.json()["data"]
|
||||
assert ndata["total"] == 1
|
||||
assert ndata["records"][0]["nodeCountsByReadiness"] == {"ready": 0, "notReady": 1}
|
||||
|
||||
# Multi-select is OR: both buckets populated (union) for the kept group.
|
||||
both = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"groupBy": [{"name": "k8s.cluster.name", "fieldDataType": "string", "fieldContext": "resource"}],
|
||||
"filter": {"filterByNodeReadiness": ["ready", "not_ready"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert both.status_code == HTTPStatus.OK, both.text
|
||||
bdata = both.json()["data"]
|
||||
assert bdata["total"] == 1
|
||||
assert bdata["records"][0]["nodeCountsByReadiness"] == {"ready": 2, "notReady": 1}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"group_key,expected",
|
||||
@@ -579,6 +793,95 @@ def test_nodes_pagination(
|
||||
assert set(seen_nodes) == {f"page-n{i}" for i in range(1, K + 1)}
|
||||
|
||||
|
||||
def test_nodes_filter_readiness_pagination_and_ordering(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token,
|
||||
insert_metrics,
|
||||
) -> None:
|
||||
"""filterByNodeReadiness composed with pagination + name ordering. The full-scope
|
||||
readiness keyset is resolved before slicing, so total reflects the full matched
|
||||
set and pages stay disjoint + complete on both the metric-ordering branch
|
||||
(paginateWithBackfill) and the name-ordering branch (PaginateMetadataByName).
|
||||
ready matches 4 nodes in nodes_conditions.jsonl: ready-n, ready-n2, ready-n3, ready-n4."""
|
||||
now = datetime.now(tz=UTC).replace(microsecond=0)
|
||||
insert_metrics(
|
||||
Metrics.load_from_file(
|
||||
get_testdata_file_path("inframonitoring/nodes_conditions.jsonl"),
|
||||
base_time=now - timedelta(minutes=4),
|
||||
)
|
||||
)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
start = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end = int(now.timestamp() * 1000)
|
||||
matched = {"ready-n", "ready-n2", "ready-n3", "ready-n4"}
|
||||
|
||||
# Metric-ordering branch (default cpu order): total is invariant across a paged
|
||||
# walk and the pages are disjoint + cover the full matched set.
|
||||
seen: list[str] = []
|
||||
totals: set[int] = set()
|
||||
for offset in (0, 2, 4):
|
||||
resp = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": start,
|
||||
"end": end,
|
||||
"limit": 2,
|
||||
"offset": offset,
|
||||
"filter": {"filterByNodeReadiness": ["ready"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.OK, resp.text
|
||||
data = resp.json()["data"]
|
||||
totals.add(data["total"])
|
||||
assert len(data["records"]) == max(0, min(2, 4 - offset)), f"offset={offset}: {data['records']!r}"
|
||||
seen.extend(r["meta"]["k8s.node.name"] for r in data["records"])
|
||||
assert totals == {4}, f"total not invariant under filter+pagination: {totals}"
|
||||
assert len(seen) == 4, f"pages overlapped: {seen}"
|
||||
assert set(seen) == matched
|
||||
|
||||
# Name-ordering branch (orderBy k8s.node.name asc, groupBy empty): the filtered
|
||||
# set is returned in name order; total is the full matched count.
|
||||
ordered = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": start,
|
||||
"end": end,
|
||||
"limit": 50,
|
||||
"filter": {"filterByNodeReadiness": ["ready"]},
|
||||
"orderBy": {"key": {"name": "k8s.node.name"}, "direction": "asc"},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert ordered.status_code == HTTPStatus.OK, ordered.text
|
||||
odata = ordered.json()["data"]
|
||||
assert odata["total"] == 4
|
||||
assert [r["meta"]["k8s.node.name"] for r in odata["records"]] == ["ready-n", "ready-n2", "ready-n3", "ready-n4"]
|
||||
|
||||
# Second page of the name branch: PaginateMetadataByName slices the filtered set
|
||||
# correctly (offset past the first 2 matched -> the last 2), total unchanged.
|
||||
page2 = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": start,
|
||||
"end": end,
|
||||
"limit": 2,
|
||||
"offset": 2,
|
||||
"filter": {"filterByNodeReadiness": ["ready"]},
|
||||
"orderBy": {"key": {"name": "k8s.node.name"}, "direction": "asc"},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert page2.status_code == HTTPStatus.OK, page2.text
|
||||
p2 = page2.json()["data"]
|
||||
assert p2["total"] == 4
|
||||
assert [r["meta"]["k8s.node.name"] for r in p2["records"]] == ["ready-n3", "ready-n4"]
|
||||
|
||||
|
||||
# orderBy keys per nodes_constants.go:33-37 (snake_case request keys,
|
||||
# camelCase response fields). k8s.node.name sorts via the metadata-name branch
|
||||
# (PaginateMetadataByName) and is only allowed when groupBy is empty.
|
||||
@@ -680,6 +983,21 @@ def test_nodes_orderby( # pylint: disable=too-many-arguments,too-many-positiona
|
||||
"is only allowed when groupBy is empty",
|
||||
id="orderby_nodename_with_groupby",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByPodStatus": ["Bogus"]}},
|
||||
"invalid filter by pod status",
|
||||
id="filter_by_pod_status_invalid",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByNodeReadiness": ["bogus"]}},
|
||||
"invalid filter by node readiness",
|
||||
id="filter_by_node_readiness_invalid",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByNodeReadiness": ["notready"]}},
|
||||
"invalid filter by node readiness",
|
||||
id="filter_by_node_readiness_missing_underscore",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_nodes_validation_errors(
|
||||
|
||||
@@ -8,6 +8,7 @@ import requests
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.fs import get_testdata_file_path
|
||||
from fixtures.inframonitoring import expected_status_counts
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.querier import compare_values, get_all_warnings
|
||||
|
||||
@@ -307,6 +308,67 @@ def test_namespaces_filter_invalid(
|
||||
assert any(err_substr in e["message"] for e in body["error"]["errors"]), f"{err_substr!r} not surfaced: {body['error']['errors']!r}"
|
||||
|
||||
|
||||
def test_namespaces_filter_by_pod_status(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token,
|
||||
insert_metrics,
|
||||
) -> None:
|
||||
"""filterByPodStatus on namespaces: a namespace is kept when >=1 of its pods
|
||||
matches the requested display status, and podCountsByStatus reflects only
|
||||
that status (others 0); an absent status yields an empty page. Reuses
|
||||
clusters_pod_phases.jsonl (carries k8s.namespace.name + full status metrics):
|
||||
ns-x has running=3, crashLoopBackOff=1, error=1, evicted=1, pending=1."""
|
||||
now = datetime.now(tz=UTC).replace(microsecond=0)
|
||||
insert_metrics(
|
||||
Metrics.load_from_file(
|
||||
get_testdata_file_path("inframonitoring/clusters_pod_phases.jsonl"),
|
||||
base_time=now - timedelta(minutes=4),
|
||||
)
|
||||
)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Multi-select is OR -> the union of requested buckets (others zeroed).
|
||||
for fbps, expected in (
|
||||
(["running"], expected_status_counts(running=3)),
|
||||
(["CrashLoopBackOff"], expected_status_counts(crashLoopBackOff=1)),
|
||||
(["running", "CrashLoopBackOff"], expected_status_counts(running=3, crashLoopBackOff=1)),
|
||||
):
|
||||
resp = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": fbps},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.OK, resp.text
|
||||
data = resp.json()["data"]
|
||||
assert data["total"] == 1
|
||||
rec = data["records"][0]
|
||||
assert rec["namespaceName"] == "ns-x"
|
||||
assert rec["podCountsByStatus"] == expected
|
||||
|
||||
# A set fully absent from the namespace -> empty page (single and multi).
|
||||
for fbps in (["completed"], ["completed", "oomKilled"]):
|
||||
absent = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": fbps},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert absent.status_code == HTTPStatus.OK, absent.text
|
||||
assert absent.json()["data"]["total"] == 0, f"namespace must be dropped by filterByPodStatus={fbps!r}"
|
||||
|
||||
|
||||
# Float record fields compared with tolerance; everything else compared with ==.
|
||||
_GROUPBY_FLOAT_FIELDS = {
|
||||
"namespaceCPU",
|
||||
@@ -352,6 +414,32 @@ _GROUPBY_FLOAT_FIELDS = {
|
||||
},
|
||||
id="cluster",
|
||||
),
|
||||
# groupBy on a counted attr: regression guard for the counts-query
|
||||
# alias collision (CH error 179).
|
||||
pytest.param(
|
||||
{
|
||||
"fixture": "namespaces_groupby.jsonl",
|
||||
"group_by": "k8s.deployment.name",
|
||||
"filter": None,
|
||||
"group_meta_keys": ["k8s.deployment.name"],
|
||||
"expected_type": "grouped_list",
|
||||
"groups": {
|
||||
"gb-dep-shared": {
|
||||
"namespaceName": "",
|
||||
"counts": {"deployments": 2, "daemonSets": 0, "jobs": 0, "statefulSets": 0},
|
||||
},
|
||||
"gb-dep-b3": {
|
||||
"namespaceName": "",
|
||||
"counts": {"deployments": 1, "daemonSets": 0, "jobs": 0, "statefulSets": 0},
|
||||
},
|
||||
"gb-dep-b4": {
|
||||
"namespaceName": "",
|
||||
"counts": {"deployments": 1, "daemonSets": 0, "jobs": 0, "statefulSets": 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
id="deployment_name_counted_attr",
|
||||
),
|
||||
# Default groupBy (no groupBy in request) => [k8s.namespace.name,
|
||||
# k8s.cluster.name] (module.go ListNamespaces), response list. Namespaces
|
||||
# are cluster-scoped, so a same-named namespace must NOT collapse across
|
||||
@@ -595,6 +683,11 @@ def test_namespaces_orderby( # pylint: disable=too-many-arguments,too-many-posi
|
||||
"is only allowed when groupBy is empty",
|
||||
id="orderby_nsname_with_groupby",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByPodStatus": ["Bogus"]}},
|
||||
"invalid filter by pod status",
|
||||
id="filter_by_pod_status_invalid",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_namespaces_validation_errors(
|
||||
|
||||
@@ -8,7 +8,7 @@ import requests
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.fs import get_testdata_file_path
|
||||
from fixtures.inframonitoring import expected_status_counts
|
||||
from fixtures.inframonitoring import expected_resource_counts, expected_status_counts
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.querier import compare_values, get_all_warnings
|
||||
|
||||
@@ -361,6 +361,51 @@ def test_clusters_node_readiness_aggregation(
|
||||
assert rec["clusterName"] == "rn-cluster"
|
||||
assert rec["nodeCountsByReadiness"] == {"ready": 3, "notReady": 2}
|
||||
|
||||
# filterByNodeReadiness: cluster kept (>=1 matching node), only the filtered
|
||||
# bucket populated.
|
||||
ready = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": "k8s.cluster.name = 'rn-cluster'", "filterByNodeReadiness": ["ready"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert ready.status_code == HTTPStatus.OK, ready.text
|
||||
assert ready.json()["data"]["records"][0]["nodeCountsByReadiness"] == {"ready": 3, "notReady": 0}
|
||||
|
||||
not_ready = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": "k8s.cluster.name = 'rn-cluster'", "filterByNodeReadiness": ["not_ready"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert not_ready.status_code == HTTPStatus.OK, not_ready.text
|
||||
assert not_ready.json()["data"]["records"][0]["nodeCountsByReadiness"] == {"ready": 0, "notReady": 2}
|
||||
|
||||
# Multi-select is OR: both buckets populated (union) for the kept cluster.
|
||||
both = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": "k8s.cluster.name = 'rn-cluster'", "filterByNodeReadiness": ["ready", "not_ready"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert both.status_code == HTTPStatus.OK, both.text
|
||||
assert both.json()["data"]["records"][0]["nodeCountsByReadiness"] == {"ready": 3, "notReady": 2}
|
||||
|
||||
|
||||
def test_clusters_pod_status_aggregation(
|
||||
signoz: types.SigNoz,
|
||||
@@ -402,19 +447,109 @@ def test_clusters_pod_status_aggregation(
|
||||
# All status metrics present -> gate satisfied -> no status warning.
|
||||
assert all("Pod status could not be computed" not in w["message"] for w in get_all_warnings(response.json()))
|
||||
|
||||
# filterByPodStatus: cluster kept (>=1 matching pod), only the filtered
|
||||
# buckets populated. Multi-select is OR -> the union of requested buckets.
|
||||
for fbps, expected in (
|
||||
(["running"], expected_status_counts(running=3)),
|
||||
(["CrashLoopBackOff"], expected_status_counts(crashLoopBackOff=1)),
|
||||
(["running", "CrashLoopBackOff"], expected_status_counts(running=3, crashLoopBackOff=1)),
|
||||
):
|
||||
resp = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": "k8s.cluster.name = 'pp-cluster'", "filterByPodStatus": fbps},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.OK, resp.text
|
||||
fdata = resp.json()["data"]
|
||||
assert fdata["total"] == 1
|
||||
assert fdata["records"][0]["podCountsByStatus"] == expected
|
||||
|
||||
# A set fully absent from the cluster -> empty page (single and multi).
|
||||
for fbps in (["completed"], ["completed", "oomKilled"]):
|
||||
absent = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": "k8s.cluster.name = 'pp-cluster'", "filterByPodStatus": fbps},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert absent.status_code == HTTPStatus.OK, absent.text
|
||||
assert absent.json()["data"]["total"] == 0, f"cluster must be dropped by filterByPodStatus={fbps!r}"
|
||||
|
||||
# Combined filterByPodStatus + filterByNodeReadiness = AND. pp-cluster has a
|
||||
# Ready node and Running pods -> kept when both match; dropped if either side
|
||||
# has no match.
|
||||
both_match = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": "k8s.cluster.name = 'pp-cluster'", "filterByPodStatus": ["running"], "filterByNodeReadiness": ["ready"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert both_match.status_code == HTTPStatus.OK, both_match.text
|
||||
bdata = both_match.json()["data"]
|
||||
assert bdata["total"] == 1
|
||||
assert bdata["records"][0]["podCountsByStatus"] == expected_status_counts(running=3)
|
||||
assert bdata["records"][0]["nodeCountsByReadiness"] == {"ready": 1, "notReady": 0}
|
||||
|
||||
# readiness side fails (no not_ready node) -> dropped.
|
||||
readiness_fails = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": "k8s.cluster.name = 'pp-cluster'", "filterByPodStatus": ["running"], "filterByNodeReadiness": ["not_ready"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert readiness_fails.status_code == HTTPStatus.OK, readiness_fails.text
|
||||
assert readiness_fails.json()["data"]["total"] == 0
|
||||
|
||||
# pod-status side fails (no completed pod) -> dropped.
|
||||
status_fails = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": "k8s.cluster.name = 'pp-cluster'", "filterByPodStatus": ["completed"], "filterByNodeReadiness": ["ready"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert status_fails.status_code == HTTPStatus.OK, status_fails.text
|
||||
assert status_fails.json()["data"]["total"] == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"group_key,expected",
|
||||
"group_key,flt,expected",
|
||||
[
|
||||
# groupBy=[k8s.cluster.name]: one record per cluster, clusterName
|
||||
# populated (clusters.go:29-32). Each cluster has 1 ready node, 1 pod.
|
||||
pytest.param(
|
||||
"k8s.cluster.name",
|
||||
None,
|
||||
{
|
||||
"gb-gcp-1": {"readiness": {"ready": 1, "notReady": 0}},
|
||||
"gb-gcp-2": {"readiness": {"ready": 1, "notReady": 0}},
|
||||
"gb-aws-1": {"readiness": {"ready": 1, "notReady": 0}},
|
||||
"gb-aws-2": {"readiness": {"ready": 1, "notReady": 0}},
|
||||
"gb-gcp-1": {"readiness": {"ready": 1, "notReady": 0}, "counts": expected_resource_counts(nodes=1, namespaces=1)},
|
||||
"gb-gcp-2": {"readiness": {"ready": 1, "notReady": 0}, "counts": expected_resource_counts(nodes=1, namespaces=1)},
|
||||
"gb-aws-1": {"readiness": {"ready": 1, "notReady": 0}, "counts": expected_resource_counts(nodes=1, namespaces=1)},
|
||||
"gb-aws-2": {"readiness": {"ready": 1, "notReady": 0}, "counts": expected_resource_counts(nodes=1, namespaces=1)},
|
||||
},
|
||||
id="cluster_name",
|
||||
),
|
||||
@@ -422,25 +557,38 @@ def test_clusters_pod_status_aggregation(
|
||||
# clusterName empty (custom-groupBy branch).
|
||||
pytest.param(
|
||||
"cloud.provider",
|
||||
None,
|
||||
{
|
||||
"gcp": {"readiness": {"ready": 2, "notReady": 0}},
|
||||
"aws": {"readiness": {"ready": 2, "notReady": 0}},
|
||||
"gcp": {"readiness": {"ready": 2, "notReady": 0}, "counts": expected_resource_counts(nodes=2, namespaces=2)},
|
||||
"aws": {"readiness": {"ready": 2, "notReady": 0}, "counts": expected_resource_counts(nodes=2, namespaces=2)},
|
||||
},
|
||||
id="cloud_provider",
|
||||
),
|
||||
# groupBy on a counted attr: regression guard for the counts-query
|
||||
# alias collision (CH error 179).
|
||||
pytest.param(
|
||||
"k8s.namespace.name",
|
||||
"k8s.namespace.name = 'ns-x'",
|
||||
{
|
||||
"ns-x": {"readiness": {"ready": 0, "notReady": 0}, "counts": expected_resource_counts(nodes=4, namespaces=4)},
|
||||
},
|
||||
id="namespace_name_counted_attr",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_clusters_groupby(
|
||||
def test_clusters_groupby( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token,
|
||||
insert_metrics,
|
||||
group_key: str,
|
||||
flt,
|
||||
expected: dict,
|
||||
) -> None:
|
||||
"""groupBy returns one record per distinct group with aggregated readiness.
|
||||
clusterName is populated only when grouping by k8s.cluster.name
|
||||
(clusters.go:29-32 list-vs-grouped branch); meta surfaces the groupBy key."""
|
||||
"""groupBy returns one record per distinct group with aggregated readiness
|
||||
and resource counts. clusterName is populated only when grouping by
|
||||
k8s.cluster.name (clusters.go:29-32 list-vs-grouped branch); meta surfaces
|
||||
the groupBy key."""
|
||||
now = datetime.now(tz=UTC).replace(microsecond=0)
|
||||
insert_metrics(
|
||||
Metrics.load_from_file(
|
||||
@@ -450,21 +598,24 @@ def test_clusters_groupby(
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
body: dict = {
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"groupBy": [
|
||||
{
|
||||
"name": group_key,
|
||||
"fieldDataType": "string",
|
||||
"fieldContext": "resource",
|
||||
}
|
||||
],
|
||||
}
|
||||
if flt is not None:
|
||||
body["filter"] = {"expression": flt}
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"groupBy": [
|
||||
{
|
||||
"name": group_key,
|
||||
"fieldDataType": "string",
|
||||
"fieldContext": "resource",
|
||||
}
|
||||
],
|
||||
},
|
||||
json=body,
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
@@ -481,6 +632,7 @@ def test_clusters_groupby(
|
||||
# empty otherwise.
|
||||
assert rec["clusterName"] == (group if group_key == "k8s.cluster.name" else "")
|
||||
assert rec["nodeCountsByReadiness"] == exp["readiness"]
|
||||
assert rec["counts"] == exp["counts"], f"{group}: got {rec['counts']}, expected {exp['counts']}"
|
||||
assert group_key in rec["meta"], rec["meta"]
|
||||
|
||||
|
||||
@@ -627,6 +779,21 @@ def test_clusters_orderby( # pylint: disable=too-many-arguments,too-many-positi
|
||||
"is only allowed when groupBy is empty",
|
||||
id="orderby_clustername_with_groupby",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByPodStatus": ["Bogus"]}},
|
||||
"invalid filter by pod status",
|
||||
id="filter_by_pod_status_invalid",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByNodeReadiness": ["bogus"]}},
|
||||
"invalid filter by node readiness",
|
||||
id="filter_by_node_readiness_invalid",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByNodeReadiness": ["notready"]}},
|
||||
"invalid filter by node readiness",
|
||||
id="filter_by_node_readiness_missing_underscore",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_clusters_validation_errors(
|
||||
|
||||
@@ -377,6 +377,45 @@ def test_deployments_pod_status_aggregation(
|
||||
# All status metrics present -> gate satisfied -> no status warning.
|
||||
assert all("Pod status could not be computed" not in w["message"] for w in get_all_warnings(response.json()))
|
||||
|
||||
# filterByPodStatus: deployment kept (>=1 matching pod), only the filtered
|
||||
# buckets populated. Multi-select is OR -> the union of requested buckets.
|
||||
for fbps, expected in (
|
||||
(["running"], expected_status_counts(running=3)),
|
||||
(["CrashLoopBackOff"], expected_status_counts(crashLoopBackOff=1)),
|
||||
(["running", "CrashLoopBackOff"], expected_status_counts(running=3, crashLoopBackOff=1)),
|
||||
):
|
||||
resp = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": "k8s.deployment.name = 'pp-dep'", "filterByPodStatus": fbps},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.OK, resp.text
|
||||
fdata = resp.json()["data"]
|
||||
assert fdata["total"] == 1
|
||||
assert fdata["records"][0]["podCountsByStatus"] == expected
|
||||
|
||||
# A set fully absent from the deployment -> empty page (single and multi).
|
||||
for fbps in (["completed"], ["completed", "oomKilled"]):
|
||||
absent = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": "k8s.deployment.name = 'pp-dep'", "filterByPodStatus": fbps},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert absent.status_code == HTTPStatus.OK, absent.text
|
||||
assert absent.json()["data"]["total"] == 0, f"deployment must be dropped by filterByPodStatus={fbps!r}"
|
||||
|
||||
|
||||
def test_deployments_desired_available_counts(
|
||||
signoz: types.SigNoz,
|
||||
@@ -783,6 +822,11 @@ def test_deployments_orderby( # pylint: disable=too-many-arguments,too-many-pos
|
||||
"is only allowed when groupBy is empty",
|
||||
id="orderby_depname_with_groupby",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByPodStatus": ["Bogus"]}},
|
||||
"invalid filter by pod status",
|
||||
id="filter_by_pod_status_invalid",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_deployments_validation_errors(
|
||||
|
||||
@@ -8,6 +8,7 @@ import requests
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.fs import get_testdata_file_path
|
||||
from fixtures.inframonitoring import expected_status_counts
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.querier import compare_values, get_all_warnings
|
||||
|
||||
@@ -326,6 +327,77 @@ def test_statefulsets_base_filter_drops_non_statefulset_pods(
|
||||
# No empty-name group leaking through.
|
||||
assert all(r["statefulSetName"] != "" for r in data["records"])
|
||||
|
||||
# filterByPodStatus: ns-ss (ns-ss-p1 Running + ns-ss-p1-clbo CrashLoopBackOff)
|
||||
# is kept when >=1 pod matches; counts reflect only the filtered status; an
|
||||
# absent status yields an empty page. Metric aggregation stays undistorted.
|
||||
unfiltered_cpu = rec["statefulSetCPU"]
|
||||
|
||||
running = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": ["running"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert running.status_code == HTTPStatus.OK, running.text
|
||||
rdata = running.json()["data"]
|
||||
assert rdata["total"] == 1
|
||||
rrec = rdata["records"][0]
|
||||
assert rrec["statefulSetName"] == "ns-ss"
|
||||
assert rrec["podCountsByStatus"] == expected_status_counts(running=1)
|
||||
assert compare_values(rrec["statefulSetCPU"], unfiltered_cpu, 1e-6), "filterByPodStatus distorted statefulSetCPU"
|
||||
|
||||
clbo = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": ["CrashLoopBackOff"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert clbo.status_code == HTTPStatus.OK, clbo.text
|
||||
cdata = clbo.json()["data"]
|
||||
assert cdata["total"] == 1
|
||||
assert cdata["records"][0]["podCountsByStatus"] == expected_status_counts(crashLoopBackOff=1)
|
||||
|
||||
# Multi-select is OR: both requested buckets populated (union), others zeroed.
|
||||
multi = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": ["running", "CrashLoopBackOff"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert multi.status_code == HTTPStatus.OK, multi.text
|
||||
assert multi.json()["data"]["records"][0]["podCountsByStatus"] == expected_status_counts(running=1, crashLoopBackOff=1)
|
||||
|
||||
# A set fully absent from the group -> empty page (single and multi-select).
|
||||
for fbps in (["pending"], ["pending", "oomKilled"]):
|
||||
absent = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": fbps},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert absent.status_code == HTTPStatus.OK, absent.text
|
||||
assert absent.json()["data"]["total"] == 0, f"group must be dropped by filterByPodStatus={fbps!r}"
|
||||
|
||||
|
||||
# Float record fields compared with tolerance; everything else compared with ==.
|
||||
_GROUPBY_FLOAT_FIELDS = {
|
||||
@@ -655,6 +727,11 @@ def test_statefulsets_orderby( # pylint: disable=too-many-arguments,too-many-po
|
||||
"is only allowed when groupBy is empty",
|
||||
id="orderby_ssname_with_groupby",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByPodStatus": ["Bogus"]}},
|
||||
"invalid filter by pod status",
|
||||
id="filter_by_pod_status_invalid",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_statefulsets_validation_errors(
|
||||
|
||||
@@ -8,6 +8,7 @@ import requests
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.fs import get_testdata_file_path
|
||||
from fixtures.inframonitoring import expected_status_counts
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.querier import compare_values, get_all_warnings
|
||||
|
||||
@@ -458,6 +459,77 @@ def test_jobs_base_filter_drops_non_job_pods(
|
||||
assert rec["jobName"] == "nj-job"
|
||||
assert all(r["jobName"] != "" for r in data["records"])
|
||||
|
||||
# filterByPodStatus: nj-job (nj-job-p1 Running + nj-job-p1-clbo CrashLoopBackOff)
|
||||
# is kept when >=1 pod matches; counts reflect only the filtered status; an
|
||||
# absent status yields an empty page. Metric aggregation stays undistorted.
|
||||
unfiltered_cpu = rec["jobCPU"]
|
||||
|
||||
running = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": ["running"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert running.status_code == HTTPStatus.OK, running.text
|
||||
rdata = running.json()["data"]
|
||||
assert rdata["total"] == 1
|
||||
rrec = rdata["records"][0]
|
||||
assert rrec["jobName"] == "nj-job"
|
||||
assert rrec["podCountsByStatus"] == expected_status_counts(running=1)
|
||||
assert compare_values(rrec["jobCPU"], unfiltered_cpu, 1e-6), "filterByPodStatus distorted jobCPU"
|
||||
|
||||
clbo = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": ["CrashLoopBackOff"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert clbo.status_code == HTTPStatus.OK, clbo.text
|
||||
cdata = clbo.json()["data"]
|
||||
assert cdata["total"] == 1
|
||||
assert cdata["records"][0]["podCountsByStatus"] == expected_status_counts(crashLoopBackOff=1)
|
||||
|
||||
# Multi-select is OR: both requested buckets populated (union), others zeroed.
|
||||
multi = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": ["running", "CrashLoopBackOff"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert multi.status_code == HTTPStatus.OK, multi.text
|
||||
assert multi.json()["data"]["records"][0]["podCountsByStatus"] == expected_status_counts(running=1, crashLoopBackOff=1)
|
||||
|
||||
# A set fully absent from the group -> empty page (single and multi-select).
|
||||
for fbps in (["pending"], ["pending", "oomKilled"]):
|
||||
absent = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": fbps},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert absent.status_code == HTTPStatus.OK, absent.text
|
||||
assert absent.json()["data"]["total"] == 0, f"group must be dropped by filterByPodStatus={fbps!r}"
|
||||
|
||||
|
||||
# Float record fields compared with tolerance; everything else compared with ==.
|
||||
_GROUPBY_FLOAT_FIELDS = {
|
||||
@@ -794,6 +866,11 @@ def test_jobs_orderby( # pylint: disable=too-many-arguments,too-many-positional
|
||||
"is only allowed when groupBy is empty",
|
||||
id="orderby_jobname_with_groupby",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByPodStatus": ["Bogus"]}},
|
||||
"invalid filter by pod status",
|
||||
id="filter_by_pod_status_invalid",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_jobs_validation_errors(
|
||||
|
||||
@@ -8,6 +8,7 @@ import requests
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.fs import get_testdata_file_path
|
||||
from fixtures.inframonitoring import expected_status_counts
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.querier import compare_values, get_all_warnings
|
||||
|
||||
@@ -333,6 +334,77 @@ def test_daemonsets_base_filter_drops_non_daemonset_pods(
|
||||
assert rec["daemonSetName"] == "nd-ds"
|
||||
assert all(r["daemonSetName"] != "" for r in data["records"])
|
||||
|
||||
# filterByPodStatus: nd-ds (nd-ds-p1 Running + nd-ds-p1-clbo CrashLoopBackOff)
|
||||
# is kept when >=1 pod matches; counts reflect only the filtered status; an
|
||||
# absent status yields an empty page. Metric aggregation stays undistorted.
|
||||
unfiltered_cpu = rec["daemonSetCPU"]
|
||||
|
||||
running = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": ["running"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert running.status_code == HTTPStatus.OK, running.text
|
||||
rdata = running.json()["data"]
|
||||
assert rdata["total"] == 1
|
||||
rrec = rdata["records"][0]
|
||||
assert rrec["daemonSetName"] == "nd-ds"
|
||||
assert rrec["podCountsByStatus"] == expected_status_counts(running=1)
|
||||
assert compare_values(rrec["daemonSetCPU"], unfiltered_cpu, 1e-6), "filterByPodStatus distorted daemonSetCPU"
|
||||
|
||||
clbo = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": ["CrashLoopBackOff"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert clbo.status_code == HTTPStatus.OK, clbo.text
|
||||
cdata = clbo.json()["data"]
|
||||
assert cdata["total"] == 1
|
||||
assert cdata["records"][0]["podCountsByStatus"] == expected_status_counts(crashLoopBackOff=1)
|
||||
|
||||
# Multi-select is OR: both requested buckets populated (union), others zeroed.
|
||||
multi = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": ["running", "CrashLoopBackOff"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert multi.status_code == HTTPStatus.OK, multi.text
|
||||
assert multi.json()["data"]["records"][0]["podCountsByStatus"] == expected_status_counts(running=1, crashLoopBackOff=1)
|
||||
|
||||
# A set fully absent from the group -> empty page (single and multi-select).
|
||||
for fbps in (["pending"], ["pending", "oomKilled"]):
|
||||
absent = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": fbps},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert absent.status_code == HTTPStatus.OK, absent.text
|
||||
assert absent.json()["data"]["total"] == 0, f"group must be dropped by filterByPodStatus={fbps!r}"
|
||||
|
||||
|
||||
# Float record fields compared with tolerance; everything else compared with ==.
|
||||
_GROUPBY_FLOAT_FIELDS = {
|
||||
@@ -663,6 +735,11 @@ def test_daemonsets_orderby( # pylint: disable=too-many-arguments,too-many-posi
|
||||
"is only allowed when groupBy is empty",
|
||||
id="orderby_dsname_with_groupby",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByPodStatus": ["Bogus"]}},
|
||||
"invalid filter by pod status",
|
||||
id="filter_by_pod_status_invalid",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_daemonsets_validation_errors(
|
||||
|
||||
@@ -120,6 +120,84 @@ def test_kube_containers_status_health_and_base_set(
|
||||
else:
|
||||
assert rec["cpu"] == -1, f"{pod}: expected cpu -1 sentinel (base-set-only), got {rec['cpu']}"
|
||||
|
||||
# filterByContainerStatus (secondary filter): matching status keeps the
|
||||
# container; a mismatched status filters it out. Wire value is
|
||||
# case-insensitive (valuer lowercases): "running" == "Running".
|
||||
crun_running = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": "k8s.pod.name = 'crun'", "filterByContainerStatus": ["running"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert crun_running.status_code == HTTPStatus.OK, crun_running.text
|
||||
cr = crun_running.json()["data"]
|
||||
assert cr["total"] == 1
|
||||
assert cr["records"][0]["meta"]["k8s.pod.name"] == "crun"
|
||||
|
||||
cclo_clbo = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": "k8s.pod.name = 'cclo'", "filterByContainerStatus": ["CrashLoopBackOff"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert cclo_clbo.status_code == HTTPStatus.OK, cclo_clbo.text
|
||||
assert cclo_clbo.json()["data"]["total"] == 1
|
||||
|
||||
# crun is Running, not CrashLoopBackOff -> filtered out.
|
||||
crun_mismatch = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": "k8s.pod.name = 'crun'", "filterByContainerStatus": ["CrashLoopBackOff"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert crun_mismatch.status_code == HTTPStatus.OK, crun_mismatch.text
|
||||
assert crun_mismatch.json()["data"]["total"] == 0
|
||||
|
||||
# Multi-select is OR: a set containing the container's status keeps it; a set
|
||||
# with none of its statuses drops it.
|
||||
crun_or_keep = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": "k8s.pod.name = 'crun'", "filterByContainerStatus": ["running", "CrashLoopBackOff"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert crun_or_keep.status_code == HTTPStatus.OK, crun_or_keep.text
|
||||
assert crun_or_keep.json()["data"]["total"] == 1
|
||||
|
||||
crun_all_mismatch = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": "k8s.pod.name = 'crun'", "filterByContainerStatus": ["CrashLoopBackOff", "OOMKilled"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert crun_all_mismatch.status_code == HTTPStatus.OK, crun_all_mismatch.text
|
||||
assert crun_all_mismatch.json()["data"]["total"] == 0
|
||||
|
||||
|
||||
def test_kube_containers_status_counts_grouped_mode(
|
||||
signoz: types.SigNoz,
|
||||
@@ -165,6 +243,95 @@ def test_kube_containers_status_counts_grouped_mode(
|
||||
assert ns_b["oomKilled"] == 1
|
||||
assert by_ns["ns-b"]["containerCountsByReady"] == {"ready": 0, "notReady": 2}
|
||||
|
||||
# filterByContainerStatus in grouped mode: keep only groups with a matching
|
||||
# container, only that bucket populated; a group with none is dropped.
|
||||
running = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"groupBy": [{"name": "k8s.namespace.name", "fieldDataType": "string", "fieldContext": "resource"}],
|
||||
"limit": 50,
|
||||
"filter": {"filterByContainerStatus": ["running"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert running.status_code == HTTPStatus.OK, running.text
|
||||
rdata = running.json()["data"]
|
||||
# ns-a has running containers, ns-b has none -> only ns-a.
|
||||
assert {r["meta"]["k8s.namespace.name"] for r in rdata["records"]} == {"ns-a"}
|
||||
assert rdata["records"][0]["containerCountsByStatus"]["running"] == 2
|
||||
|
||||
oom = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"groupBy": [{"name": "k8s.namespace.name", "fieldDataType": "string", "fieldContext": "resource"}],
|
||||
"limit": 50,
|
||||
"filter": {"filterByContainerStatus": ["OOMKilled"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert oom.status_code == HTTPStatus.OK, oom.text
|
||||
odata = oom.json()["data"]
|
||||
assert {r["meta"]["k8s.namespace.name"] for r in odata["records"]} == {"ns-b"}
|
||||
assert odata["records"][0]["containerCountsByStatus"]["oomKilled"] == 1
|
||||
|
||||
# A status absent from every group -> empty page.
|
||||
absent = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"groupBy": [{"name": "k8s.namespace.name", "fieldDataType": "string", "fieldContext": "resource"}],
|
||||
"limit": 50,
|
||||
"filter": {"filterByContainerStatus": ["Terminated"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert absent.status_code == HTTPStatus.OK, absent.text
|
||||
assert absent.json()["data"]["total"] == 0
|
||||
|
||||
# Multi-select is OR: keeps every group with any matching container (union
|
||||
# across groups); a fully-absent set drops all.
|
||||
union = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"groupBy": [{"name": "k8s.namespace.name", "fieldDataType": "string", "fieldContext": "resource"}],
|
||||
"limit": 50,
|
||||
"filter": {"filterByContainerStatus": ["running", "OOMKilled"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert union.status_code == HTTPStatus.OK, union.text
|
||||
udata = union.json()["data"]
|
||||
u_by_ns = {r["meta"]["k8s.namespace.name"]: r for r in udata["records"]}
|
||||
assert set(u_by_ns) == {"ns-a", "ns-b"}
|
||||
assert u_by_ns["ns-a"]["containerCountsByStatus"]["running"] == 2
|
||||
assert u_by_ns["ns-b"]["containerCountsByStatus"]["oomKilled"] == 1
|
||||
|
||||
absent_multi = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"groupBy": [{"name": "k8s.namespace.name", "fieldDataType": "string", "fieldContext": "resource"}],
|
||||
"limit": 50,
|
||||
"filter": {"filterByContainerStatus": ["Terminated", "Waiting"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert absent_multi.status_code == HTTPStatus.OK, absent_multi.text
|
||||
assert absent_multi.json()["data"]["total"] == 0
|
||||
|
||||
|
||||
def test_kube_containers_status_recency(
|
||||
signoz: types.SigNoz,
|
||||
@@ -283,6 +450,31 @@ def test_kube_containers_status_warning_missing_metrics(
|
||||
warnings = get_all_warnings(body)
|
||||
assert any("status.state" in w["message"] and "status.reason" in w["message"] for w in warnings), f"status warning naming the missing metrics not surfaced: {warnings!r}"
|
||||
|
||||
# filterByContainerStatus + missing status metrics: the up-front gate returns
|
||||
# the warning and an empty page (Total 0) rather than silently filtering
|
||||
# everything out.
|
||||
filtered = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByContainerStatus": ["Running"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert filtered.status_code == HTTPStatus.OK, filtered.text
|
||||
fdata = filtered.json()["data"]
|
||||
assert fdata["total"] == 0
|
||||
assert fdata["records"] == []
|
||||
# The gate warning on the filtered path is surfaced at the top-level
|
||||
# warning.message (the up-front early return sets it directly); collect both
|
||||
# that and any nested warnings.
|
||||
fwarn = fdata.get("warning") or {}
|
||||
fmsgs = ([fwarn["message"]] if fwarn.get("message") else []) + [w["message"] for w in fwarn.get("warnings", [])]
|
||||
assert any("status.state" in m and "status.reason" in m for m in fmsgs), f"gate warning missing on filtered call: {fmsgs!r}"
|
||||
|
||||
|
||||
def test_kube_containers_filter(
|
||||
signoz: types.SigNoz,
|
||||
@@ -374,6 +566,107 @@ def test_kube_containers_orderby_and_pagination(
|
||||
assert seen == order, f"paginated sequence {seen} != full-page order {order}"
|
||||
|
||||
|
||||
def test_kube_containers_filter_pagination_and_ordering(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token,
|
||||
insert_metrics,
|
||||
) -> None:
|
||||
"""filterByContainerStatus (multi-select) composed with pagination + name
|
||||
ordering. The full-scope status keyset is resolved before slicing, so total
|
||||
reflects the full matched set and pages stay disjoint + complete on both the
|
||||
metric-ordering branch (paginateWithBackfill -- including its metadata-only
|
||||
backfill arm, since coom has no cpu metric) and the name-ordering branch
|
||||
(PaginateMetadataByName). ['running','crashloopbackoff','oomkilled'] matches 4
|
||||
containers in kube_containers_dataset.jsonl: crun, cnr, cclo, coom.
|
||||
|
||||
Name-branch order is not asserted: every container shares k8s.container.name
|
||||
('app'), so the sort ties -- we assert the filtered set + disjoint/complete pages."""
|
||||
now = datetime.now(tz=UTC).replace(microsecond=0)
|
||||
insert_metrics(
|
||||
Metrics.load_from_file(
|
||||
get_testdata_file_path("inframonitoring/kube_containers_dataset.jsonl"),
|
||||
base_time=now - timedelta(minutes=4),
|
||||
)
|
||||
)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
start = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end = int(now.timestamp() * 1000)
|
||||
statuses = ["running", "crashloopbackoff", "oomkilled"]
|
||||
matched = {"crun", "cnr", "cclo", "coom"}
|
||||
|
||||
# Metric-ordering branch (default cpu order): total is invariant across a paged
|
||||
# walk and the pages are disjoint + cover the full matched set (coom arrives via
|
||||
# the metadata-only backfill arm). Non-matching containers never appear.
|
||||
seen: list[str] = []
|
||||
totals: set[int] = set()
|
||||
for offset in (0, 2, 4):
|
||||
resp = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": start,
|
||||
"end": end,
|
||||
"limit": 2,
|
||||
"offset": offset,
|
||||
"filter": {"filterByContainerStatus": statuses},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.OK, resp.text
|
||||
data = resp.json()["data"]
|
||||
totals.add(data["total"])
|
||||
assert len(data["records"]) == max(0, min(2, 4 - offset)), f"offset={offset}: {data['records']!r}"
|
||||
seen.extend(r["meta"]["k8s.pod.name"] for r in data["records"])
|
||||
assert totals == {4}, f"total not invariant under filter+pagination: {totals}"
|
||||
assert len(seen) == 4, f"pages overlapped: {seen}"
|
||||
assert set(seen) == matched
|
||||
|
||||
# Name-ordering branch (orderBy k8s.container.name asc, groupBy empty): the
|
||||
# filtered set is returned and total is the full matched count.
|
||||
ordered = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": start,
|
||||
"end": end,
|
||||
"limit": 50,
|
||||
"filter": {"filterByContainerStatus": statuses},
|
||||
"orderBy": {"key": {"name": "k8s.container.name"}, "direction": "asc"},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert ordered.status_code == HTTPStatus.OK, ordered.text
|
||||
odata = ordered.json()["data"]
|
||||
assert odata["total"] == 4
|
||||
assert {r["meta"]["k8s.pod.name"] for r in odata["records"]} == matched
|
||||
|
||||
# Name branch paginated: PaginateMetadataByName slices the filtered set into
|
||||
# disjoint pages that together cover it, with total unchanged.
|
||||
name_seen: list[str] = []
|
||||
for offset in (0, 2):
|
||||
resp = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": start,
|
||||
"end": end,
|
||||
"limit": 2,
|
||||
"offset": offset,
|
||||
"filter": {"filterByContainerStatus": statuses},
|
||||
"orderBy": {"key": {"name": "k8s.container.name"}, "direction": "asc"},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.OK, resp.text
|
||||
data = resp.json()["data"]
|
||||
assert data["total"] == 4
|
||||
assert len(data["records"]) == 2, f"offset={offset}: {data['records']!r}"
|
||||
name_seen.extend(r["meta"]["k8s.pod.name"] for r in data["records"])
|
||||
assert len(name_seen) == 4, f"name-branch pages overlapped: {name_seen}"
|
||||
assert set(name_seen) == matched
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("payload_override", "err_substr"),
|
||||
[
|
||||
@@ -398,6 +691,16 @@ def test_kube_containers_orderby_and_pagination(
|
||||
"is only allowed when groupBy is empty",
|
||||
id="orderby_container_name_with_groupby",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByContainerStatus": ["bogus"]}},
|
||||
"invalid filter by container status",
|
||||
id="filter_by_container_status_invalid",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByContainerStatus": ["no_data"]}},
|
||||
"invalid filter by container status",
|
||||
id="filter_by_container_status_no_data",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_kube_containers_validation_errors(
|
||||
|
||||
@@ -26,15 +26,13 @@ def test_create_rejects_wrong_schema_version(
|
||||
"name": "my-view",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v9",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v9",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -59,15 +57,13 @@ def test_create_rejects_invalid_panel_type(
|
||||
"name": "my-view",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "bogus",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "bogus",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -91,15 +87,13 @@ def test_create_rejects_empty_queries(
|
||||
"name": "my-view",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -127,15 +121,13 @@ def test_create_rejects_empty_display_name(
|
||||
"name": "my-view",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -160,15 +152,13 @@ def test_create_rejects_invalid_source(
|
||||
"name": "my-view",
|
||||
"generateName": False,
|
||||
"source": "bogus",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -192,15 +182,13 @@ def test_create_rejects_invalid_name(
|
||||
"name": "Not A Valid Slug",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -226,15 +214,13 @@ def test_create_rejects_empty_name_without_generate_name(
|
||||
"name": "",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -261,15 +247,13 @@ def test_create_rejects_name_when_generate_name_is_true(
|
||||
"name": "explicit-name",
|
||||
"generateName": True,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -298,15 +282,13 @@ def test_create_rejects_unknown_field(
|
||||
"name": "my-view",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"unknownfield": "boom",
|
||||
},
|
||||
@@ -366,15 +348,13 @@ def test_update_missing_view_returns_not_found(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{uuid.uuid4()}"),
|
||||
json={
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -402,15 +382,13 @@ def test_update_rejects_name_field(
|
||||
"name": "update-rejects-name-field",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
@@ -424,15 +402,13 @@ def test_update_rejects_name_field(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
json={
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"name": "update-rejects-name-field",
|
||||
},
|
||||
@@ -485,15 +461,13 @@ def test_saved_view_lifecycle(
|
||||
"name": "lc-logs-overview",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "lc-logs-overview",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "lc-logs-overview",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
@@ -508,15 +482,13 @@ def test_saved_view_lifecycle(
|
||||
"name": "lc-traces-overview",
|
||||
"generateName": False,
|
||||
"source": "traces",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "lc-traces-overview",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "lc-traces-overview",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
@@ -535,9 +507,9 @@ def test_saved_view_lifecycle(
|
||||
got = response.json()["data"]
|
||||
assert got["id"] == view_id
|
||||
assert got["name"] == "lc-logs-overview"
|
||||
assert got["data"]["spec"]["displayName"] == "lc-logs-overview"
|
||||
assert got["spec"]["displayName"] == "lc-logs-overview"
|
||||
assert got["source"] == "logs"
|
||||
assert got["data"]["spec"]["panelType"] == "table"
|
||||
assert got["spec"]["panelType"] == "table"
|
||||
|
||||
# ── list filters by source and name ──────────────────────────────
|
||||
response = requests.get(
|
||||
@@ -564,15 +536,13 @@ def test_saved_view_lifecycle(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
json={
|
||||
"source": "metrics",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "lc-logs-overview-renamed",
|
||||
"panelType": "graph",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "lc-logs-overview-renamed",
|
||||
"panelType": "graph",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
@@ -588,9 +558,9 @@ def test_saved_view_lifecycle(
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
updated = response.json()["data"]
|
||||
assert updated["name"] == "lc-logs-overview", "name is immutable"
|
||||
assert updated["data"]["spec"]["displayName"] == "lc-logs-overview-renamed"
|
||||
assert updated["spec"]["displayName"] == "lc-logs-overview-renamed"
|
||||
assert updated["source"] == "metrics"
|
||||
assert updated["data"]["spec"]["panelType"] == "graph"
|
||||
assert updated["spec"]["panelType"] == "graph"
|
||||
finally:
|
||||
requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
@@ -621,15 +591,13 @@ def test_empty_name_derives_a_slug_from_display_name(
|
||||
"name": "",
|
||||
"generateName": True,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My Generated View!",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My Generated View!",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
@@ -646,7 +614,7 @@ def test_empty_name_derives_a_slug_from_display_name(
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
got = response.json()["data"]
|
||||
assert got["data"]["spec"]["displayName"] == "My Generated View!"
|
||||
assert got["spec"]["displayName"] == "My Generated View!"
|
||||
assert got["name"].startswith("my-generated-view-")
|
||||
assert got["name"] != "my-generated-view-", "expected a random suffix, not just the slugified prefix"
|
||||
finally:
|
||||
@@ -681,15 +649,13 @@ def test_create_roundtrip_preserves_zero_values(
|
||||
"name": "create-zero-values",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "create-zero-values",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "create-zero-values",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
@@ -705,7 +671,7 @@ def test_create_roundtrip_preserves_zero_values(
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
spec = response.json()["data"]["data"]["spec"]
|
||||
spec = response.json()["data"]["spec"]
|
||||
query = spec["queries"][0]["spec"]
|
||||
|
||||
cases = [
|
||||
@@ -741,14 +707,12 @@ def test_selected_fields_omitted_on_create_reads_back_as_empty_list_not_null(
|
||||
"name": "omitted-selected-fields",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "omitted-selected-fields",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "omitted-selected-fields",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
@@ -764,7 +728,151 @@ def test_selected_fields_omitted_on_create_reads_back_as_empty_list_not_null(
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["data"]["data"]["spec"]["selectedFields"] == []
|
||||
assert response.json()["data"]["spec"]["selectedFields"] == []
|
||||
finally:
|
||||
requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
|
||||
def test_display_omitted_on_create_reads_back_as_zero_value(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
"""display's counterpart to test_selected_fields_omitted_on_create_reads_back_as_empty_list_not_null:
|
||||
neither field is required, so omitting display entirely must not 400 or
|
||||
leave it null -- it reads back as the zero-value Display object."""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(BASE_URL),
|
||||
json={
|
||||
"name": "omitted-display",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "omitted-display",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}],
|
||||
"selectedFields": [],
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
view_id = response.json()["data"]["id"]
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["data"]["spec"]["display"] == {"maxLines": 0, "fontSize": "", "format": "", "color": ""}
|
||||
finally:
|
||||
requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
|
||||
def test_selected_fields_and_display_explicit_null_on_create(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
"""JSON null decodes as a no-op onto a non-pointer Go field (struct/slice), so
|
||||
an explicit null is expected to behave identically to omitting the field."""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(BASE_URL),
|
||||
json={
|
||||
"name": "null-selected-fields-and-display",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "null-selected-fields-and-display",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}],
|
||||
"selectedFields": None,
|
||||
"display": None,
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
view_id = response.json()["data"]["id"]
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
spec = response.json()["data"]["spec"]
|
||||
assert spec["selectedFields"] == []
|
||||
assert spec["display"] == {"maxLines": 0, "fontSize": "", "format": "", "color": ""}
|
||||
finally:
|
||||
requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
|
||||
def test_create_with_partial_display_defaults_missing_fields(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
"""display's fields are each independently optional -- sending only one
|
||||
(color) must not 400, and the fields left unset must default to their own
|
||||
zero value rather than being rejected or dropped."""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(BASE_URL),
|
||||
json={
|
||||
"name": "partial-display-color-only",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "partial-display-color-only",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}],
|
||||
"selectedFields": [],
|
||||
"display": {"color": "test"},
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
view_id = response.json()["data"]["id"]
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["data"]["spec"]["display"] == {"maxLines": 0, "fontSize": "", "format": "", "color": "test"}
|
||||
finally:
|
||||
requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
@@ -793,15 +901,13 @@ def test_update_does_not_corrupt_zero_values(
|
||||
"name": "update-zero-values",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "update-zero-values",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": True, "legend": "Custom Legend"}}],
|
||||
"selectedFields": [{"name": "service.name"}],
|
||||
"display": {"maxLines": 25, "fontSize": "large", "format": "table", "color": "blue"},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "update-zero-values",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": True, "legend": "Custom Legend"}}],
|
||||
"selectedFields": [{"name": "service.name"}],
|
||||
"display": {"maxLines": 25, "fontSize": "large", "format": "table", "color": "blue"},
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
@@ -817,7 +923,7 @@ def test_update_does_not_corrupt_zero_values(
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
spec = response.json()["data"]["data"]["spec"]
|
||||
spec = response.json()["data"]["spec"]
|
||||
assert spec["display"]["maxLines"] == 25
|
||||
# signal/fieldContext/fieldDataType always serialize on TelemetryFieldKey
|
||||
# (no omitempty -- see pkg/types/telemetrytypes/field.go), so an entry sent
|
||||
@@ -830,15 +936,13 @@ def test_update_does_not_corrupt_zero_values(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
json={
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "update-zero-values",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "update-zero-values",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
@@ -853,7 +957,7 @@ def test_update_does_not_corrupt_zero_values(
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
spec = response.json()["data"]["data"]["spec"]
|
||||
spec = response.json()["data"]["spec"]
|
||||
query = spec["queries"][0]["spec"]
|
||||
|
||||
cases = [
|
||||
@@ -873,3 +977,69 @@ def test_update_does_not_corrupt_zero_values(
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
|
||||
def test_update_with_partial_display_replaces_whole_object(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
"""Update is a whole-object replace, not a merge: sending only "color" on
|
||||
update must not preserve the previous fontSize/format/maxLines -- those
|
||||
reset to their zero value exactly as if display had been sent in full."""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(BASE_URL),
|
||||
json={
|
||||
"name": "update-partial-display",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "update-partial-display",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 10, "fontSize": "large", "format": "table", "color": "blue"},
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
view_id = response.json()["data"]["id"]
|
||||
|
||||
try:
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
json={
|
||||
"source": "logs",
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "update-partial-display",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}],
|
||||
"selectedFields": [],
|
||||
"display": {"color": "green"},
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["data"]["spec"]["display"] == {"maxLines": 0, "fontSize": "", "format": "", "color": "green"}
|
||||
finally:
|
||||
requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
@@ -112,15 +112,13 @@ def test_write_forbidden_without_grant(
|
||||
signoz.self.host_configs["8080"].get(f"{SAVED_VIEW_BASE}/{target_id}"),
|
||||
json={
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": _SAVED_VIEW_FGA_TARGET_NAME,
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": _SAVED_VIEW_FGA_TARGET_NAME,
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -133,15 +131,13 @@ def test_write_forbidden_without_grant(
|
||||
json={
|
||||
"name": "saved-view-fga-create-attempt",
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "saved-view-fga-create-attempt",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "saved-view-fga-create-attempt",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -212,15 +208,13 @@ def test_update_scoped_to_granted_view(
|
||||
token = get_token(_SAVED_VIEW_FGA_CUSTOM_USER_EMAIL, _SAVED_VIEW_FGA_CUSTOM_USER_PASSWORD)
|
||||
updated_body = {
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": _SAVED_VIEW_FGA_TARGET_NAME,
|
||||
"panelType": "graph",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": _SAVED_VIEW_FGA_TARGET_NAME,
|
||||
"panelType": "graph",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user