mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-26 16:00:36 +01:00
Compare commits
16 Commits
nv/sql-das
...
infraM/con
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2714310f8 | ||
|
|
51105d91d6 | ||
|
|
b2f0b8f3c6 | ||
|
|
d089d2eec1 | ||
|
|
2a5d5c870c | ||
|
|
b46d1b5ada | ||
|
|
575e91b6f2 | ||
|
|
8184cfbda8 | ||
|
|
f04023018f | ||
|
|
b1dfeac5fe | ||
|
|
05d0db2029 | ||
|
|
fc56590a7a | ||
|
|
d59d50f6dd | ||
|
|
7aa5573cd7 | ||
|
|
a5a28bae2a | ||
|
|
23e0ffba75 |
@@ -4256,6 +4256,15 @@ components:
|
||||
- missingOptionalMetrics
|
||||
- missingRequiredAttributes
|
||||
type: object
|
||||
InframonitoringtypesClusterFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByNodeReadiness:
|
||||
$ref: '#/components/schemas/InframonitoringtypesNodeCondition'
|
||||
filterByPodStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
type: object
|
||||
InframonitoringtypesClusterRecord:
|
||||
properties:
|
||||
clusterCPU:
|
||||
@@ -4393,6 +4402,13 @@ components:
|
||||
- containerCannotRun
|
||||
- unknown
|
||||
type: object
|
||||
InframonitoringtypesContainerFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByContainerStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesContainerStatus'
|
||||
type: object
|
||||
InframonitoringtypesContainerReady:
|
||||
enum:
|
||||
- ready
|
||||
@@ -4492,6 +4508,13 @@ components:
|
||||
- total
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesDaemonSetFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByPodStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
type: object
|
||||
InframonitoringtypesDaemonSetRecord:
|
||||
properties:
|
||||
currentNodes:
|
||||
@@ -4564,6 +4587,13 @@ components:
|
||||
- total
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesDeploymentFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByPodStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
type: object
|
||||
InframonitoringtypesDeploymentRecord:
|
||||
properties:
|
||||
availablePods:
|
||||
@@ -4705,6 +4735,13 @@ components:
|
||||
- total
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesJobFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByPodStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
type: object
|
||||
InframonitoringtypesJobRecord:
|
||||
properties:
|
||||
activePods:
|
||||
@@ -4828,6 +4865,13 @@ components:
|
||||
- message
|
||||
- documentationLink
|
||||
type: object
|
||||
InframonitoringtypesNamespaceFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByPodStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
type: object
|
||||
InframonitoringtypesNamespaceRecord:
|
||||
properties:
|
||||
counts:
|
||||
@@ -4909,6 +4953,15 @@ components:
|
||||
- ready
|
||||
- notReady
|
||||
type: object
|
||||
InframonitoringtypesNodeFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByNodeReadiness:
|
||||
$ref: '#/components/schemas/InframonitoringtypesNodeCondition'
|
||||
filterByPodStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
type: object
|
||||
InframonitoringtypesNodeRecord:
|
||||
properties:
|
||||
condition:
|
||||
@@ -5025,6 +5078,13 @@ components:
|
||||
- shutdown
|
||||
- unexpectedAdmissionError
|
||||
type: object
|
||||
InframonitoringtypesPodFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByPodStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
type: object
|
||||
InframonitoringtypesPodRecord:
|
||||
properties:
|
||||
meta:
|
||||
@@ -5124,7 +5184,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesClusterFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5150,7 +5210,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesContainerFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5176,7 +5236,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesDaemonSetFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5202,7 +5262,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesDeploymentFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5254,7 +5314,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesJobFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5280,7 +5340,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesNamespaceFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5306,7 +5366,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesNodeFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5332,7 +5392,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5358,7 +5418,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesStatefulSetFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5409,6 +5469,13 @@ components:
|
||||
- list
|
||||
- grouped_list
|
||||
type: string
|
||||
InframonitoringtypesStatefulSetFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByPodStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
type: object
|
||||
InframonitoringtypesStatefulSetRecord:
|
||||
properties:
|
||||
currentPods:
|
||||
|
||||
@@ -5688,6 +5688,41 @@ 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;
|
||||
filterByNodeReadiness?: InframonitoringtypesNodeConditionDTO;
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesClusterRecordDTOCounts = {
|
||||
/**
|
||||
* @type integer
|
||||
@@ -5963,21 +5998,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',
|
||||
@@ -5994,6 +6014,29 @@ export enum InframonitoringtypesContainerStatusDTO {
|
||||
unknown = 'unknown',
|
||||
no_data = 'no_data',
|
||||
}
|
||||
export interface InframonitoringtypesContainerFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
filterByContainerStatus?: InframonitoringtypesContainerStatusDTO;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -6065,6 +6108,14 @@ export interface InframonitoringtypesContainersDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesDaemonSetFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesDaemonSetRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -6150,6 +6201,14 @@ export interface InframonitoringtypesDaemonSetsDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesDeploymentFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesDeploymentRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -6312,6 +6371,14 @@ export interface InframonitoringtypesHostsDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesJobFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesJobRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -6397,6 +6464,14 @@ export interface InframonitoringtypesJobsDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesNamespaceFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesNamespaceRecordDTOCounts = {
|
||||
/**
|
||||
* @type integer
|
||||
@@ -6473,11 +6548,15 @@ 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;
|
||||
filterByNodeReadiness?: InframonitoringtypesNodeConditionDTO;
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesNodeRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -6539,6 +6618,14 @@ export interface InframonitoringtypesNodesDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesPodFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesPodRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -6549,27 +6636,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
|
||||
@@ -6646,7 +6712,7 @@ export interface InframonitoringtypesPostableClustersDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesClusterFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6673,7 +6739,7 @@ export interface InframonitoringtypesPostableContainersDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesContainerFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6700,7 +6766,7 @@ export interface InframonitoringtypesPostableDaemonSetsDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesDaemonSetFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6727,7 +6793,7 @@ export interface InframonitoringtypesPostableDeploymentsDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesDeploymentFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6781,7 +6847,7 @@ export interface InframonitoringtypesPostableJobsDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesJobFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6808,7 +6874,7 @@ export interface InframonitoringtypesPostableNamespacesDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesNamespaceFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6835,7 +6901,7 @@ export interface InframonitoringtypesPostableNodesDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesNodeFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6862,7 +6928,7 @@ export interface InframonitoringtypesPostablePodsDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesPodFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6883,13 +6949,21 @@ export interface InframonitoringtypesPostablePodsDTO {
|
||||
start: number;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesStatefulSetFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesPostableStatefulSetsDTO {
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesStatefulSetFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
|
||||
@@ -88,9 +88,13 @@ func (m *module) getTopClusterGroups(
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableClusters,
|
||||
metadataMap map[string]map[string]string,
|
||||
podStatusCountsMap map[string]podStatusCounts,
|
||||
nodeConditionCountsMap map[string]nodeConditionCounts,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.ClusterNameAttrKey {
|
||||
// metadataMap is already status-filtered by the caller, so the name
|
||||
// branch needs no extra intersection.
|
||||
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.ClusterNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToClustersQueryNames[orderByKey]
|
||||
@@ -132,6 +136,17 @@ func (m *module) getTopClusterGroups(
|
||||
}
|
||||
|
||||
allMetricGroups := parseAndSortGroups(resp, rankingQueryName, req.GroupBy, req.OrderBy.Direction)
|
||||
// When filtering by a secondary status, intersect the ranked groups against
|
||||
// its keyset — an empty map (no match) must yield an empty page, not the full
|
||||
// set. Both filters compose as AND.
|
||||
if req.Filter != nil {
|
||||
if !req.Filter.FilterByPodStatus.IsZero() {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, podStatusCountsMap)
|
||||
}
|
||||
if !req.Filter.FilterByNodeReadiness.IsZero() {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, nodeConditionCountsMap)
|
||||
}
|
||||
}
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), nil
|
||||
}
|
||||
|
||||
@@ -142,5 +157,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)
|
||||
}
|
||||
|
||||
@@ -143,9 +143,12 @@ func (m *module) getTopContainerGroups(
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableContainers,
|
||||
metadataMap map[string]map[string]string,
|
||||
containerStatusCountsMap map[string]containerStatusCounts,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.ContainerNameAttrKey {
|
||||
// metadataMap is already status-filtered by the caller, so the name
|
||||
// branch needs no extra intersection.
|
||||
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.ContainerNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToContainersQueryNames[orderByKey]
|
||||
@@ -187,6 +190,11 @@ func (m *module) getTopContainerGroups(
|
||||
}
|
||||
|
||||
allMetricGroups := parseAndSortGroups(resp, rankingQueryName, req.GroupBy, req.OrderBy.Direction)
|
||||
// When filtering by status, intersect the ranked groups against the keyset —
|
||||
// an empty map (no container matched) must yield an empty page, not the full set.
|
||||
if req.Filter != nil && !req.Filter.FilterByContainerStatus.IsZero() {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, containerStatusCountsMap)
|
||||
}
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), nil
|
||||
}
|
||||
|
||||
@@ -197,7 +205,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
|
||||
@@ -213,6 +225,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 {
|
||||
@@ -238,13 +251,24 @@ 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
|
||||
}
|
||||
|
||||
// containerStatusFilterClause returns the outer WHERE clause (and its arg) that
|
||||
// restricts container_status to a single display status. valuer lowercases the
|
||||
// wire value while display_status is kubectl-cased, so we compare lower() on
|
||||
// both. Empty status returns no clause.
|
||||
func containerStatusFilterClause(filterByContainerStatus inframonitoringtypes.ContainerStatus) (string, []any) {
|
||||
if filterByContainerStatus.IsZero() {
|
||||
return "", nil
|
||||
}
|
||||
return " WHERE lower(display_status) = ? ", []any{filterByContainerStatus.StringValue()}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -269,8 +293,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 && filterByContainerStatus.IsZero()) {
|
||||
return map[string]containerStatusCounts{}, nil
|
||||
}
|
||||
|
||||
@@ -454,9 +481,12 @@ func (m *module) getPerGroupContainerStatusCounts(
|
||||
countGroupBy = append(countGroupBy, col)
|
||||
}
|
||||
countSelectCols = append(countSelectCols, statusCountCols...)
|
||||
// Push-down: keep only containers whose display status matches the requested one.
|
||||
statusWhereClause, statusWhereArgs := containerStatusFilterClause(filterByContainerStatus)
|
||||
countSQL := fmt.Sprintf(
|
||||
"SELECT %s FROM container_status GROUP BY %s",
|
||||
"SELECT %s FROM container_status%s GROUP BY %s",
|
||||
strings.Join(countSelectCols, ", "),
|
||||
statusWhereClause,
|
||||
strings.Join(countGroupBy, ", "),
|
||||
)
|
||||
|
||||
@@ -471,7 +501,7 @@ func (m *module) getPerGroupContainerStatusCounts(
|
||||
finalSQL := querybuilder.CombineCTEs(cteFragments) + countSQL
|
||||
finalArgs := querybuilder.PrependArgs([][]any{
|
||||
stateFpsArgs, containerStateArgs, reasonFpsArgs, reasonInnerArgs,
|
||||
}, nil)
|
||||
}, statusWhereArgs)
|
||||
|
||||
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, finalSQL, finalArgs...)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
)
|
||||
|
||||
func TestContainerStatusFilterClause(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
status inframonitoringtypes.ContainerStatus
|
||||
wantClause string
|
||||
wantArgs []any
|
||||
}{
|
||||
{
|
||||
name: "empty status yields no clause",
|
||||
status: inframonitoringtypes.ContainerStatus{},
|
||||
wantClause: "",
|
||||
wantArgs: nil,
|
||||
},
|
||||
{
|
||||
name: "running pushes lowercased arg",
|
||||
status: inframonitoringtypes.ContainerStatusRunning,
|
||||
wantClause: " WHERE lower(display_status) = ? ",
|
||||
wantArgs: []any{"running"},
|
||||
},
|
||||
{
|
||||
name: "crashloopbackoff pushes lowercased arg",
|
||||
status: inframonitoringtypes.ContainerStatusCrashLoopBackOff,
|
||||
wantClause: " WHERE lower(display_status) = ? ",
|
||||
wantArgs: []any{"crashloopbackoff"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotClause, gotArgs := containerStatusFilterClause(tt.status)
|
||||
if gotClause != tt.wantClause {
|
||||
t.Errorf("containerStatusFilterClause(%v) clause = %q, want %q", tt.status, gotClause, tt.wantClause)
|
||||
}
|
||||
if !reflect.DeepEqual(gotArgs, tt.wantArgs) {
|
||||
t.Errorf("containerStatusFilterClause(%v) args = %v, want %v", tt.status, gotArgs, tt.wantArgs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -94,9 +94,12 @@ func (m *module) getTopDaemonSetGroups(
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableDaemonSets,
|
||||
metadataMap map[string]map[string]string,
|
||||
podStatusCountsMap map[string]podStatusCounts,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.DaemonSetNameAttrKey {
|
||||
// metadataMap is already status-filtered by the caller, so the name
|
||||
// branch needs no extra intersection.
|
||||
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.DaemonSetNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToDaemonSetsQueryNames[orderByKey]
|
||||
@@ -138,6 +141,11 @@ func (m *module) getTopDaemonSetGroups(
|
||||
}
|
||||
|
||||
allMetricGroups := parseAndSortGroups(resp, rankingQueryName, req.GroupBy, req.OrderBy.Direction)
|
||||
// When filtering by status, intersect the ranked groups against the keyset —
|
||||
// an empty map (no group matched) must yield an empty page, not the full set.
|
||||
if req.Filter != nil && !req.Filter.FilterByPodStatus.IsZero() {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, podStatusCountsMap)
|
||||
}
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), nil
|
||||
}
|
||||
|
||||
@@ -148,5 +156,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)
|
||||
}
|
||||
|
||||
@@ -86,9 +86,12 @@ func (m *module) getTopDeploymentGroups(
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableDeployments,
|
||||
metadataMap map[string]map[string]string,
|
||||
podStatusCountsMap map[string]podStatusCounts,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.DeploymentNameAttrKey {
|
||||
// metadataMap is already status-filtered by the caller, so the name
|
||||
// branch needs no extra intersection.
|
||||
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.DeploymentNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToDeploymentsQueryNames[orderByKey]
|
||||
@@ -130,6 +133,11 @@ func (m *module) getTopDeploymentGroups(
|
||||
}
|
||||
|
||||
allMetricGroups := parseAndSortGroups(resp, rankingQueryName, req.GroupBy, req.OrderBy.Direction)
|
||||
// When filtering by status, intersect the ranked groups against the keyset —
|
||||
// an empty map (no group matched) must yield an empty page, not the full set.
|
||||
if req.Filter != nil && !req.Filter.FilterByPodStatus.IsZero() {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, podStatusCountsMap)
|
||||
}
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), nil
|
||||
}
|
||||
|
||||
@@ -140,5 +148,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(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
@@ -383,3 +384,81 @@ func TestCompositeKeyFromLabels(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if !reflect.DeepEqual(got, tt.expected) {
|
||||
t.Errorf("intersectMap(%v, keys=%v) = %v, want %v", tt.m, tt.keep, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
if !reflect.DeepEqual(gotKeys, tt.expected) {
|
||||
t.Errorf("intersectRankedGroups keys = %v, want %v", gotKeys, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,9 +94,12 @@ func (m *module) getTopJobGroups(
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableJobs,
|
||||
metadataMap map[string]map[string]string,
|
||||
podStatusCountsMap map[string]podStatusCounts,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.JobNameAttrKey {
|
||||
// metadataMap is already status-filtered by the caller, so the name
|
||||
// branch needs no extra intersection.
|
||||
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.JobNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToJobsQueryNames[orderByKey]
|
||||
@@ -138,6 +141,11 @@ func (m *module) getTopJobGroups(
|
||||
}
|
||||
|
||||
allMetricGroups := parseAndSortGroups(resp, rankingQueryName, req.GroupBy, req.OrderBy.Direction)
|
||||
// When filtering by status, intersect the ranked groups against the keyset —
|
||||
// an empty map (no group matched) must yield an empty page, not the full set.
|
||||
if req.Filter != nil && !req.Filter.FilterByPodStatus.IsZero() {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, podStatusCountsMap)
|
||||
}
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), nil
|
||||
}
|
||||
|
||||
@@ -148,5 +156,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)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -69,9 +69,12 @@ func (m *module) getTopNamespaceGroups(
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableNamespaces,
|
||||
metadataMap map[string]map[string]string,
|
||||
podStatusCountsMap map[string]podStatusCounts,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.NamespaceNameAttrKey {
|
||||
// metadataMap is already status-filtered by the caller, so the name
|
||||
// branch needs no extra intersection.
|
||||
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.NamespaceNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToNamespacesQueryNames[orderByKey]
|
||||
@@ -113,6 +116,11 @@ func (m *module) getTopNamespaceGroups(
|
||||
}
|
||||
|
||||
allMetricGroups := parseAndSortGroups(resp, rankingQueryName, req.GroupBy, req.OrderBy.Direction)
|
||||
// When filtering by status, intersect the ranked groups against the keyset —
|
||||
// an empty map (no group matched) must yield an empty page, not the full set.
|
||||
if req.Filter != nil && !req.Filter.FilterByPodStatus.IsZero() {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, podStatusCountsMap)
|
||||
}
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), nil
|
||||
}
|
||||
|
||||
@@ -123,5 +131,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)
|
||||
}
|
||||
|
||||
@@ -96,9 +96,13 @@ func (m *module) getTopNodeGroups(
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableNodes,
|
||||
metadataMap map[string]map[string]string,
|
||||
podStatusCountsMap map[string]podStatusCounts,
|
||||
nodeConditionCountsMap map[string]nodeConditionCounts,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.NodeNameAttrKey {
|
||||
// metadataMap is already status-filtered by the caller, so the name
|
||||
// branch needs no extra intersection.
|
||||
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.NodeNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToNodesQueryNames[orderByKey]
|
||||
@@ -140,6 +144,17 @@ func (m *module) getTopNodeGroups(
|
||||
}
|
||||
|
||||
allMetricGroups := parseAndSortGroups(resp, rankingQueryName, req.GroupBy, req.OrderBy.Direction)
|
||||
// When filtering by a secondary status, intersect the ranked groups against
|
||||
// its keyset — an empty map (no match) must yield an empty page, not the full
|
||||
// set. Both filters compose as AND.
|
||||
if req.Filter != nil {
|
||||
if !req.Filter.FilterByPodStatus.IsZero() {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, podStatusCountsMap)
|
||||
}
|
||||
if !req.Filter.FilterByNodeReadiness.IsZero() {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, nodeConditionCountsMap)
|
||||
}
|
||||
}
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), nil
|
||||
}
|
||||
|
||||
@@ -150,7 +165,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
|
||||
@@ -164,6 +183,21 @@ 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).
|
||||
// nodeReadinessFilterClause returns the outer WHERE clause (and its arg) that
|
||||
// restricts latest_condition_per_node to a single readiness. condition_value is
|
||||
// numeric (1=Ready, 0=NotReady), so we map the enum to its int. Empty readiness
|
||||
// returns no clause.
|
||||
func nodeReadinessFilterClause(filterByNodeReadiness inframonitoringtypes.NodeCondition) (string, []any) {
|
||||
if filterByNodeReadiness.IsZero() {
|
||||
return "", nil
|
||||
}
|
||||
v := inframonitoringtypes.NodeConditionNumNotReady
|
||||
if filterByNodeReadiness == inframonitoringtypes.NodeConditionReady {
|
||||
v = inframonitoringtypes.NodeConditionNumReady
|
||||
}
|
||||
return " WHERE condition_value = ? ", []any{v}
|
||||
}
|
||||
|
||||
func (m *module) getPerGroupNodeConditionCounts(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
@@ -171,8 +205,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 && filterByNodeReadiness.IsZero()) {
|
||||
return map[string]nodeConditionCounts{}, nil
|
||||
}
|
||||
|
||||
@@ -260,9 +297,12 @@ 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),
|
||||
)
|
||||
// Push-down: keep only nodes whose readiness matches the requested one.
|
||||
readinessWhereClause, readinessWhereArgs := nodeReadinessFilterClause(filterByNodeReadiness)
|
||||
countNodesPerConditionSQL := fmt.Sprintf(
|
||||
"SELECT %s FROM latest_condition_per_node GROUP BY %s",
|
||||
"SELECT %s FROM latest_condition_per_node%s GROUP BY %s",
|
||||
strings.Join(countNodesPerConditionSelectCols, ", "),
|
||||
readinessWhereClause,
|
||||
strings.Join(countNodesPerConditionGroupBy, ", "),
|
||||
)
|
||||
|
||||
@@ -272,7 +312,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}, readinessWhereArgs)
|
||||
|
||||
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, finalSQL, finalArgs...)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
)
|
||||
|
||||
func TestNodeReadinessFilterClause(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
readiness inframonitoringtypes.NodeCondition
|
||||
wantClause string
|
||||
wantArgs []any
|
||||
}{
|
||||
{
|
||||
name: "empty readiness yields no clause",
|
||||
readiness: inframonitoringtypes.NodeCondition{},
|
||||
wantClause: "",
|
||||
wantArgs: nil,
|
||||
},
|
||||
{
|
||||
name: "ready maps to 1",
|
||||
readiness: inframonitoringtypes.NodeConditionReady,
|
||||
wantClause: " WHERE condition_value = ? ",
|
||||
wantArgs: []any{inframonitoringtypes.NodeConditionNumReady},
|
||||
},
|
||||
{
|
||||
name: "not_ready maps to 0",
|
||||
readiness: inframonitoringtypes.NodeConditionNotReady,
|
||||
wantClause: " WHERE condition_value = ? ",
|
||||
wantArgs: []any{inframonitoringtypes.NodeConditionNumNotReady},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotClause, gotArgs := nodeReadinessFilterClause(tt.readiness)
|
||||
if gotClause != tt.wantClause {
|
||||
t.Errorf("nodeReadinessFilterClause(%v) clause = %q, want %q", tt.readiness, gotClause, tt.wantClause)
|
||||
}
|
||||
if !reflect.DeepEqual(gotArgs, tt.wantArgs) {
|
||||
t.Errorf("nodeReadinessFilterClause(%v) args = %v, want %v", tt.readiness, gotArgs, tt.wantArgs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -150,9 +150,12 @@ func (m *module) getTopPodGroups(
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostablePods,
|
||||
metadataMap map[string]map[string]string,
|
||||
podStatusCountsMap map[string]podStatusCounts,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.PodNameAttrKey {
|
||||
// metadataMap is already status-filtered by the caller, so the name
|
||||
// branch needs no extra intersection.
|
||||
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.PodNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToPodsQueryNames[orderByKey]
|
||||
@@ -194,6 +197,10 @@ func (m *module) getTopPodGroups(
|
||||
}
|
||||
|
||||
allMetricGroups := parseAndSortGroups(resp, rankingQueryName, req.GroupBy, req.OrderBy.Direction)
|
||||
// When filtering by status, intersect the rankedGroup
|
||||
if req.Filter != nil && !req.Filter.FilterByPodStatus.IsZero() {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, podStatusCountsMap)
|
||||
}
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), nil
|
||||
}
|
||||
|
||||
@@ -204,7 +211,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
|
||||
@@ -219,6 +230,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 {
|
||||
@@ -244,13 +256,24 @@ 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
|
||||
}
|
||||
|
||||
// podStatusFilterClause returns the outer WHERE clause (and its arg) that
|
||||
// restricts pod_status to a single display status. valuer lowercases the wire
|
||||
// value while display_status is kubectl-cased, so we compare lower() on both.
|
||||
// Empty status returns no clause.
|
||||
func podStatusFilterClause(filterByPodStatus inframonitoringtypes.PodStatus) (string, []any) {
|
||||
if filterByPodStatus.IsZero() {
|
||||
return "", nil
|
||||
}
|
||||
return " WHERE lower(display_status) = ? ", []any{filterByPodStatus.StringValue()}
|
||||
}
|
||||
|
||||
// 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).
|
||||
@@ -271,8 +294,10 @@ 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 && filterByPodStatus.IsZero()) {
|
||||
return map[string]podStatusCounts{}, nil
|
||||
}
|
||||
|
||||
@@ -508,9 +533,12 @@ func (m *module) getPerGroupPodStatusCounts(
|
||||
countGroupBy = append(countGroupBy, col)
|
||||
}
|
||||
countSelectCols = append(countSelectCols, statusCountCols...)
|
||||
// Push-down: keep only pods whose display status matches the requested one.
|
||||
statusWhereClause, statusWhereArgs := podStatusFilterClause(filterByPodStatus)
|
||||
countSQL := fmt.Sprintf(
|
||||
"SELECT %s FROM pod_status GROUP BY %s",
|
||||
"SELECT %s FROM pod_status%s GROUP BY %s",
|
||||
strings.Join(countSelectCols, ", "),
|
||||
statusWhereClause,
|
||||
strings.Join(countGroupBy, ", "),
|
||||
)
|
||||
|
||||
@@ -529,7 +557,7 @@ func (m *module) getPerGroupPodStatusCounts(
|
||||
phaseFpsArgs, phasePerPodArgs,
|
||||
podReasonFpsArgs, podReasonPerPodArgs,
|
||||
containerReasonFpsArgs, containerInnerArgs,
|
||||
}, nil)
|
||||
}, statusWhereArgs)
|
||||
|
||||
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, finalSQL, finalArgs...)
|
||||
if err != nil {
|
||||
|
||||
48
pkg/modules/inframonitoring/implinframonitoring/pods_test.go
Normal file
48
pkg/modules/inframonitoring/implinframonitoring/pods_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
)
|
||||
|
||||
func TestPodStatusFilterClause(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
status inframonitoringtypes.PodStatus
|
||||
wantClause string
|
||||
wantArgs []any
|
||||
}{
|
||||
{
|
||||
name: "empty status yields no clause",
|
||||
status: inframonitoringtypes.PodStatus{},
|
||||
wantClause: "",
|
||||
wantArgs: nil,
|
||||
},
|
||||
{
|
||||
name: "crashloopbackoff pushes lowercased arg",
|
||||
status: inframonitoringtypes.PodStatusCrashLoopBackOff,
|
||||
wantClause: " WHERE lower(display_status) = ? ",
|
||||
wantArgs: []any{"crashloopbackoff"},
|
||||
},
|
||||
{
|
||||
name: "running pushes lowercased arg",
|
||||
status: inframonitoringtypes.PodStatusRunning,
|
||||
wantClause: " WHERE lower(display_status) = ? ",
|
||||
wantArgs: []any{"running"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotClause, gotArgs := podStatusFilterClause(tt.status)
|
||||
if gotClause != tt.wantClause {
|
||||
t.Errorf("podStatusFilterClause(%v) clause = %q, want %q", tt.status, gotClause, tt.wantClause)
|
||||
}
|
||||
if !reflect.DeepEqual(gotArgs, tt.wantArgs) {
|
||||
t.Errorf("podStatusFilterClause(%v) args = %v, want %v", tt.status, gotArgs, tt.wantArgs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -86,9 +86,12 @@ func (m *module) getTopStatefulSetGroups(
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableStatefulSets,
|
||||
metadataMap map[string]map[string]string,
|
||||
podStatusCountsMap map[string]podStatusCounts,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.StatefulSetNameAttrKey {
|
||||
// metadataMap is already status-filtered by the caller, so the name
|
||||
// branch needs no extra intersection.
|
||||
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.StatefulSetNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToStatefulSetsQueryNames[orderByKey]
|
||||
@@ -130,6 +133,11 @@ func (m *module) getTopStatefulSetGroups(
|
||||
}
|
||||
|
||||
allMetricGroups := parseAndSortGroups(resp, rankingQueryName, req.GroupBy, req.OrderBy.Direction)
|
||||
// When filtering by status, intersect the ranked groups against the keyset —
|
||||
// an empty map (no group matched) must yield an empty page, not the full set.
|
||||
if req.Filter != nil && !req.Filter.FilterByPodStatus.IsZero() {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, podStatusCountsMap)
|
||||
}
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), nil
|
||||
}
|
||||
|
||||
@@ -140,5 +148,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)
|
||||
}
|
||||
|
||||
@@ -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 (see PodStatus) and node readiness (see
|
||||
// NodeCondition). 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,14 @@ func (req *PostableClusters) Validate() error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.Filter != nil && !req.Filter.FilterByPodStatus.IsZero() && !IsFilterablePodStatus(req.Filter.FilterByPodStatus) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", req.Filter.FilterByPodStatus)
|
||||
}
|
||||
|
||||
if req.Filter != nil && !req.Filter.FilterByNodeReadiness.IsZero() && !IsFilterableNodeCondition(req.Filter.FilterByNodeReadiness) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by node readiness: %s", req.Filter.FilterByNodeReadiness)
|
||||
}
|
||||
|
||||
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 (see ContainerStatus). 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,10 @@ func (req *PostableContainers) Validate() error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.Filter != nil && !req.Filter.FilterByContainerStatus.IsZero() && !IsFilterableContainerStatus(req.Filter.FilterByContainerStatus) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by container status: %s", req.Filter.FilterByContainerStatus)
|
||||
}
|
||||
|
||||
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")}
|
||||
)
|
||||
|
||||
// IsFilterableContainerStatus reports whether c is a concrete, user-filterable
|
||||
// container status: any Enum() member except the no_data sentinel.
|
||||
func IsFilterableContainerStatus(c ContainerStatus) 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 (see PodStatus). Empty FilterByPodStatus = 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,10 @@ func (req *PostableDaemonSets) Validate() error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.Filter != nil && !req.Filter.FilterByPodStatus.IsZero() && !IsFilterablePodStatus(req.Filter.FilterByPodStatus) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", req.Filter.FilterByPodStatus)
|
||||
}
|
||||
|
||||
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 (see PodStatus). Empty FilterByPodStatus = 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,10 @@ func (req *PostableDeployments) Validate() error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.Filter != nil && !req.Filter.FilterByPodStatus.IsZero() && !IsFilterablePodStatus(req.Filter.FilterByPodStatus) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", req.Filter.FilterByPodStatus)
|
||||
}
|
||||
|
||||
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 (see PodStatus). Empty FilterByPodStatus = 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,10 @@ func (req *PostableJobs) Validate() error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.Filter != nil && !req.Filter.FilterByPodStatus.IsZero() && !IsFilterablePodStatus(req.Filter.FilterByPodStatus) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", req.Filter.FilterByPodStatus)
|
||||
}
|
||||
|
||||
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 (see PodStatus). Empty FilterByPodStatus = 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,10 @@ func (req *PostableNamespaces) Validate() error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.Filter != nil && !req.Filter.FilterByPodStatus.IsZero() && !IsFilterablePodStatus(req.Filter.FilterByPodStatus) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", req.Filter.FilterByPodStatus)
|
||||
}
|
||||
|
||||
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 (see PodStatus) and node readiness (see
|
||||
// NodeCondition). 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,14 @@ func (req *PostableNodes) Validate() error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.Filter != nil && !req.Filter.FilterByPodStatus.IsZero() && !IsFilterablePodStatus(req.Filter.FilterByPodStatus) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", req.Filter.FilterByPodStatus)
|
||||
}
|
||||
|
||||
if req.Filter != nil && !req.Filter.FilterByNodeReadiness.IsZero() && !IsFilterableNodeCondition(req.Filter.FilterByNodeReadiness) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by node readiness: %s", req.Filter.FilterByNodeReadiness)
|
||||
}
|
||||
|
||||
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 {
|
||||
}
|
||||
}
|
||||
|
||||
// IsFilterableNodeCondition reports whether c is a concrete, user-filterable
|
||||
// node readiness: any Enum() member except the no_data sentinel.
|
||||
func IsFilterableNodeCondition(c NodeCondition) 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 (see PodStatus). Empty FilterByPodStatus = 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,10 @@ func (req *PostablePods) Validate() error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.Filter != nil && !req.Filter.FilterByPodStatus.IsZero() && !IsFilterablePodStatus(req.Filter.FilterByPodStatus) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", req.Filter.FilterByPodStatus)
|
||||
}
|
||||
|
||||
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 {
|
||||
}
|
||||
}
|
||||
|
||||
// IsFilterablePodStatus reports whether s is a concrete, user-filterable pod
|
||||
// status: any Enum() member except the no_data sentinel.
|
||||
func IsFilterablePodStatus(s PodStatus) 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 (see PodStatus). Empty FilterByPodStatus = 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,10 @@ func (req *PostableStatefulSets) Validate() error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.Filter != nil && !req.Filter.FilterByPodStatus.IsZero() && !IsFilterablePodStatus(req.Filter.FilterByPodStatus) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", req.Filter.FilterByPodStatus)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -10,7 +10,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
|
||||
from fixtures.time import parse_timestamp
|
||||
@@ -592,6 +596,11 @@ 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",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_pods_validation_errors(
|
||||
@@ -697,6 +706,42 @@ 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):
|
||||
# matching status keeps the pod; a mismatched status filters it out. Wire
|
||||
# value is case-insensitive (valuer lowercases).
|
||||
for variant in (expected_status, expected_status.upper()):
|
||||
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 plain Running -> a reliable mismatch.
|
||||
mismatch = "running" if expected_status != "running" else "pending"
|
||||
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": mismatch},
|
||||
},
|
||||
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={mismatch!r}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"pod_name,expected_restarts",
|
||||
@@ -877,6 +922,41 @@ 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 bucket is 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)
|
||||
|
||||
# A status absent from the group -> group dropped, 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),
|
||||
"limit": 50,
|
||||
"groupBy": [{"name": "k8s.namespace.name", "fieldDataType": "string", "fieldContext": "resource"}],
|
||||
"filter": {"filterByPodStatus": "oomKilled"},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert absent.status_code == HTTPStatus.OK, absent.text
|
||||
assert absent.json()["data"]["total"] == 0
|
||||
|
||||
|
||||
def test_pods_restarts_grouped_mode(
|
||||
signoz: types.SigNoz,
|
||||
@@ -966,3 +1046,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}"
|
||||
|
||||
@@ -10,6 +10,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
|
||||
|
||||
@@ -312,6 +313,111 @@ 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)
|
||||
|
||||
for fbps, expected in (
|
||||
("running", expected_status_counts(running=3)),
|
||||
("CrashLoopBackOff", expected_status_counts(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
|
||||
|
||||
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": "completed"},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert absent.status_code == HTTPStatus.OK, absent.text
|
||||
assert absent.json()["data"]["total"] == 0
|
||||
|
||||
# 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",
|
||||
[
|
||||
@@ -360,6 +466,39 @@ 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"
|
||||
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,
|
||||
@@ -451,6 +590,42 @@ 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}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"group_key,expected",
|
||||
@@ -682,6 +857,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(
|
||||
|
||||
@@ -10,6 +10,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
|
||||
|
||||
@@ -309,6 +310,63 @@ 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)
|
||||
|
||||
for fbps, expected in (
|
||||
("running", expected_status_counts(running=3)),
|
||||
("CrashLoopBackOff", expected_status_counts(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
|
||||
|
||||
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": "completed"},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert absent.status_code == HTTPStatus.OK, absent.text
|
||||
assert absent.json()["data"]["total"] == 0
|
||||
|
||||
|
||||
# Float record fields compared with tolerance; everything else compared with ==.
|
||||
_GROUPBY_FLOAT_FIELDS = {
|
||||
"namespaceCPU",
|
||||
@@ -597,6 +655,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(
|
||||
|
||||
@@ -363,6 +363,36 @@ 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}
|
||||
|
||||
|
||||
def test_clusters_pod_status_aggregation(
|
||||
signoz: types.SigNoz,
|
||||
@@ -404,6 +434,92 @@ 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
|
||||
# bucket populated; an absent status yields an empty page.
|
||||
for fbps, expected in (
|
||||
("running", expected_status_counts(running=3)),
|
||||
("CrashLoopBackOff", expected_status_counts(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
|
||||
|
||||
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": "completed"},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert absent.status_code == HTTPStatus.OK, absent.text
|
||||
assert absent.json()["data"]["total"] == 0
|
||||
|
||||
# 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",
|
||||
@@ -629,6 +745,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(
|
||||
|
||||
@@ -379,6 +379,42 @@ 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
|
||||
# bucket populated; an absent status yields an empty page.
|
||||
for fbps, expected in (
|
||||
("running", expected_status_counts(running=3)),
|
||||
("CrashLoopBackOff", expected_status_counts(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
|
||||
|
||||
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": "completed"},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert absent.status_code == HTTPStatus.OK, absent.text
|
||||
assert absent.json()["data"]["total"] == 0
|
||||
|
||||
|
||||
def test_deployments_desired_available_counts(
|
||||
signoz: types.SigNoz,
|
||||
@@ -785,6 +821,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(
|
||||
|
||||
@@ -10,6 +10,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
|
||||
|
||||
@@ -328,6 +329,60 @@ 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)
|
||||
|
||||
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": "pending"},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert absent.status_code == HTTPStatus.OK, absent.text
|
||||
assert absent.json()["data"]["total"] == 0
|
||||
|
||||
|
||||
# Float record fields compared with tolerance; everything else compared with ==.
|
||||
_GROUPBY_FLOAT_FIELDS = {
|
||||
@@ -657,6 +712,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(
|
||||
|
||||
@@ -10,6 +10,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
|
||||
|
||||
@@ -460,6 +461,60 @@ 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)
|
||||
|
||||
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": "pending"},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert absent.status_code == HTTPStatus.OK, absent.text
|
||||
assert absent.json()["data"]["total"] == 0
|
||||
|
||||
|
||||
# Float record fields compared with tolerance; everything else compared with ==.
|
||||
_GROUPBY_FLOAT_FIELDS = {
|
||||
@@ -796,6 +851,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(
|
||||
|
||||
@@ -10,6 +10,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
|
||||
|
||||
@@ -335,6 +336,60 @@ 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)
|
||||
|
||||
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": "pending"},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert absent.status_code == HTTPStatus.OK, absent.text
|
||||
assert absent.json()["data"]["total"] == 0
|
||||
|
||||
|
||||
# Float record fields compared with tolerance; everything else compared with ==.
|
||||
_GROUPBY_FLOAT_FIELDS = {
|
||||
@@ -665,6 +720,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(
|
||||
|
||||
Reference in New Issue
Block a user