mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-04 12:10:43 +01:00
Compare commits
23 Commits
fix/dashbo
...
infraM/nod
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c353a25f60 | ||
|
|
a57452a522 | ||
|
|
b100ce8ef8 | ||
|
|
f0362409e7 | ||
|
|
8f13dc97d8 | ||
|
|
fed7a6b912 | ||
|
|
8b08c10986 | ||
|
|
6b09bf6ee2 | ||
|
|
9a1e190009 | ||
|
|
b2d6790c67 | ||
|
|
d089d2eec1 | ||
|
|
2a5d5c870c | ||
|
|
b46d1b5ada | ||
|
|
575e91b6f2 | ||
|
|
8184cfbda8 | ||
|
|
f04023018f | ||
|
|
b1dfeac5fe | ||
|
|
05d0db2029 | ||
|
|
fc56590a7a | ||
|
|
d59d50f6dd | ||
|
|
7aa5573cd7 | ||
|
|
a5a28bae2a | ||
|
|
23e0ffba75 |
@@ -4212,6 +4212,21 @@ components:
|
||||
- missingOptionalMetrics
|
||||
- missingRequiredAttributes
|
||||
type: object
|
||||
InframonitoringtypesClusterFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByNodeReadiness:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesNodeCondition'
|
||||
nullable: true
|
||||
type: array
|
||||
filterByPodStatus:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
InframonitoringtypesClusterRecord:
|
||||
properties:
|
||||
clusterCPU:
|
||||
@@ -4448,6 +4463,16 @@ components:
|
||||
- total
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesDaemonSetFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByPodStatus:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
InframonitoringtypesDaemonSetRecord:
|
||||
properties:
|
||||
currentNodes:
|
||||
@@ -4520,6 +4545,16 @@ components:
|
||||
- total
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesDeploymentFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByPodStatus:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
InframonitoringtypesDeploymentRecord:
|
||||
properties:
|
||||
availablePods:
|
||||
@@ -4661,6 +4696,16 @@ components:
|
||||
- total
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesJobFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByPodStatus:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
InframonitoringtypesJobRecord:
|
||||
properties:
|
||||
activePods:
|
||||
@@ -4784,6 +4829,16 @@ components:
|
||||
- message
|
||||
- documentationLink
|
||||
type: object
|
||||
InframonitoringtypesNamespaceFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByPodStatus:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
InframonitoringtypesNamespaceRecord:
|
||||
properties:
|
||||
counts:
|
||||
@@ -4865,6 +4920,21 @@ components:
|
||||
- ready
|
||||
- notReady
|
||||
type: object
|
||||
InframonitoringtypesNodeFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByNodeReadiness:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesNodeCondition'
|
||||
nullable: true
|
||||
type: array
|
||||
filterByPodStatus:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
InframonitoringtypesNodeRecord:
|
||||
properties:
|
||||
condition:
|
||||
@@ -4981,6 +5051,16 @@ components:
|
||||
- shutdown
|
||||
- unexpectedAdmissionError
|
||||
type: object
|
||||
InframonitoringtypesPodFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByPodStatus:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
InframonitoringtypesPodRecord:
|
||||
properties:
|
||||
meta:
|
||||
@@ -5080,7 +5160,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesClusterFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5132,7 +5212,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesDaemonSetFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5158,7 +5238,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesDeploymentFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5210,7 +5290,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesJobFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5236,7 +5316,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesNamespaceFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5262,7 +5342,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesNodeFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5288,7 +5368,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5314,7 +5394,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesStatefulSetFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5365,6 +5445,16 @@ components:
|
||||
- list
|
||||
- grouped_list
|
||||
type: string
|
||||
InframonitoringtypesStatefulSetFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByPodStatus:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
InframonitoringtypesStatefulSetRecord:
|
||||
properties:
|
||||
currentPods:
|
||||
|
||||
@@ -5635,6 +5635,47 @@ export interface InframonitoringtypesChecksDTO {
|
||||
type: InframonitoringtypesCheckTypeDTO;
|
||||
}
|
||||
|
||||
export enum InframonitoringtypesNodeConditionDTO {
|
||||
ready = 'ready',
|
||||
not_ready = 'not_ready',
|
||||
no_data = 'no_data',
|
||||
}
|
||||
export enum InframonitoringtypesPodStatusDTO {
|
||||
pending = 'pending',
|
||||
running = 'running',
|
||||
failed = 'failed',
|
||||
unknown = 'unknown',
|
||||
crashloopbackoff = 'crashloopbackoff',
|
||||
imagepullbackoff = 'imagepullbackoff',
|
||||
errimagepull = 'errimagepull',
|
||||
createcontainerconfigerror = 'createcontainerconfigerror',
|
||||
containercreating = 'containercreating',
|
||||
oomkilled = 'oomkilled',
|
||||
completed = 'completed',
|
||||
error = 'error',
|
||||
containercannotrun = 'containercannotrun',
|
||||
evicted = 'evicted',
|
||||
nodeaffinity = 'nodeaffinity',
|
||||
nodelost = 'nodelost',
|
||||
shutdown = 'shutdown',
|
||||
unexpectedadmissionerror = 'unexpectedadmissionerror',
|
||||
no_data = 'no_data',
|
||||
}
|
||||
export interface InframonitoringtypesClusterFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByNodeReadiness?: InframonitoringtypesNodeConditionDTO[] | null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesClusterRecordDTOCounts = {
|
||||
/**
|
||||
* @type integer
|
||||
@@ -6012,6 +6053,17 @@ export interface InframonitoringtypesContainersDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesDaemonSetFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesDaemonSetRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -6097,6 +6149,17 @@ export interface InframonitoringtypesDaemonSetsDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesDeploymentFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesDeploymentRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -6259,6 +6322,17 @@ export interface InframonitoringtypesHostsDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesJobFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesJobRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -6344,6 +6418,17 @@ export interface InframonitoringtypesJobsDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesNamespaceFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesNamespaceRecordDTOCounts = {
|
||||
/**
|
||||
* @type integer
|
||||
@@ -6420,11 +6505,21 @@ export interface InframonitoringtypesNamespacesDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export enum InframonitoringtypesNodeConditionDTO {
|
||||
ready = 'ready',
|
||||
not_ready = 'not_ready',
|
||||
no_data = 'no_data',
|
||||
export interface InframonitoringtypesNodeFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByNodeReadiness?: InframonitoringtypesNodeConditionDTO[] | null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesNodeRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -6486,6 +6581,17 @@ export interface InframonitoringtypesNodesDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesPodFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesPodRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -6496,27 +6602,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
|
||||
@@ -6593,7 +6678,7 @@ export interface InframonitoringtypesPostableClustersDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesClusterFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6647,7 +6732,7 @@ export interface InframonitoringtypesPostableDaemonSetsDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesDaemonSetFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6674,7 +6759,7 @@ export interface InframonitoringtypesPostableDeploymentsDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesDeploymentFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6728,7 +6813,7 @@ export interface InframonitoringtypesPostableJobsDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesJobFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6755,7 +6840,7 @@ export interface InframonitoringtypesPostableNamespacesDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesNamespaceFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6782,7 +6867,7 @@ export interface InframonitoringtypesPostableNodesDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesNodeFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6809,7 +6894,7 @@ export interface InframonitoringtypesPostablePodsDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesPodFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6830,13 +6915,24 @@ export interface InframonitoringtypesPostablePodsDTO {
|
||||
start: number;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesStatefulSetFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesPostableStatefulSetsDTO {
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesStatefulSetFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
|
||||
@@ -84,20 +84,38 @@ func buildClusterRecords(
|
||||
return records
|
||||
}
|
||||
|
||||
// getTopClusterGroupsAndMetadata concurrently fetches metadata + the ordering-metric
|
||||
// ranking (plus the full-scope pod-status / node-readiness keysets when filtering,
|
||||
// to intersect all).
|
||||
func (m *module) getTopClusterGroupsAndMetadata(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableClusters,
|
||||
) ([]map[string]string, map[string]map[string]string, error) {
|
||||
) ([]map[string]string, map[string]map[string]string, map[string]podStatusCounts, *qbtypes.QueryWarnData, map[string]nodeConditionCounts, error) {
|
||||
|
||||
var (
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
statusCounts map[string]podStatusCounts
|
||||
statusWarning *qbtypes.QueryWarnData
|
||||
nodeConditionCounts map[string]nodeConditionCounts
|
||||
filter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
filterByNodeReadiness []inframonitoringtypes.NodeCondition
|
||||
)
|
||||
|
||||
orderByKey = req.OrderBy.Key.Name
|
||||
|
||||
// When filtering by pod status / node readiness, resolve the full-scope
|
||||
// keyset(s) concurrently (pageGroups=nil spans all groups under the user
|
||||
// filter) to intersect metadata + ranked groups below. Filters compose as AND.
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
filterByNodeReadiness = req.Filter.FilterByNodeReadiness
|
||||
}
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -106,12 +124,37 @@ func (m *module) getTopClusterGroupsAndMetadata(
|
||||
return err
|
||||
})
|
||||
|
||||
if len(filterByPodStatus) != 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByPodStatus)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if len(filterByNodeReadiness) != 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
nodeConditionCounts, err = m.getPerGroupNodeConditionCounts(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByNodeReadiness)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if orderByKey == inframonitoringtypes.ClusterNameAttrKey {
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
// Secondary filter: keep only status/readiness-matching groups. A missing
|
||||
// metric yields an empty statusCounts, so this correctly empties the result
|
||||
// (the caller also surfaces the warning). Filters compose as AND.
|
||||
if len(filterByPodStatus) != 0 {
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
if len(filterByNodeReadiness) != 0 {
|
||||
metadataMap = intersectMap(metadataMap, nodeConditionCounts)
|
||||
}
|
||||
pageGroups := inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.ClusterNameAttrKey)
|
||||
return pageGroups, metadataMap, nil
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nodeConditionCounts, nil
|
||||
}
|
||||
|
||||
queryNamesForOrderBy := orderByToClustersQueryNames[orderByKey]
|
||||
@@ -157,10 +200,23 @@ func (m *module) getTopClusterGroupsAndMetadata(
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), metadataMap, nil
|
||||
// Secondary filter: intersect ranked groups + metadata with the status/readiness
|
||||
// keyset. A missing metric yields an empty keyset, correctly emptying the result
|
||||
// (the caller also surfaces the warning). Filters compose as AND.
|
||||
if len(filterByPodStatus) != 0 {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, statusCounts)
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
if len(filterByNodeReadiness) != 0 {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, nodeConditionCounts)
|
||||
metadataMap = intersectMap(metadataMap, nodeConditionCounts)
|
||||
}
|
||||
|
||||
pageGroups := paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit)
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nodeConditionCounts, nil
|
||||
}
|
||||
|
||||
func (m *module) getClustersTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableClusters) (map[string]map[string]string, error) {
|
||||
@@ -170,5 +226,9 @@ func (m *module) getClustersTableMetadata(ctx context.Context, orgID valuer.UUID
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, clustersTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
|
||||
var filter *qbtypes.Filter
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, clustersTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
|
||||
}
|
||||
|
||||
@@ -90,20 +90,34 @@ func buildDaemonSetRecords(
|
||||
return records
|
||||
}
|
||||
|
||||
// getTopDaemonSetGroupsAndMetadata concurrently fetches metadata + the ordering-metric
|
||||
// ranking (plus the full-scope pod-status keyset when filtering, to intersect both).
|
||||
func (m *module) getTopDaemonSetGroupsAndMetadata(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableDaemonSets,
|
||||
) ([]map[string]string, map[string]map[string]string, error) {
|
||||
) ([]map[string]string, map[string]map[string]string, map[string]podStatusCounts, *qbtypes.QueryWarnData, error) {
|
||||
|
||||
var (
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
statusCounts map[string]podStatusCounts
|
||||
statusWarning *qbtypes.QueryWarnData
|
||||
filter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
)
|
||||
|
||||
orderByKey = req.OrderBy.Key.Name
|
||||
|
||||
// When filtering by pod status, resolve the full-scope status keyset
|
||||
// concurrently (pageGroups=nil spans all groups under the user filter) so it
|
||||
// can intersect metadata + ranked groups below.
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
}
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -112,12 +126,26 @@ func (m *module) getTopDaemonSetGroupsAndMetadata(
|
||||
return err
|
||||
})
|
||||
|
||||
if len(filterByPodStatus) != 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByPodStatus)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if orderByKey == inframonitoringtypes.DaemonSetNameAttrKey {
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
// Secondary filter: keep only status-matching groups. A missing metric
|
||||
// yields an empty statusCounts, so this correctly empties the result
|
||||
// (the caller also surfaces the warning).
|
||||
if len(filterByPodStatus) != 0 {
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
pageGroups := inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.DaemonSetNameAttrKey)
|
||||
return pageGroups, metadataMap, nil
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nil
|
||||
}
|
||||
|
||||
queryNamesForOrderBy := orderByToDaemonSetsQueryNames[orderByKey]
|
||||
@@ -163,10 +191,19 @@ func (m *module) getTopDaemonSetGroupsAndMetadata(
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), metadataMap, nil
|
||||
// Secondary filter: intersect ranked groups + metadata with the status keyset.
|
||||
// A missing metric yields an empty statusCounts, correctly emptying the result
|
||||
// (the caller also surfaces the warning).
|
||||
if len(filterByPodStatus) != 0 {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, statusCounts)
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
|
||||
pageGroups := paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit)
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nil
|
||||
}
|
||||
|
||||
func (m *module) getDaemonSetsTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableDaemonSets) (map[string]map[string]string, error) {
|
||||
@@ -176,5 +213,9 @@ func (m *module) getDaemonSetsTableMetadata(ctx context.Context, orgID valuer.UU
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, daemonSetsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
|
||||
var filter *qbtypes.Filter
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, daemonSetsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
|
||||
}
|
||||
|
||||
@@ -82,20 +82,34 @@ func buildDeploymentRecords(
|
||||
return records
|
||||
}
|
||||
|
||||
// getTopDeploymentGroupsAndMetadata concurrently fetches metadata + the ordering-metric
|
||||
// ranking (plus the full-scope pod-status keyset when filtering, to intersect both).
|
||||
func (m *module) getTopDeploymentGroupsAndMetadata(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableDeployments,
|
||||
) ([]map[string]string, map[string]map[string]string, error) {
|
||||
) ([]map[string]string, map[string]map[string]string, map[string]podStatusCounts, *qbtypes.QueryWarnData, error) {
|
||||
|
||||
var (
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
statusCounts map[string]podStatusCounts
|
||||
statusWarning *qbtypes.QueryWarnData
|
||||
filter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
)
|
||||
|
||||
orderByKey = req.OrderBy.Key.Name
|
||||
|
||||
// When filtering by pod status, resolve the full-scope status keyset
|
||||
// concurrently (pageGroups=nil spans all groups under the user filter) so it
|
||||
// can intersect metadata + ranked groups below.
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
}
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -104,12 +118,26 @@ func (m *module) getTopDeploymentGroupsAndMetadata(
|
||||
return err
|
||||
})
|
||||
|
||||
if len(filterByPodStatus) != 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByPodStatus)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if orderByKey == inframonitoringtypes.DeploymentNameAttrKey {
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
// Secondary filter: keep only status-matching groups. A missing metric
|
||||
// yields an empty statusCounts, so this correctly empties the result
|
||||
// (the caller also surfaces the warning).
|
||||
if len(filterByPodStatus) != 0 {
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
pageGroups := inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.DeploymentNameAttrKey)
|
||||
return pageGroups, metadataMap, nil
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nil
|
||||
}
|
||||
|
||||
queryNamesForOrderBy := orderByToDeploymentsQueryNames[orderByKey]
|
||||
@@ -155,10 +183,19 @@ func (m *module) getTopDeploymentGroupsAndMetadata(
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), metadataMap, nil
|
||||
// Secondary filter: intersect ranked groups + metadata with the status keyset.
|
||||
// A missing metric yields an empty statusCounts, correctly emptying the result
|
||||
// (the caller also surfaces the warning).
|
||||
if len(filterByPodStatus) != 0 {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, statusCounts)
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
|
||||
pageGroups := paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit)
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nil
|
||||
}
|
||||
|
||||
func (m *module) getDeploymentsTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableDeployments) (map[string]map[string]string, error) {
|
||||
@@ -168,5 +205,9 @@ func (m *module) getDeploymentsTableMetadata(ctx context.Context, orgID valuer.U
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, deploymentsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
|
||||
var filter *qbtypes.Filter
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, deploymentsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
|
||||
}
|
||||
|
||||
@@ -63,6 +63,33 @@ func compositeKeyFromLabels(labels map[string]string, groupBy []qbtypes.GroupByK
|
||||
return compositeKeyFromList(parts)
|
||||
}
|
||||
|
||||
// intersectMap returns the entries of m whose key is present in keep (a new
|
||||
// map). keep's value type is irrelevant — only its keys are read — so a
|
||||
// per-group counts map (already filtered by the SQL push-down) can be passed
|
||||
// directly. Used to trim metadataMap to the status-matching groups.
|
||||
func intersectMap[V any, K any](m map[string]V, keep map[string]K) map[string]V {
|
||||
out := make(map[string]V, len(m))
|
||||
for k, v := range m {
|
||||
if _, ok := keep[k]; ok {
|
||||
out[k] = v
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// intersectRankedGroups returns the ranked groups whose compositeKey is present
|
||||
// in keep, preserving order. Keeps status-unmatched groups out of the ranked
|
||||
// page slots.
|
||||
func intersectRankedGroups[K any](groups []rankedGroup, keep map[string]K) []rankedGroup {
|
||||
out := make([]rankedGroup, 0, len(groups))
|
||||
for _, g := range groups {
|
||||
if _, ok := keep[g.compositeKey]; ok {
|
||||
out = append(out, g)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// parseAndSortGroups extracts group label maps from a ScalarData response and
|
||||
// sorts them by the ranking query's aggregation value.
|
||||
func parseAndSortGroups(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,20 +90,34 @@ func buildJobRecords(
|
||||
return records
|
||||
}
|
||||
|
||||
// getTopJobGroupsAndMetadata concurrently fetches metadata + the ordering-metric
|
||||
// ranking (plus the full-scope pod-status keyset when filtering, to intersect both).
|
||||
func (m *module) getTopJobGroupsAndMetadata(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableJobs,
|
||||
) ([]map[string]string, map[string]map[string]string, error) {
|
||||
) ([]map[string]string, map[string]map[string]string, map[string]podStatusCounts, *qbtypes.QueryWarnData, error) {
|
||||
|
||||
var (
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
statusCounts map[string]podStatusCounts
|
||||
statusWarning *qbtypes.QueryWarnData
|
||||
filter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
)
|
||||
|
||||
orderByKey = req.OrderBy.Key.Name
|
||||
|
||||
// When filtering by pod status, resolve the full-scope status keyset
|
||||
// concurrently (pageGroups=nil spans all groups under the user filter) so it
|
||||
// can intersect metadata + ranked groups below.
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
}
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -112,12 +126,26 @@ func (m *module) getTopJobGroupsAndMetadata(
|
||||
return err
|
||||
})
|
||||
|
||||
if len(filterByPodStatus) != 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByPodStatus)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if orderByKey == inframonitoringtypes.JobNameAttrKey {
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
// Secondary filter: keep only status-matching groups. A missing metric
|
||||
// yields an empty statusCounts, so this correctly empties the result
|
||||
// (the caller also surfaces the warning).
|
||||
if len(filterByPodStatus) != 0 {
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
pageGroups := inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.JobNameAttrKey)
|
||||
return pageGroups, metadataMap, nil
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nil
|
||||
}
|
||||
|
||||
queryNamesForOrderBy := orderByToJobsQueryNames[orderByKey]
|
||||
@@ -163,10 +191,19 @@ func (m *module) getTopJobGroupsAndMetadata(
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), metadataMap, nil
|
||||
// Secondary filter: intersect ranked groups + metadata with the status keyset.
|
||||
// A missing metric yields an empty statusCounts, correctly emptying the result
|
||||
// (the caller also surfaces the warning).
|
||||
if len(filterByPodStatus) != 0 {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, statusCounts)
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
|
||||
pageGroups := paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit)
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nil
|
||||
}
|
||||
|
||||
func (m *module) getJobsTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableJobs) (map[string]map[string]string, error) {
|
||||
@@ -176,5 +213,9 @@ func (m *module) getJobsTableMetadata(ctx context.Context, orgID valuer.UUID, re
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, jobsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
|
||||
var filter *qbtypes.Filter
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, jobsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
|
||||
}
|
||||
|
||||
@@ -286,11 +286,36 @@ func (m *module) ListPods(ctx context.Context, orgID valuer.UUID, req *inframoni
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
pageGroups, metadataMap, err := m.getTopPodGroupsAndMetadata(ctx, orgID, req)
|
||||
var (
|
||||
filterExpr string
|
||||
podFilter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
restartCounts map[string]int64
|
||||
)
|
||||
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
podFilter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
}
|
||||
|
||||
// getTopPodGroupsAndMetadata fetches metadata + ranking (+ full-scope pod
|
||||
// status when filtering) concurrently, intersecting metadata/ranked groups
|
||||
// against the status keyset. It returns the keyset + its warning.
|
||||
pageGroups, metadataMap, statusCounts, statusWarning, err := m.getTopPodGroupsAndMetadata(ctx, orgID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Required metric missing while filtering: surface the warning + empty result.
|
||||
if len(filterByPodStatus) != 0 && statusWarning != nil {
|
||||
resp.Warning = statusWarning
|
||||
resp.Records = []inframonitoringtypes.PodRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.Total = len(metadataMap)
|
||||
|
||||
if len(pageGroups) == 0 {
|
||||
@@ -298,20 +323,8 @@ func (m *module) ListPods(ctx context.Context, orgID valuer.UUID, req *inframoni
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
filterExpr := ""
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newPodsTableListQuery())
|
||||
|
||||
var (
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
statusCounts map[string]podStatusCounts
|
||||
statusWarning *qbtypes.QueryWarnData
|
||||
restartCounts map[string]int64
|
||||
)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -321,14 +334,18 @@ func (m *module) ListPods(ctx context.Context, orgID valuer.UUID, req *inframoni
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
restartCounts, err = m.getPerGroupPodRestartCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
restartCounts, err = m.getPerGroupPodRestartCounts(gCtx, orgID, req.Start, req.End, podFilter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
// When filtering, statusCounts already holds the full-scope map (a superset
|
||||
// of the page); otherwise compute it page-scoped here.
|
||||
if len(filterByPodStatus) == 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, podFilter, req.GroupBy, pageGroups, nil)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
@@ -478,11 +495,37 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
pageGroups, metadataMap, err := m.getTopNodeGroupsAndMetadata(ctx, orgID, req)
|
||||
var (
|
||||
filterExpr string
|
||||
nodeFilter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
filterByNodeReadiness []inframonitoringtypes.NodeCondition
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
)
|
||||
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
nodeFilter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
filterByNodeReadiness = req.Filter.FilterByNodeReadiness
|
||||
}
|
||||
|
||||
// getTopNodeGroupsAndMetadata fetches metadata + ranking (+ full-scope pod
|
||||
// status / node readiness when filtering) concurrently, intersecting
|
||||
// metadata/ranked groups against the keysets. It returns the keysets + warning.
|
||||
pageGroups, metadataMap, podStatusCounts, podStatusWarning, nodeConditionCounts, err := m.getTopNodeGroupsAndMetadata(ctx, orgID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Required metric missing while filtering: surface the warning + empty result.
|
||||
if len(filterByPodStatus) != 0 && podStatusWarning != nil {
|
||||
resp.Warning = podStatusWarning
|
||||
resp.Records = []inframonitoringtypes.NodeRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.Total = len(metadataMap)
|
||||
|
||||
if len(pageGroups) == 0 {
|
||||
@@ -490,20 +533,8 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
filterExpr := ""
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newNodesTableListQuery())
|
||||
|
||||
var (
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
nodeConditionCounts map[string]nodeConditionCounts
|
||||
podStatusCounts map[string]podStatusCounts
|
||||
podStatusWarning *qbtypes.QueryWarnData
|
||||
)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -511,16 +542,24 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
nodeConditionCounts, err = m.getPerGroupNodeConditionCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
// When filtering by readiness, nodeConditionCounts already holds the full-scope
|
||||
// map (a superset of the page); otherwise compute it page-scoped here.
|
||||
if len(filterByNodeReadiness) == 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
nodeConditionCounts, err = m.getPerGroupNodeConditionCounts(gCtx, orgID, req.Start, req.End, nodeFilter, req.GroupBy, pageGroups, nil)
|
||||
return err
|
||||
})
|
||||
}
|
||||
// When filtering by pod status, podStatusCounts already holds the full-scope
|
||||
// map; otherwise compute it page-scoped here.
|
||||
if len(filterByPodStatus) == 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, nodeFilter, req.GroupBy, pageGroups, nil)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
@@ -571,11 +610,36 @@ func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
pageGroups, metadataMap, err := m.getTopNamespaceGroupsAndMetadata(ctx, orgID, req)
|
||||
var (
|
||||
filterExpr string
|
||||
namespaceFilter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
resourceCounts map[string]map[string]int64
|
||||
)
|
||||
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
namespaceFilter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
}
|
||||
|
||||
// getTopNamespaceGroupsAndMetadata fetches metadata + ranking (+ full-scope pod
|
||||
// status when filtering) concurrently, intersecting metadata/ranked groups
|
||||
// against the status keyset. It returns the keyset + its warning.
|
||||
pageGroups, metadataMap, podStatusCounts, podStatusWarning, err := m.getTopNamespaceGroupsAndMetadata(ctx, orgID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Required metric missing while filtering: surface the warning + empty result.
|
||||
if len(filterByPodStatus) != 0 && podStatusWarning != nil {
|
||||
resp.Warning = podStatusWarning
|
||||
resp.Records = []inframonitoringtypes.NamespaceRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.Total = len(metadataMap)
|
||||
|
||||
if len(pageGroups) == 0 {
|
||||
@@ -583,20 +647,8 @@ func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
filterExpr := ""
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newNamespacesTableListQuery())
|
||||
|
||||
var (
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
podStatusCounts map[string]podStatusCounts
|
||||
podStatusWarning *qbtypes.QueryWarnData
|
||||
resourceCounts map[string]map[string]int64
|
||||
)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -606,14 +658,18 @@ func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
resourceCounts, err = m.getPerGroupDistinctCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups, namespaceCountAttrKeys, namespacesMetricNamesListForCounts)
|
||||
resourceCounts, err = m.getPerGroupDistinctCounts(gCtx, orgID, req.Start, req.End, namespaceFilter, req.GroupBy, pageGroups, namespaceCountAttrKeys, namespacesMetricNamesListForCounts)
|
||||
return err
|
||||
})
|
||||
// When filtering, podStatusCounts already holds the full-scope map (a superset
|
||||
// of the page); otherwise compute it page-scoped here.
|
||||
if len(filterByPodStatus) == 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, namespaceFilter, req.GroupBy, pageGroups, nil)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
@@ -663,11 +719,39 @@ func (m *module) ListClusters(ctx context.Context, orgID valuer.UUID, req *infra
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
pageGroups, metadataMap, err := m.getTopClusterGroupsAndMetadata(ctx, orgID, req)
|
||||
var (
|
||||
filterExpr string
|
||||
clusterFilter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
filterByNodeReadiness []inframonitoringtypes.NodeCondition
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
nodeConditionCountsMap map[string]nodeConditionCounts
|
||||
resourceCounts map[string]map[string]int64
|
||||
)
|
||||
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
clusterFilter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
filterByNodeReadiness = req.Filter.FilterByNodeReadiness
|
||||
}
|
||||
|
||||
// getTopClusterGroupsAndMetadata fetches metadata + ranking (+ full-scope pod
|
||||
// status / node readiness when filtering) concurrently, intersecting
|
||||
// metadata/ranked groups against the keysets. It returns the keysets + warning.
|
||||
pageGroups, metadataMap, podStatusCounts, podStatusWarning, nodeConditionCountsMap, err := m.getTopClusterGroupsAndMetadata(ctx, orgID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Required metric missing while filtering: surface the warning + empty result.
|
||||
if len(filterByPodStatus) != 0 && podStatusWarning != nil {
|
||||
resp.Warning = podStatusWarning
|
||||
resp.Records = []inframonitoringtypes.ClusterRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.Total = len(metadataMap)
|
||||
|
||||
if len(pageGroups) == 0 {
|
||||
@@ -675,23 +759,8 @@ func (m *module) ListClusters(ctx context.Context, orgID valuer.UUID, req *infra
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
filterExpr := ""
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newClustersTableListQuery())
|
||||
|
||||
// With default groupBy [k8s.cluster.name], counts are bucketed per cluster;
|
||||
// with a custom groupBy, they aggregate across clusters in that group.
|
||||
var (
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
nodeConditionCountsMap map[string]nodeConditionCounts
|
||||
podStatusCounts map[string]podStatusCounts
|
||||
podStatusWarning *qbtypes.QueryWarnData
|
||||
resourceCounts map[string]map[string]int64
|
||||
)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -699,21 +768,29 @@ func (m *module) ListClusters(ctx context.Context, orgID valuer.UUID, req *infra
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
// When filtering by readiness, nodeConditionCountsMap already holds the
|
||||
// full-scope map (a superset of the page); otherwise compute it page-scoped here.
|
||||
if len(filterByNodeReadiness) == 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
nodeConditionCountsMap, err = m.getPerGroupNodeConditionCounts(gCtx, orgID, req.Start, req.End, clusterFilter, req.GroupBy, pageGroups, nil)
|
||||
return err
|
||||
})
|
||||
}
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
nodeConditionCountsMap, err = m.getPerGroupNodeConditionCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
resourceCounts, err = m.getPerGroupDistinctCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups, clusterCountAttrKeys, clusterMetricNamesListForCounts)
|
||||
resourceCounts, err = m.getPerGroupDistinctCounts(gCtx, orgID, req.Start, req.End, clusterFilter, req.GroupBy, pageGroups, clusterCountAttrKeys, clusterMetricNamesListForCounts)
|
||||
return err
|
||||
})
|
||||
// When filtering by pod status, podStatusCounts already holds the full-scope
|
||||
// map; otherwise compute it page-scoped here.
|
||||
if len(filterByPodStatus) == 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, clusterFilter, req.GroupBy, pageGroups, nil)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
@@ -827,7 +904,7 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
|
||||
|
||||
// Bake the deployments base filter into req.Filter so all downstream helpers pick it up.
|
||||
if req.Filter == nil {
|
||||
req.Filter = &qbtypes.Filter{}
|
||||
req.Filter = &inframonitoringtypes.DeploymentFilter{}
|
||||
}
|
||||
req.Filter.Expression = mergeFilterExpressions(deploymentsBaseFilterExpr, req.Filter.Expression)
|
||||
|
||||
@@ -842,11 +919,35 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
pageGroups, metadataMap, err := m.getTopDeploymentGroupsAndMetadata(ctx, orgID, req)
|
||||
var (
|
||||
filterExpr string
|
||||
deploymentFilter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
)
|
||||
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
deploymentFilter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
}
|
||||
|
||||
// getTopDeploymentGroupsAndMetadata fetches metadata + ranking (+ full-scope pod
|
||||
// status when filtering) concurrently, intersecting metadata/ranked groups
|
||||
// against the status keyset. It returns the keyset + its warning.
|
||||
pageGroups, metadataMap, podStatusCounts, podStatusWarning, err := m.getTopDeploymentGroupsAndMetadata(ctx, orgID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Required metric missing while filtering: surface the warning + empty result.
|
||||
if len(filterByPodStatus) != 0 && podStatusWarning != nil {
|
||||
resp.Warning = podStatusWarning
|
||||
resp.Records = []inframonitoringtypes.DeploymentRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.Total = len(metadataMap)
|
||||
|
||||
if len(pageGroups) == 0 {
|
||||
@@ -854,19 +955,8 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
filterExpr := ""
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newDeploymentsTableListQuery())
|
||||
|
||||
var (
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
podStatusCounts map[string]podStatusCounts
|
||||
podStatusWarning *qbtypes.QueryWarnData
|
||||
)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -874,11 +964,15 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
// When filtering, podStatusCounts already holds the full-scope map (a superset
|
||||
// of the page); otherwise compute it page-scoped here.
|
||||
if len(filterByPodStatus) == 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, deploymentFilter, req.GroupBy, pageGroups, nil)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
@@ -919,7 +1013,7 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
|
||||
|
||||
// Bake the workload base filter into req.Filter so all downstream helpers pick it up.
|
||||
if req.Filter == nil {
|
||||
req.Filter = &qbtypes.Filter{}
|
||||
req.Filter = &inframonitoringtypes.StatefulSetFilter{}
|
||||
}
|
||||
req.Filter.Expression = mergeFilterExpressions(statefulSetsBaseFilterExpr, req.Filter.Expression)
|
||||
|
||||
@@ -934,11 +1028,35 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
pageGroups, metadataMap, err := m.getTopStatefulSetGroupsAndMetadata(ctx, orgID, req)
|
||||
var (
|
||||
filterExpr string
|
||||
statefulSetFilter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
)
|
||||
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
statefulSetFilter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
}
|
||||
|
||||
// getTopStatefulSetGroupsAndMetadata fetches metadata + ranking (+ full-scope pod
|
||||
// status when filtering) concurrently, intersecting metadata/ranked groups
|
||||
// against the status keyset. It returns the keyset + its warning.
|
||||
pageGroups, metadataMap, podStatusCounts, podStatusWarning, err := m.getTopStatefulSetGroupsAndMetadata(ctx, orgID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Required metric missing while filtering: surface the warning + empty result.
|
||||
if len(filterByPodStatus) != 0 && podStatusWarning != nil {
|
||||
resp.Warning = podStatusWarning
|
||||
resp.Records = []inframonitoringtypes.StatefulSetRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.Total = len(metadataMap)
|
||||
|
||||
if len(pageGroups) == 0 {
|
||||
@@ -946,21 +1064,8 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
filterExpr := ""
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newStatefulSetsTableListQuery())
|
||||
|
||||
// Pods owned by a StatefulSet carry k8s.statefulset.name as a resource attribute,
|
||||
// so default-groupBy gives per-statefulset status counts automatically.
|
||||
var (
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
podStatusCounts map[string]podStatusCounts
|
||||
podStatusWarning *qbtypes.QueryWarnData
|
||||
)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -968,11 +1073,15 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
// When filtering, podStatusCounts already holds the full-scope map (a superset
|
||||
// of the page); otherwise compute it page-scoped here.
|
||||
if len(filterByPodStatus) == 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, statefulSetFilter, req.GroupBy, pageGroups, nil)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
@@ -1013,7 +1122,7 @@ func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframoni
|
||||
|
||||
// Bake the jobs base filter into req.Filter so all downstream helpers pick it up.
|
||||
if req.Filter == nil {
|
||||
req.Filter = &qbtypes.Filter{}
|
||||
req.Filter = &inframonitoringtypes.JobFilter{}
|
||||
}
|
||||
req.Filter.Expression = mergeFilterExpressions(jobsBaseFilterExpr, req.Filter.Expression)
|
||||
|
||||
@@ -1028,11 +1137,35 @@ func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframoni
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
pageGroups, metadataMap, err := m.getTopJobGroupsAndMetadata(ctx, orgID, req)
|
||||
var (
|
||||
filterExpr string
|
||||
jobFilter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
)
|
||||
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
jobFilter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
}
|
||||
|
||||
// getTopJobGroupsAndMetadata fetches metadata + ranking (+ full-scope pod
|
||||
// status when filtering) concurrently, intersecting metadata/ranked groups
|
||||
// against the status keyset. It returns the keyset + its warning.
|
||||
pageGroups, metadataMap, podStatusCounts, podStatusWarning, err := m.getTopJobGroupsAndMetadata(ctx, orgID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Required metric missing while filtering: surface the warning + empty result.
|
||||
if len(filterByPodStatus) != 0 && podStatusWarning != nil {
|
||||
resp.Warning = podStatusWarning
|
||||
resp.Records = []inframonitoringtypes.JobRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.Total = len(metadataMap)
|
||||
|
||||
if len(pageGroups) == 0 {
|
||||
@@ -1040,21 +1173,8 @@ func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframoni
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
filterExpr := ""
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newJobsTableListQuery())
|
||||
|
||||
// Pods owned by a Job carry k8s.job.name as a resource attribute, so default-groupBy
|
||||
// gives per-job status counts automatically.
|
||||
var (
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
podStatusCounts map[string]podStatusCounts
|
||||
podStatusWarning *qbtypes.QueryWarnData
|
||||
)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -1062,11 +1182,15 @@ func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframoni
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
// When filtering, podStatusCounts already holds the full-scope map (a superset
|
||||
// of the page); otherwise compute it page-scoped here.
|
||||
if len(filterByPodStatus) == 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, jobFilter, req.GroupBy, pageGroups, nil)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
@@ -1107,7 +1231,7 @@ func (m *module) ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
|
||||
// Bake the workload base filter into req.Filter so all downstream helpers pick it up.
|
||||
if req.Filter == nil {
|
||||
req.Filter = &qbtypes.Filter{}
|
||||
req.Filter = &inframonitoringtypes.DaemonSetFilter{}
|
||||
}
|
||||
req.Filter.Expression = mergeFilterExpressions(daemonSetsBaseFilterExpr, req.Filter.Expression)
|
||||
|
||||
@@ -1122,11 +1246,35 @@ func (m *module) ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
pageGroups, metadataMap, err := m.getTopDaemonSetGroupsAndMetadata(ctx, orgID, req)
|
||||
var (
|
||||
filterExpr string
|
||||
daemonSetFilter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
)
|
||||
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
daemonSetFilter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
}
|
||||
|
||||
// getTopDaemonSetGroupsAndMetadata fetches metadata + ranking (+ full-scope pod
|
||||
// status when filtering) concurrently, intersecting metadata/ranked groups
|
||||
// against the status keyset. It returns the keyset + its warning.
|
||||
pageGroups, metadataMap, podStatusCounts, podStatusWarning, err := m.getTopDaemonSetGroupsAndMetadata(ctx, orgID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Required metric missing while filtering: surface the warning + empty result.
|
||||
if len(filterByPodStatus) != 0 && podStatusWarning != nil {
|
||||
resp.Warning = podStatusWarning
|
||||
resp.Records = []inframonitoringtypes.DaemonSetRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.Total = len(metadataMap)
|
||||
|
||||
if len(pageGroups) == 0 {
|
||||
@@ -1134,21 +1282,8 @@ func (m *module) ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
filterExpr := ""
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newDaemonSetsTableListQuery())
|
||||
|
||||
// Pods owned by a DaemonSet carry k8s.daemonset.name as a resource attribute,
|
||||
// so default-groupBy gives per-daemonset status counts automatically.
|
||||
var (
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
podStatusCounts map[string]podStatusCounts
|
||||
podStatusWarning *qbtypes.QueryWarnData
|
||||
)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -1156,11 +1291,15 @@ func (m *module) ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
// When filtering, podStatusCounts already holds the full-scope map (a superset
|
||||
// of the page); otherwise compute it page-scoped here.
|
||||
if len(filterByPodStatus) == 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, daemonSetFilter, req.GroupBy, pageGroups, nil)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -65,20 +65,34 @@ func buildNamespaceRecords(
|
||||
return records
|
||||
}
|
||||
|
||||
// getTopNamespaceGroupsAndMetadata concurrently fetches metadata + the ordering-metric
|
||||
// ranking (plus the full-scope pod-status keyset when filtering, to intersect both).
|
||||
func (m *module) getTopNamespaceGroupsAndMetadata(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableNamespaces,
|
||||
) ([]map[string]string, map[string]map[string]string, error) {
|
||||
) ([]map[string]string, map[string]map[string]string, map[string]podStatusCounts, *qbtypes.QueryWarnData, error) {
|
||||
|
||||
var (
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
statusCounts map[string]podStatusCounts
|
||||
statusWarning *qbtypes.QueryWarnData
|
||||
filter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
)
|
||||
|
||||
orderByKey = req.OrderBy.Key.Name
|
||||
|
||||
// When filtering by pod status, resolve the full-scope status keyset
|
||||
// concurrently (pageGroups=nil spans all groups under the user filter) so it
|
||||
// can intersect metadata + ranked groups below.
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
}
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -87,12 +101,26 @@ func (m *module) getTopNamespaceGroupsAndMetadata(
|
||||
return err
|
||||
})
|
||||
|
||||
if len(filterByPodStatus) != 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByPodStatus)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if orderByKey == inframonitoringtypes.NamespaceNameAttrKey {
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
// Secondary filter: keep only status-matching groups. A missing metric
|
||||
// yields an empty statusCounts, so this correctly empties the result
|
||||
// (the caller also surfaces the warning).
|
||||
if len(filterByPodStatus) != 0 {
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
pageGroups := inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.NamespaceNameAttrKey)
|
||||
return pageGroups, metadataMap, nil
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nil
|
||||
}
|
||||
|
||||
queryNamesForOrderBy := orderByToNamespacesQueryNames[orderByKey]
|
||||
@@ -138,10 +166,19 @@ func (m *module) getTopNamespaceGroupsAndMetadata(
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), metadataMap, nil
|
||||
// Secondary filter: intersect ranked groups + metadata with the status keyset.
|
||||
// A missing metric yields an empty statusCounts, correctly emptying the result
|
||||
// (the caller also surfaces the warning).
|
||||
if len(filterByPodStatus) != 0 {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, statusCounts)
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
|
||||
pageGroups := paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit)
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nil
|
||||
}
|
||||
|
||||
func (m *module) getNamespacesTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableNamespaces) (map[string]map[string]string, error) {
|
||||
@@ -151,5 +188,9 @@ func (m *module) getNamespacesTableMetadata(ctx context.Context, orgID valuer.UU
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, namespacesTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
|
||||
var filter *qbtypes.Filter
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, namespacesTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/telemetryschema/metricstelemetryschema"
|
||||
@@ -92,20 +91,38 @@ func buildNodeRecords(
|
||||
return records
|
||||
}
|
||||
|
||||
// getTopNodeGroupsAndMetadata concurrently fetches metadata + the ordering-metric
|
||||
// ranking (plus the full-scope pod-status / node-readiness keysets when filtering,
|
||||
// to intersect all).
|
||||
func (m *module) getTopNodeGroupsAndMetadata(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableNodes,
|
||||
) ([]map[string]string, map[string]map[string]string, error) {
|
||||
) ([]map[string]string, map[string]map[string]string, map[string]podStatusCounts, *qbtypes.QueryWarnData, map[string]nodeConditionCounts, error) {
|
||||
|
||||
var (
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
statusCounts map[string]podStatusCounts
|
||||
statusWarning *qbtypes.QueryWarnData
|
||||
nodeConditionCounts map[string]nodeConditionCounts
|
||||
filter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
filterByNodeReadiness []inframonitoringtypes.NodeCondition
|
||||
)
|
||||
|
||||
orderByKey = req.OrderBy.Key.Name
|
||||
|
||||
// When filtering by pod status / node readiness, resolve the full-scope
|
||||
// keyset(s) concurrently (pageGroups=nil spans all groups under the user
|
||||
// filter) to intersect metadata + ranked groups below. Filters compose as AND.
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
filterByNodeReadiness = req.Filter.FilterByNodeReadiness
|
||||
}
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -114,12 +131,37 @@ func (m *module) getTopNodeGroupsAndMetadata(
|
||||
return err
|
||||
})
|
||||
|
||||
if len(filterByPodStatus) != 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByPodStatus)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if len(filterByNodeReadiness) != 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
nodeConditionCounts, err = m.getPerGroupNodeConditionCounts(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByNodeReadiness)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if orderByKey == inframonitoringtypes.NodeNameAttrKey {
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
// Secondary filter: keep only status/readiness-matching groups. A missing
|
||||
// metric yields an empty statusCounts, so this correctly empties the result
|
||||
// (the caller also surfaces the warning). Filters compose as AND.
|
||||
if len(filterByPodStatus) != 0 {
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
if len(filterByNodeReadiness) != 0 {
|
||||
metadataMap = intersectMap(metadataMap, nodeConditionCounts)
|
||||
}
|
||||
pageGroups := inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.NodeNameAttrKey)
|
||||
return pageGroups, metadataMap, nil
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nodeConditionCounts, nil
|
||||
}
|
||||
|
||||
queryNamesForOrderBy := orderByToNodesQueryNames[orderByKey]
|
||||
@@ -165,10 +207,23 @@ func (m *module) getTopNodeGroupsAndMetadata(
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), metadataMap, nil
|
||||
// Secondary filter: intersect ranked groups + metadata with the status/readiness
|
||||
// keyset. A missing metric yields an empty keyset, correctly emptying the result
|
||||
// (the caller also surfaces the warning). Filters compose as AND.
|
||||
if len(filterByPodStatus) != 0 {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, statusCounts)
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
if len(filterByNodeReadiness) != 0 {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, nodeConditionCounts)
|
||||
metadataMap = intersectMap(metadataMap, nodeConditionCounts)
|
||||
}
|
||||
|
||||
pageGroups := paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit)
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nodeConditionCounts, nil
|
||||
}
|
||||
|
||||
func (m *module) getNodesTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableNodes) (map[string]map[string]string, error) {
|
||||
@@ -178,7 +233,11 @@ func (m *module) getNodesTableMetadata(ctx context.Context, orgID valuer.UUID, r
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, nodesTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
|
||||
var filter *qbtypes.Filter
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, nodesTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
|
||||
}
|
||||
|
||||
// getPerGroupNodeConditionCounts computes per-group node counts bucketed by each
|
||||
@@ -192,6 +251,24 @@ func (m *module) getNodesTableMetadata(ctx context.Context, orgID valuer.UUID, r
|
||||
// countNodesPerCondition: per-group uniqExactIf into ready/not_ready buckets.
|
||||
//
|
||||
// Groups absent from the result map have implicit zero counts (caller default).
|
||||
// applyNodeReadinessFilter adds the readiness push-down (condition_value IN (...))
|
||||
// to the outer count builder. condition_value is numeric (1=Ready, 0=NotReady), so
|
||||
// we map each requested enum to its int. No-op when the requested set is empty.
|
||||
func applyNodeReadinessFilter(cb *sqlbuilder.SelectBuilder, filterByNodeReadiness []inframonitoringtypes.NodeCondition) {
|
||||
if len(filterByNodeReadiness) == 0 {
|
||||
return
|
||||
}
|
||||
nums := make([]int, len(filterByNodeReadiness))
|
||||
for i, c := range filterByNodeReadiness {
|
||||
v := inframonitoringtypes.NodeConditionNumNotReady
|
||||
if c == inframonitoringtypes.NodeConditionReady {
|
||||
v = inframonitoringtypes.NodeConditionNumReady
|
||||
}
|
||||
nums[i] = v
|
||||
}
|
||||
cb.Where(cb.In("condition_value", sqlbuilder.List(nums)))
|
||||
}
|
||||
|
||||
func (m *module) getPerGroupNodeConditionCounts(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
@@ -199,8 +276,11 @@ func (m *module) getPerGroupNodeConditionCounts(
|
||||
filter *qbtypes.Filter,
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
pageGroups []map[string]string,
|
||||
filterByNodeReadiness []inframonitoringtypes.NodeCondition,
|
||||
) (map[string]nodeConditionCounts, error) {
|
||||
if len(pageGroups) == 0 || len(groupBy) == 0 {
|
||||
// Empty pageGroups means "span all under user filter", allowed only in
|
||||
// full-scope mode (filtering by readiness). Otherwise it's an empty page.
|
||||
if len(groupBy) == 0 || (len(pageGroups) == 0 && len(filterByNodeReadiness) == 0) {
|
||||
return map[string]nodeConditionCounts{}, nil
|
||||
}
|
||||
|
||||
@@ -288,11 +368,14 @@ func (m *module) getPerGroupNodeConditionCounts(
|
||||
fmt.Sprintf("uniqExactIf(node_name, condition_value = %d) AS ready_count", inframonitoringtypes.NodeConditionNumReady),
|
||||
fmt.Sprintf("uniqExactIf(node_name, condition_value = %d) AS not_ready_count", inframonitoringtypes.NodeConditionNumNotReady),
|
||||
)
|
||||
countNodesPerConditionSQL := fmt.Sprintf(
|
||||
"SELECT %s FROM latest_condition_per_node GROUP BY %s",
|
||||
strings.Join(countNodesPerConditionSelectCols, ", "),
|
||||
strings.Join(countNodesPerConditionGroupBy, ", "),
|
||||
)
|
||||
// Outer count query. Built with sqlbuilder so the readiness push-down uses a
|
||||
// proper IN (keep only nodes whose readiness is in the requested set).
|
||||
countBuilder := sqlbuilder.NewSelectBuilder()
|
||||
countBuilder.Select(countNodesPerConditionSelectCols...)
|
||||
countBuilder.From("latest_condition_per_node")
|
||||
applyNodeReadinessFilter(countBuilder, filterByNodeReadiness)
|
||||
countBuilder.GroupBy(countNodesPerConditionGroupBy...)
|
||||
countNodesPerConditionSQL, countArgs := countBuilder.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
// Combine CTEs + outer.
|
||||
cteFragments := []string{
|
||||
@@ -300,7 +383,7 @@ func (m *module) getPerGroupNodeConditionCounts(
|
||||
fmt.Sprintf("latest_condition_per_node AS (%s)", latestConditionPerNodeSQL),
|
||||
}
|
||||
finalSQL := querybuilder.CombineCTEs(cteFragments) + countNodesPerConditionSQL
|
||||
finalArgs := querybuilder.PrependArgs([][]any{timeSeriesFPsArgs, latestConditionPerNodeArgs}, nil)
|
||||
finalArgs := querybuilder.PrependArgs([][]any{timeSeriesFPsArgs, latestConditionPerNodeArgs}, countArgs)
|
||||
|
||||
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, finalSQL, finalArgs...)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
)
|
||||
|
||||
func TestApplyNodeReadinessFilter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
readiness []inframonitoringtypes.NodeCondition
|
||||
wantWhere bool
|
||||
wantArgs []any
|
||||
}{
|
||||
{
|
||||
name: "empty set yields no clause",
|
||||
readiness: nil,
|
||||
wantWhere: false,
|
||||
wantArgs: nil,
|
||||
},
|
||||
{
|
||||
name: "ready maps to 1 via IN",
|
||||
readiness: []inframonitoringtypes.NodeCondition{inframonitoringtypes.NodeConditionReady},
|
||||
wantWhere: true,
|
||||
wantArgs: []any{inframonitoringtypes.NodeConditionNumReady},
|
||||
},
|
||||
{
|
||||
name: "not_ready maps to 0 via IN",
|
||||
readiness: []inframonitoringtypes.NodeCondition{inframonitoringtypes.NodeConditionNotReady},
|
||||
wantWhere: true,
|
||||
wantArgs: []any{inframonitoringtypes.NodeConditionNumNotReady},
|
||||
},
|
||||
{
|
||||
name: "multiple conditions map to their ints via IN",
|
||||
readiness: []inframonitoringtypes.NodeCondition{
|
||||
inframonitoringtypes.NodeConditionReady,
|
||||
inframonitoringtypes.NodeConditionNotReady,
|
||||
},
|
||||
wantWhere: true,
|
||||
wantArgs: []any{inframonitoringtypes.NodeConditionNumReady, inframonitoringtypes.NodeConditionNumNotReady},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cb := sqlbuilder.NewSelectBuilder()
|
||||
cb.Select("node_name")
|
||||
cb.From("latest_condition_per_node")
|
||||
applyNodeReadinessFilter(cb, tt.readiness)
|
||||
sql, args := cb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
hasWhere := strings.Contains(sql, "condition_value IN (")
|
||||
if hasWhere != tt.wantWhere {
|
||||
t.Errorf("applyNodeReadinessFilter(%v) sql = %q, wantWhere = %v", tt.readiness, sql, tt.wantWhere)
|
||||
}
|
||||
if len(tt.wantArgs) == 0 {
|
||||
if len(args) != 0 {
|
||||
t.Errorf("applyNodeReadinessFilter(%v) args = %v, want none", tt.readiness, args)
|
||||
}
|
||||
} else if !reflect.DeepEqual(args, tt.wantArgs) {
|
||||
t.Errorf("applyNodeReadinessFilter(%v) args = %v, want %v", tt.readiness, args, tt.wantArgs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -146,24 +146,34 @@ func buildPodRecords(
|
||||
return records
|
||||
}
|
||||
|
||||
// getTopPodGroupsAndMetadata fetches the group metadata and the ordering-metric
|
||||
// ranking concurrently, then pages the ranked groups, backfilling from metadata
|
||||
// when the page extends past the metric-ranked groups. Returns the page of
|
||||
// groups and the metadata map (needed by the caller for Total and records).
|
||||
// getTopPodGroupsAndMetadata concurrently fetches metadata + the ordering-metric
|
||||
// ranking (plus the full-scope pod-status keyset when filtering, to intersect both).
|
||||
func (m *module) getTopPodGroupsAndMetadata(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostablePods,
|
||||
) ([]map[string]string, map[string]map[string]string, error) {
|
||||
) ([]map[string]string, map[string]map[string]string, map[string]podStatusCounts, *qbtypes.QueryWarnData, error) {
|
||||
|
||||
var (
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
statusCounts map[string]podStatusCounts
|
||||
statusWarning *qbtypes.QueryWarnData
|
||||
filter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
)
|
||||
|
||||
orderByKey = req.OrderBy.Key.Name
|
||||
|
||||
// When filtering by pod status, resolve the full-scope status keyset
|
||||
// concurrently (pageGroups=nil spans all groups under the user filter) so it
|
||||
// can intersect metadata + ranked groups below.
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
}
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -172,12 +182,26 @@ func (m *module) getTopPodGroupsAndMetadata(
|
||||
return err
|
||||
})
|
||||
|
||||
if len(filterByPodStatus) != 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByPodStatus)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if orderByKey == inframonitoringtypes.PodNameAttrKey {
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
// Secondary filter: keep only status-matching groups. A missing metric
|
||||
// yields an empty statusCounts, so this correctly empties the result
|
||||
// (the caller also surfaces the warning).
|
||||
if len(filterByPodStatus) != 0 {
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
pageGroups := inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.PodNameAttrKey)
|
||||
return pageGroups, metadataMap, nil
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nil
|
||||
}
|
||||
|
||||
queryNamesForOrderBy := orderByToPodsQueryNames[orderByKey]
|
||||
@@ -223,10 +247,19 @@ func (m *module) getTopPodGroupsAndMetadata(
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), metadataMap, nil
|
||||
// Secondary filter: intersect ranked groups + metadata with the status keyset.
|
||||
// A missing metric yields an empty statusCounts, correctly emptying the result
|
||||
// (the caller also surfaces the warning).
|
||||
if len(filterByPodStatus) != 0 {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, statusCounts)
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
|
||||
pageGroups := paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit)
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nil
|
||||
}
|
||||
|
||||
func (m *module) getPodsTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostablePods) (map[string]map[string]string, error) {
|
||||
@@ -236,7 +269,11 @@ func (m *module) getPodsTableMetadata(ctx context.Context, orgID valuer.UUID, re
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, podsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
|
||||
var filter *qbtypes.Filter
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, podsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
|
||||
}
|
||||
|
||||
// getPerGroupPodStatusCountsWithReqMetricChecks gates getPerGroupPodStatusCounts
|
||||
@@ -251,6 +288,7 @@ func (m *module) getPerGroupPodStatusCountsWithReqMetricChecks(
|
||||
filter *qbtypes.Filter,
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
pageGroups []map[string]string,
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus,
|
||||
) (map[string]podStatusCounts, *qbtypes.QueryWarnData, error) {
|
||||
present, err := m.getMetricsExistence(ctx, podStatusMetricNamesList)
|
||||
if err != nil {
|
||||
@@ -276,13 +314,28 @@ func (m *module) getPerGroupPodStatusCountsWithReqMetricChecks(
|
||||
return map[string]podStatusCounts{}, warning, nil
|
||||
}
|
||||
|
||||
counts, err := m.getPerGroupPodStatusCounts(ctx, orgID, start, end, filter, groupBy, pageGroups)
|
||||
counts, err := m.getPerGroupPodStatusCounts(ctx, orgID, start, end, filter, groupBy, pageGroups, filterByPodStatus)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return counts, nil, nil
|
||||
}
|
||||
|
||||
// applyPodStatusFilter adds the display-status push-down (lower(display_status)
|
||||
// IN (...)) to the outer count builder. valuer lowercases the wire value while
|
||||
// display_status is kubectl-cased, so we compare lower() on both. No-op when the
|
||||
// requested set is empty.
|
||||
func applyPodStatusFilter(cb *sqlbuilder.SelectBuilder, filterByPodStatus []inframonitoringtypes.PodStatus) {
|
||||
if len(filterByPodStatus) == 0 {
|
||||
return
|
||||
}
|
||||
vals := make([]string, len(filterByPodStatus))
|
||||
for i, s := range filterByPodStatus {
|
||||
vals[i] = s.StringValue()
|
||||
}
|
||||
cb.Where(cb.In("lower(display_status)", sqlbuilder.List(vals)))
|
||||
}
|
||||
|
||||
// getPerGroupPodStatusCounts computes per-group pod counts bucketed by each
|
||||
// pod's latest kubectl-style display status in the requested window. Caller
|
||||
// must ensure the required metrics exist (getPerGroupPodStatusCountsWithReqMetricChecks).
|
||||
@@ -303,13 +356,21 @@ func (m *module) getPerGroupPodStatusCounts(
|
||||
filter *qbtypes.Filter,
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
pageGroups []map[string]string,
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus,
|
||||
) (map[string]podStatusCounts, error) {
|
||||
if len(pageGroups) == 0 || len(groupBy) == 0 {
|
||||
// return early if no group by or (no pagegroups provided plus no filterBystatus given for a full scan)
|
||||
if len(groupBy) == 0 || (len(pageGroups) == 0 && len(filterByPodStatus) == 0) {
|
||||
return map[string]podStatusCounts{}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
filterClause *sqlbuilder.WhereClause
|
||||
err error
|
||||
userFilterExpr string
|
||||
)
|
||||
|
||||
// Merge user filter with page-groups IN clauses.
|
||||
userFilterExpr := ""
|
||||
userFilterExpr = ""
|
||||
if filter != nil {
|
||||
userFilterExpr = filter.Expression
|
||||
}
|
||||
@@ -322,10 +383,7 @@ func (m *module) getPerGroupPodStatusCounts(
|
||||
// CTEs, and buildFilterClause hits the metadata store + parses the
|
||||
// expression, so we don't want to repeat it per CTE. AddWhereClause only
|
||||
// reads the clause, so the same instance is safe to attach to each builder.
|
||||
var (
|
||||
filterClause *sqlbuilder.WhereClause
|
||||
err error
|
||||
)
|
||||
|
||||
if mergedFilterExpr != "" {
|
||||
filterClause, err = m.buildFilterClause(ctx, orgID, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
|
||||
if err != nil {
|
||||
@@ -540,11 +598,15 @@ func (m *module) getPerGroupPodStatusCounts(
|
||||
countGroupBy = append(countGroupBy, col)
|
||||
}
|
||||
countSelectCols = append(countSelectCols, statusCountCols...)
|
||||
countSQL := fmt.Sprintf(
|
||||
"SELECT %s FROM pod_status GROUP BY %s",
|
||||
strings.Join(countSelectCols, ", "),
|
||||
strings.Join(countGroupBy, ", "),
|
||||
)
|
||||
|
||||
// Outer count query. Built with sqlbuilder so the status push-down uses a
|
||||
// proper IN (keep only pods whose display status is in the requested set).
|
||||
countBuilder := sqlbuilder.NewSelectBuilder()
|
||||
countBuilder.Select(countSelectCols...)
|
||||
countBuilder.From("pod_status")
|
||||
applyPodStatusFilter(countBuilder, filterByPodStatus)
|
||||
countBuilder.GroupBy(countGroupBy...)
|
||||
countSQL, countArgs := countBuilder.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
// Combine CTEs + outer. Arg order mirrors CTE declaration order.
|
||||
cteFragments := []string{
|
||||
@@ -561,7 +623,7 @@ func (m *module) getPerGroupPodStatusCounts(
|
||||
phaseFpsArgs, phasePerPodArgs,
|
||||
podReasonFpsArgs, podReasonPerPodArgs,
|
||||
containerReasonFpsArgs, containerInnerArgs,
|
||||
}, nil)
|
||||
}, countArgs)
|
||||
|
||||
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, finalSQL, finalArgs...)
|
||||
if err != nil {
|
||||
|
||||
63
pkg/modules/inframonitoring/implinframonitoring/pods_test.go
Normal file
63
pkg/modules/inframonitoring/implinframonitoring/pods_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
)
|
||||
|
||||
func TestApplyPodStatusFilter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
statuses []inframonitoringtypes.PodStatus
|
||||
wantWhere bool
|
||||
wantArgs []any
|
||||
}{
|
||||
{
|
||||
name: "empty set yields no clause",
|
||||
statuses: nil,
|
||||
wantWhere: false,
|
||||
wantArgs: nil,
|
||||
},
|
||||
{
|
||||
name: "single status pushes lowercased arg via IN",
|
||||
statuses: []inframonitoringtypes.PodStatus{inframonitoringtypes.PodStatusRunning},
|
||||
wantWhere: true,
|
||||
wantArgs: []any{"running"},
|
||||
},
|
||||
{
|
||||
name: "multiple statuses push lowercased args via IN",
|
||||
statuses: []inframonitoringtypes.PodStatus{
|
||||
inframonitoringtypes.PodStatusRunning,
|
||||
inframonitoringtypes.PodStatusCrashLoopBackOff,
|
||||
},
|
||||
wantWhere: true,
|
||||
wantArgs: []any{"running", "crashloopbackoff"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cb := sqlbuilder.NewSelectBuilder()
|
||||
cb.Select("pod_uid")
|
||||
cb.From("pod_status")
|
||||
applyPodStatusFilter(cb, tt.statuses)
|
||||
sql, args := cb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
hasWhere := strings.Contains(sql, "lower(display_status) IN (")
|
||||
if hasWhere != tt.wantWhere {
|
||||
t.Errorf("applyPodStatusFilter(%v) sql = %q, wantWhere = %v", tt.statuses, sql, tt.wantWhere)
|
||||
}
|
||||
if len(tt.wantArgs) == 0 {
|
||||
if len(args) != 0 {
|
||||
t.Errorf("applyPodStatusFilter(%v) args = %v, want none", tt.statuses, args)
|
||||
}
|
||||
} else if !reflect.DeepEqual(args, tt.wantArgs) {
|
||||
t.Errorf("applyPodStatusFilter(%v) args = %v, want %v", tt.statuses, args, tt.wantArgs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -82,20 +82,34 @@ func buildStatefulSetRecords(
|
||||
return records
|
||||
}
|
||||
|
||||
// getTopStatefulSetGroupsAndMetadata concurrently fetches metadata + the ordering-metric
|
||||
// ranking (plus the full-scope pod-status keyset when filtering, to intersect both).
|
||||
func (m *module) getTopStatefulSetGroupsAndMetadata(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableStatefulSets,
|
||||
) ([]map[string]string, map[string]map[string]string, error) {
|
||||
) ([]map[string]string, map[string]map[string]string, map[string]podStatusCounts, *qbtypes.QueryWarnData, error) {
|
||||
|
||||
var (
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
orderByKey string
|
||||
metadataMap map[string]map[string]string
|
||||
allMetricGroups []rankedGroup
|
||||
statusCounts map[string]podStatusCounts
|
||||
statusWarning *qbtypes.QueryWarnData
|
||||
filter *qbtypes.Filter
|
||||
filterByPodStatus []inframonitoringtypes.PodStatus
|
||||
)
|
||||
|
||||
orderByKey = req.OrderBy.Key.Name
|
||||
|
||||
// When filtering by pod status, resolve the full-scope status keyset
|
||||
// concurrently (pageGroups=nil spans all groups under the user filter) so it
|
||||
// can intersect metadata + ranked groups below.
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
filterByPodStatus = req.Filter.FilterByPodStatus
|
||||
}
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
@@ -104,12 +118,26 @@ func (m *module) getTopStatefulSetGroupsAndMetadata(
|
||||
return err
|
||||
})
|
||||
|
||||
if len(filterByPodStatus) != 0 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByPodStatus)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if orderByKey == inframonitoringtypes.StatefulSetNameAttrKey {
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
// Secondary filter: keep only status-matching groups. A missing metric
|
||||
// yields an empty statusCounts, so this correctly empties the result
|
||||
// (the caller also surfaces the warning).
|
||||
if len(filterByPodStatus) != 0 {
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
pageGroups := inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.StatefulSetNameAttrKey)
|
||||
return pageGroups, metadataMap, nil
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nil
|
||||
}
|
||||
|
||||
queryNamesForOrderBy := orderByToStatefulSetsQueryNames[orderByKey]
|
||||
@@ -155,10 +183,19 @@ func (m *module) getTopStatefulSetGroupsAndMetadata(
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), metadataMap, nil
|
||||
// Secondary filter: intersect ranked groups + metadata with the status keyset.
|
||||
// A missing metric yields an empty statusCounts, correctly emptying the result
|
||||
// (the caller also surfaces the warning).
|
||||
if len(filterByPodStatus) != 0 {
|
||||
allMetricGroups = intersectRankedGroups(allMetricGroups, statusCounts)
|
||||
metadataMap = intersectMap(metadataMap, statusCounts)
|
||||
}
|
||||
|
||||
pageGroups := paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit)
|
||||
return pageGroups, metadataMap, statusCounts, statusWarning, nil
|
||||
}
|
||||
|
||||
func (m *module) getStatefulSetsTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableStatefulSets) (map[string]map[string]string, error) {
|
||||
@@ -168,5 +205,9 @@ func (m *module) getStatefulSetsTableMetadata(ctx context.Context, orgID valuer.
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, statefulSetsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
|
||||
var filter *qbtypes.Filter
|
||||
if req.Filter != nil {
|
||||
filter = &req.Filter.Filter
|
||||
}
|
||||
return m.getMetadata(ctx, orgID, statefulSetsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
|
||||
}
|
||||
|
||||
@@ -42,13 +42,22 @@ type ClusterRecord struct {
|
||||
type PostableClusters struct {
|
||||
Start int64 `json:"start" required:"true"`
|
||||
End int64 `json:"end" required:"true"`
|
||||
Filter *qbtypes.Filter `json:"filter"`
|
||||
Filter *ClusterFilter `json:"filter"`
|
||||
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
|
||||
OrderBy *qbtypes.OrderBy `json:"orderBy"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit" required:"true"`
|
||||
}
|
||||
|
||||
// ClusterFilter is the attribute filter plus optional secondary filters on the
|
||||
// derived pod display status(es) (see PodStatus; matches any listed, OR) and node
|
||||
// readiness (see NodeCondition; matches any listed, OR). Empty FilterByPodStatus / FilterByNodeReadiness = off.
|
||||
type ClusterFilter struct {
|
||||
qbtypes.Filter `json:",inline"`
|
||||
FilterByPodStatus []PodStatus `json:"filterByPodStatus"`
|
||||
FilterByNodeReadiness []NodeCondition `json:"filterByNodeReadiness"`
|
||||
}
|
||||
|
||||
// Validate ensures PostableClusters contains acceptable values.
|
||||
func (req *PostableClusters) Validate() error {
|
||||
if req == nil {
|
||||
@@ -88,6 +97,22 @@ func (req *PostableClusters) Validate() error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.Filter != nil {
|
||||
for _, s := range req.Filter.FilterByPodStatus {
|
||||
if !IsFilterablePodStatus(s) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.Filter != nil {
|
||||
for _, c := range req.Filter.FilterByNodeReadiness {
|
||||
if !IsFilterableNodeCondition(c) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by node readiness: %s", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.OrderBy != nil {
|
||||
if !slices.Contains(ClustersValidOrderByKeys, req.OrderBy.Key.Name) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)
|
||||
|
||||
@@ -36,13 +36,20 @@ type DaemonSetRecord struct {
|
||||
type PostableDaemonSets struct {
|
||||
Start int64 `json:"start" required:"true"`
|
||||
End int64 `json:"end" required:"true"`
|
||||
Filter *qbtypes.Filter `json:"filter"`
|
||||
Filter *DaemonSetFilter `json:"filter"`
|
||||
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
|
||||
OrderBy *qbtypes.OrderBy `json:"orderBy"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit" required:"true"`
|
||||
}
|
||||
|
||||
// DaemonSetFilter is the attribute filter plus an optional secondary filter on the
|
||||
// derived pod display status(es) (see PodStatus); matches any listed (OR). Empty = off.
|
||||
type DaemonSetFilter struct {
|
||||
qbtypes.Filter `json:",inline"`
|
||||
FilterByPodStatus []PodStatus `json:"filterByPodStatus"`
|
||||
}
|
||||
|
||||
// Validate ensures PostableDaemonSets contains acceptable values.
|
||||
func (req *PostableDaemonSets) Validate() error {
|
||||
if req == nil {
|
||||
@@ -82,6 +89,14 @@ func (req *PostableDaemonSets) Validate() error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.Filter != nil {
|
||||
for _, s := range req.Filter.FilterByPodStatus {
|
||||
if !IsFilterablePodStatus(s) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.OrderBy != nil {
|
||||
if !slices.Contains(DaemonSetsValidOrderByKeys, req.OrderBy.Key.Name) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)
|
||||
|
||||
@@ -34,13 +34,20 @@ type DeploymentRecord struct {
|
||||
type PostableDeployments struct {
|
||||
Start int64 `json:"start" required:"true"`
|
||||
End int64 `json:"end" required:"true"`
|
||||
Filter *qbtypes.Filter `json:"filter"`
|
||||
Filter *DeploymentFilter `json:"filter"`
|
||||
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
|
||||
OrderBy *qbtypes.OrderBy `json:"orderBy"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit" required:"true"`
|
||||
}
|
||||
|
||||
// DeploymentFilter is the attribute filter plus an optional secondary filter on the
|
||||
// derived pod display status(es) (see PodStatus); matches any listed (OR). Empty = off.
|
||||
type DeploymentFilter struct {
|
||||
qbtypes.Filter `json:",inline"`
|
||||
FilterByPodStatus []PodStatus `json:"filterByPodStatus"`
|
||||
}
|
||||
|
||||
// Validate ensures PostableDeployments contains acceptable values.
|
||||
func (req *PostableDeployments) Validate() error {
|
||||
if req == nil {
|
||||
@@ -80,6 +87,14 @@ func (req *PostableDeployments) Validate() error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.Filter != nil {
|
||||
for _, s := range req.Filter.FilterByPodStatus {
|
||||
if !IsFilterablePodStatus(s) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.OrderBy != nil {
|
||||
if !slices.Contains(DeploymentsValidOrderByKeys, req.OrderBy.Key.Name) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)
|
||||
|
||||
@@ -36,13 +36,20 @@ type JobRecord struct {
|
||||
type PostableJobs struct {
|
||||
Start int64 `json:"start" required:"true"`
|
||||
End int64 `json:"end" required:"true"`
|
||||
Filter *qbtypes.Filter `json:"filter"`
|
||||
Filter *JobFilter `json:"filter"`
|
||||
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
|
||||
OrderBy *qbtypes.OrderBy `json:"orderBy"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit" required:"true"`
|
||||
}
|
||||
|
||||
// JobFilter is the attribute filter plus an optional secondary filter on the
|
||||
// derived pod display status(es) (see PodStatus); matches any listed (OR). Empty = off.
|
||||
type JobFilter struct {
|
||||
qbtypes.Filter `json:",inline"`
|
||||
FilterByPodStatus []PodStatus `json:"filterByPodStatus"`
|
||||
}
|
||||
|
||||
// Validate ensures PostableJobs contains acceptable values.
|
||||
func (req *PostableJobs) Validate() error {
|
||||
if req == nil {
|
||||
@@ -82,6 +89,14 @@ func (req *PostableJobs) Validate() error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.Filter != nil {
|
||||
for _, s := range req.Filter.FilterByPodStatus {
|
||||
if !IsFilterablePodStatus(s) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.OrderBy != nil {
|
||||
if !slices.Contains(JobsValidOrderByKeys, req.OrderBy.Key.Name) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)
|
||||
|
||||
@@ -34,13 +34,20 @@ type NamespaceRecord struct {
|
||||
type PostableNamespaces struct {
|
||||
Start int64 `json:"start" required:"true"`
|
||||
End int64 `json:"end" required:"true"`
|
||||
Filter *qbtypes.Filter `json:"filter"`
|
||||
Filter *NamespaceFilter `json:"filter"`
|
||||
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
|
||||
OrderBy *qbtypes.OrderBy `json:"orderBy"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit" required:"true"`
|
||||
}
|
||||
|
||||
// NamespaceFilter is the attribute filter plus an optional secondary filter on the
|
||||
// derived pod display status(es) (see PodStatus); matches any listed (OR). Empty = off.
|
||||
type NamespaceFilter struct {
|
||||
qbtypes.Filter `json:",inline"`
|
||||
FilterByPodStatus []PodStatus `json:"filterByPodStatus"`
|
||||
}
|
||||
|
||||
// Validate ensures PostableNamespaces contains acceptable values.
|
||||
func (req *PostableNamespaces) Validate() error {
|
||||
if req == nil {
|
||||
@@ -80,6 +87,14 @@ func (req *PostableNamespaces) Validate() error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.Filter != nil {
|
||||
for _, s := range req.Filter.FilterByPodStatus {
|
||||
if !IsFilterablePodStatus(s) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.OrderBy != nil {
|
||||
if !slices.Contains(NamespacesValidOrderByKeys, req.OrderBy.Key.Name) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)
|
||||
|
||||
@@ -39,13 +39,22 @@ type NodeRecord struct {
|
||||
type PostableNodes struct {
|
||||
Start int64 `json:"start" required:"true"`
|
||||
End int64 `json:"end" required:"true"`
|
||||
Filter *qbtypes.Filter `json:"filter"`
|
||||
Filter *NodeFilter `json:"filter"`
|
||||
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
|
||||
OrderBy *qbtypes.OrderBy `json:"orderBy"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit" required:"true"`
|
||||
}
|
||||
|
||||
// NodeFilter is the attribute filter plus an optional secondary filter on the
|
||||
// derived pod display status(es) (see PodStatus; matches any listed, OR) and node
|
||||
// readiness (see NodeCondition; matches any listed, OR). Empty FilterByPodStatus / FilterByNodeReadiness = off.
|
||||
type NodeFilter struct {
|
||||
qbtypes.Filter `json:",inline"`
|
||||
FilterByPodStatus []PodStatus `json:"filterByPodStatus"`
|
||||
FilterByNodeReadiness []NodeCondition `json:"filterByNodeReadiness"`
|
||||
}
|
||||
|
||||
// Validate ensures PostableNodes contains acceptable values.
|
||||
func (req *PostableNodes) Validate() error {
|
||||
if req == nil {
|
||||
@@ -85,6 +94,22 @@ func (req *PostableNodes) Validate() error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.Filter != nil {
|
||||
for _, s := range req.Filter.FilterByPodStatus {
|
||||
if !IsFilterablePodStatus(s) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.Filter != nil {
|
||||
for _, c := range req.Filter.FilterByNodeReadiness {
|
||||
if !IsFilterableNodeCondition(c) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by node readiness: %s", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.OrderBy != nil {
|
||||
if !slices.Contains(NodesValidOrderByKeys, req.OrderBy.Key.Name) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/valuer"
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type NodeCondition struct {
|
||||
valuer.String
|
||||
@@ -20,6 +24,12 @@ func (NodeCondition) Enum() []any {
|
||||
}
|
||||
}
|
||||
|
||||
// IsFilterableNodeCondition reports whether c is a concrete, user-filterable
|
||||
// node readiness: any Enum() member except the no_data sentinel.
|
||||
func IsFilterableNodeCondition(c NodeCondition) bool {
|
||||
return c != NodeConditionNoData && slices.Contains((NodeCondition{}).Enum(), any(c))
|
||||
}
|
||||
|
||||
// Numeric values emitted by the k8s.node.condition_ready metric
|
||||
// (source: OTel kubeletstats receiver).
|
||||
const (
|
||||
|
||||
@@ -63,13 +63,20 @@ type PodRecord struct {
|
||||
type PostablePods struct {
|
||||
Start int64 `json:"start" required:"true"`
|
||||
End int64 `json:"end" required:"true"`
|
||||
Filter *qbtypes.Filter `json:"filter"`
|
||||
Filter *PodFilter `json:"filter"`
|
||||
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
|
||||
OrderBy *qbtypes.OrderBy `json:"orderBy"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit" required:"true"`
|
||||
}
|
||||
|
||||
// PodFilter is the attribute filter plus an optional secondary filter on the
|
||||
// derived pod display status(es) (see PodStatus); matches any listed (OR). Empty = off.
|
||||
type PodFilter struct {
|
||||
qbtypes.Filter `json:",inline"`
|
||||
FilterByPodStatus []PodStatus `json:"filterByPodStatus"`
|
||||
}
|
||||
|
||||
// Validate ensures PostablePods contains acceptable values.
|
||||
func (req *PostablePods) Validate() error {
|
||||
if req == nil {
|
||||
@@ -109,6 +116,14 @@ func (req *PostablePods) Validate() error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.Filter != nil {
|
||||
for _, s := range req.Filter.FilterByPodStatus {
|
||||
if !IsFilterablePodStatus(s) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.OrderBy != nil {
|
||||
if !slices.Contains(PodsValidOrderByKeys, req.OrderBy.Key.Name) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/valuer"
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// PodStatus is the kubectl-style pod display status, derived from
|
||||
// k8s.pod.phase + k8s.pod.status_reason + k8s.container.status.reason
|
||||
@@ -66,6 +70,12 @@ func (PodStatus) Enum() []any {
|
||||
}
|
||||
}
|
||||
|
||||
// IsFilterablePodStatus reports whether s is a concrete, user-filterable pod
|
||||
// status: any Enum() member except the no_data sentinel.
|
||||
func IsFilterablePodStatus(s PodStatus) bool {
|
||||
return s != PodStatusNoData && slices.Contains((PodStatus{}).Enum(), any(s))
|
||||
}
|
||||
|
||||
const PodNameAttrKey = "k8s.pod.name"
|
||||
|
||||
const (
|
||||
|
||||
@@ -34,13 +34,20 @@ type StatefulSetRecord struct {
|
||||
type PostableStatefulSets struct {
|
||||
Start int64 `json:"start" required:"true"`
|
||||
End int64 `json:"end" required:"true"`
|
||||
Filter *qbtypes.Filter `json:"filter"`
|
||||
Filter *StatefulSetFilter `json:"filter"`
|
||||
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
|
||||
OrderBy *qbtypes.OrderBy `json:"orderBy"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit" required:"true"`
|
||||
}
|
||||
|
||||
// StatefulSetFilter is the attribute filter plus an optional secondary filter on the
|
||||
// derived pod display status(es) (see PodStatus); matches any listed (OR). Empty = off.
|
||||
type StatefulSetFilter struct {
|
||||
qbtypes.Filter `json:",inline"`
|
||||
FilterByPodStatus []PodStatus `json:"filterByPodStatus"`
|
||||
}
|
||||
|
||||
// Validate ensures PostableStatefulSets contains acceptable values.
|
||||
func (req *PostableStatefulSets) Validate() error {
|
||||
if req == nil {
|
||||
@@ -80,6 +87,14 @@ func (req *PostableStatefulSets) Validate() error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.Filter != nil {
|
||||
for _, s := range req.Filter.FilterByPodStatus {
|
||||
if !IsFilterablePodStatus(s) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.OrderBy != nil {
|
||||
if !slices.Contains(StatefulSetsValidOrderByKeys, req.OrderBy.Key.Name) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)
|
||||
|
||||
@@ -88,3 +88,15 @@
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"nd-ss-uid","k8s.pod.name":"nd-ss-pod","k8s.statefulset.name":"nd-ss","k8s.namespace.name":"ns-nd","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"nd-ss-uid","k8s.pod.name":"nd-ss-pod","k8s.statefulset.name":"nd-ss","k8s.namespace.name":"ns-nd","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"nd-ss-uid","k8s.pod.name":"nd-ss-pod","k8s.statefulset.name":"nd-ss","k8s.namespace.name":"ns-nd","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nd-ds-p1-uid", "k8s.pod.name": "nd-ds-p1", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "nd-ds-p1-clbo-uid", "k8s.pod.name": "nd-ds-p1-clbo", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nd-ds-p1-clbo-uid", "k8s.pod.name": "nd-ds-p1-clbo", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "nd-ds-p1-clbo-uid", "k8s.pod.name": "nd-ds-p1-clbo", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nd-ds-p1-uid", "k8s.pod.name": "nd-ds-p1", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "nd-ds-p1-clbo-uid", "k8s.pod.name": "nd-ds-p1-clbo", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nd-ds-p1-clbo-uid", "k8s.pod.name": "nd-ds-p1-clbo", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "nd-ds-p1-clbo-uid", "k8s.pod.name": "nd-ds-p1-clbo", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nd-ds-p1-uid", "k8s.pod.name": "nd-ds-p1", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "nd-ds-p1-clbo-uid", "k8s.pod.name": "nd-ds-p1-clbo", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nd-ds-p1-clbo-uid", "k8s.pod.name": "nd-ds-p1-clbo", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "nd-ds-p1-clbo-uid", "k8s.pod.name": "nd-ds-p1-clbo", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
|
||||
@@ -94,3 +94,15 @@
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"nj-ss-uid","k8s.pod.name":"nj-ss-pod","k8s.statefulset.name":"nj-ss","k8s.namespace.name":"ns-nj","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"nj-ss-uid","k8s.pod.name":"nj-ss-pod","k8s.statefulset.name":"nj-ss","k8s.namespace.name":"ns-nj","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"nj-ss-uid","k8s.pod.name":"nj-ss-pod","k8s.statefulset.name":"nj-ss","k8s.namespace.name":"ns-nj","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nj-job-p1-uid", "k8s.pod.name": "nj-job-p1", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "nj-job-p1-clbo-uid", "k8s.pod.name": "nj-job-p1-clbo", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nj-job-p1-clbo-uid", "k8s.pod.name": "nj-job-p1-clbo", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "nj-job-p1-clbo-uid", "k8s.pod.name": "nj-job-p1-clbo", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "k8s.cluster.name": "cluster-x", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nj-job-p1-uid", "k8s.pod.name": "nj-job-p1", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "nj-job-p1-clbo-uid", "k8s.pod.name": "nj-job-p1-clbo", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nj-job-p1-clbo-uid", "k8s.pod.name": "nj-job-p1-clbo", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "nj-job-p1-clbo-uid", "k8s.pod.name": "nj-job-p1-clbo", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "k8s.cluster.name": "cluster-x", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nj-job-p1-uid", "k8s.pod.name": "nj-job-p1", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "nj-job-p1-clbo-uid", "k8s.pod.name": "nj-job-p1-clbo", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nj-job-p1-clbo-uid", "k8s.pod.name": "nj-job-p1-clbo", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "nj-job-p1-clbo-uid", "k8s.pod.name": "nj-job-p1-clbo", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "k8s.cluster.name": "cluster-x", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
|
||||
@@ -31,3 +31,48 @@
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"carrier-pod-uid","k8s.pod.name":"carrier-pod","k8s.namespace.name":"carrier-ns","k8s.node.name":"carrier-phantom-host","k8s.cluster.name":"carrier-cluster"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"carrier-pod-uid","k8s.pod.name":"carrier-pod","k8s.namespace.name":"carrier-ns","k8s.node.name":"carrier-phantom-host","k8s.cluster.name":"carrier-cluster"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"carrier-pod-uid","k8s.pod.name":"carrier-pod","k8s.namespace.name":"carrier-ns","k8s.node.name":"carrier-phantom-host","k8s.cluster.name":"carrier-cluster"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name": "k8s.node.cpu.usage", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.8, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.cpu.usage", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.8, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.cpu.usage", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.8, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_cpu", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 4.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_cpu", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 4.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_cpu", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 4.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.memory.working_set", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.memory.working_set", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.memory.working_set", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_memory", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 8000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_memory", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 8000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_memory", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 8000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.condition_ready", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.condition_ready", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.condition_ready", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.cpu.usage", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.3, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.cpu.usage", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.3, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.cpu.usage", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.3, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_cpu", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 4.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_cpu", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 4.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_cpu", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 4.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.memory.working_set", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.memory.working_set", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.memory.working_set", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_memory", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 8000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_memory", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 8000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_memory", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 8000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.condition_ready", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.condition_ready", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.condition_ready", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.cpu.usage", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.cpu.usage", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.cpu.usage", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_cpu", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 4.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_cpu", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 4.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_cpu", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 4.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.memory.working_set", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.memory.working_set", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.memory.working_set", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_memory", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 8000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_memory", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 8000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.allocatable_memory", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 8000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.condition_ready", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.condition_ready", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.node.condition_ready", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
|
||||
@@ -145,3 +145,63 @@
|
||||
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "unk-p-uid", "k8s.pod.name": "unk-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "c-clbo"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 8, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "unk-p-uid", "k8s.pod.name": "unk-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "c-clbo"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 8, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "unk-p-uid", "k8s.pod.name": "unk-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "c-clbo"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 8, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu.usage", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.7, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu.usage", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.7, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu.usage", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.7, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu_request_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu_request_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu_request_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu_limit_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu_limit_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu_limit_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory.working_set", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 100000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory.working_set", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 100000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory.working_set", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 100000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory_request_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory_request_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory_request_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory_limit_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory_limit_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory_limit_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu.usage", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.3, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu.usage", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.3, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu.usage", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.3, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu_request_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu_request_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu_request_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu_limit_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu_limit_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu_limit_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory.working_set", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 100000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory.working_set", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 100000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory.working_set", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 100000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory_request_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory_request_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory_request_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory_limit_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory_limit_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory_limit_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
|
||||
@@ -67,3 +67,15 @@
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"nd-dep-uid","k8s.pod.name":"nd-dep-pod","k8s.deployment.name":"nd-dep","k8s.namespace.name":"ns-nd","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"nd-dep-uid","k8s.pod.name":"nd-dep-pod","k8s.deployment.name":"nd-dep","k8s.namespace.name":"ns-nd","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"nd-dep-uid","k8s.pod.name":"nd-dep-pod","k8s.deployment.name":"nd-dep","k8s.namespace.name":"ns-nd","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "ns-ss-p1-uid", "k8s.pod.name": "ns-ss-p1", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "ns-ss-p1-clbo-uid", "k8s.pod.name": "ns-ss-p1-clbo", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "ns-ss-p1-clbo-uid", "k8s.pod.name": "ns-ss-p1-clbo", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "ns-ss-p1-clbo-uid", "k8s.pod.name": "ns-ss-p1-clbo", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "ns-ss-p1-uid", "k8s.pod.name": "ns-ss-p1", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "ns-ss-p1-clbo-uid", "k8s.pod.name": "ns-ss-p1-clbo", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "ns-ss-p1-clbo-uid", "k8s.pod.name": "ns-ss-p1-clbo", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "ns-ss-p1-clbo-uid", "k8s.pod.name": "ns-ss-p1-clbo", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "ns-ss-p1-uid", "k8s.pod.name": "ns-ss-p1", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "ns-ss-p1-clbo-uid", "k8s.pod.name": "ns-ss-p1-clbo", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "ns-ss-p1-clbo-uid", "k8s.pod.name": "ns-ss-p1-clbo", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "ns-ss-p1-clbo-uid", "k8s.pod.name": "ns-ss-p1-clbo", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
|
||||
@@ -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
|
||||
@@ -490,6 +494,95 @@ def test_pods_pagination(
|
||||
assert set(seen_pods) == {f"page-p{i}" for i in range(1, K + 1)}
|
||||
|
||||
|
||||
def test_pods_filter_pagination_and_ordering(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token,
|
||||
insert_metrics,
|
||||
) -> None:
|
||||
"""filterByPodStatus composed with pagination + name ordering. The full-scope
|
||||
status keyset is resolved before slicing, so total reflects the full matched
|
||||
set and pages stay disjoint + complete on both the metric-ordering branch
|
||||
(paginateWithBackfill) and the name-ordering branch (PaginateMetadataByName).
|
||||
crashloopbackoff matches 4 pods in pods_phases.jsonl: clbo-a, clbo-b, run-p, unk-p."""
|
||||
now = datetime.now(tz=UTC).replace(microsecond=0)
|
||||
insert_metrics(
|
||||
_load_pods_metrics(
|
||||
"inframonitoring/pods_phases.jsonl",
|
||||
base_time=now - timedelta(minutes=4),
|
||||
)
|
||||
)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
start = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end = int(now.timestamp() * 1000)
|
||||
matched = {"clbo-a", "clbo-b", "run-p", "unk-p"}
|
||||
|
||||
# Metric-ordering branch (default cpu order): total is invariant across a paged
|
||||
# walk and the pages are disjoint + cover the full matched set.
|
||||
seen: list[str] = []
|
||||
totals: set[int] = set()
|
||||
for offset in (0, 2, 4):
|
||||
resp = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": start,
|
||||
"end": end,
|
||||
"limit": 2,
|
||||
"offset": offset,
|
||||
"filter": {"filterByPodStatus": ["crashloopbackoff"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.OK, resp.text
|
||||
data = resp.json()["data"]
|
||||
totals.add(data["total"])
|
||||
assert len(data["records"]) == max(0, min(2, 4 - offset)), f"offset={offset}: {data['records']!r}"
|
||||
seen.extend(r["meta"]["k8s.pod.name"] for r in data["records"])
|
||||
assert totals == {4}, f"total not invariant under filter+pagination: {totals}"
|
||||
assert len(seen) == 4, f"pages overlapped: {seen}"
|
||||
assert set(seen) == matched
|
||||
|
||||
# Name-ordering branch (orderBy k8s.pod.name asc, groupBy empty): the filtered
|
||||
# set is returned in name order; total is the full matched count.
|
||||
ordered = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": start,
|
||||
"end": end,
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": ["crashloopbackoff"]},
|
||||
"orderBy": {"key": {"name": "k8s.pod.name"}, "direction": "asc"},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert ordered.status_code == HTTPStatus.OK, ordered.text
|
||||
odata = ordered.json()["data"]
|
||||
assert odata["total"] == 4
|
||||
assert [r["meta"]["k8s.pod.name"] for r in odata["records"]] == ["clbo-a", "clbo-b", "run-p", "unk-p"]
|
||||
|
||||
# Second page of the name branch: PaginateMetadataByName slices the filtered set
|
||||
# correctly (offset past the first 2 matched -> the last 2), total unchanged.
|
||||
page2 = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": start,
|
||||
"end": end,
|
||||
"limit": 2,
|
||||
"offset": 2,
|
||||
"filter": {"filterByPodStatus": ["crashloopbackoff"]},
|
||||
"orderBy": {"key": {"name": "k8s.pod.name"}, "direction": "asc"},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert page2.status_code == HTTPStatus.OK, page2.text
|
||||
p2 = page2.json()["data"]
|
||||
assert p2["total"] == 4
|
||||
assert [r["meta"]["k8s.pod.name"] for r in p2["records"]] == ["run-p", "unk-p"]
|
||||
|
||||
|
||||
# orderBy keys per pods_constants.go:42-48 (snake_case request keys, camelCase
|
||||
# response fields). k8s.pod.name sorts via the metadata-name branch
|
||||
# (PaginateMetadataByName) and is only allowed when groupBy is empty.
|
||||
@@ -592,6 +685,16 @@ def test_pods_orderby( # pylint: disable=too-many-arguments,too-many-positional
|
||||
"is only allowed when groupBy is empty",
|
||||
id="orderby_podname_with_groupby",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByPodStatus": ["Bogus"]}},
|
||||
"invalid filter by pod status",
|
||||
id="filter_by_pod_status_invalid",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByPodStatus": ["running", "Bogus"]}},
|
||||
"invalid filter by pod status",
|
||||
id="filter_by_pod_status_invalid_member",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_pods_validation_errors(
|
||||
@@ -697,6 +800,44 @@ def test_pods_status_list_mode(
|
||||
if other != bucket:
|
||||
assert rec["podCountsByStatus"][other] == 0, f"expected {other}=0 when status={expected_status}, got {rec['podCountsByStatus']}"
|
||||
|
||||
# filterByPodStatus (secondary filter, applied after status is assigned):
|
||||
# a set containing the pod's status keeps it; a set without it filters it
|
||||
# out. Wire value is case-insensitive (valuer lowercases). Multi-select is
|
||||
# OR: [own_status, other] still keeps the pod.
|
||||
for variant in ([expected_status], [expected_status.upper()], [expected_status, "running"]):
|
||||
matched = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": f"k8s.pod.name = '{pod_name}'", "filterByPodStatus": variant},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert matched.status_code == HTTPStatus.OK, matched.text
|
||||
mdata = matched.json()["data"]
|
||||
assert mdata["total"] == 1, f"filterByPodStatus={variant!r} should keep {pod_name}"
|
||||
assert mdata["records"][0]["podCountsByStatus"][bucket] == 1
|
||||
|
||||
# None of the seeded pods resolves to Running or OOMKilled -> reliable
|
||||
# mismatches, as a single value and as an all-absent multi-select set.
|
||||
for fbps in (["running"], ["running", "oomKilled"]):
|
||||
filtered_out = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": f"k8s.pod.name = '{pod_name}'", "filterByPodStatus": fbps},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert filtered_out.status_code == HTTPStatus.OK, filtered_out.text
|
||||
assert filtered_out.json()["data"]["total"] == 0, f"{pod_name} must be filtered out by filterByPodStatus={fbps!r}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"pod_name,expected_restarts",
|
||||
@@ -877,6 +1018,61 @@ def test_pods_status_grouped_mode(
|
||||
)
|
||||
assert rec["podCountsByStatus"] == expected_counts
|
||||
|
||||
# filterByPodStatus in grouped mode: the group is kept because >=1 pod
|
||||
# matches, and only the filtered buckets are populated (others zeroed).
|
||||
running = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"groupBy": [{"name": "k8s.namespace.name", "fieldDataType": "string", "fieldContext": "resource"}],
|
||||
"filter": {"filterByPodStatus": ["running"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert running.status_code == HTTPStatus.OK, running.text
|
||||
rdata = running.json()["data"]
|
||||
assert rdata["total"] == 1
|
||||
assert rdata["records"][0]["podCountsByStatus"] == expected_status_counts(running=2)
|
||||
|
||||
# Multi-select is OR: both requested buckets populated (union), others zeroed.
|
||||
multi = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"groupBy": [{"name": "k8s.namespace.name", "fieldDataType": "string", "fieldContext": "resource"}],
|
||||
"filter": {"filterByPodStatus": ["running", "crashLoopBackOff"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert multi.status_code == HTTPStatus.OK, multi.text
|
||||
mdata = multi.json()["data"]
|
||||
assert mdata["total"] == 1
|
||||
assert mdata["records"][0]["podCountsByStatus"] == expected_status_counts(running=2, crashLoopBackOff=1)
|
||||
|
||||
# A set fully absent from the group -> group dropped, empty page (single
|
||||
# and multi-select both).
|
||||
for fbps in (["oomKilled"], ["oomKilled", "unknown"]):
|
||||
absent = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"groupBy": [{"name": "k8s.namespace.name", "fieldDataType": "string", "fieldContext": "resource"}],
|
||||
"filter": {"filterByPodStatus": fbps},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert absent.status_code == HTTPStatus.OK, absent.text
|
||||
assert absent.json()["data"]["total"] == 0, f"group must be dropped by filterByPodStatus={fbps!r}"
|
||||
|
||||
|
||||
def test_pods_restarts_grouped_mode(
|
||||
signoz: types.SigNoz,
|
||||
@@ -966,3 +1162,25 @@ def test_pods_status_missing_metric_warning(
|
||||
for bucket in STATUS_BUCKETS:
|
||||
assert rec["podCountsByStatus"][bucket] == 0, f"expected {bucket}=0 when gated off, got {rec['podCountsByStatus']}"
|
||||
assert rec["podRestarts"] == -1
|
||||
|
||||
# filterByPodStatus + missing status metric: the up-front gate surfaces the
|
||||
# warning and returns an empty page (Total 0) instead of silently filtering
|
||||
# everything out.
|
||||
filtered = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": "k8s.pod.name = 'miss-p1'", "filterByPodStatus": ["running"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert filtered.status_code == HTTPStatus.OK, filtered.text
|
||||
fdata = filtered.json()["data"]
|
||||
assert fdata["total"] == 0
|
||||
assert fdata["records"] == []
|
||||
fwarn = fdata.get("warning") or {}
|
||||
fmsgs = ([fwarn["message"]] if fwarn.get("message") else []) + [w["message"] for w in fwarn.get("warnings", [])]
|
||||
assert any("Pod status could not be computed" in m for m in fmsgs), f"gate warning missing on filtered call: {fmsgs!r}"
|
||||
|
||||
@@ -10,6 +10,7 @@ import requests
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.fs import get_testdata_file_path
|
||||
from fixtures.inframonitoring import expected_status_counts
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.querier import compare_values, get_all_warnings
|
||||
|
||||
@@ -312,6 +313,115 @@ def test_nodes_filter_invalid(
|
||||
assert any(err_substr in e["message"] for e in body["error"]["errors"]), f"{err_substr!r} not surfaced: {body['error']['errors']!r}"
|
||||
|
||||
|
||||
def test_nodes_filter_by_pod_status(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token,
|
||||
insert_metrics,
|
||||
) -> None:
|
||||
"""filterByPodStatus on nodes: a node is kept when >=1 of its pods matches
|
||||
the requested display status, and podCountsByStatus reflects only that
|
||||
status (others 0); an absent status yields an empty page. Reuses
|
||||
clusters_pod_phases.jsonl (carries k8s.node.name + full status metrics):
|
||||
pp-node has running=3, crashLoopBackOff=1, error=1, evicted=1, pending=1."""
|
||||
now = datetime.now(tz=UTC).replace(microsecond=0)
|
||||
insert_metrics(
|
||||
Metrics.load_from_file(
|
||||
get_testdata_file_path("inframonitoring/clusters_pod_phases.jsonl"),
|
||||
base_time=now - timedelta(minutes=4),
|
||||
)
|
||||
)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Multi-select is OR -> the union of requested buckets (others zeroed).
|
||||
for fbps, expected in (
|
||||
(["running"], expected_status_counts(running=3)),
|
||||
(["CrashLoopBackOff"], expected_status_counts(crashLoopBackOff=1)),
|
||||
(["running", "CrashLoopBackOff"], expected_status_counts(running=3, crashLoopBackOff=1)),
|
||||
):
|
||||
resp = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": fbps},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.OK, resp.text
|
||||
data = resp.json()["data"]
|
||||
assert data["total"] == 1
|
||||
rec = data["records"][0]
|
||||
assert rec["nodeName"] == "pp-node"
|
||||
assert rec["podCountsByStatus"] == expected
|
||||
|
||||
# A set fully absent from the node -> empty page (single and multi).
|
||||
for fbps in (["completed"], ["completed", "oomKilled"]):
|
||||
absent = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": fbps},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert absent.status_code == HTTPStatus.OK, absent.text
|
||||
assert absent.json()["data"]["total"] == 0, f"node must be dropped by filterByPodStatus={fbps!r}"
|
||||
|
||||
# Combined filterByPodStatus + filterByNodeReadiness = AND. pp-node is Ready
|
||||
# and runs pods -> kept when both match; dropped if either side has no match.
|
||||
both_match = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": ["running"], "filterByNodeReadiness": ["ready"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert both_match.status_code == HTTPStatus.OK, both_match.text
|
||||
bdata = both_match.json()["data"]
|
||||
assert bdata["total"] == 1
|
||||
assert bdata["records"][0]["nodeName"] == "pp-node"
|
||||
|
||||
# readiness side fails (pp-node is not not_ready) -> dropped.
|
||||
readiness_fails = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": ["running"], "filterByNodeReadiness": ["not_ready"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert readiness_fails.status_code == HTTPStatus.OK, readiness_fails.text
|
||||
assert readiness_fails.json()["data"]["total"] == 0
|
||||
|
||||
# pod-status side fails (no completed pod) -> dropped.
|
||||
status_fails = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": ["completed"], "filterByNodeReadiness": ["ready"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert status_fails.status_code == HTTPStatus.OK, status_fails.text
|
||||
assert status_fails.json()["data"]["total"] == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"node_name,expected_condition",
|
||||
[
|
||||
@@ -360,6 +470,56 @@ def test_nodes_condition_list_mode(
|
||||
else:
|
||||
assert rec["nodeCountsByReadiness"] == {"ready": 0, "notReady": 1}
|
||||
|
||||
# filterByNodeReadiness (secondary filter): matching readiness keeps the node,
|
||||
# the opposite readiness filters it out.
|
||||
matched = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": f"k8s.node.name = '{node_name}'", "filterByNodeReadiness": [expected_condition]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert matched.status_code == HTTPStatus.OK, matched.text
|
||||
mdata = matched.json()["data"]
|
||||
assert mdata["total"] == 1
|
||||
assert mdata["records"][0]["nodeName"] == node_name
|
||||
|
||||
opposite = "not_ready" if expected_condition == "ready" else "ready"
|
||||
|
||||
# Multi-select is OR: a set containing the node's condition keeps it even
|
||||
# alongside the opposite readiness.
|
||||
or_keep = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": f"k8s.node.name = '{node_name}'", "filterByNodeReadiness": [expected_condition, opposite]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert or_keep.status_code == HTTPStatus.OK, or_keep.text
|
||||
assert or_keep.json()["data"]["total"] == 1, f"multi-select should keep {node_name}"
|
||||
|
||||
dropped = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": f"k8s.node.name = '{node_name}'", "filterByNodeReadiness": [opposite]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert dropped.status_code == HTTPStatus.OK, dropped.text
|
||||
assert dropped.json()["data"]["total"] == 0
|
||||
|
||||
|
||||
def test_nodes_condition_latest_wins(
|
||||
signoz: types.SigNoz,
|
||||
@@ -451,6 +611,60 @@ def test_nodes_condition_grouped_mode(
|
||||
# meta surfaces the groupBy key.
|
||||
assert rec["meta"].get("k8s.cluster.name") == "cluster-mixed"
|
||||
|
||||
# filterByNodeReadiness in grouped mode: the group is kept (>=1 matching
|
||||
# node) and only the filtered bucket is populated.
|
||||
ready = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"groupBy": [{"name": "k8s.cluster.name", "fieldDataType": "string", "fieldContext": "resource"}],
|
||||
"filter": {"filterByNodeReadiness": ["ready"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert ready.status_code == HTTPStatus.OK, ready.text
|
||||
rdata = ready.json()["data"]
|
||||
assert rdata["total"] == 1
|
||||
assert rdata["records"][0]["nodeCountsByReadiness"] == {"ready": 2, "notReady": 0}
|
||||
|
||||
not_ready = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"groupBy": [{"name": "k8s.cluster.name", "fieldDataType": "string", "fieldContext": "resource"}],
|
||||
"filter": {"filterByNodeReadiness": ["not_ready"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert not_ready.status_code == HTTPStatus.OK, not_ready.text
|
||||
ndata = not_ready.json()["data"]
|
||||
assert ndata["total"] == 1
|
||||
assert ndata["records"][0]["nodeCountsByReadiness"] == {"ready": 0, "notReady": 1}
|
||||
|
||||
# Multi-select is OR: both buckets populated (union) for the kept group.
|
||||
both = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"groupBy": [{"name": "k8s.cluster.name", "fieldDataType": "string", "fieldContext": "resource"}],
|
||||
"filter": {"filterByNodeReadiness": ["ready", "not_ready"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert both.status_code == HTTPStatus.OK, both.text
|
||||
bdata = both.json()["data"]
|
||||
assert bdata["total"] == 1
|
||||
assert bdata["records"][0]["nodeCountsByReadiness"] == {"ready": 2, "notReady": 1}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"group_key,expected",
|
||||
@@ -581,6 +795,95 @@ def test_nodes_pagination(
|
||||
assert set(seen_nodes) == {f"page-n{i}" for i in range(1, K + 1)}
|
||||
|
||||
|
||||
def test_nodes_filter_readiness_pagination_and_ordering(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token,
|
||||
insert_metrics,
|
||||
) -> None:
|
||||
"""filterByNodeReadiness composed with pagination + name ordering. The full-scope
|
||||
readiness keyset is resolved before slicing, so total reflects the full matched
|
||||
set and pages stay disjoint + complete on both the metric-ordering branch
|
||||
(paginateWithBackfill) and the name-ordering branch (PaginateMetadataByName).
|
||||
ready matches 4 nodes in nodes_conditions.jsonl: ready-n, ready-n2, ready-n3, ready-n4."""
|
||||
now = datetime.now(tz=UTC).replace(microsecond=0)
|
||||
insert_metrics(
|
||||
Metrics.load_from_file(
|
||||
get_testdata_file_path("inframonitoring/nodes_conditions.jsonl"),
|
||||
base_time=now - timedelta(minutes=4),
|
||||
)
|
||||
)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
start = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end = int(now.timestamp() * 1000)
|
||||
matched = {"ready-n", "ready-n2", "ready-n3", "ready-n4"}
|
||||
|
||||
# Metric-ordering branch (default cpu order): total is invariant across a paged
|
||||
# walk and the pages are disjoint + cover the full matched set.
|
||||
seen: list[str] = []
|
||||
totals: set[int] = set()
|
||||
for offset in (0, 2, 4):
|
||||
resp = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": start,
|
||||
"end": end,
|
||||
"limit": 2,
|
||||
"offset": offset,
|
||||
"filter": {"filterByNodeReadiness": ["ready"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.OK, resp.text
|
||||
data = resp.json()["data"]
|
||||
totals.add(data["total"])
|
||||
assert len(data["records"]) == max(0, min(2, 4 - offset)), f"offset={offset}: {data['records']!r}"
|
||||
seen.extend(r["meta"]["k8s.node.name"] for r in data["records"])
|
||||
assert totals == {4}, f"total not invariant under filter+pagination: {totals}"
|
||||
assert len(seen) == 4, f"pages overlapped: {seen}"
|
||||
assert set(seen) == matched
|
||||
|
||||
# Name-ordering branch (orderBy k8s.node.name asc, groupBy empty): the filtered
|
||||
# set is returned in name order; total is the full matched count.
|
||||
ordered = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": start,
|
||||
"end": end,
|
||||
"limit": 50,
|
||||
"filter": {"filterByNodeReadiness": ["ready"]},
|
||||
"orderBy": {"key": {"name": "k8s.node.name"}, "direction": "asc"},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert ordered.status_code == HTTPStatus.OK, ordered.text
|
||||
odata = ordered.json()["data"]
|
||||
assert odata["total"] == 4
|
||||
assert [r["meta"]["k8s.node.name"] for r in odata["records"]] == ["ready-n", "ready-n2", "ready-n3", "ready-n4"]
|
||||
|
||||
# Second page of the name branch: PaginateMetadataByName slices the filtered set
|
||||
# correctly (offset past the first 2 matched -> the last 2), total unchanged.
|
||||
page2 = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": start,
|
||||
"end": end,
|
||||
"limit": 2,
|
||||
"offset": 2,
|
||||
"filter": {"filterByNodeReadiness": ["ready"]},
|
||||
"orderBy": {"key": {"name": "k8s.node.name"}, "direction": "asc"},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert page2.status_code == HTTPStatus.OK, page2.text
|
||||
p2 = page2.json()["data"]
|
||||
assert p2["total"] == 4
|
||||
assert [r["meta"]["k8s.node.name"] for r in p2["records"]] == ["ready-n3", "ready-n4"]
|
||||
|
||||
|
||||
# orderBy keys per nodes_constants.go:33-37 (snake_case request keys,
|
||||
# camelCase response fields). k8s.node.name sorts via the metadata-name branch
|
||||
# (PaginateMetadataByName) and is only allowed when groupBy is empty.
|
||||
@@ -682,6 +985,21 @@ def test_nodes_orderby( # pylint: disable=too-many-arguments,too-many-positiona
|
||||
"is only allowed when groupBy is empty",
|
||||
id="orderby_nodename_with_groupby",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByPodStatus": ["Bogus"]}},
|
||||
"invalid filter by pod status",
|
||||
id="filter_by_pod_status_invalid",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByNodeReadiness": ["bogus"]}},
|
||||
"invalid filter by node readiness",
|
||||
id="filter_by_node_readiness_invalid",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByNodeReadiness": ["notready"]}},
|
||||
"invalid filter by node readiness",
|
||||
id="filter_by_node_readiness_missing_underscore",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_nodes_validation_errors(
|
||||
|
||||
@@ -10,6 +10,7 @@ import requests
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.fs import get_testdata_file_path
|
||||
from fixtures.inframonitoring import expected_status_counts
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.querier import compare_values, get_all_warnings
|
||||
|
||||
@@ -309,6 +310,67 @@ def test_namespaces_filter_invalid(
|
||||
assert any(err_substr in e["message"] for e in body["error"]["errors"]), f"{err_substr!r} not surfaced: {body['error']['errors']!r}"
|
||||
|
||||
|
||||
def test_namespaces_filter_by_pod_status(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token,
|
||||
insert_metrics,
|
||||
) -> None:
|
||||
"""filterByPodStatus on namespaces: a namespace is kept when >=1 of its pods
|
||||
matches the requested display status, and podCountsByStatus reflects only
|
||||
that status (others 0); an absent status yields an empty page. Reuses
|
||||
clusters_pod_phases.jsonl (carries k8s.namespace.name + full status metrics):
|
||||
ns-x has running=3, crashLoopBackOff=1, error=1, evicted=1, pending=1."""
|
||||
now = datetime.now(tz=UTC).replace(microsecond=0)
|
||||
insert_metrics(
|
||||
Metrics.load_from_file(
|
||||
get_testdata_file_path("inframonitoring/clusters_pod_phases.jsonl"),
|
||||
base_time=now - timedelta(minutes=4),
|
||||
)
|
||||
)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Multi-select is OR -> the union of requested buckets (others zeroed).
|
||||
for fbps, expected in (
|
||||
(["running"], expected_status_counts(running=3)),
|
||||
(["CrashLoopBackOff"], expected_status_counts(crashLoopBackOff=1)),
|
||||
(["running", "CrashLoopBackOff"], expected_status_counts(running=3, crashLoopBackOff=1)),
|
||||
):
|
||||
resp = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": fbps},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.OK, resp.text
|
||||
data = resp.json()["data"]
|
||||
assert data["total"] == 1
|
||||
rec = data["records"][0]
|
||||
assert rec["namespaceName"] == "ns-x"
|
||||
assert rec["podCountsByStatus"] == expected
|
||||
|
||||
# A set fully absent from the namespace -> empty page (single and multi).
|
||||
for fbps in (["completed"], ["completed", "oomKilled"]):
|
||||
absent = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": fbps},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert absent.status_code == HTTPStatus.OK, absent.text
|
||||
assert absent.json()["data"]["total"] == 0, f"namespace must be dropped by filterByPodStatus={fbps!r}"
|
||||
|
||||
|
||||
# Float record fields compared with tolerance; everything else compared with ==.
|
||||
_GROUPBY_FLOAT_FIELDS = {
|
||||
"namespaceCPU",
|
||||
@@ -597,6 +659,11 @@ def test_namespaces_orderby( # pylint: disable=too-many-arguments,too-many-posi
|
||||
"is only allowed when groupBy is empty",
|
||||
id="orderby_nsname_with_groupby",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByPodStatus": ["Bogus"]}},
|
||||
"invalid filter by pod status",
|
||||
id="filter_by_pod_status_invalid",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_namespaces_validation_errors(
|
||||
|
||||
@@ -363,6 +363,51 @@ def test_clusters_node_readiness_aggregation(
|
||||
assert rec["clusterName"] == "rn-cluster"
|
||||
assert rec["nodeCountsByReadiness"] == {"ready": 3, "notReady": 2}
|
||||
|
||||
# filterByNodeReadiness: cluster kept (>=1 matching node), only the filtered
|
||||
# bucket populated.
|
||||
ready = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": "k8s.cluster.name = 'rn-cluster'", "filterByNodeReadiness": ["ready"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert ready.status_code == HTTPStatus.OK, ready.text
|
||||
assert ready.json()["data"]["records"][0]["nodeCountsByReadiness"] == {"ready": 3, "notReady": 0}
|
||||
|
||||
not_ready = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": "k8s.cluster.name = 'rn-cluster'", "filterByNodeReadiness": ["not_ready"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert not_ready.status_code == HTTPStatus.OK, not_ready.text
|
||||
assert not_ready.json()["data"]["records"][0]["nodeCountsByReadiness"] == {"ready": 0, "notReady": 2}
|
||||
|
||||
# Multi-select is OR: both buckets populated (union) for the kept cluster.
|
||||
both = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": "k8s.cluster.name = 'rn-cluster'", "filterByNodeReadiness": ["ready", "not_ready"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert both.status_code == HTTPStatus.OK, both.text
|
||||
assert both.json()["data"]["records"][0]["nodeCountsByReadiness"] == {"ready": 3, "notReady": 2}
|
||||
|
||||
|
||||
def test_clusters_pod_status_aggregation(
|
||||
signoz: types.SigNoz,
|
||||
@@ -404,6 +449,95 @@ def test_clusters_pod_status_aggregation(
|
||||
# All status metrics present -> gate satisfied -> no status warning.
|
||||
assert all("Pod status could not be computed" not in w["message"] for w in get_all_warnings(response.json()))
|
||||
|
||||
# filterByPodStatus: cluster kept (>=1 matching pod), only the filtered
|
||||
# buckets populated. Multi-select is OR -> the union of requested buckets.
|
||||
for fbps, expected in (
|
||||
(["running"], expected_status_counts(running=3)),
|
||||
(["CrashLoopBackOff"], expected_status_counts(crashLoopBackOff=1)),
|
||||
(["running", "CrashLoopBackOff"], expected_status_counts(running=3, crashLoopBackOff=1)),
|
||||
):
|
||||
resp = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": "k8s.cluster.name = 'pp-cluster'", "filterByPodStatus": fbps},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.OK, resp.text
|
||||
fdata = resp.json()["data"]
|
||||
assert fdata["total"] == 1
|
||||
assert fdata["records"][0]["podCountsByStatus"] == expected
|
||||
|
||||
# A set fully absent from the cluster -> empty page (single and multi).
|
||||
for fbps in (["completed"], ["completed", "oomKilled"]):
|
||||
absent = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": "k8s.cluster.name = 'pp-cluster'", "filterByPodStatus": fbps},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert absent.status_code == HTTPStatus.OK, absent.text
|
||||
assert absent.json()["data"]["total"] == 0, f"cluster must be dropped by filterByPodStatus={fbps!r}"
|
||||
|
||||
# Combined filterByPodStatus + filterByNodeReadiness = AND. pp-cluster has a
|
||||
# Ready node and Running pods -> kept when both match; dropped if either side
|
||||
# has no match.
|
||||
both_match = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": "k8s.cluster.name = 'pp-cluster'", "filterByPodStatus": ["running"], "filterByNodeReadiness": ["ready"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert both_match.status_code == HTTPStatus.OK, both_match.text
|
||||
bdata = both_match.json()["data"]
|
||||
assert bdata["total"] == 1
|
||||
assert bdata["records"][0]["podCountsByStatus"] == expected_status_counts(running=3)
|
||||
assert bdata["records"][0]["nodeCountsByReadiness"] == {"ready": 1, "notReady": 0}
|
||||
|
||||
# readiness side fails (no not_ready node) -> dropped.
|
||||
readiness_fails = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": "k8s.cluster.name = 'pp-cluster'", "filterByPodStatus": ["running"], "filterByNodeReadiness": ["not_ready"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert readiness_fails.status_code == HTTPStatus.OK, readiness_fails.text
|
||||
assert readiness_fails.json()["data"]["total"] == 0
|
||||
|
||||
# pod-status side fails (no completed pod) -> dropped.
|
||||
status_fails = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": "k8s.cluster.name = 'pp-cluster'", "filterByPodStatus": ["completed"], "filterByNodeReadiness": ["ready"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert status_fails.status_code == HTTPStatus.OK, status_fails.text
|
||||
assert status_fails.json()["data"]["total"] == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"group_key,expected",
|
||||
@@ -629,6 +763,21 @@ def test_clusters_orderby( # pylint: disable=too-many-arguments,too-many-positi
|
||||
"is only allowed when groupBy is empty",
|
||||
id="orderby_clustername_with_groupby",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByPodStatus": ["Bogus"]}},
|
||||
"invalid filter by pod status",
|
||||
id="filter_by_pod_status_invalid",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByNodeReadiness": ["bogus"]}},
|
||||
"invalid filter by node readiness",
|
||||
id="filter_by_node_readiness_invalid",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByNodeReadiness": ["notready"]}},
|
||||
"invalid filter by node readiness",
|
||||
id="filter_by_node_readiness_missing_underscore",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_clusters_validation_errors(
|
||||
|
||||
@@ -379,6 +379,45 @@ def test_deployments_pod_status_aggregation(
|
||||
# All status metrics present -> gate satisfied -> no status warning.
|
||||
assert all("Pod status could not be computed" not in w["message"] for w in get_all_warnings(response.json()))
|
||||
|
||||
# filterByPodStatus: deployment kept (>=1 matching pod), only the filtered
|
||||
# buckets populated. Multi-select is OR -> the union of requested buckets.
|
||||
for fbps, expected in (
|
||||
(["running"], expected_status_counts(running=3)),
|
||||
(["CrashLoopBackOff"], expected_status_counts(crashLoopBackOff=1)),
|
||||
(["running", "CrashLoopBackOff"], expected_status_counts(running=3, crashLoopBackOff=1)),
|
||||
):
|
||||
resp = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": "k8s.deployment.name = 'pp-dep'", "filterByPodStatus": fbps},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.OK, resp.text
|
||||
fdata = resp.json()["data"]
|
||||
assert fdata["total"] == 1
|
||||
assert fdata["records"][0]["podCountsByStatus"] == expected
|
||||
|
||||
# A set fully absent from the deployment -> empty page (single and multi).
|
||||
for fbps in (["completed"], ["completed", "oomKilled"]):
|
||||
absent = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"expression": "k8s.deployment.name = 'pp-dep'", "filterByPodStatus": fbps},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert absent.status_code == HTTPStatus.OK, absent.text
|
||||
assert absent.json()["data"]["total"] == 0, f"deployment must be dropped by filterByPodStatus={fbps!r}"
|
||||
|
||||
|
||||
def test_deployments_desired_available_counts(
|
||||
signoz: types.SigNoz,
|
||||
@@ -785,6 +824,11 @@ def test_deployments_orderby( # pylint: disable=too-many-arguments,too-many-pos
|
||||
"is only allowed when groupBy is empty",
|
||||
id="orderby_depname_with_groupby",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByPodStatus": ["Bogus"]}},
|
||||
"invalid filter by pod status",
|
||||
id="filter_by_pod_status_invalid",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_deployments_validation_errors(
|
||||
|
||||
@@ -10,6 +10,7 @@ import requests
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.fs import get_testdata_file_path
|
||||
from fixtures.inframonitoring import expected_status_counts
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.querier import compare_values, get_all_warnings
|
||||
|
||||
@@ -328,6 +329,77 @@ def test_statefulsets_base_filter_drops_non_statefulset_pods(
|
||||
# No empty-name group leaking through.
|
||||
assert all(r["statefulSetName"] != "" for r in data["records"])
|
||||
|
||||
# filterByPodStatus: ns-ss (ns-ss-p1 Running + ns-ss-p1-clbo CrashLoopBackOff)
|
||||
# is kept when >=1 pod matches; counts reflect only the filtered status; an
|
||||
# absent status yields an empty page. Metric aggregation stays undistorted.
|
||||
unfiltered_cpu = rec["statefulSetCPU"]
|
||||
|
||||
running = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": ["running"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert running.status_code == HTTPStatus.OK, running.text
|
||||
rdata = running.json()["data"]
|
||||
assert rdata["total"] == 1
|
||||
rrec = rdata["records"][0]
|
||||
assert rrec["statefulSetName"] == "ns-ss"
|
||||
assert rrec["podCountsByStatus"] == expected_status_counts(running=1)
|
||||
assert compare_values(rrec["statefulSetCPU"], unfiltered_cpu, 1e-6), "filterByPodStatus distorted statefulSetCPU"
|
||||
|
||||
clbo = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": ["CrashLoopBackOff"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert clbo.status_code == HTTPStatus.OK, clbo.text
|
||||
cdata = clbo.json()["data"]
|
||||
assert cdata["total"] == 1
|
||||
assert cdata["records"][0]["podCountsByStatus"] == expected_status_counts(crashLoopBackOff=1)
|
||||
|
||||
# Multi-select is OR: both requested buckets populated (union), others zeroed.
|
||||
multi = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": ["running", "CrashLoopBackOff"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert multi.status_code == HTTPStatus.OK, multi.text
|
||||
assert multi.json()["data"]["records"][0]["podCountsByStatus"] == expected_status_counts(running=1, crashLoopBackOff=1)
|
||||
|
||||
# A set fully absent from the group -> empty page (single and multi-select).
|
||||
for fbps in (["pending"], ["pending", "oomKilled"]):
|
||||
absent = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": fbps},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert absent.status_code == HTTPStatus.OK, absent.text
|
||||
assert absent.json()["data"]["total"] == 0, f"group must be dropped by filterByPodStatus={fbps!r}"
|
||||
|
||||
|
||||
# Float record fields compared with tolerance; everything else compared with ==.
|
||||
_GROUPBY_FLOAT_FIELDS = {
|
||||
@@ -657,6 +729,11 @@ def test_statefulsets_orderby( # pylint: disable=too-many-arguments,too-many-po
|
||||
"is only allowed when groupBy is empty",
|
||||
id="orderby_ssname_with_groupby",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByPodStatus": ["Bogus"]}},
|
||||
"invalid filter by pod status",
|
||||
id="filter_by_pod_status_invalid",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_statefulsets_validation_errors(
|
||||
|
||||
@@ -10,6 +10,7 @@ import requests
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.fs import get_testdata_file_path
|
||||
from fixtures.inframonitoring import expected_status_counts
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.querier import compare_values, get_all_warnings
|
||||
|
||||
@@ -460,6 +461,77 @@ def test_jobs_base_filter_drops_non_job_pods(
|
||||
assert rec["jobName"] == "nj-job"
|
||||
assert all(r["jobName"] != "" for r in data["records"])
|
||||
|
||||
# filterByPodStatus: nj-job (nj-job-p1 Running + nj-job-p1-clbo CrashLoopBackOff)
|
||||
# is kept when >=1 pod matches; counts reflect only the filtered status; an
|
||||
# absent status yields an empty page. Metric aggregation stays undistorted.
|
||||
unfiltered_cpu = rec["jobCPU"]
|
||||
|
||||
running = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": ["running"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert running.status_code == HTTPStatus.OK, running.text
|
||||
rdata = running.json()["data"]
|
||||
assert rdata["total"] == 1
|
||||
rrec = rdata["records"][0]
|
||||
assert rrec["jobName"] == "nj-job"
|
||||
assert rrec["podCountsByStatus"] == expected_status_counts(running=1)
|
||||
assert compare_values(rrec["jobCPU"], unfiltered_cpu, 1e-6), "filterByPodStatus distorted jobCPU"
|
||||
|
||||
clbo = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": ["CrashLoopBackOff"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert clbo.status_code == HTTPStatus.OK, clbo.text
|
||||
cdata = clbo.json()["data"]
|
||||
assert cdata["total"] == 1
|
||||
assert cdata["records"][0]["podCountsByStatus"] == expected_status_counts(crashLoopBackOff=1)
|
||||
|
||||
# Multi-select is OR: both requested buckets populated (union), others zeroed.
|
||||
multi = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": ["running", "CrashLoopBackOff"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert multi.status_code == HTTPStatus.OK, multi.text
|
||||
assert multi.json()["data"]["records"][0]["podCountsByStatus"] == expected_status_counts(running=1, crashLoopBackOff=1)
|
||||
|
||||
# A set fully absent from the group -> empty page (single and multi-select).
|
||||
for fbps in (["pending"], ["pending", "oomKilled"]):
|
||||
absent = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": fbps},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert absent.status_code == HTTPStatus.OK, absent.text
|
||||
assert absent.json()["data"]["total"] == 0, f"group must be dropped by filterByPodStatus={fbps!r}"
|
||||
|
||||
|
||||
# Float record fields compared with tolerance; everything else compared with ==.
|
||||
_GROUPBY_FLOAT_FIELDS = {
|
||||
@@ -796,6 +868,11 @@ def test_jobs_orderby( # pylint: disable=too-many-arguments,too-many-positional
|
||||
"is only allowed when groupBy is empty",
|
||||
id="orderby_jobname_with_groupby",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByPodStatus": ["Bogus"]}},
|
||||
"invalid filter by pod status",
|
||||
id="filter_by_pod_status_invalid",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_jobs_validation_errors(
|
||||
|
||||
@@ -10,6 +10,7 @@ import requests
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.fs import get_testdata_file_path
|
||||
from fixtures.inframonitoring import expected_status_counts
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.querier import compare_values, get_all_warnings
|
||||
|
||||
@@ -335,6 +336,77 @@ def test_daemonsets_base_filter_drops_non_daemonset_pods(
|
||||
assert rec["daemonSetName"] == "nd-ds"
|
||||
assert all(r["daemonSetName"] != "" for r in data["records"])
|
||||
|
||||
# filterByPodStatus: nd-ds (nd-ds-p1 Running + nd-ds-p1-clbo CrashLoopBackOff)
|
||||
# is kept when >=1 pod matches; counts reflect only the filtered status; an
|
||||
# absent status yields an empty page. Metric aggregation stays undistorted.
|
||||
unfiltered_cpu = rec["daemonSetCPU"]
|
||||
|
||||
running = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": ["running"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert running.status_code == HTTPStatus.OK, running.text
|
||||
rdata = running.json()["data"]
|
||||
assert rdata["total"] == 1
|
||||
rrec = rdata["records"][0]
|
||||
assert rrec["daemonSetName"] == "nd-ds"
|
||||
assert rrec["podCountsByStatus"] == expected_status_counts(running=1)
|
||||
assert compare_values(rrec["daemonSetCPU"], unfiltered_cpu, 1e-6), "filterByPodStatus distorted daemonSetCPU"
|
||||
|
||||
clbo = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": ["CrashLoopBackOff"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert clbo.status_code == HTTPStatus.OK, clbo.text
|
||||
cdata = clbo.json()["data"]
|
||||
assert cdata["total"] == 1
|
||||
assert cdata["records"][0]["podCountsByStatus"] == expected_status_counts(crashLoopBackOff=1)
|
||||
|
||||
# Multi-select is OR: both requested buckets populated (union), others zeroed.
|
||||
multi = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": ["running", "CrashLoopBackOff"]},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert multi.status_code == HTTPStatus.OK, multi.text
|
||||
assert multi.json()["data"]["records"][0]["podCountsByStatus"] == expected_status_counts(running=1, crashLoopBackOff=1)
|
||||
|
||||
# A set fully absent from the group -> empty page (single and multi-select).
|
||||
for fbps in (["pending"], ["pending", "oomKilled"]):
|
||||
absent = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"filter": {"filterByPodStatus": fbps},
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert absent.status_code == HTTPStatus.OK, absent.text
|
||||
assert absent.json()["data"]["total"] == 0, f"group must be dropped by filterByPodStatus={fbps!r}"
|
||||
|
||||
|
||||
# Float record fields compared with tolerance; everything else compared with ==.
|
||||
_GROUPBY_FLOAT_FIELDS = {
|
||||
@@ -665,6 +737,11 @@ def test_daemonsets_orderby( # pylint: disable=too-many-arguments,too-many-posi
|
||||
"is only allowed when groupBy is empty",
|
||||
id="orderby_dsname_with_groupby",
|
||||
),
|
||||
pytest.param(
|
||||
{"filter": {"filterByPodStatus": ["Bogus"]}},
|
||||
"invalid filter by pod status",
|
||||
id="filter_by_pod_status_invalid",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_daemonsets_validation_errors(
|
||||
|
||||
Reference in New Issue
Block a user