diff --git a/api/schema.graphqls b/api/schema.graphqls index 410bdd5..8f5e1c7 100644 --- a/api/schema.graphqls +++ b/api/schema.graphqls @@ -262,6 +262,7 @@ enum SortByAggregate { type NodeMetrics { host: String! + state: String! subCluster: String! metrics: [JobMetricWithName!]! } @@ -375,6 +376,7 @@ type Query { nodeMetricsList( cluster: String! subCluster: String! + stateFilter: String! nodeFilter: String! scopes: [MetricScope!] metrics: [String!] diff --git a/internal/graph/generated/generated.go b/internal/graph/generated/generated.go index e5f59e4..a3b1a1d 100644 --- a/internal/graph/generated/generated.go +++ b/internal/graph/generated/generated.go @@ -288,6 +288,7 @@ type ComplexityRoot struct { NodeMetrics struct { Host func(childComplexity int) int Metrics func(childComplexity int) int + State func(childComplexity int) int SubCluster func(childComplexity int) int } @@ -329,7 +330,7 @@ type ComplexityRoot struct { JobsStatistics func(childComplexity int, filter []*model.JobFilter, metrics []string, page *model.PageRequest, sortBy *model.SortByAggregate, groupBy *model.Aggregate, numDurationBins *string, numMetricBins *int) int Node func(childComplexity int, id string) int NodeMetrics func(childComplexity int, cluster string, nodes []string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time) int - NodeMetricsList func(childComplexity int, cluster string, subCluster string, nodeFilter string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time, page *model.PageRequest, resolution *int) int + NodeMetricsList func(childComplexity int, cluster string, subCluster string, stateFilter string, nodeFilter string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time, page *model.PageRequest, resolution *int) int NodeStates func(childComplexity int, filter []*model.NodeFilter) int NodeStatesTimed func(childComplexity int, filter []*model.NodeFilter, typeArg string) int Nodes func(childComplexity int, filter []*model.NodeFilter, order *model.OrderByInput) int @@ -483,7 +484,7 @@ type QueryResolver interface { JobsFootprints(ctx context.Context, filter []*model.JobFilter, metrics []string) (*model.Footprints, error) RooflineHeatmap(ctx context.Context, filter []*model.JobFilter, rows int, cols int, minX float64, minY float64, maxX float64, maxY float64) ([][]float64, error) NodeMetrics(ctx context.Context, cluster string, nodes []string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time) ([]*model.NodeMetrics, error) - NodeMetricsList(ctx context.Context, cluster string, subCluster string, nodeFilter string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time, page *model.PageRequest, resolution *int) (*model.NodesResultList, error) + NodeMetricsList(ctx context.Context, cluster string, subCluster string, stateFilter string, nodeFilter string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time, page *model.PageRequest, resolution *int) (*model.NodesResultList, error) } type SubClusterResolver interface { NumberOfNodes(ctx context.Context, obj *schema.SubCluster) (int, error) @@ -1581,6 +1582,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.NodeMetrics.Metrics(childComplexity), true + case "NodeMetrics.state": + if e.complexity.NodeMetrics.State == nil { + break + } + + return e.complexity.NodeMetrics.State(childComplexity), true + case "NodeMetrics.subCluster": if e.complexity.NodeMetrics.SubCluster == nil { break @@ -1823,7 +1831,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.complexity.Query.NodeMetricsList(childComplexity, args["cluster"].(string), args["subCluster"].(string), args["nodeFilter"].(string), args["scopes"].([]schema.MetricScope), args["metrics"].([]string), args["from"].(time.Time), args["to"].(time.Time), args["page"].(*model.PageRequest), args["resolution"].(*int)), true + return e.complexity.Query.NodeMetricsList(childComplexity, args["cluster"].(string), args["subCluster"].(string), args["stateFilter"].(string), args["nodeFilter"].(string), args["scopes"].([]schema.MetricScope), args["metrics"].([]string), args["from"].(time.Time), args["to"].(time.Time), args["page"].(*model.PageRequest), args["resolution"].(*int)), true case "Query.nodeStates": if e.complexity.Query.NodeStates == nil { @@ -2667,6 +2675,7 @@ enum SortByAggregate { type NodeMetrics { host: String! + state: String! subCluster: String! metrics: [JobMetricWithName!]! } @@ -2780,6 +2789,7 @@ type Query { nodeMetricsList( cluster: String! subCluster: String! + stateFilter: String! nodeFilter: String! scopes: [MetricScope!] metrics: [String!] @@ -3224,41 +3234,46 @@ func (ec *executionContext) field_Query_nodeMetricsList_args(ctx context.Context return nil, err } args["subCluster"] = arg1 - arg2, err := graphql.ProcessArgField(ctx, rawArgs, "nodeFilter", ec.unmarshalNString2string) + arg2, err := graphql.ProcessArgField(ctx, rawArgs, "stateFilter", ec.unmarshalNString2string) if err != nil { return nil, err } - args["nodeFilter"] = arg2 - arg3, err := graphql.ProcessArgField(ctx, rawArgs, "scopes", ec.unmarshalOMetricScope2ᚕgithubᚗcomᚋClusterCockpitᚋccᚑlibᚋschemaᚐMetricScopeᚄ) + args["stateFilter"] = arg2 + arg3, err := graphql.ProcessArgField(ctx, rawArgs, "nodeFilter", ec.unmarshalNString2string) if err != nil { return nil, err } - args["scopes"] = arg3 - arg4, err := graphql.ProcessArgField(ctx, rawArgs, "metrics", ec.unmarshalOString2ᚕstringᚄ) + args["nodeFilter"] = arg3 + arg4, err := graphql.ProcessArgField(ctx, rawArgs, "scopes", ec.unmarshalOMetricScope2ᚕgithubᚗcomᚋClusterCockpitᚋccᚑlibᚋschemaᚐMetricScopeᚄ) if err != nil { return nil, err } - args["metrics"] = arg4 - arg5, err := graphql.ProcessArgField(ctx, rawArgs, "from", ec.unmarshalNTime2timeᚐTime) + args["scopes"] = arg4 + arg5, err := graphql.ProcessArgField(ctx, rawArgs, "metrics", ec.unmarshalOString2ᚕstringᚄ) if err != nil { return nil, err } - args["from"] = arg5 - arg6, err := graphql.ProcessArgField(ctx, rawArgs, "to", ec.unmarshalNTime2timeᚐTime) + args["metrics"] = arg5 + arg6, err := graphql.ProcessArgField(ctx, rawArgs, "from", ec.unmarshalNTime2timeᚐTime) if err != nil { return nil, err } - args["to"] = arg6 - arg7, err := graphql.ProcessArgField(ctx, rawArgs, "page", ec.unmarshalOPageRequest2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐPageRequest) + args["from"] = arg6 + arg7, err := graphql.ProcessArgField(ctx, rawArgs, "to", ec.unmarshalNTime2timeᚐTime) if err != nil { return nil, err } - args["page"] = arg7 - arg8, err := graphql.ProcessArgField(ctx, rawArgs, "resolution", ec.unmarshalOInt2ᚖint) + args["to"] = arg7 + arg8, err := graphql.ProcessArgField(ctx, rawArgs, "page", ec.unmarshalOPageRequest2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐPageRequest) if err != nil { return nil, err } - args["resolution"] = arg8 + args["page"] = arg8 + arg9, err := graphql.ProcessArgField(ctx, rawArgs, "resolution", ec.unmarshalOInt2ᚖint) + if err != nil { + return nil, err + } + args["resolution"] = arg9 return args, nil } @@ -10289,6 +10304,50 @@ func (ec *executionContext) fieldContext_NodeMetrics_host(_ context.Context, fie return fc, nil } +func (ec *executionContext) _NodeMetrics_state(ctx context.Context, field graphql.CollectedField, obj *model.NodeMetrics) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_NodeMetrics_state(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.State, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_NodeMetrics_state(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "NodeMetrics", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _NodeMetrics_subCluster(ctx context.Context, field graphql.CollectedField, obj *model.NodeMetrics) (ret graphql.Marshaler) { fc, err := ec.fieldContext_NodeMetrics_subCluster(ctx, field) if err != nil { @@ -10755,6 +10814,8 @@ func (ec *executionContext) fieldContext_NodesResultList_items(_ context.Context switch field.Name { case "host": return ec.fieldContext_NodeMetrics_host(ctx, field) + case "state": + return ec.fieldContext_NodeMetrics_state(ctx, field) case "subCluster": return ec.fieldContext_NodeMetrics_subCluster(ctx, field) case "metrics": @@ -12199,6 +12260,8 @@ func (ec *executionContext) fieldContext_Query_nodeMetrics(ctx context.Context, switch field.Name { case "host": return ec.fieldContext_NodeMetrics_host(ctx, field) + case "state": + return ec.fieldContext_NodeMetrics_state(ctx, field) case "subCluster": return ec.fieldContext_NodeMetrics_subCluster(ctx, field) case "metrics": @@ -12235,7 +12298,7 @@ func (ec *executionContext) _Query_nodeMetricsList(ctx context.Context, field gr }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().NodeMetricsList(rctx, fc.Args["cluster"].(string), fc.Args["subCluster"].(string), fc.Args["nodeFilter"].(string), fc.Args["scopes"].([]schema.MetricScope), fc.Args["metrics"].([]string), fc.Args["from"].(time.Time), fc.Args["to"].(time.Time), fc.Args["page"].(*model.PageRequest), fc.Args["resolution"].(*int)) + return ec.resolvers.Query().NodeMetricsList(rctx, fc.Args["cluster"].(string), fc.Args["subCluster"].(string), fc.Args["stateFilter"].(string), fc.Args["nodeFilter"].(string), fc.Args["scopes"].([]schema.MetricScope), fc.Args["metrics"].([]string), fc.Args["from"].(time.Time), fc.Args["to"].(time.Time), fc.Args["page"].(*model.PageRequest), fc.Args["resolution"].(*int)) }) if err != nil { ec.Error(ctx, err) @@ -19371,6 +19434,11 @@ func (ec *executionContext) _NodeMetrics(ctx context.Context, sel ast.SelectionS if out.Values[i] == graphql.Null { out.Invalids++ } + case "state": + out.Values[i] = ec._NodeMetrics_state(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } case "subCluster": out.Values[i] = ec._NodeMetrics_subCluster(ctx, field, obj) if out.Values[i] == graphql.Null { diff --git a/internal/graph/model/models_gen.go b/internal/graph/model/models_gen.go index cd9bc87..4cb414e 100644 --- a/internal/graph/model/models_gen.go +++ b/internal/graph/model/models_gen.go @@ -181,6 +181,7 @@ type NodeFilter struct { type NodeMetrics struct { Host string `json:"host"` + State string `json:"state"` SubCluster string `json:"subCluster"` Metrics []*JobMetricWithName `json:"metrics"` } diff --git a/internal/graph/schema.resolvers.go b/internal/graph/schema.resolvers.go index debe54a..ccd5c82 100644 --- a/internal/graph/schema.resolvers.go +++ b/internal/graph/schema.resolvers.go @@ -756,10 +756,14 @@ func (r *queryResolver) NodeMetrics(ctx context.Context, cluster string, nodes [ return nil, err } + nodeRepo := repository.GetNodeRepository() + stateMap, _ := nodeRepo.MapNodes(cluster) + nodeMetrics := make([]*model.NodeMetrics, 0, len(data)) for hostname, metrics := range data { host := &model.NodeMetrics{ Host: hostname, + State: stateMap[hostname], Metrics: make([]*model.JobMetricWithName, 0, len(metrics)*len(scopes)), } host.SubCluster, err = archive.GetSubClusterByNode(cluster, hostname) @@ -784,7 +788,7 @@ func (r *queryResolver) NodeMetrics(ctx context.Context, cluster string, nodes [ } // NodeMetricsList is the resolver for the nodeMetricsList field. -func (r *queryResolver) NodeMetricsList(ctx context.Context, cluster string, subCluster string, nodeFilter string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time, page *model.PageRequest, resolution *int) (*model.NodesResultList, error) { +func (r *queryResolver) NodeMetricsList(ctx context.Context, cluster string, subCluster string, stateFilter string, nodeFilter string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time, page *model.PageRequest, resolution *int) (*model.NodesResultList, error) { if resolution == nil { // Load from Config if config.Keys.EnableResampling != nil { defaultRes := slices.Max(config.Keys.EnableResampling.Resolutions) @@ -806,9 +810,47 @@ func (r *queryResolver) NodeMetricsList(ctx context.Context, cluster string, sub } } - data, totalNodes, hasNextPage, err := metricDataDispatcher.LoadNodeListData(cluster, subCluster, nodeFilter, metrics, scopes, *resolution, from, to, page, ctx) + // Note: This Prefilter Logic Can Be Used To Completely Switch Node Source Of Truth To SQLite DB + // Adapt and extend filters/paging/sorting in QueryNodes Function to return []string array of hostnames, input array to LoadNodeListData + // LoadNodeListData, instead of building queried nodes from topoplogy anew, directly will use QueryNodes hostname array + // Caveat: "notindb" state will not be resolvable anymore by default, or needs reverse lookup by dedicated comparison to topology data after all + preFiltered := make([]string, 0) + stateMap := make(map[string]string) + if stateFilter != "all" { + nodeRepo := repository.GetNodeRepository() + stateQuery := make([]*model.NodeFilter, 0) + // Required Filters + stateQuery = append(stateQuery, &model.NodeFilter{Cluster: &model.StringInput{Eq: &cluster}}) + if subCluster != "" { + stateQuery = append(stateQuery, &model.NodeFilter{Subcluster: &model.StringInput{Eq: &subCluster}}) + } + + if stateFilter == "notindb" { + // Backward Filtering: Add Keyword, No Additional FIlters: Returns All Nodes For Cluster (and SubCluster) + preFiltered = append(preFiltered, "exclude") + } else { + // Workaround: If no nodes match, we need at least one element for trigger in LoadNodeListData + preFiltered = append(preFiltered, stateFilter) + // Forward Filtering: Match Only selected stateFilter + var queryState schema.SchedulerState = schema.SchedulerState(stateFilter) + stateQuery = append(stateQuery, &model.NodeFilter{SchedulerState: &queryState}) + } + + stateNodes, serr := nodeRepo.QueryNodes(ctx, stateQuery, &model.OrderByInput{}) // Order not Used + if serr != nil { + cclog.Warn("error while loading node database data (Resolver.NodeMetricsList)") + return nil, serr + } + + for _, node := range stateNodes { + preFiltered = append(preFiltered, node.Hostname) + stateMap[node.Hostname] = string(node.NodeState) + } + } + + data, totalNodes, hasNextPage, err := metricDataDispatcher.LoadNodeListData(cluster, subCluster, nodeFilter, preFiltered, metrics, scopes, *resolution, from, to, page, ctx) if err != nil { - cclog.Warn("error while loading node data") + cclog.Warn("error while loading node data (Resolver.NodeMetricsList") return nil, err } @@ -816,6 +858,7 @@ func (r *queryResolver) NodeMetricsList(ctx context.Context, cluster string, sub for hostname, metrics := range data { host := &model.NodeMetrics{ Host: hostname, + State: stateMap[hostname], Metrics: make([]*model.JobMetricWithName, 0, len(metrics)*len(scopes)), } host.SubCluster, err = archive.GetSubClusterByNode(cluster, hostname) diff --git a/internal/metricDataDispatcher/dataLoader.go b/internal/metricDataDispatcher/dataLoader.go index 4f8e3b5..1257370 100644 --- a/internal/metricDataDispatcher/dataLoader.go +++ b/internal/metricDataDispatcher/dataLoader.go @@ -333,6 +333,7 @@ func LoadNodeData( func LoadNodeListData( cluster, subCluster, nodeFilter string, + preFiltered []string, metrics []string, scopes []schema.MetricScope, resolution int, @@ -351,7 +352,7 @@ func LoadNodeListData( } } - data, totalNodes, hasNextPage, err := repo.LoadNodeListData(cluster, subCluster, nodeFilter, metrics, scopes, resolution, from, to, page, ctx) + data, totalNodes, hasNextPage, err := repo.LoadNodeListData(cluster, subCluster, nodeFilter, preFiltered, metrics, scopes, resolution, from, to, page, ctx) if err != nil { if len(data) != 0 { cclog.Warnf("partial error: %s", err.Error()) diff --git a/internal/metricdata/cc-metric-store-internal.go b/internal/metricdata/cc-metric-store-internal.go index 01f4a05..54046d0 100644 --- a/internal/metricdata/cc-metric-store-internal.go +++ b/internal/metricdata/cc-metric-store-internal.go @@ -9,6 +9,7 @@ import ( "context" "encoding/json" "fmt" + "slices" "sort" "strconv" "strings" @@ -678,6 +679,7 @@ func (ccms *CCMetricStoreInternal) LoadNodeData( // Used for Systems-View Node-List func (ccms *CCMetricStoreInternal) LoadNodeListData( cluster, subCluster, nodeFilter string, + preFiltered []string, metrics []string, scopes []schema.MetricScope, resolution int, @@ -701,18 +703,37 @@ func (ccms *CCMetricStoreInternal) LoadNodeListData( } } - // 2) Filter nodes + // 2.1) Filter nodes by name if nodeFilter != "" { - filteredNodes := []string{} + filteredNodesByName := []string{} for _, node := range nodes { if strings.Contains(node, nodeFilter) { - filteredNodes = append(filteredNodes, node) + filteredNodesByName = append(filteredNodesByName, node) } } - nodes = filteredNodes + nodes = filteredNodesByName } - // 2.1) Count total nodes && Sort nodes -> Sorting invalidated after ccms return ... + // 2.2) Filter nodes by state using prefiltered match array + if len(preFiltered) > 0 { + filteredNodesByState := []string{} + if preFiltered[0] == "exclude" { // Backwards: PreFiltered contains all Nodes in DB > Return Missing Nodes + for _, node := range nodes { + if !slices.Contains(preFiltered, node) { + filteredNodesByState = append(filteredNodesByState, node) + } + } + } else { // Forwards: Prefiltered contains specific nodeState > Return Matches + for _, node := range nodes { + if slices.Contains(preFiltered, node) { + filteredNodesByState = append(filteredNodesByState, node) + } + } + } + nodes = filteredNodesByState + } + + // 2.3) Count total nodes && Sort nodes -> Sorting invalidated after return ... totalNodes = len(nodes) sort.Strings(nodes) diff --git a/internal/metricdata/cc-metric-store.go b/internal/metricdata/cc-metric-store.go index a188686..b36ad08 100644 --- a/internal/metricdata/cc-metric-store.go +++ b/internal/metricdata/cc-metric-store.go @@ -11,6 +11,7 @@ import ( "encoding/json" "fmt" "net/http" + "slices" "sort" "strings" "time" @@ -800,6 +801,7 @@ func (ccms *CCMetricStore) LoadNodeData( // Used for Systems-View Node-List func (ccms *CCMetricStore) LoadNodeListData( cluster, subCluster, nodeFilter string, + preFiltered []string, metrics []string, scopes []schema.MetricScope, resolution int, @@ -824,18 +826,37 @@ func (ccms *CCMetricStore) LoadNodeListData( } } - // 2) Filter nodes + // 2.1) Filter nodes by name if nodeFilter != "" { - filteredNodes := []string{} + filteredNodesByName := []string{} for _, node := range nodes { if strings.Contains(node, nodeFilter) { - filteredNodes = append(filteredNodes, node) + filteredNodesByName = append(filteredNodesByName, node) } } - nodes = filteredNodes + nodes = filteredNodesByName } - // 2.1) Count total nodes && Sort nodes -> Sorting invalidated after ccms return ... + // 2.2) Filter nodes by state using prefiltered match array + if len(preFiltered) > 0 { + filteredNodesByState := []string{} + if preFiltered[0] == "exclude" { // Backwards: PreFiltered contains all Nodes in DB > Return Missing Nodes + for _, node := range nodes { + if !slices.Contains(preFiltered, node) { + filteredNodesByState = append(filteredNodesByState, node) + } + } + } else { // Forwards: Prefiltered contains specific nodeState > Return Matches + for _, node := range nodes { + if slices.Contains(preFiltered, node) { + filteredNodesByState = append(filteredNodesByState, node) + } + } + } + nodes = filteredNodesByState + } + + // 2.3) Count total nodes && Sort nodes -> Sorting invalidated after return ... totalNodes = len(nodes) sort.Strings(nodes) diff --git a/internal/metricdata/metricdata.go b/internal/metricdata/metricdata.go index 3219611..cab7284 100644 --- a/internal/metricdata/metricdata.go +++ b/internal/metricdata/metricdata.go @@ -36,7 +36,7 @@ type MetricDataRepository interface { LoadNodeData(cluster string, metrics, nodes []string, scopes []schema.MetricScope, from, to time.Time, ctx context.Context) (map[string]map[string][]*schema.JobMetric, error) // Return a map of hosts to a map of metrics to a map of scopes for multiple nodes. - LoadNodeListData(cluster, subCluster, nodeFilter string, metrics []string, scopes []schema.MetricScope, resolution int, from, to time.Time, page *model.PageRequest, ctx context.Context) (map[string]schema.JobData, int, bool, error) + LoadNodeListData(cluster, subCluster, nodeFilter string, preFiltered []string, metrics []string, scopes []schema.MetricScope, resolution int, from, to time.Time, page *model.PageRequest, ctx context.Context) (map[string]schema.JobData, int, bool, error) } var metricDataRepos map[string]MetricDataRepository = map[string]MetricDataRepository{} diff --git a/internal/metricdata/prometheus.go b/internal/metricdata/prometheus.go index 2ec8558..d441d28 100644 --- a/internal/metricdata/prometheus.go +++ b/internal/metricdata/prometheus.go @@ -14,6 +14,7 @@ import ( "net/http" "os" "regexp" + "slices" "sort" "strings" "sync" @@ -495,6 +496,7 @@ func (pdb *PrometheusDataRepository) LoadScopedStats( // Implemented by NHR@FAU; Used in NodeList-View func (pdb *PrometheusDataRepository) LoadNodeListData( cluster, subCluster, nodeFilter string, + preFiltered []string, metrics []string, scopes []schema.MetricScope, resolution int, @@ -520,18 +522,37 @@ func (pdb *PrometheusDataRepository) LoadNodeListData( } } - // 2) Filter nodes + // 2.1) Filter nodes by name if nodeFilter != "" { - filteredNodes := []string{} + filteredNodesByName := []string{} for _, node := range nodes { if strings.Contains(node, nodeFilter) { - filteredNodes = append(filteredNodes, node) + filteredNodesByName = append(filteredNodesByName, node) } } - nodes = filteredNodes + nodes = filteredNodesByName } - // 2.1) Count total nodes && Sort nodes -> Sorting invalidated after return ... + // 2.2) Filter nodes by state using prefiltered match array + if len(preFiltered) > 0 { + filteredNodesByState := []string{} + if preFiltered[0] == "exclude" { // Backwards: PreFiltered contains all Nodes in DB > Return Missing Nodes + for _, node := range nodes { + if !slices.Contains(preFiltered, node) { + filteredNodesByState = append(filteredNodesByState, node) + } + } + } else { // Forwards: Prefiltered contains specific nodeState > Return Matches + for _, node := range nodes { + if slices.Contains(preFiltered, node) { + filteredNodesByState = append(filteredNodesByState, node) + } + } + } + nodes = filteredNodesByState + } + + // 2.3) Count total nodes && Sort nodes -> Sorting invalidated after return ... totalNodes = len(nodes) sort.Strings(nodes) diff --git a/internal/metricdata/utils.go b/internal/metricdata/utils.go index faae0dac..71f4a43 100644 --- a/internal/metricdata/utils.go +++ b/internal/metricdata/utils.go @@ -64,6 +64,7 @@ func (tmdr *TestMetricDataRepository) LoadNodeData( func (tmdr *TestMetricDataRepository) LoadNodeListData( cluster, subCluster, nodeFilter string, + preFiltered []string, metrics []string, scopes []schema.MetricScope, resolution int, diff --git a/internal/repository/node.go b/internal/repository/node.go index 02ccf20..45e0006 100644 --- a/internal/repository/node.go +++ b/internal/repository/node.go @@ -277,8 +277,8 @@ func (r *NodeRepository) QueryNodes( order *model.OrderByInput, // Currently unused! ) ([]*schema.Node, error) { query, qerr := AccessCheck(ctx, - sq.Select("node.hostname", "node.cluster", "node.subcluster", "node_state.node_state", - "node_state.health_state", "MAX(node_state.time_stamp) as time"). + sq.Select("hostname", "cluster", "subcluster", "node_state", + "health_state", "MAX(time_stamp) as time"). From("node"). Join("node_state ON node_state.node_id = node.id")) if qerr != nil { @@ -287,24 +287,31 @@ func (r *NodeRepository) QueryNodes( for _, f := range filters { if f.Hostname != nil { - query = buildStringCondition("node.hostname", f.Hostname, query) + query = buildStringCondition("hostname", f.Hostname, query) } if f.Cluster != nil { - query = buildStringCondition("node.cluster", f.Cluster, query) + query = buildStringCondition("cluster", f.Cluster, query) } if f.Subcluster != nil { - query = buildStringCondition("node.subcluster", f.Subcluster, query) + query = buildStringCondition("subcluster", f.Subcluster, query) } if f.SchedulerState != nil { - query = query.Where("node.node_state = ?", f.SchedulerState) + query = query.Where("node_state = ?", f.SchedulerState) + // Requires Additional time_stamp Filter: Else the last (past!) time_stamp with queried state will be returned + now := time.Now().Unix() + query = query.Where(sq.Gt{"time_stamp": (now - 60)}) } if f.HealthState != nil { - query = query.Where("node.health_state = ?", f.HealthState) + query = query.Where("health_state = ?", f.HealthState) + // Requires Additional time_stamp Filter: Else the last (past!) time_stamp with queried state will be returned + now := time.Now().Unix() + query = query.Where(sq.Gt{"time_stamp": (now - 60)}) } } - // Add Grouping after filters - query = query.GroupBy("node_state.node_id") + // Add Grouping and ORder after filters + query = query.GroupBy("node_id"). + OrderBy("hostname ASC") rows, err := query.RunWith(r.stmtCache).Query() if err != nil { @@ -360,6 +367,36 @@ func (r *NodeRepository) ListNodes(cluster string) ([]*schema.Node, error) { return nodeList, nil } +func (r *NodeRepository) MapNodes(cluster string) (map[string]string, error) { + q := sq.Select("node.hostname", "node_state.node_state", "MAX(node_state.time_stamp) as time"). + From("node"). + Join("node_state ON node_state.node_id = node.id"). + Where("node.cluster = ?", cluster). + GroupBy("node_state.node_id"). + OrderBy("node.hostname ASC") + + rows, err := q.RunWith(r.DB).Query() + if err != nil { + cclog.Warn("Error while querying node list") + return nil, err + } + + stateMap := make(map[string]string) + defer rows.Close() + for rows.Next() { + var hostname, nodestate string + var timestamp int + if err := rows.Scan(&hostname, &nodestate, ×tamp); err != nil { + cclog.Warnf("Error while scanning node list (MapNodes) at time '%d'", timestamp) + return nil, err + } + + stateMap[hostname] = nodestate + } + + return stateMap, nil +} + func (r *NodeRepository) CountStates(ctx context.Context, filters []*model.NodeFilter, column string) ([]*model.NodeStates, error) { query, qerr := AccessCheck(ctx, sq.Select("hostname", column, "MAX(time_stamp) as time").From("node")) if qerr != nil { diff --git a/web/frontend/src/Node.root.svelte b/web/frontend/src/Node.root.svelte index 85ea2f0..cf11902 100644 --- a/web/frontend/src/Node.root.svelte +++ b/web/frontend/src/Node.root.svelte @@ -16,6 +16,7 @@ Row, Col, Input, + Button, InputGroup, InputGroupText, Icon, @@ -63,6 +64,7 @@ query ($cluster: String!, $nodes: [String!], $from: Time!, $to: Time!) { nodeMetrics(cluster: $cluster, nodes: $nodes, from: $from, to: $to) { host + state subCluster metrics { name @@ -97,6 +99,16 @@ } } `; + // Node State Colors + const stateColors = { + allocated: 'success', + reserved: 'info', + idle: 'primary', + mixed: 'warning', + down: 'danger', + unknown: 'dark', + notindb: 'secondary' + } /* State Init */ let from = $state(presetFrom ? presetFrom : new Date(nowEpoch - (4 * 3600 * 1000))); @@ -123,6 +135,8 @@ }) ); + const thisNodeState = $derived($nodeMetricsData?.data?.nodeMetrics[0]?.state ? $nodeMetricsData.data.nodeMetrics[0].state : 'notindb'); + /* Effect */ $effect(() => { loadUnits($initialized); @@ -138,7 +152,7 @@ } - + {#if $initq.error} {$initq.error.message} {:else if $initq.fetching} @@ -149,19 +163,18 @@ Selected Node - + - + - { - from = newFrom; - to = newTo; - }} - /> + + + Node State + + @@ -184,6 +197,17 @@ {/if} + + + { + from = newFrom; + to = newTo; + }} + /> + - + {#if $initq.data} {#if !displayNodeOverview} @@ -191,7 +193,7 @@ - Find Node(s) + Node(s) + + + + + State + + {#each stateOptions as so} + + {/each} + + + - + {:else} - + {/if} {/if} diff --git a/web/frontend/src/systems/NodeList.svelte b/web/frontend/src/systems/NodeList.svelte index a4cec02..66bedf2 100644 --- a/web/frontend/src/systems/NodeList.svelte +++ b/web/frontend/src/systems/NodeList.svelte @@ -8,6 +8,7 @@ - `selectedMetrics [String]`: The array of selected metrics [Default []] - `selectedResolution Number?`: The selected data resolution [Default: 0] - `hostnameFilter String?`: The active hostnamefilter [Default: ""] + - `hoststateFilter String?`: The active hoststatefilter [Default: ""] - `presetSystemUnits Object`: The object of metric units [Default: null] - `from Date?`: The selected "from" date [Default: null] - `to Date?`: The selected "to" date [Default: null] @@ -28,6 +29,7 @@ selectedMetrics = [], selectedResolution = 0, hostnameFilter = "", + hoststateFilter = "", presetSystemUnits = null, from = null, to = null @@ -37,11 +39,14 @@ const client = getContextClient(); const usePaging = ccconfig?.nodeList_usePaging || false; const nodeListQuery = gql` - query ($cluster: String!, $subCluster: String!, $nodeFilter: String!, $metrics: [String!], $scopes: [MetricScope!]!, $from: Time!, $to: Time!, $paging: PageRequest!, $selectedResolution: Int) { + query ($cluster: String!, $subCluster: String!, $nodeFilter: String!, $stateFilter: String!, $metrics: [String!], + $scopes: [MetricScope!]!, $from: Time!, $to: Time!, $paging: PageRequest!, $selectedResolution: Int + ) { nodeMetricsList( cluster: $cluster subCluster: $subCluster nodeFilter: $nodeFilter + stateFilter: $stateFilter, scopes: $scopes metrics: $metrics from: $from @@ -51,6 +56,7 @@ ) { items { host + state subCluster metrics { name @@ -100,6 +106,7 @@ variables: { cluster: cluster, subCluster: subCluster, + stateFilter: hoststateFilter, nodeFilter: hostnameFilter, scopes: ["core", "socket", "accelerator"], metrics: selectedMetrics, @@ -137,7 +144,7 @@ // Triggers (Except Paging) from, to selectedMetrics, selectedResolution - hostnameFilter + hostnameFilter, hoststateFilter // Continous Scroll: Reset nodes and paging if parameters change: Existing entries will not match new selections if (!usePaging) { nodes = []; diff --git a/web/frontend/src/systems/NodeOverview.svelte b/web/frontend/src/systems/NodeOverview.svelte index 5051374..6e89398 100644 --- a/web/frontend/src/systems/NodeOverview.svelte +++ b/web/frontend/src/systems/NodeOverview.svelte @@ -6,6 +6,7 @@ - `cluster String`: The cluster to show status information for - `selectedMetric String?`: The selectedMetric input [Default: ""] - `hostnameFilter String?`: The active hostnamefilter [Default: ""] + - `hostnameFilter String?`: The active hoststatefilter [Default: ""] - `from Date?`: The selected "from" date [Default: null] - `to Date?`: The selected "to" date [Default: null] --> @@ -13,7 +14,7 @@