Compare commits

...

16 Commits

Author SHA1 Message Date
nikhilmantri0902
e2714310f8 chore: added tests 2026-07-26 16:59:53 +05:30
nikhilmantri0902
51105d91d6 chore: updated sql 2026-07-26 16:50:00 +05:30
nikhilmantri0902
b2f0b8f3c6 chore: updated types 2026-07-26 16:44:59 +05:30
nikhilmantri0902
d089d2eec1 chore: added integration tests for node readiness 2026-07-26 16:27:07 +05:30
nikhilmantri0902
2a5d5c870c chore: updated map intersections 2026-07-26 15:56:18 +05:30
nikhilmantri0902
b46d1b5ada chore: sql update 2026-07-26 15:48:54 +05:30
nikhilmantri0902
575e91b6f2 chore: types update 2026-07-26 15:43:28 +05:30
nikhilmantri0902
8184cfbda8 chore: update integration tests for filterByPodStatus 2026-07-26 15:23:17 +05:30
nikhilmantri0902
f04023018f chore: updated workloads with filtering capability 2026-07-26 13:50:20 +05:30
nikhilmantri0902
b1dfeac5fe chore: added for clusters and namespaces 2026-07-26 13:41:33 +05:30
nikhilmantri0902
05d0db2029 chore: added setup for nodes 2026-07-26 13:30:57 +05:30
nikhilmantri0902
fc56590a7a chore: scaffolding for other entities 2026-07-26 13:17:34 +05:30
nikhilmantri0902
d59d50f6dd chore: updated the pod status comment 2026-07-26 12:47:12 +05:30
nikhilmantri0902
7aa5573cd7 chore: pushed filterByPodStatus 2026-07-25 21:11:16 +05:30
nikhilmantri0902
a5a28bae2a chore: added intersection helper functions 2026-07-25 18:28:20 +05:30
nikhilmantri0902
23e0ffba75 chore: scaffolding 2026-07-25 18:09:06 +05:30
40 changed files with 2129 additions and 296 deletions

View File

@@ -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:

View File

@@ -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
*/

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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)
}
})
}
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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(

View File

@@ -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)
}
})
}
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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)
}
})
}
}

View File

@@ -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 {

View 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)
}
})
}
}

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 (

View File

@@ -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)

View File

@@ -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 (

View File

@@ -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)

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}"

View File

@@ -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(

View File

@@ -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(

View File

@@ -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(

View File

@@ -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(

View File

@@ -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(

View File

@@ -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(

View File

@@ -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(