mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-04-01 18:15:54 +02:00
Merge pull request #365 from ClusterCockpit/split_statsTable_query
Split StatsTable DataQuery from JobMetrics Query In Job-View
This commit is contained in:
commit
318f70f34c
@ -137,11 +137,6 @@ type JobMetricWithName {
|
|||||||
metric: JobMetric!
|
metric: JobMetric!
|
||||||
}
|
}
|
||||||
|
|
||||||
type JobMetricStatWithName {
|
|
||||||
name: String!
|
|
||||||
stats: MetricStatistics!
|
|
||||||
}
|
|
||||||
|
|
||||||
type JobMetric {
|
type JobMetric {
|
||||||
unit: Unit
|
unit: Unit
|
||||||
timestep: Int!
|
timestep: Int!
|
||||||
@ -156,6 +151,30 @@ type Series {
|
|||||||
data: [NullableFloat!]!
|
data: [NullableFloat!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StatsSeries {
|
||||||
|
mean: [NullableFloat!]!
|
||||||
|
median: [NullableFloat!]!
|
||||||
|
min: [NullableFloat!]!
|
||||||
|
max: [NullableFloat!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type JobStatsWithScope {
|
||||||
|
name: String!
|
||||||
|
scope: MetricScope!
|
||||||
|
stats: [ScopedStats!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScopedStats {
|
||||||
|
hostname: String!
|
||||||
|
id: String
|
||||||
|
data: MetricStatistics!
|
||||||
|
}
|
||||||
|
|
||||||
|
type JobStats {
|
||||||
|
name: String!
|
||||||
|
stats: MetricStatistics!
|
||||||
|
}
|
||||||
|
|
||||||
type Unit {
|
type Unit {
|
||||||
base: String!
|
base: String!
|
||||||
prefix: String
|
prefix: String
|
||||||
@ -167,13 +186,6 @@ type MetricStatistics {
|
|||||||
max: Float!
|
max: Float!
|
||||||
}
|
}
|
||||||
|
|
||||||
type StatsSeries {
|
|
||||||
mean: [NullableFloat!]!
|
|
||||||
median: [NullableFloat!]!
|
|
||||||
min: [NullableFloat!]!
|
|
||||||
max: [NullableFloat!]!
|
|
||||||
}
|
|
||||||
|
|
||||||
type MetricFootprints {
|
type MetricFootprints {
|
||||||
metric: String!
|
metric: String!
|
||||||
data: [NullableFloat!]!
|
data: [NullableFloat!]!
|
||||||
@ -247,7 +259,8 @@ type Query {
|
|||||||
|
|
||||||
job(id: ID!): Job
|
job(id: ID!): Job
|
||||||
jobMetrics(id: ID!, metrics: [String!], scopes: [MetricScope!], resolution: Int): [JobMetricWithName!]!
|
jobMetrics(id: ID!, metrics: [String!], scopes: [MetricScope!], resolution: Int): [JobMetricWithName!]!
|
||||||
jobMetricStats(id: ID!, metrics: [String!]): [JobMetricStatWithName!]!
|
jobStats(id: ID!, metrics: [String!]): [JobStats!]!
|
||||||
|
scopedJobStats(id: ID!, metrics: [String!], scopes: [MetricScope!]): [JobStatsWithScope!]!
|
||||||
jobsFootprints(filter: [JobFilter!], metrics: [String!]!): Footprints
|
jobsFootprints(filter: [JobFilter!], metrics: [String!]!): Footprints
|
||||||
|
|
||||||
jobs(filter: [JobFilter!], page: PageRequest, order: OrderByInput): JobResultList!
|
jobs(filter: [JobFilter!], page: PageRequest, order: OrderByInput): JobResultList!
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -81,11 +81,6 @@ type JobLinkResultList struct {
|
|||||||
Count *int `json:"count,omitempty"`
|
Count *int `json:"count,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type JobMetricStatWithName struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Stats *schema.MetricStatistics `json:"stats"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type JobMetricWithName struct {
|
type JobMetricWithName struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Scope schema.MetricScope `json:"scope"`
|
Scope schema.MetricScope `json:"scope"`
|
||||||
@ -100,6 +95,17 @@ type JobResultList struct {
|
|||||||
HasNextPage *bool `json:"hasNextPage,omitempty"`
|
HasNextPage *bool `json:"hasNextPage,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type JobStats struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Stats *schema.MetricStatistics `json:"stats"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JobStatsWithScope struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Scope schema.MetricScope `json:"scope"`
|
||||||
|
Stats []*ScopedStats `json:"stats"`
|
||||||
|
}
|
||||||
|
|
||||||
type JobsStatistics struct {
|
type JobsStatistics struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@ -173,6 +179,12 @@ type PageRequest struct {
|
|||||||
Page int `json:"page"`
|
Page int `json:"page"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ScopedStats struct {
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
ID *string `json:"id,omitempty"`
|
||||||
|
Data *schema.MetricStatistics `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
type StringInput struct {
|
type StringInput struct {
|
||||||
Eq *string `json:"eq,omitempty"`
|
Eq *string `json:"eq,omitempty"`
|
||||||
Neq *string `json:"neq,omitempty"`
|
Neq *string `json:"neq,omitempty"`
|
||||||
|
@ -301,24 +301,23 @@ func (r *queryResolver) JobMetrics(ctx context.Context, id string, metrics []str
|
|||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// JobMetricStats is the resolver for the jobMetricStats field.
|
// JobStats is the resolver for the jobStats field.
|
||||||
func (r *queryResolver) JobMetricStats(ctx context.Context, id string, metrics []string) ([]*model.JobMetricStatWithName, error) {
|
func (r *queryResolver) JobStats(ctx context.Context, id string, metrics []string) ([]*model.JobStats, error) {
|
||||||
|
|
||||||
job, err := r.Query().Job(ctx, id)
|
job, err := r.Query().Job(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Error while querying job for metrics")
|
log.Warnf("Error while querying job %s for metadata", id)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := metricDataDispatcher.LoadStatData(job, metrics, ctx)
|
data, err := metricDataDispatcher.LoadJobStats(job, metrics, ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Error while loading job stat data")
|
log.Warnf("Error while loading jobStats data for job id %s", id)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
res := []*model.JobMetricStatWithName{}
|
res := []*model.JobStats{}
|
||||||
for name, md := range data {
|
for name, md := range data {
|
||||||
res = append(res, &model.JobMetricStatWithName{
|
res = append(res, &model.JobStats{
|
||||||
Name: name,
|
Name: name,
|
||||||
Stats: &md,
|
Stats: &md,
|
||||||
})
|
})
|
||||||
@ -327,6 +326,44 @@ func (r *queryResolver) JobMetricStats(ctx context.Context, id string, metrics [
|
|||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ScopedJobStats is the resolver for the scopedJobStats field.
|
||||||
|
func (r *queryResolver) ScopedJobStats(ctx context.Context, id string, metrics []string, scopes []schema.MetricScope) ([]*model.JobStatsWithScope, error) {
|
||||||
|
job, err := r.Query().Job(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Error while querying job %s for metadata", id)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := metricDataDispatcher.LoadScopedJobStats(job, metrics, scopes, ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Error while loading scopedJobStats data for job id %s", id)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res := make([]*model.JobStatsWithScope, 0)
|
||||||
|
for name, scoped := range data {
|
||||||
|
for scope, stats := range scoped {
|
||||||
|
|
||||||
|
mdlStats := make([]*model.ScopedStats, 0)
|
||||||
|
for _, stat := range stats {
|
||||||
|
mdlStats = append(mdlStats, &model.ScopedStats{
|
||||||
|
Hostname: stat.Hostname,
|
||||||
|
ID: stat.Id,
|
||||||
|
Data: stat.Data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
res = append(res, &model.JobStatsWithScope{
|
||||||
|
Name: name,
|
||||||
|
Scope: scope,
|
||||||
|
Stats: mdlStats,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
// JobsFootprints is the resolver for the jobsFootprints field.
|
// JobsFootprints is the resolver for the jobsFootprints field.
|
||||||
func (r *queryResolver) JobsFootprints(ctx context.Context, filter []*model.JobFilter, metrics []string) (*model.Footprints, error) {
|
func (r *queryResolver) JobsFootprints(ctx context.Context, filter []*model.JobFilter, metrics []string) (*model.Footprints, error) {
|
||||||
// NOTE: Legacy Naming! This resolver is for normalized histograms in analysis view only - *Not* related to DB "footprint" column!
|
// NOTE: Legacy Naming! This resolver is for normalized histograms in analysis view only - *Not* related to DB "footprint" column!
|
||||||
|
@ -224,8 +224,34 @@ func LoadAverages(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used for polar plots in frontend
|
// Used for statsTable in frontend: Return scoped statistics by metric.
|
||||||
func LoadStatData(
|
func LoadScopedJobStats(
|
||||||
|
job *schema.Job,
|
||||||
|
metrics []string,
|
||||||
|
scopes []schema.MetricScope,
|
||||||
|
ctx context.Context,
|
||||||
|
) (schema.ScopedJobStats, error) {
|
||||||
|
|
||||||
|
if job.State != schema.JobStateRunning && !config.Keys.DisableArchive {
|
||||||
|
return archive.LoadScopedStatsFromArchive(job, metrics, scopes)
|
||||||
|
}
|
||||||
|
|
||||||
|
repo, err := metricdata.GetMetricDataRepo(job.Cluster)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("job %d: no metric data repository configured for '%s'", job.JobID, job.Cluster)
|
||||||
|
}
|
||||||
|
|
||||||
|
scopedStats, err := repo.LoadScopedStats(job, metrics, scopes, ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error while loading scoped statistics for job %d (User %s, Project %s)", job.JobID, job.User, job.Project)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return scopedStats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used for polar plots in frontend: Aggregates statistics for all nodes to single values for job per metric.
|
||||||
|
func LoadJobStats(
|
||||||
job *schema.Job,
|
job *schema.Job,
|
||||||
metrics []string,
|
metrics []string,
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
@ -237,12 +263,12 @@ func LoadStatData(
|
|||||||
data := make(map[string]schema.MetricStatistics, len(metrics))
|
data := make(map[string]schema.MetricStatistics, len(metrics))
|
||||||
repo, err := metricdata.GetMetricDataRepo(job.Cluster)
|
repo, err := metricdata.GetMetricDataRepo(job.Cluster)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data, fmt.Errorf("METRICDATA/METRICDATA > no metric data repository configured for '%s'", job.Cluster)
|
return data, fmt.Errorf("job %d: no metric data repository configured for '%s'", job.JobID, job.Cluster)
|
||||||
}
|
}
|
||||||
|
|
||||||
stats, err := repo.LoadStats(job, metrics, ctx)
|
stats, err := repo.LoadStats(job, metrics, ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Error while loading statistics for job %v (User %v, Project %v)", job.JobID, job.User, job.Project)
|
log.Errorf("error while loading statistics for job %d (User %s, Project %s)", job.JobID, job.User, job.Project)
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,13 +129,13 @@ func (ccms *CCMetricStore) doRequest(
|
|||||||
) (*ApiQueryResponse, error) {
|
) (*ApiQueryResponse, error) {
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
if err := json.NewEncoder(buf).Encode(body); err != nil {
|
if err := json.NewEncoder(buf).Encode(body); err != nil {
|
||||||
log.Warn("Error while encoding request body")
|
log.Errorf("Error while encoding request body: %s", err.Error())
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ccms.queryEndpoint, buf)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ccms.queryEndpoint, buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Error while building request body")
|
log.Errorf("Error while building request body: %s", err.Error())
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if ccms.jwt != "" {
|
if ccms.jwt != "" {
|
||||||
@ -151,7 +151,7 @@ func (ccms *CCMetricStore) doRequest(
|
|||||||
|
|
||||||
res, err := ccms.client.Do(req)
|
res, err := ccms.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error while performing request")
|
log.Errorf("Error while performing request: %s", err.Error())
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,7 +161,7 @@ func (ccms *CCMetricStore) doRequest(
|
|||||||
|
|
||||||
var resBody ApiQueryResponse
|
var resBody ApiQueryResponse
|
||||||
if err := json.NewDecoder(bufio.NewReader(res.Body)).Decode(&resBody); err != nil {
|
if err := json.NewDecoder(bufio.NewReader(res.Body)).Decode(&resBody); err != nil {
|
||||||
log.Warn("Error while decoding result body")
|
log.Errorf("Error while decoding result body: %s", err.Error())
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,7 +177,7 @@ func (ccms *CCMetricStore) LoadData(
|
|||||||
) (schema.JobData, error) {
|
) (schema.JobData, error) {
|
||||||
queries, assignedScope, err := ccms.buildQueries(job, metrics, scopes, resolution)
|
queries, assignedScope, err := ccms.buildQueries(job, metrics, scopes, resolution)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Error while building queries")
|
log.Errorf("Error while building queries for jobId %d, Metrics %v, Scopes %v: %s", job.JobID, metrics, scopes, err.Error())
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,7 +192,7 @@ func (ccms *CCMetricStore) LoadData(
|
|||||||
|
|
||||||
resBody, err := ccms.doRequest(ctx, &req)
|
resBody, err := ccms.doRequest(ctx, &req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error while performing request")
|
log.Errorf("Error while performing request: %s", err.Error())
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -557,16 +557,9 @@ func (ccms *CCMetricStore) LoadStats(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
) (map[string]map[string]schema.MetricStatistics, error) {
|
) (map[string]map[string]schema.MetricStatistics, error) {
|
||||||
|
|
||||||
// metricConfigs := archive.GetCluster(job.Cluster).MetricConfig
|
|
||||||
// resolution := 9000
|
|
||||||
|
|
||||||
// for _, mc := range metricConfigs {
|
|
||||||
// resolution = min(resolution, mc.Timestep)
|
|
||||||
// }
|
|
||||||
|
|
||||||
queries, _, err := ccms.buildQueries(job, metrics, []schema.MetricScope{schema.MetricScopeNode}, 0) // #166 Add scope shere for analysis view accelerator normalization?
|
queries, _, err := ccms.buildQueries(job, metrics, []schema.MetricScope{schema.MetricScopeNode}, 0) // #166 Add scope shere for analysis view accelerator normalization?
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Error while building query")
|
log.Errorf("Error while building queries for jobId %d, Metrics %v: %s", job.JobID, metrics, err.Error())
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -581,7 +574,7 @@ func (ccms *CCMetricStore) LoadStats(
|
|||||||
|
|
||||||
resBody, err := ccms.doRequest(ctx, &req)
|
resBody, err := ccms.doRequest(ctx, &req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error while performing request")
|
log.Errorf("Error while performing request: %s", err.Error())
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -591,9 +584,8 @@ func (ccms *CCMetricStore) LoadStats(
|
|||||||
metric := ccms.toLocalName(query.Metric)
|
metric := ccms.toLocalName(query.Metric)
|
||||||
data := res[0]
|
data := res[0]
|
||||||
if data.Error != nil {
|
if data.Error != nil {
|
||||||
log.Infof("fetching %s for node %s failed: %s", metric, query.Hostname, *data.Error)
|
log.Errorf("fetching %s for node %s failed: %s", metric, query.Hostname, *data.Error)
|
||||||
continue
|
continue
|
||||||
// return nil, fmt.Errorf("METRICDATA/CCMS > fetching %s for node %s failed: %s", metric, query.Hostname, *data.Error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
metricdata, ok := stats[metric]
|
metricdata, ok := stats[metric]
|
||||||
@ -603,9 +595,8 @@ func (ccms *CCMetricStore) LoadStats(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if data.Avg.IsNaN() || data.Min.IsNaN() || data.Max.IsNaN() {
|
if data.Avg.IsNaN() || data.Min.IsNaN() || data.Max.IsNaN() {
|
||||||
log.Infof("fetching %s for node %s failed: one of avg/min/max is NaN", metric, query.Hostname)
|
log.Warnf("fetching %s for node %s failed: one of avg/min/max is NaN", metric, query.Hostname)
|
||||||
continue
|
continue
|
||||||
// return nil, fmt.Errorf("METRICDATA/CCMS > fetching %s for node %s failed: %s", metric, query.Hostname, "avg/min/max is NaN")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
metricdata[query.Hostname] = schema.MetricStatistics{
|
metricdata[query.Hostname] = schema.MetricStatistics{
|
||||||
@ -618,7 +609,98 @@ func (ccms *CCMetricStore) LoadStats(
|
|||||||
return stats, nil
|
return stats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Support sub-node-scope metrics! For this, the partition of a node needs to be known!
|
// Used for Job-View Statistics Table
|
||||||
|
func (ccms *CCMetricStore) LoadScopedStats(
|
||||||
|
job *schema.Job,
|
||||||
|
metrics []string,
|
||||||
|
scopes []schema.MetricScope,
|
||||||
|
ctx context.Context,
|
||||||
|
) (schema.ScopedJobStats, error) {
|
||||||
|
queries, assignedScope, err := ccms.buildQueries(job, metrics, scopes, 0)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error while building queries for jobId %d, Metrics %v, Scopes %v: %s", job.JobID, metrics, scopes, err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req := ApiQueryRequest{
|
||||||
|
Cluster: job.Cluster,
|
||||||
|
From: job.StartTime.Unix(),
|
||||||
|
To: job.StartTime.Add(time.Duration(job.Duration) * time.Second).Unix(),
|
||||||
|
Queries: queries,
|
||||||
|
WithStats: true,
|
||||||
|
WithData: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
resBody, err := ccms.doRequest(ctx, &req)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error while performing request: %s", err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var errors []string
|
||||||
|
scopedJobStats := make(schema.ScopedJobStats)
|
||||||
|
|
||||||
|
for i, row := range resBody.Results {
|
||||||
|
query := req.Queries[i]
|
||||||
|
metric := ccms.toLocalName(query.Metric)
|
||||||
|
scope := assignedScope[i]
|
||||||
|
|
||||||
|
if _, ok := scopedJobStats[metric]; !ok {
|
||||||
|
scopedJobStats[metric] = make(map[schema.MetricScope][]*schema.ScopedStats)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := scopedJobStats[metric][scope]; !ok {
|
||||||
|
scopedJobStats[metric][scope] = make([]*schema.ScopedStats, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
for ndx, res := range row {
|
||||||
|
if res.Error != nil {
|
||||||
|
/* Build list for "partial errors", if any */
|
||||||
|
errors = append(errors, fmt.Sprintf("failed to fetch '%s' from host '%s': %s", query.Metric, query.Hostname, *res.Error))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
id := (*string)(nil)
|
||||||
|
if query.Type != nil {
|
||||||
|
id = new(string)
|
||||||
|
*id = query.TypeIds[ndx]
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.Avg.IsNaN() || res.Min.IsNaN() || res.Max.IsNaN() {
|
||||||
|
// "schema.Float()" because regular float64 can not be JSONed when NaN.
|
||||||
|
res.Avg = schema.Float(0)
|
||||||
|
res.Min = schema.Float(0)
|
||||||
|
res.Max = schema.Float(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
scopedJobStats[metric][scope] = append(scopedJobStats[metric][scope], &schema.ScopedStats{
|
||||||
|
Hostname: query.Hostname,
|
||||||
|
Id: id,
|
||||||
|
Data: &schema.MetricStatistics{
|
||||||
|
Avg: float64(res.Avg),
|
||||||
|
Min: float64(res.Min),
|
||||||
|
Max: float64(res.Max),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// So that one can later check len(scopedJobStats[metric][scope]): Remove from map if empty
|
||||||
|
if len(scopedJobStats[metric][scope]) == 0 {
|
||||||
|
delete(scopedJobStats[metric], scope)
|
||||||
|
if len(scopedJobStats[metric]) == 0 {
|
||||||
|
delete(scopedJobStats, metric)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errors) != 0 {
|
||||||
|
/* Returns list for "partial errors" */
|
||||||
|
return scopedJobStats, fmt.Errorf("METRICDATA/CCMS > Errors: %s", strings.Join(errors, ", "))
|
||||||
|
}
|
||||||
|
return scopedJobStats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used for Systems-View Node-Overview
|
||||||
func (ccms *CCMetricStore) LoadNodeData(
|
func (ccms *CCMetricStore) LoadNodeData(
|
||||||
cluster string,
|
cluster string,
|
||||||
metrics, nodes []string,
|
metrics, nodes []string,
|
||||||
@ -652,7 +734,7 @@ func (ccms *CCMetricStore) LoadNodeData(
|
|||||||
|
|
||||||
resBody, err := ccms.doRequest(ctx, &req)
|
resBody, err := ccms.doRequest(ctx, &req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(fmt.Sprintf("Error while performing request %#v\n", err))
|
log.Errorf("Error while performing request: %s", err.Error())
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -710,6 +792,7 @@ func (ccms *CCMetricStore) LoadNodeData(
|
|||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used for Systems-View Node-List
|
||||||
func (ccms *CCMetricStore) LoadNodeListData(
|
func (ccms *CCMetricStore) LoadNodeListData(
|
||||||
cluster, subCluster, nodeFilter string,
|
cluster, subCluster, nodeFilter string,
|
||||||
metrics []string,
|
metrics []string,
|
||||||
@ -768,7 +851,7 @@ func (ccms *CCMetricStore) LoadNodeListData(
|
|||||||
|
|
||||||
queries, assignedScope, err := ccms.buildNodeQueries(cluster, subCluster, nodes, metrics, scopes, resolution)
|
queries, assignedScope, err := ccms.buildNodeQueries(cluster, subCluster, nodes, metrics, scopes, resolution)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Error while building queries")
|
log.Errorf("Error while building node queries for Cluster %s, SubCLuster %s, Metrics %v, Scopes %v: %s", cluster, subCluster, metrics, scopes, err.Error())
|
||||||
return nil, totalNodes, hasNextPage, err
|
return nil, totalNodes, hasNextPage, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -783,7 +866,7 @@ func (ccms *CCMetricStore) LoadNodeListData(
|
|||||||
|
|
||||||
resBody, err := ccms.doRequest(ctx, &req)
|
resBody, err := ccms.doRequest(ctx, &req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(fmt.Sprintf("Error while performing request %#v\n", err))
|
log.Errorf("Error while performing request: %s", err.Error())
|
||||||
return nil, totalNodes, hasNextPage, err
|
return nil, totalNodes, hasNextPage, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -888,7 +971,7 @@ func (ccms *CCMetricStore) buildNodeQueries(
|
|||||||
if subCluster != "" {
|
if subCluster != "" {
|
||||||
subClusterTopol, scterr = archive.GetSubCluster(cluster, subCluster)
|
subClusterTopol, scterr = archive.GetSubCluster(cluster, subCluster)
|
||||||
if scterr != nil {
|
if scterr != nil {
|
||||||
// TODO: Log
|
log.Errorf("could not load cluster %s subCluster %s topology: %s", cluster, subCluster, scterr.Error())
|
||||||
return nil, nil, scterr
|
return nil, nil, scterr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -898,7 +981,7 @@ func (ccms *CCMetricStore) buildNodeQueries(
|
|||||||
mc := archive.GetMetricConfig(cluster, metric)
|
mc := archive.GetMetricConfig(cluster, metric)
|
||||||
if mc == nil {
|
if mc == nil {
|
||||||
// return nil, fmt.Errorf("METRICDATA/CCMS > metric '%s' is not specified for cluster '%s'", metric, cluster)
|
// return nil, fmt.Errorf("METRICDATA/CCMS > metric '%s' is not specified for cluster '%s'", metric, cluster)
|
||||||
log.Infof("metric '%s' is not specified for cluster '%s'", metric, cluster)
|
log.Warnf("metric '%s' is not specified for cluster '%s'", metric, cluster)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -64,6 +66,8 @@ func (idb *InfluxDBv2DataRepository) LoadData(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
resolution int) (schema.JobData, error) {
|
resolution int) (schema.JobData, error) {
|
||||||
|
|
||||||
|
log.Infof("InfluxDB 2 Backend: Resolution Scaling not Implemented, will return default timestep. Requested Resolution %d", resolution)
|
||||||
|
|
||||||
measurementsConds := make([]string, 0, len(metrics))
|
measurementsConds := make([]string, 0, len(metrics))
|
||||||
for _, m := range metrics {
|
for _, m := range metrics {
|
||||||
measurementsConds = append(measurementsConds, fmt.Sprintf(`r["_measurement"] == "%s"`, m))
|
measurementsConds = append(measurementsConds, fmt.Sprintf(`r["_measurement"] == "%s"`, m))
|
||||||
@ -86,7 +90,7 @@ func (idb *InfluxDBv2DataRepository) LoadData(
|
|||||||
query := ""
|
query := ""
|
||||||
switch scope {
|
switch scope {
|
||||||
case "node":
|
case "node":
|
||||||
// Get Finest Granularity, Groupy By Measurement and Hostname (== Metric / Node), Calculate Mean for 60s windows
|
// Get Finest Granularity, Groupy By Measurement and Hostname (== Metric / Node), Calculate Mean for 60s windows <-- Resolution could be added here?
|
||||||
// log.Info("Scope 'node' requested. ")
|
// log.Info("Scope 'node' requested. ")
|
||||||
query = fmt.Sprintf(`
|
query = fmt.Sprintf(`
|
||||||
from(bucket: "%s")
|
from(bucket: "%s")
|
||||||
@ -116,6 +120,12 @@ func (idb *InfluxDBv2DataRepository) LoadData(
|
|||||||
// idb.bucket,
|
// idb.bucket,
|
||||||
// idb.formatTime(job.StartTime), idb.formatTime(idb.epochToTime(job.StartTimeUnix + int64(job.Duration) + int64(1) )),
|
// idb.formatTime(job.StartTime), idb.formatTime(idb.epochToTime(job.StartTimeUnix + int64(job.Duration) + int64(1) )),
|
||||||
// measurementsCond, hostsCond)
|
// measurementsCond, hostsCond)
|
||||||
|
case "hwthread":
|
||||||
|
log.Info(" Scope 'hwthread' requested, but not yet supported: Will return 'node' scope only. ")
|
||||||
|
continue
|
||||||
|
case "accelerator":
|
||||||
|
log.Info(" Scope 'accelerator' requested, but not yet supported: Will return 'node' scope only. ")
|
||||||
|
continue
|
||||||
default:
|
default:
|
||||||
log.Infof("Unknown scope '%s' requested: Will return 'node' scope.", scope)
|
log.Infof("Unknown scope '%s' requested: Will return 'node' scope.", scope)
|
||||||
continue
|
continue
|
||||||
@ -173,6 +183,11 @@ func (idb *InfluxDBv2DataRepository) LoadData(
|
|||||||
}
|
}
|
||||||
case "socket":
|
case "socket":
|
||||||
continue
|
continue
|
||||||
|
case "accelerator":
|
||||||
|
continue
|
||||||
|
case "hwthread":
|
||||||
|
// See below @ core
|
||||||
|
continue
|
||||||
case "core":
|
case "core":
|
||||||
continue
|
continue
|
||||||
// Include Series.Id in hostSeries
|
// Include Series.Id in hostSeries
|
||||||
@ -301,6 +316,53 @@ func (idb *InfluxDBv2DataRepository) LoadStats(
|
|||||||
return stats, nil
|
return stats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used in Job-View StatsTable
|
||||||
|
// UNTESTED
|
||||||
|
func (idb *InfluxDBv2DataRepository) LoadScopedStats(
|
||||||
|
job *schema.Job,
|
||||||
|
metrics []string,
|
||||||
|
scopes []schema.MetricScope,
|
||||||
|
ctx context.Context) (schema.ScopedJobStats, error) {
|
||||||
|
|
||||||
|
// Assumption: idb.loadData() only returns series node-scope - use node scope for statsTable
|
||||||
|
scopedJobStats := make(schema.ScopedJobStats)
|
||||||
|
data, err := idb.LoadData(job, metrics, []schema.MetricScope{schema.MetricScopeNode}, ctx, 0 /*resolution here*/)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Error while loading job for scopedJobStats")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for metric, metricData := range data {
|
||||||
|
for _, scope := range scopes {
|
||||||
|
if scope != schema.MetricScopeNode {
|
||||||
|
logOnce.Do(func() {
|
||||||
|
log.Infof("Note: Scope '%s' requested, but not yet supported: Will return 'node' scope only.", scope)
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := scopedJobStats[metric]; !ok {
|
||||||
|
scopedJobStats[metric] = make(map[schema.MetricScope][]*schema.ScopedStats)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := scopedJobStats[metric][scope]; !ok {
|
||||||
|
scopedJobStats[metric][scope] = make([]*schema.ScopedStats, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, series := range metricData[scope].Series {
|
||||||
|
scopedJobStats[metric][scope] = append(scopedJobStats[metric][scope], &schema.ScopedStats{
|
||||||
|
Hostname: series.Hostname,
|
||||||
|
Data: &series.Statistics,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return scopedJobStats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used in Systems-View @ Node-Overview
|
||||||
|
// UNTESTED
|
||||||
func (idb *InfluxDBv2DataRepository) LoadNodeData(
|
func (idb *InfluxDBv2DataRepository) LoadNodeData(
|
||||||
cluster string,
|
cluster string,
|
||||||
metrics, nodes []string,
|
metrics, nodes []string,
|
||||||
@ -308,12 +370,123 @@ func (idb *InfluxDBv2DataRepository) LoadNodeData(
|
|||||||
from, to time.Time,
|
from, to time.Time,
|
||||||
ctx context.Context) (map[string]map[string][]*schema.JobMetric, error) {
|
ctx context.Context) (map[string]map[string][]*schema.JobMetric, error) {
|
||||||
|
|
||||||
// TODO : Implement to be used in Analysis- und System/Node-View
|
// Note: scopes[] Array will be ignored, only return node scope
|
||||||
log.Infof("LoadNodeData unimplemented for InfluxDBv2DataRepository, Args: cluster %s, metrics %v, nodes %v, scopes %v", cluster, metrics, nodes, scopes)
|
|
||||||
|
|
||||||
return nil, errors.New("METRICDATA/INFLUXV2 > unimplemented for InfluxDBv2DataRepository")
|
// CONVERT ARGS TO INFLUX
|
||||||
|
measurementsConds := make([]string, 0)
|
||||||
|
for _, m := range metrics {
|
||||||
|
measurementsConds = append(measurementsConds, fmt.Sprintf(`r["_measurement"] == "%s"`, m))
|
||||||
|
}
|
||||||
|
measurementsCond := strings.Join(measurementsConds, " or ")
|
||||||
|
|
||||||
|
hostsConds := make([]string, 0)
|
||||||
|
if nodes == nil {
|
||||||
|
var allNodes []string
|
||||||
|
subClusterNodeLists := archive.NodeLists[cluster]
|
||||||
|
for _, nodeList := range subClusterNodeLists {
|
||||||
|
allNodes = append(nodes, nodeList.PrintList()...)
|
||||||
|
}
|
||||||
|
for _, node := range allNodes {
|
||||||
|
nodes = append(nodes, node)
|
||||||
|
hostsConds = append(hostsConds, fmt.Sprintf(`r["hostname"] == "%s"`, node))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, node := range nodes {
|
||||||
|
hostsConds = append(hostsConds, fmt.Sprintf(`r["hostname"] == "%s"`, node))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hostsCond := strings.Join(hostsConds, " or ")
|
||||||
|
|
||||||
|
// BUILD AND PERFORM QUERY
|
||||||
|
query := fmt.Sprintf(`
|
||||||
|
from(bucket: "%s")
|
||||||
|
|> range(start: %s, stop: %s)
|
||||||
|
|> filter(fn: (r) => (%s) and (%s) )
|
||||||
|
|> drop(columns: ["_start", "_stop"])
|
||||||
|
|> group(columns: ["hostname", "_measurement"])
|
||||||
|
|> aggregateWindow(every: 60s, fn: mean)
|
||||||
|
|> drop(columns: ["_time"])`,
|
||||||
|
idb.bucket,
|
||||||
|
idb.formatTime(from), idb.formatTime(to),
|
||||||
|
measurementsCond, hostsCond)
|
||||||
|
|
||||||
|
rows, err := idb.queryClient.Query(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error while performing query")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// HANDLE QUERY RETURN
|
||||||
|
// Collect Float Arrays for Node@Metric -> No Scope Handling!
|
||||||
|
influxData := make(map[string]map[string][]schema.Float)
|
||||||
|
for rows.Next() {
|
||||||
|
row := rows.Record()
|
||||||
|
host, field := row.ValueByKey("hostname").(string), row.Measurement()
|
||||||
|
|
||||||
|
influxHostData, ok := influxData[host]
|
||||||
|
if !ok {
|
||||||
|
influxHostData = make(map[string][]schema.Float)
|
||||||
|
influxData[host] = influxHostData
|
||||||
|
}
|
||||||
|
|
||||||
|
influxFieldData, ok := influxData[host][field]
|
||||||
|
if !ok {
|
||||||
|
influxFieldData = make([]schema.Float, 0)
|
||||||
|
influxData[host][field] = influxFieldData
|
||||||
|
}
|
||||||
|
|
||||||
|
val, ok := row.Value().(float64)
|
||||||
|
if ok {
|
||||||
|
influxData[host][field] = append(influxData[host][field], schema.Float(val))
|
||||||
|
} else {
|
||||||
|
influxData[host][field] = append(influxData[host][field], schema.Float(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BUILD FUNCTION RETURN
|
||||||
|
data := make(map[string]map[string][]*schema.JobMetric)
|
||||||
|
for node, metricData := range influxData {
|
||||||
|
|
||||||
|
nodeData, ok := data[node]
|
||||||
|
if !ok {
|
||||||
|
nodeData = make(map[string][]*schema.JobMetric)
|
||||||
|
data[node] = nodeData
|
||||||
|
}
|
||||||
|
|
||||||
|
for metric, floatArray := range metricData {
|
||||||
|
avg, min, max := 0.0, 0.0, 0.0
|
||||||
|
for _, val := range floatArray {
|
||||||
|
avg += float64(val)
|
||||||
|
min = math.Min(min, float64(val))
|
||||||
|
max = math.Max(max, float64(val))
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := schema.MetricStatistics{
|
||||||
|
Avg: (math.Round((avg/float64(len(floatArray)))*100) / 100),
|
||||||
|
Min: (math.Round(min*100) / 100),
|
||||||
|
Max: (math.Round(max*100) / 100),
|
||||||
|
}
|
||||||
|
|
||||||
|
mc := archive.GetMetricConfig(cluster, metric)
|
||||||
|
nodeData[metric] = append(nodeData[metric], &schema.JobMetric{
|
||||||
|
Unit: mc.Unit,
|
||||||
|
Timestep: mc.Timestep,
|
||||||
|
Series: []schema.Series{
|
||||||
|
{
|
||||||
|
Hostname: node,
|
||||||
|
Statistics: stats,
|
||||||
|
Data: floatArray,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used in Systems-View @ Node-List
|
||||||
|
// UNTESTED
|
||||||
func (idb *InfluxDBv2DataRepository) LoadNodeListData(
|
func (idb *InfluxDBv2DataRepository) LoadNodeListData(
|
||||||
cluster, subCluster, nodeFilter string,
|
cluster, subCluster, nodeFilter string,
|
||||||
metrics []string,
|
metrics []string,
|
||||||
@ -324,10 +497,79 @@ func (idb *InfluxDBv2DataRepository) LoadNodeListData(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
) (map[string]schema.JobData, int, bool, error) {
|
) (map[string]schema.JobData, int, bool, error) {
|
||||||
|
|
||||||
|
// Assumption: idb.loadData() only returns series node-scope - use node scope for NodeList
|
||||||
|
|
||||||
|
// 0) Init additional vars
|
||||||
var totalNodes int = 0
|
var totalNodes int = 0
|
||||||
var hasNextPage bool = false
|
var hasNextPage bool = false
|
||||||
// TODO : Implement to be used in NodeList-View
|
|
||||||
log.Infof("LoadNodeListData unimplemented for InfluxDBv2DataRepository, Args: cluster %s, metrics %v, nodeFilter %v, scopes %v", cluster, metrics, nodeFilter, scopes)
|
|
||||||
|
|
||||||
return nil, totalNodes, hasNextPage, errors.New("METRICDATA/INFLUXV2 > unimplemented for InfluxDBv2DataRepository")
|
// 1) Get list of all nodes
|
||||||
|
var nodes []string
|
||||||
|
if subCluster != "" {
|
||||||
|
scNodes := archive.NodeLists[cluster][subCluster]
|
||||||
|
nodes = scNodes.PrintList()
|
||||||
|
} else {
|
||||||
|
subClusterNodeLists := archive.NodeLists[cluster]
|
||||||
|
for _, nodeList := range subClusterNodeLists {
|
||||||
|
nodes = append(nodes, nodeList.PrintList()...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Filter nodes
|
||||||
|
if nodeFilter != "" {
|
||||||
|
filteredNodes := []string{}
|
||||||
|
for _, node := range nodes {
|
||||||
|
if strings.Contains(node, nodeFilter) {
|
||||||
|
filteredNodes = append(filteredNodes, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes = filteredNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2.1) Count total nodes && Sort nodes -> Sorting invalidated after return ...
|
||||||
|
totalNodes = len(nodes)
|
||||||
|
sort.Strings(nodes)
|
||||||
|
|
||||||
|
// 3) Apply paging
|
||||||
|
if len(nodes) > page.ItemsPerPage {
|
||||||
|
start := (page.Page - 1) * page.ItemsPerPage
|
||||||
|
end := start + page.ItemsPerPage
|
||||||
|
if end > len(nodes) {
|
||||||
|
end = len(nodes)
|
||||||
|
hasNextPage = false
|
||||||
|
} else {
|
||||||
|
hasNextPage = true
|
||||||
|
}
|
||||||
|
nodes = nodes[start:end]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Fetch And Convert Data, use idb.LoadNodeData() for query
|
||||||
|
|
||||||
|
rawNodeData, err := idb.LoadNodeData(cluster, metrics, nodes, scopes, from, to, ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(fmt.Sprintf("Error while loading influx nodeData for nodeListData %#v\n", err))
|
||||||
|
return nil, totalNodes, hasNextPage, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make(map[string]schema.JobData)
|
||||||
|
for node, nodeData := range rawNodeData {
|
||||||
|
// Init Nested Map Data Structures If Not Found
|
||||||
|
hostData, ok := data[node]
|
||||||
|
if !ok {
|
||||||
|
hostData = make(schema.JobData)
|
||||||
|
data[node] = hostData
|
||||||
|
}
|
||||||
|
|
||||||
|
for metric, nodeMetricData := range nodeData {
|
||||||
|
metricData, ok := hostData[metric]
|
||||||
|
if !ok {
|
||||||
|
metricData = make(map[schema.MetricScope]*schema.JobMetric)
|
||||||
|
data[node][metric] = metricData
|
||||||
|
}
|
||||||
|
|
||||||
|
data[node][metric][schema.MetricScopeNode] = nodeMetricData[0] // Only Node Scope Returned from loadNodeData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, totalNodes, hasNextPage, nil
|
||||||
}
|
}
|
||||||
|
@ -24,9 +24,12 @@ type MetricDataRepository interface {
|
|||||||
// Return the JobData for the given job, only with the requested metrics.
|
// Return the JobData for the given job, only with the requested metrics.
|
||||||
LoadData(job *schema.Job, metrics []string, scopes []schema.MetricScope, ctx context.Context, resolution int) (schema.JobData, error)
|
LoadData(job *schema.Job, metrics []string, scopes []schema.MetricScope, ctx context.Context, resolution int) (schema.JobData, error)
|
||||||
|
|
||||||
// Return a map of metrics to a map of nodes to the metric statistics of the job. node scope assumed for now.
|
// Return a map of metrics to a map of nodes to the metric statistics of the job. node scope only.
|
||||||
LoadStats(job *schema.Job, metrics []string, ctx context.Context) (map[string]map[string]schema.MetricStatistics, error)
|
LoadStats(job *schema.Job, metrics []string, ctx context.Context) (map[string]map[string]schema.MetricStatistics, error)
|
||||||
|
|
||||||
|
// Return a map of metrics to a map of scopes to the scoped metric statistics of the job.
|
||||||
|
LoadScopedStats(job *schema.Job, metrics []string, scopes []schema.MetricScope, ctx context.Context) (schema.ScopedJobStats, error)
|
||||||
|
|
||||||
// Return a map of hosts to a map of metrics at the requested scopes (currently only node) for that node.
|
// Return a map of hosts to a map of metrics at the requested scopes (currently only node) for that node.
|
||||||
LoadNodeData(cluster string, metrics, nodes []string, scopes []schema.MetricScope, from, to time.Time, ctx context.Context) (map[string]map[string][]*schema.JobMetric, error)
|
LoadNodeData(cluster string, metrics, nodes []string, scopes []schema.MetricScope, from, to time.Time, ctx context.Context) (map[string]map[string][]*schema.JobMetric, error)
|
||||||
|
|
||||||
|
@ -448,6 +448,51 @@ func (pdb *PrometheusDataRepository) LoadNodeData(
|
|||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implemented by NHR@FAU; Used in Job-View StatsTable
|
||||||
|
func (pdb *PrometheusDataRepository) LoadScopedStats(
|
||||||
|
job *schema.Job,
|
||||||
|
metrics []string,
|
||||||
|
scopes []schema.MetricScope,
|
||||||
|
ctx context.Context) (schema.ScopedJobStats, error) {
|
||||||
|
|
||||||
|
// Assumption: pdb.loadData() only returns series node-scope - use node scope for statsTable
|
||||||
|
scopedJobStats := make(schema.ScopedJobStats)
|
||||||
|
data, err := pdb.LoadData(job, metrics, []schema.MetricScope{schema.MetricScopeNode}, ctx, 0 /*resolution here*/)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Error while loading job for scopedJobStats")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for metric, metricData := range data {
|
||||||
|
for _, scope := range scopes {
|
||||||
|
if scope != schema.MetricScopeNode {
|
||||||
|
logOnce.Do(func() {
|
||||||
|
log.Infof("Note: Scope '%s' requested, but not yet supported: Will return 'node' scope only.", scope)
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := scopedJobStats[metric]; !ok {
|
||||||
|
scopedJobStats[metric] = make(map[schema.MetricScope][]*schema.ScopedStats)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := scopedJobStats[metric][scope]; !ok {
|
||||||
|
scopedJobStats[metric][scope] = make([]*schema.ScopedStats, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, series := range metricData[scope].Series {
|
||||||
|
scopedJobStats[metric][scope] = append(scopedJobStats[metric][scope], &schema.ScopedStats{
|
||||||
|
Hostname: series.Hostname,
|
||||||
|
Data: &series.Statistics,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return scopedJobStats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implemented by NHR@FAU; Used in NodeList-View
|
||||||
func (pdb *PrometheusDataRepository) LoadNodeListData(
|
func (pdb *PrometheusDataRepository) LoadNodeListData(
|
||||||
cluster, subCluster, nodeFilter string,
|
cluster, subCluster, nodeFilter string,
|
||||||
metrics []string,
|
metrics []string,
|
||||||
@ -458,10 +503,132 @@ func (pdb *PrometheusDataRepository) LoadNodeListData(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
) (map[string]schema.JobData, int, bool, error) {
|
) (map[string]schema.JobData, int, bool, error) {
|
||||||
|
|
||||||
|
// Assumption: pdb.loadData() only returns series node-scope - use node scope for NodeList
|
||||||
|
|
||||||
|
// 0) Init additional vars
|
||||||
var totalNodes int = 0
|
var totalNodes int = 0
|
||||||
var hasNextPage bool = false
|
var hasNextPage bool = false
|
||||||
// TODO : Implement to be used in NodeList-View
|
|
||||||
log.Infof("LoadNodeListData unimplemented for PrometheusDataRepository, Args: cluster %s, metrics %v, nodeFilter %v, scopes %v", cluster, metrics, nodeFilter, scopes)
|
|
||||||
|
|
||||||
return nil, totalNodes, hasNextPage, errors.New("METRICDATA/INFLUXV2 > unimplemented for PrometheusDataRepository")
|
// 1) Get list of all nodes
|
||||||
|
var nodes []string
|
||||||
|
if subCluster != "" {
|
||||||
|
scNodes := archive.NodeLists[cluster][subCluster]
|
||||||
|
nodes = scNodes.PrintList()
|
||||||
|
} else {
|
||||||
|
subClusterNodeLists := archive.NodeLists[cluster]
|
||||||
|
for _, nodeList := range subClusterNodeLists {
|
||||||
|
nodes = append(nodes, nodeList.PrintList()...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Filter nodes
|
||||||
|
if nodeFilter != "" {
|
||||||
|
filteredNodes := []string{}
|
||||||
|
for _, node := range nodes {
|
||||||
|
if strings.Contains(node, nodeFilter) {
|
||||||
|
filteredNodes = append(filteredNodes, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes = filteredNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2.1) Count total nodes && Sort nodes -> Sorting invalidated after return ...
|
||||||
|
totalNodes = len(nodes)
|
||||||
|
sort.Strings(nodes)
|
||||||
|
|
||||||
|
// 3) Apply paging
|
||||||
|
if len(nodes) > page.ItemsPerPage {
|
||||||
|
start := (page.Page - 1) * page.ItemsPerPage
|
||||||
|
end := start + page.ItemsPerPage
|
||||||
|
if end > len(nodes) {
|
||||||
|
end = len(nodes)
|
||||||
|
hasNextPage = false
|
||||||
|
} else {
|
||||||
|
hasNextPage = true
|
||||||
|
}
|
||||||
|
nodes = nodes[start:end]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Fetch Data, based on pdb.LoadNodeData()
|
||||||
|
|
||||||
|
t0 := time.Now()
|
||||||
|
// Map of hosts of jobData
|
||||||
|
data := make(map[string]schema.JobData)
|
||||||
|
|
||||||
|
// query db for each metric
|
||||||
|
// TODO: scopes seems to be always empty
|
||||||
|
if len(scopes) == 0 || !contains(scopes, schema.MetricScopeNode) {
|
||||||
|
scopes = append(scopes, schema.MetricScopeNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, scope := range scopes {
|
||||||
|
if scope != schema.MetricScopeNode {
|
||||||
|
logOnce.Do(func() {
|
||||||
|
log.Infof("Note: Scope '%s' requested, but not yet supported: Will return 'node' scope only.", scope)
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, metric := range metrics {
|
||||||
|
metricConfig := archive.GetMetricConfig(cluster, metric)
|
||||||
|
if metricConfig == nil {
|
||||||
|
log.Warnf("Error in LoadNodeListData: Metric %s for cluster %s not configured", metric, cluster)
|
||||||
|
return nil, totalNodes, hasNextPage, errors.New("Prometheus config error")
|
||||||
|
}
|
||||||
|
query, err := pdb.FormatQuery(metric, scope, nodes, cluster)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Error while formatting prometheus query")
|
||||||
|
return nil, totalNodes, hasNextPage, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ranged query over all nodes
|
||||||
|
r := promv1.Range{
|
||||||
|
Start: from,
|
||||||
|
End: to,
|
||||||
|
Step: time.Duration(metricConfig.Timestep * 1e9),
|
||||||
|
}
|
||||||
|
result, warnings, err := pdb.queryClient.QueryRange(ctx, query, r)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Prometheus query error in LoadNodeData: %v\n", err)
|
||||||
|
return nil, totalNodes, hasNextPage, errors.New("Prometheus query error")
|
||||||
|
}
|
||||||
|
if len(warnings) > 0 {
|
||||||
|
log.Warnf("Warnings: %v\n", warnings)
|
||||||
|
}
|
||||||
|
|
||||||
|
step := int64(metricConfig.Timestep)
|
||||||
|
steps := int64(to.Sub(from).Seconds()) / step
|
||||||
|
|
||||||
|
// iter rows of host, metric, values
|
||||||
|
for _, row := range result.(promm.Matrix) {
|
||||||
|
hostname := strings.TrimSuffix(string(row.Metric["exported_instance"]), pdb.suffix)
|
||||||
|
|
||||||
|
hostdata, ok := data[hostname]
|
||||||
|
if !ok {
|
||||||
|
hostdata = make(schema.JobData)
|
||||||
|
data[hostname] = hostdata
|
||||||
|
}
|
||||||
|
|
||||||
|
metricdata, ok := hostdata[metric]
|
||||||
|
if !ok {
|
||||||
|
metricdata = make(map[schema.MetricScope]*schema.JobMetric)
|
||||||
|
data[hostname][metric] = metricdata
|
||||||
|
}
|
||||||
|
|
||||||
|
// output per host, metric and scope
|
||||||
|
scopeData, ok := metricdata[scope]
|
||||||
|
if !ok {
|
||||||
|
scopeData = &schema.JobMetric{
|
||||||
|
Unit: metricConfig.Unit,
|
||||||
|
Timestep: metricConfig.Timestep,
|
||||||
|
Series: []schema.Series{pdb.RowToSeries(from, step, steps, row)},
|
||||||
|
}
|
||||||
|
data[hostname][metric][scope] = scopeData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t1 := time.Since(t0)
|
||||||
|
log.Debugf("LoadNodeListData of %v nodes took %s", len(data), t1)
|
||||||
|
return data, totalNodes, hasNextPage, nil
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,17 @@ func (tmdr *TestMetricDataRepository) LoadData(
|
|||||||
|
|
||||||
func (tmdr *TestMetricDataRepository) LoadStats(
|
func (tmdr *TestMetricDataRepository) LoadStats(
|
||||||
job *schema.Job,
|
job *schema.Job,
|
||||||
metrics []string, ctx context.Context) (map[string]map[string]schema.MetricStatistics, error) {
|
metrics []string,
|
||||||
|
ctx context.Context) (map[string]map[string]schema.MetricStatistics, error) {
|
||||||
|
|
||||||
|
panic("TODO")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tmdr *TestMetricDataRepository) LoadScopedStats(
|
||||||
|
job *schema.Job,
|
||||||
|
metrics []string,
|
||||||
|
scopes []schema.MetricScope,
|
||||||
|
ctx context.Context) (schema.ScopedJobStats, error) {
|
||||||
|
|
||||||
panic("TODO")
|
panic("TODO")
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,8 @@ type ArchiveBackend interface {
|
|||||||
|
|
||||||
LoadJobData(job *schema.Job) (schema.JobData, error)
|
LoadJobData(job *schema.Job) (schema.JobData, error)
|
||||||
|
|
||||||
|
LoadJobStats(job *schema.Job) (schema.ScopedJobStats, error)
|
||||||
|
|
||||||
LoadClusterCfg(name string) (*schema.Cluster, error)
|
LoadClusterCfg(name string) (*schema.Cluster, error)
|
||||||
|
|
||||||
StoreJobMeta(jobMeta *schema.JobMeta) error
|
StoreJobMeta(jobMeta *schema.JobMeta) error
|
||||||
@ -87,7 +89,7 @@ func Init(rawConfig json.RawMessage, disableArchive bool) error {
|
|||||||
var version uint64
|
var version uint64
|
||||||
version, err = ar.Init(rawConfig)
|
version, err = ar.Init(rawConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error while initializing archiveBackend")
|
log.Errorf("Error while initializing archiveBackend: %s", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Infof("Load archive version %d", version)
|
log.Infof("Load archive version %d", version)
|
||||||
@ -110,7 +112,7 @@ func LoadAveragesFromArchive(
|
|||||||
) error {
|
) error {
|
||||||
metaFile, err := ar.LoadJobMeta(job)
|
metaFile, err := ar.LoadJobMeta(job)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Error while loading job metadata from archiveBackend")
|
log.Errorf("Error while loading job metadata from archiveBackend: %s", err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,7 +127,7 @@ func LoadAveragesFromArchive(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to metricdataloader.LoadStatData().
|
// Helper to metricdataloader.LoadJobStats().
|
||||||
func LoadStatsFromArchive(
|
func LoadStatsFromArchive(
|
||||||
job *schema.Job,
|
job *schema.Job,
|
||||||
metrics []string,
|
metrics []string,
|
||||||
@ -133,7 +135,7 @@ func LoadStatsFromArchive(
|
|||||||
data := make(map[string]schema.MetricStatistics, len(metrics))
|
data := make(map[string]schema.MetricStatistics, len(metrics))
|
||||||
metaFile, err := ar.LoadJobMeta(job)
|
metaFile, err := ar.LoadJobMeta(job)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Error while loading job metadata from archiveBackend")
|
log.Errorf("Error while loading job metadata from archiveBackend: %s", err.Error())
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,10 +156,26 @@ func LoadStatsFromArchive(
|
|||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to metricdataloader.LoadScopedJobStats().
|
||||||
|
func LoadScopedStatsFromArchive(
|
||||||
|
job *schema.Job,
|
||||||
|
metrics []string,
|
||||||
|
scopes []schema.MetricScope,
|
||||||
|
) (schema.ScopedJobStats, error) {
|
||||||
|
|
||||||
|
data, err := ar.LoadJobStats(job)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error while loading job stats from archiveBackend: %s", err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
func GetStatistics(job *schema.Job) (map[string]schema.JobStatistics, error) {
|
func GetStatistics(job *schema.Job) (map[string]schema.JobStatistics, error) {
|
||||||
metaFile, err := ar.LoadJobMeta(job)
|
metaFile, err := ar.LoadJobMeta(job)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Error while loading job metadata from archiveBackend")
|
log.Errorf("Error while loading job metadata from archiveBackend: %s", err.Error())
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,7 +191,7 @@ func UpdateMetadata(job *schema.Job, metadata map[string]string) error {
|
|||||||
|
|
||||||
jobMeta, err := ar.LoadJobMeta(job)
|
jobMeta, err := ar.LoadJobMeta(job)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Error while loading job metadata from archiveBackend")
|
log.Errorf("Error while loading job metadata from archiveBackend: %s", err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,7 +211,7 @@ func UpdateTags(job *schema.Job, tags []*schema.Tag) error {
|
|||||||
|
|
||||||
jobMeta, err := ar.LoadJobMeta(job)
|
jobMeta, err := ar.LoadJobMeta(job)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Error while loading job metadata from archiveBackend")
|
log.Errorf("Error while loading job metadata from archiveBackend: %s", err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,6 +115,40 @@ func loadJobData(filename string, isCompressed bool) (schema.JobData, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadJobStats(filename string, isCompressed bool) (schema.ScopedJobStats, error) {
|
||||||
|
f, err := os.Open(filename)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("fsBackend LoadJobStats()- %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if isCompressed {
|
||||||
|
r, err := gzip.NewReader(f)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(" %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
if config.Keys.Validate {
|
||||||
|
if err := schema.Validate(schema.Data, r); err != nil {
|
||||||
|
return nil, fmt.Errorf("validate job data: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DecodeJobStats(r, filename)
|
||||||
|
} else {
|
||||||
|
if config.Keys.Validate {
|
||||||
|
if err := schema.Validate(schema.Data, bufio.NewReader(f)); err != nil {
|
||||||
|
return nil, fmt.Errorf("validate job data: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DecodeJobStats(bufio.NewReader(f), filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (fsa *FsArchive) Init(rawConfig json.RawMessage) (uint64, error) {
|
func (fsa *FsArchive) Init(rawConfig json.RawMessage) (uint64, error) {
|
||||||
|
|
||||||
var config FsArchiveConfig
|
var config FsArchiveConfig
|
||||||
@ -389,6 +423,18 @@ func (fsa *FsArchive) LoadJobData(job *schema.Job) (schema.JobData, error) {
|
|||||||
return loadJobData(filename, isCompressed)
|
return loadJobData(filename, isCompressed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fsa *FsArchive) LoadJobStats(job *schema.Job) (schema.ScopedJobStats, error) {
|
||||||
|
var isCompressed bool = true
|
||||||
|
filename := getPath(job, fsa.path, "data.json.gz")
|
||||||
|
|
||||||
|
if !util.CheckFileExists(filename) {
|
||||||
|
filename = getPath(job, fsa.path, "data.json")
|
||||||
|
isCompressed = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadJobStats(filename, isCompressed)
|
||||||
|
}
|
||||||
|
|
||||||
func (fsa *FsArchive) LoadJobMeta(job *schema.Job) (*schema.JobMeta, error) {
|
func (fsa *FsArchive) LoadJobMeta(job *schema.Job) (*schema.JobMeta, error) {
|
||||||
filename := getPath(job, fsa.path, "meta.json")
|
filename := getPath(job, fsa.path, "meta.json")
|
||||||
return loadJobMeta(filename)
|
return loadJobMeta(filename)
|
||||||
|
@ -32,6 +32,43 @@ func DecodeJobData(r io.Reader, k string) (schema.JobData, error) {
|
|||||||
return data.(schema.JobData), nil
|
return data.(schema.JobData), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DecodeJobStats(r io.Reader, k string) (schema.ScopedJobStats, error) {
|
||||||
|
jobData, err := DecodeJobData(r, k)
|
||||||
|
// Convert schema.JobData to schema.ScopedJobStats
|
||||||
|
if jobData != nil {
|
||||||
|
scopedJobStats := make(schema.ScopedJobStats)
|
||||||
|
for metric, metricData := range jobData {
|
||||||
|
if _, ok := scopedJobStats[metric]; !ok {
|
||||||
|
scopedJobStats[metric] = make(map[schema.MetricScope][]*schema.ScopedStats)
|
||||||
|
}
|
||||||
|
|
||||||
|
for scope, jobMetric := range metricData {
|
||||||
|
if _, ok := scopedJobStats[metric][scope]; !ok {
|
||||||
|
scopedJobStats[metric][scope] = make([]*schema.ScopedStats, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, series := range jobMetric.Series {
|
||||||
|
scopedJobStats[metric][scope] = append(scopedJobStats[metric][scope], &schema.ScopedStats{
|
||||||
|
Hostname: series.Hostname,
|
||||||
|
Id: series.Id,
|
||||||
|
Data: &series.Statistics,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// So that one can later check len(scopedJobStats[metric][scope]): Remove from map if empty
|
||||||
|
if len(scopedJobStats[metric][scope]) == 0 {
|
||||||
|
delete(scopedJobStats[metric], scope)
|
||||||
|
if len(scopedJobStats[metric]) == 0 {
|
||||||
|
delete(scopedJobStats, metric)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return scopedJobStats, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
func DecodeJobMeta(r io.Reader) (*schema.JobMeta, error) {
|
func DecodeJobMeta(r io.Reader) (*schema.JobMeta, error) {
|
||||||
var d schema.JobMeta
|
var d schema.JobMeta
|
||||||
if err := json.NewDecoder(r).Decode(&d); err != nil {
|
if err := json.NewDecoder(r).Decode(&d); err != nil {
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type JobData map[string]map[MetricScope]*JobMetric
|
type JobData map[string]map[MetricScope]*JobMetric
|
||||||
|
type ScopedJobStats map[string]map[MetricScope][]*ScopedStats
|
||||||
|
|
||||||
type JobMetric struct {
|
type JobMetric struct {
|
||||||
StatisticsSeries *StatsSeries `json:"statisticsSeries,omitempty"`
|
StatisticsSeries *StatsSeries `json:"statisticsSeries,omitempty"`
|
||||||
@ -30,6 +31,12 @@ type Series struct {
|
|||||||
Statistics MetricStatistics `json:"statistics"`
|
Statistics MetricStatistics `json:"statistics"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ScopedStats struct {
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
Id *string `json:"id,omitempty"`
|
||||||
|
Data *MetricStatistics `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
type MetricStatistics struct {
|
type MetricStatistics struct {
|
||||||
Avg float64 `json:"avg"`
|
Avg float64 `json:"avg"`
|
||||||
Min float64 `json:"min"`
|
Min float64 `json:"min"`
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
import JobRoofline from "./job/JobRoofline.svelte";
|
import JobRoofline from "./job/JobRoofline.svelte";
|
||||||
import EnergySummary from "./job/EnergySummary.svelte";
|
import EnergySummary from "./job/EnergySummary.svelte";
|
||||||
import PlotGrid from "./generic/PlotGrid.svelte";
|
import PlotGrid from "./generic/PlotGrid.svelte";
|
||||||
import StatsTable from "./job/StatsTable.svelte";
|
import StatsTab from "./job/StatsTab.svelte";
|
||||||
|
|
||||||
export let dbid;
|
export let dbid;
|
||||||
export let username;
|
export let username;
|
||||||
@ -53,10 +53,8 @@
|
|||||||
|
|
||||||
let isMetricsSelectionOpen = false,
|
let isMetricsSelectionOpen = false,
|
||||||
selectedMetrics = [],
|
selectedMetrics = [],
|
||||||
selectedScopes = [];
|
selectedScopes = [],
|
||||||
|
plots = {};
|
||||||
let plots = {},
|
|
||||||
statsTable
|
|
||||||
|
|
||||||
let availableMetrics = new Set(),
|
let availableMetrics = new Set(),
|
||||||
missingMetrics = [],
|
missingMetrics = [],
|
||||||
@ -127,28 +125,17 @@
|
|||||||
let job = $initq.data.job;
|
let job = $initq.data.job;
|
||||||
if (!job) return;
|
if (!job) return;
|
||||||
|
|
||||||
const pendingMetrics = [
|
const pendingMetrics = (
|
||||||
...(
|
ccconfig[`job_view_selectedMetrics:${job.cluster}:${job.subCluster}`] ||
|
||||||
(
|
ccconfig[`job_view_selectedMetrics:${job.cluster}`]
|
||||||
ccconfig[`job_view_selectedMetrics:${job.cluster}:${job.subCluster}`] ||
|
) ||
|
||||||
ccconfig[`job_view_selectedMetrics:${job.cluster}`]
|
$initq.data.globalMetrics
|
||||||
) ||
|
.reduce((names, gm) => {
|
||||||
$initq.data.globalMetrics
|
if (gm.availability.find((av) => av.cluster === job.cluster && av.subClusters.includes(job.subCluster))) {
|
||||||
.reduce((names, gm) => {
|
names.push(gm.name);
|
||||||
if (gm.availability.find((av) => av.cluster === job.cluster && av.subClusters.includes(job.subCluster))) {
|
}
|
||||||
names.push(gm.name);
|
return names;
|
||||||
}
|
}, [])
|
||||||
return names;
|
|
||||||
}, [])
|
|
||||||
),
|
|
||||||
...(
|
|
||||||
(
|
|
||||||
ccconfig[`job_view_nodestats_selectedMetrics:${job.cluster}:${job.subCluster}`] ||
|
|
||||||
ccconfig[`job_view_nodestats_selectedMetrics:${job.cluster}`]
|
|
||||||
) ||
|
|
||||||
ccconfig[`job_view_nodestats_selectedMetrics`]
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Select default Scopes to load: Check before if any metric has accelerator scope by default
|
// Select default Scopes to load: Check before if any metric has accelerator scope by default
|
||||||
const accScopeDefault = [...pendingMetrics].some(function (m) {
|
const accScopeDefault = [...pendingMetrics].some(function (m) {
|
||||||
@ -343,7 +330,6 @@
|
|||||||
{#if item.data}
|
{#if item.data}
|
||||||
<Metric
|
<Metric
|
||||||
bind:this={plots[item.metric]}
|
bind:this={plots[item.metric]}
|
||||||
on:more-loaded={({ detail }) => statsTable.moreLoaded(detail)}
|
|
||||||
job={$initq.data.job}
|
job={$initq.data.job}
|
||||||
metricName={item.metric}
|
metricName={item.metric}
|
||||||
metricUnit={$initq.data.globalMetrics.find((gm) => gm.name == item.metric)?.unit}
|
metricUnit={$initq.data.globalMetrics.find((gm) => gm.name == item.metric)?.unit}
|
||||||
@ -398,22 +384,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
{/if}
|
{/if}
|
||||||
<TabPane
|
<!-- Includes <TabPane> Statistics Table with Independent GQL Query -->
|
||||||
tabId="stats"
|
<StatsTab job={$initq.data.job} clusters={$initq.data.clusters} tabActive={!somethingMissing}/>
|
||||||
tab="Statistics Table"
|
|
||||||
class="overflow-x-auto"
|
|
||||||
active={!somethingMissing}
|
|
||||||
>
|
|
||||||
{#if $jobMetrics?.data?.jobMetrics}
|
|
||||||
{#key $jobMetrics.data.jobMetrics}
|
|
||||||
<StatsTable
|
|
||||||
bind:this={statsTable}
|
|
||||||
job={$initq.data.job}
|
|
||||||
jobMetrics={$jobMetrics.data.jobMetrics}
|
|
||||||
/>
|
|
||||||
{/key}
|
|
||||||
{/if}
|
|
||||||
</TabPane>
|
|
||||||
<TabPane tabId="job-script" tab="Job Script">
|
<TabPane tabId="job-script" tab="Job Script">
|
||||||
<div class="pre-wrapper">
|
<div class="pre-wrapper">
|
||||||
{#if $initq.data.job.metaData?.jobScript}
|
{#if $initq.data.job.metaData?.jobScript}
|
||||||
|
@ -150,11 +150,6 @@
|
|||||||
|
|
||||||
// On additional scope request
|
// On additional scope request
|
||||||
if (selectedScope == "load-all") {
|
if (selectedScope == "load-all") {
|
||||||
// Push scope to statsTable (Needs to be in this case, else newly selected 'Metric.svelte' renders cause statsTable race condition)
|
|
||||||
const statsTableData = $metricData.data.singleUpdate.filter((x) => x.scope !== "node")
|
|
||||||
if (statsTableData.length > 0) {
|
|
||||||
dispatch("more-loaded", statsTableData);
|
|
||||||
}
|
|
||||||
// Set selected scope to min of returned scopes
|
// Set selected scope to min of returned scopes
|
||||||
selectedScope = minScope(scopes)
|
selectedScope = minScope(scopes)
|
||||||
nodeOnly = (selectedScope == "node") // "node" still only scope after load-all
|
nodeOnly = (selectedScope == "node") // "node" still only scope after load-all
|
||||||
|
145
web/frontend/src/job/StatsTab.svelte
Normal file
145
web/frontend/src/job/StatsTab.svelte
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
<!--
|
||||||
|
@component Job-View subcomponent; Wraps the statsTable in a TabPane and contains GQL query for scoped statsData
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
- `job Object`: The job object
|
||||||
|
- `clusters Object`: The clusters object
|
||||||
|
- `tabActive bool`: Boolean if StatsTabe Tab is Active on Creation
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
queryStore,
|
||||||
|
gql,
|
||||||
|
getContextClient
|
||||||
|
} from "@urql/svelte";
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
TabPane,
|
||||||
|
Spinner,
|
||||||
|
Icon
|
||||||
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
import MetricSelection from "../generic/select/MetricSelection.svelte";
|
||||||
|
import StatsTable from "./statstab/StatsTable.svelte";
|
||||||
|
|
||||||
|
export let job;
|
||||||
|
export let clusters;
|
||||||
|
export let tabActive;
|
||||||
|
|
||||||
|
let loadScopes = false;
|
||||||
|
let selectedScopes = [];
|
||||||
|
let selectedMetrics = [];
|
||||||
|
let availableMetrics = new Set(); // For Info Only, filled by MetricSelection Component
|
||||||
|
let isMetricSelectionOpen = false;
|
||||||
|
|
||||||
|
const client = getContextClient();
|
||||||
|
const query = gql`
|
||||||
|
query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!) {
|
||||||
|
scopedJobStats(id: $dbid, metrics: $selectedMetrics, scopes: $selectedScopes) {
|
||||||
|
name
|
||||||
|
scope
|
||||||
|
stats {
|
||||||
|
hostname
|
||||||
|
id
|
||||||
|
data {
|
||||||
|
min
|
||||||
|
avg
|
||||||
|
max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
$: scopedStats = queryStore({
|
||||||
|
client: client,
|
||||||
|
query: query,
|
||||||
|
variables: { dbid: job.id, selectedMetrics, selectedScopes },
|
||||||
|
});
|
||||||
|
|
||||||
|
$: if (loadScopes) {
|
||||||
|
selectedScopes = ["node", "socket", "core", "hwthread", "accelerator"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Job Query on Init -> is not executed anymore
|
||||||
|
getContext("on-init")(() => {
|
||||||
|
if (!job) return;
|
||||||
|
|
||||||
|
const pendingMetrics = (
|
||||||
|
getContext("cc-config")[`job_view_nodestats_selectedMetrics:${job.cluster}:${job.subCluster}`] ||
|
||||||
|
getContext("cc-config")[`job_view_nodestats_selectedMetrics:${job.cluster}`]
|
||||||
|
) || getContext("cc-config")["job_view_nodestats_selectedMetrics"];
|
||||||
|
|
||||||
|
// Select default Scopes to load: Check before if any metric has accelerator scope by default
|
||||||
|
const accScopeDefault = [...pendingMetrics].some(function (m) {
|
||||||
|
const cluster = clusters.find((c) => c.name == job.cluster);
|
||||||
|
const subCluster = cluster.subClusters.find((sc) => sc.name == job.subCluster);
|
||||||
|
return subCluster.metricConfig.find((smc) => smc.name == m)?.scope === "accelerator";
|
||||||
|
});
|
||||||
|
|
||||||
|
const pendingScopes = ["node"]
|
||||||
|
if (job.numNodes === 1) {
|
||||||
|
pendingScopes.push("socket")
|
||||||
|
pendingScopes.push("core")
|
||||||
|
pendingScopes.push("hwthread")
|
||||||
|
if (accScopeDefault) { pendingScopes.push("accelerator") }
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedMetrics = [...pendingMetrics];
|
||||||
|
selectedScopes = [...pendingScopes];
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<TabPane tabId="stats" tab="Statistics Table" class="overflow-x-auto" active={tabActive}>
|
||||||
|
<Row>
|
||||||
|
<Col class="m-2">
|
||||||
|
<Button outline on:click={() => (isMetricSelectionOpen = true)} class="px-2" color="primary" style="margin-right:0.5rem">
|
||||||
|
Select Metrics (Selected {selectedMetrics.length} of {availableMetrics.size} available)
|
||||||
|
</Button>
|
||||||
|
{#if job.numNodes > 1}
|
||||||
|
<Button class="px-2 ml-auto" color="success" outline on:click={() => (loadScopes = !loadScopes)} disabled={loadScopes}>
|
||||||
|
{#if !loadScopes}
|
||||||
|
<Icon name="plus-square-fill" style="margin-right:0.25rem"/> Add More Scopes
|
||||||
|
{:else}
|
||||||
|
<Icon name="check-square-fill" style="margin-right:0.25rem"/> OK: Scopes Added
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<hr class="mb-1 mt-1"/>
|
||||||
|
<!-- ROW1: Status-->
|
||||||
|
{#if $scopedStats.fetching}
|
||||||
|
<Row>
|
||||||
|
<Col class="m-3" style="text-align: center;">
|
||||||
|
<Spinner secondary/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
{:else if $scopedStats.error}
|
||||||
|
<Row>
|
||||||
|
<Col class="m-2">
|
||||||
|
<Card body color="danger">{$scopedStats.error.message}</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
{:else}
|
||||||
|
<StatsTable
|
||||||
|
hosts={job.resources.map((r) => r.hostname).sort()}
|
||||||
|
data={$scopedStats?.data?.scopedJobStats}
|
||||||
|
{selectedMetrics}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</TabPane>
|
||||||
|
|
||||||
|
<MetricSelection
|
||||||
|
cluster={job.cluster}
|
||||||
|
subCluster={job.subCluster}
|
||||||
|
configName="job_view_nodestats_selectedMetrics"
|
||||||
|
bind:allMetrics={availableMetrics}
|
||||||
|
bind:metrics={selectedMetrics}
|
||||||
|
bind:isOpen={isMetricSelectionOpen}
|
||||||
|
/>
|
@ -1,178 +0,0 @@
|
|||||||
<!--
|
|
||||||
@component Job-View subcomponent; display table of metric data statistics with selectable scopes
|
|
||||||
|
|
||||||
Properties:
|
|
||||||
- `job Object`: The job object
|
|
||||||
- `jobMetrics [Object]`: The jobs metricdata
|
|
||||||
|
|
||||||
Exported:
|
|
||||||
- `moreLoaded`: Adds additional scopes requested from Metric.svelte in Job-View
|
|
||||||
-->
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { getContext } from "svelte";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Table,
|
|
||||||
Input,
|
|
||||||
InputGroup,
|
|
||||||
InputGroupText,
|
|
||||||
Icon,
|
|
||||||
Row,
|
|
||||||
Col
|
|
||||||
} from "@sveltestrap/sveltestrap";
|
|
||||||
import { maxScope } from "../generic/utils.js";
|
|
||||||
import StatsTableEntry from "./StatsTableEntry.svelte";
|
|
||||||
import MetricSelection from "../generic/select/MetricSelection.svelte";
|
|
||||||
|
|
||||||
export let job;
|
|
||||||
export let jobMetrics;
|
|
||||||
|
|
||||||
const sortedJobMetrics = [...new Set(jobMetrics.map((m) => m.name))].sort()
|
|
||||||
const scopesForMetric = (metric) =>
|
|
||||||
jobMetrics.filter((jm) => jm.name == metric).map((jm) => jm.scope);
|
|
||||||
|
|
||||||
let hosts = job.resources.map((r) => r.hostname).sort(),
|
|
||||||
selectedScopes = {},
|
|
||||||
sorting = {},
|
|
||||||
isMetricSelectionOpen = false,
|
|
||||||
availableMetrics = new Set(),
|
|
||||||
selectedMetrics = (
|
|
||||||
getContext("cc-config")[`job_view_nodestats_selectedMetrics:${job.cluster}:${job.subCluster}`] ||
|
|
||||||
getContext("cc-config")[`job_view_nodestats_selectedMetrics:${job.cluster}`]
|
|
||||||
) || getContext("cc-config")["job_view_nodestats_selectedMetrics"];
|
|
||||||
|
|
||||||
for (let metric of sortedJobMetrics) {
|
|
||||||
// Not Exclusive or Multi-Node: get maxScope directly (mostly: node)
|
|
||||||
// -> Else: Load smallest available granularity as default as per availability
|
|
||||||
const availableScopes = scopesForMetric(metric);
|
|
||||||
if (job.exclusive != 1 || job.numNodes == 1) {
|
|
||||||
if (availableScopes.includes("accelerator")) {
|
|
||||||
selectedScopes[metric] = "accelerator";
|
|
||||||
} else if (availableScopes.includes("core")) {
|
|
||||||
selectedScopes[metric] = "core";
|
|
||||||
} else if (availableScopes.includes("socket")) {
|
|
||||||
selectedScopes[metric] = "socket";
|
|
||||||
} else {
|
|
||||||
selectedScopes[metric] = "node";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
selectedScopes[metric] = maxScope(availableScopes);
|
|
||||||
}
|
|
||||||
|
|
||||||
sorting[metric] = {
|
|
||||||
min: { dir: "up", active: false },
|
|
||||||
avg: { dir: "up", active: false },
|
|
||||||
max: { dir: "up", active: false },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortBy(metric, stat) {
|
|
||||||
let s = sorting[metric][stat];
|
|
||||||
if (s.active) {
|
|
||||||
s.dir = s.dir == "up" ? "down" : "up";
|
|
||||||
} else {
|
|
||||||
for (let metric in sorting)
|
|
||||||
for (let stat in sorting[metric]) sorting[metric][stat].active = false;
|
|
||||||
s.active = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let series = jobMetrics.find(
|
|
||||||
(jm) => jm.name == metric && jm.scope == "node",
|
|
||||||
)?.metric.series;
|
|
||||||
sorting = { ...sorting };
|
|
||||||
hosts = hosts.sort((h1, h2) => {
|
|
||||||
let s1 = series.find((s) => s.hostname == h1)?.statistics;
|
|
||||||
let s2 = series.find((s) => s.hostname == h2)?.statistics;
|
|
||||||
if (s1 == null || s2 == null) return -1;
|
|
||||||
|
|
||||||
return s.dir != "up" ? s1[stat] - s2[stat] : s2[stat] - s1[stat];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function moreLoaded(moreJobMetrics) {
|
|
||||||
moreJobMetrics.forEach(function (newMetric) {
|
|
||||||
if (!jobMetrics.some((m) => m.scope == newMetric.scope)) {
|
|
||||||
jobMetrics = [...jobMetrics, newMetric]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Row>
|
|
||||||
<Col class="m-2">
|
|
||||||
<Button outline on:click={() => (isMetricSelectionOpen = true)} class="w-auto px-2" color="primary">
|
|
||||||
Select Metrics (Selected {selectedMetrics.length} of {availableMetrics.size} available)
|
|
||||||
</Button>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<hr class="mb-1 mt-1"/>
|
|
||||||
<Table class="mb-0">
|
|
||||||
<thead>
|
|
||||||
<!-- Header Row 1: Selectors -->
|
|
||||||
<tr>
|
|
||||||
<th/>
|
|
||||||
{#each selectedMetrics as metric}
|
|
||||||
<!-- To Match Row-2 Header Field Count-->
|
|
||||||
<th colspan={selectedScopes[metric] == "node" ? 3 : 4}>
|
|
||||||
<InputGroup>
|
|
||||||
<InputGroupText>
|
|
||||||
{metric}
|
|
||||||
</InputGroupText>
|
|
||||||
<Input type="select" bind:value={selectedScopes[metric]}>
|
|
||||||
{#each scopesForMetric(metric, jobMetrics) as scope}
|
|
||||||
<option value={scope}>{scope}</option>
|
|
||||||
{/each}
|
|
||||||
</Input>
|
|
||||||
</InputGroup>
|
|
||||||
</th>
|
|
||||||
{/each}
|
|
||||||
</tr>
|
|
||||||
<!-- Header Row 2: Fields -->
|
|
||||||
<tr>
|
|
||||||
<th>Node</th>
|
|
||||||
{#each selectedMetrics as metric}
|
|
||||||
{#if selectedScopes[metric] != "node"}
|
|
||||||
<th>Id</th>
|
|
||||||
{/if}
|
|
||||||
{#each ["min", "avg", "max"] as stat}
|
|
||||||
<th on:click={() => sortBy(metric, stat)}>
|
|
||||||
{stat}
|
|
||||||
{#if selectedScopes[metric] == "node"}
|
|
||||||
<Icon
|
|
||||||
name="caret-{sorting[metric][stat].dir}{sorting[metric][stat]
|
|
||||||
.active
|
|
||||||
? '-fill'
|
|
||||||
: ''}"
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</th>
|
|
||||||
{/each}
|
|
||||||
{/each}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#each hosts as host (host)}
|
|
||||||
<tr>
|
|
||||||
<th scope="col">{host}</th>
|
|
||||||
{#each selectedMetrics as metric (metric)}
|
|
||||||
<StatsTableEntry
|
|
||||||
{host}
|
|
||||||
{metric}
|
|
||||||
scope={selectedScopes[metric]}
|
|
||||||
{jobMetrics}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
<MetricSelection
|
|
||||||
cluster={job.cluster}
|
|
||||||
subCluster={job.subCluster}
|
|
||||||
configName="job_view_nodestats_selectedMetrics"
|
|
||||||
bind:allMetrics={availableMetrics}
|
|
||||||
bind:metrics={selectedMetrics}
|
|
||||||
bind:isOpen={isMetricSelectionOpen}
|
|
||||||
/>
|
|
@ -40,14 +40,14 @@
|
|||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
const polarQuery = gql`
|
const polarQuery = gql`
|
||||||
query ($dbid: ID!, $selectedMetrics: [String!]!) {
|
query ($dbid: ID!, $selectedMetrics: [String!]!) {
|
||||||
jobMetricStats(id: $dbid, metrics: $selectedMetrics) {
|
jobStats(id: $dbid, metrics: $selectedMetrics) {
|
||||||
name
|
name
|
||||||
stats {
|
stats {
|
||||||
min
|
min
|
||||||
avg
|
avg
|
||||||
max
|
max
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -66,7 +66,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<Polar
|
<Polar
|
||||||
{polarMetrics}
|
{polarMetrics}
|
||||||
polarData={$polarData.data.jobMetricStats}
|
polarData={$polarData.data.jobStats}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</CardBody>
|
</CardBody>
|
139
web/frontend/src/job/statstab/StatsTable.svelte
Normal file
139
web/frontend/src/job/statstab/StatsTable.svelte
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
<!--:
|
||||||
|
@component Job-View subcomponent; display table of metric data statistics with selectable scopes
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
- `data Object`: The data object
|
||||||
|
- `selectedMetrics [String]`: The selected metrics
|
||||||
|
- `hosts [String]`: The list of hostnames of this job
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
Input,
|
||||||
|
InputGroup,
|
||||||
|
InputGroupText,
|
||||||
|
Icon,
|
||||||
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
import StatsTableEntry from "./StatsTableEntry.svelte";
|
||||||
|
|
||||||
|
export let data = [];
|
||||||
|
export let selectedMetrics = [];
|
||||||
|
export let hosts = [];
|
||||||
|
|
||||||
|
let sorting = {};
|
||||||
|
let availableScopes = {};
|
||||||
|
let selectedScopes = {};
|
||||||
|
|
||||||
|
const scopesForMetric = (metric) =>
|
||||||
|
data?.filter((jm) => jm.name == metric)?.map((jm) => jm.scope) || [];
|
||||||
|
const setScopeForMetric = (metric, scope) =>
|
||||||
|
selectedScopes[metric] = scope
|
||||||
|
|
||||||
|
$: if (data && selectedMetrics) {
|
||||||
|
for (let metric of selectedMetrics) {
|
||||||
|
availableScopes[metric] = scopesForMetric(metric);
|
||||||
|
// Set Initial Selection, but do not use selectedScopes: Skips reactivity
|
||||||
|
if (availableScopes[metric].includes("accelerator")) {
|
||||||
|
setScopeForMetric(metric, "accelerator");
|
||||||
|
} else if (availableScopes[metric].includes("core")) {
|
||||||
|
setScopeForMetric(metric, "core");
|
||||||
|
} else if (availableScopes[metric].includes("socket")) {
|
||||||
|
setScopeForMetric(metric, "socket");
|
||||||
|
} else {
|
||||||
|
setScopeForMetric(metric, "node");
|
||||||
|
}
|
||||||
|
|
||||||
|
sorting[metric] = {
|
||||||
|
min: { dir: "up", active: false },
|
||||||
|
avg: { dir: "up", active: false },
|
||||||
|
max: { dir: "up", active: false },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortBy(metric, stat) {
|
||||||
|
let s = sorting[metric][stat];
|
||||||
|
if (s.active) {
|
||||||
|
s.dir = s.dir == "up" ? "down" : "up";
|
||||||
|
} else {
|
||||||
|
for (let metric in sorting)
|
||||||
|
for (let stat in sorting[metric]) sorting[metric][stat].active = false;
|
||||||
|
s.active = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stats = data.find(
|
||||||
|
(d) => d.name == metric && d.scope == "node",
|
||||||
|
)?.stats || [];
|
||||||
|
sorting = { ...sorting };
|
||||||
|
hosts = hosts.sort((h1, h2) => {
|
||||||
|
let s1 = stats.find((s) => s.hostname == h1)?.data;
|
||||||
|
let s2 = stats.find((s) => s.hostname == h2)?.data;
|
||||||
|
if (s1 == null || s2 == null) return -1;
|
||||||
|
|
||||||
|
return s.dir != "up" ? s1[stat] - s2[stat] : s2[stat] - s1[stat];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Table class="mb-0">
|
||||||
|
<thead>
|
||||||
|
<!-- Header Row 1: Selectors -->
|
||||||
|
<tr>
|
||||||
|
<th/>
|
||||||
|
{#each selectedMetrics as metric}
|
||||||
|
<!-- To Match Row-2 Header Field Count-->
|
||||||
|
<th colspan={selectedScopes[metric] == "node" ? 3 : 4}>
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroupText>
|
||||||
|
{metric}
|
||||||
|
</InputGroupText>
|
||||||
|
<Input type="select" bind:value={selectedScopes[metric]} disabled={availableScopes[metric].length === 1}>
|
||||||
|
{#each (availableScopes[metric] || []) as scope}
|
||||||
|
<option value={scope}>{scope}</option>
|
||||||
|
{/each}
|
||||||
|
</Input>
|
||||||
|
</InputGroup>
|
||||||
|
</th>
|
||||||
|
{/each}
|
||||||
|
</tr>
|
||||||
|
<!-- Header Row 2: Fields -->
|
||||||
|
<tr>
|
||||||
|
<th>Node</th>
|
||||||
|
{#each selectedMetrics as metric}
|
||||||
|
{#if selectedScopes[metric] != "node"}
|
||||||
|
<th>Id</th>
|
||||||
|
{/if}
|
||||||
|
{#each ["min", "avg", "max"] as stat}
|
||||||
|
<th on:click={() => sortBy(metric, stat)}>
|
||||||
|
{stat}
|
||||||
|
{#if selectedScopes[metric] == "node"}
|
||||||
|
<Icon
|
||||||
|
name="caret-{sorting[metric][stat].dir}{sorting[metric][stat]
|
||||||
|
.active
|
||||||
|
? '-fill'
|
||||||
|
: ''}"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</th>
|
||||||
|
{/each}
|
||||||
|
{/each}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each hosts as host (host)}
|
||||||
|
<tr>
|
||||||
|
<th scope="col">{host}</th>
|
||||||
|
{#each selectedMetrics as metric (metric)}
|
||||||
|
<StatsTableEntry
|
||||||
|
{data}
|
||||||
|
{host}
|
||||||
|
{metric}
|
||||||
|
scope={selectedScopes[metric]}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
@ -1,11 +1,11 @@
|
|||||||
<!--
|
<!--
|
||||||
@component Job-View subcomponent; Single Statistics entry component fpr statstable
|
@component Job-View subcomponent; Single Statistics entry component for statstable
|
||||||
|
|
||||||
Properties:
|
Properties:
|
||||||
- `host String`: The hostname (== node)
|
- `host String`: The hostname (== node)
|
||||||
- `metric String`: The metric name
|
- `metric String`: The metric name
|
||||||
- `scope String`: The selected scope
|
- `scope String`: The selected scope
|
||||||
- `jobMetrics [Object]`: The jobs metricdata
|
- `data [Object]`: The jobs statsdata
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -14,59 +14,59 @@
|
|||||||
export let host;
|
export let host;
|
||||||
export let metric;
|
export let metric;
|
||||||
export let scope;
|
export let scope;
|
||||||
export let jobMetrics;
|
export let data;
|
||||||
|
|
||||||
function compareNumbers(a, b) {
|
let entrySorting = {
|
||||||
return a.id - b.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortByField(field) {
|
|
||||||
let s = sorting[field];
|
|
||||||
if (s.active) {
|
|
||||||
s.dir = s.dir == "up" ? "down" : "up";
|
|
||||||
} else {
|
|
||||||
for (let field in sorting) sorting[field].active = false;
|
|
||||||
s.active = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
sorting = { ...sorting };
|
|
||||||
series = series.sort((a, b) => {
|
|
||||||
if (a == null || b == null) return -1;
|
|
||||||
|
|
||||||
if (field === "id") {
|
|
||||||
return s.dir != "up" ? a[field] - b[field] : b[field] - a[field];
|
|
||||||
} else {
|
|
||||||
return s.dir != "up"
|
|
||||||
? a.statistics[field] - b.statistics[field]
|
|
||||||
: b.statistics[field] - a.statistics[field];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let sorting = {
|
|
||||||
id: { dir: "down", active: true },
|
id: { dir: "down", active: true },
|
||||||
min: { dir: "up", active: false },
|
min: { dir: "up", active: false },
|
||||||
avg: { dir: "up", active: false },
|
avg: { dir: "up", active: false },
|
||||||
max: { dir: "up", active: false },
|
max: { dir: "up", active: false },
|
||||||
};
|
};
|
||||||
|
|
||||||
$: series = jobMetrics
|
function compareNumbers(a, b) {
|
||||||
.find((jm) => jm.name == metric && jm.scope == scope)
|
return a.id - b.id;
|
||||||
?.metric.series.filter((s) => s.hostname == host && s.statistics != null)
|
}
|
||||||
?.sort(compareNumbers);
|
|
||||||
|
function sortByField(field) {
|
||||||
|
let s = entrySorting[field];
|
||||||
|
if (s.active) {
|
||||||
|
s.dir = s.dir == "up" ? "down" : "up";
|
||||||
|
} else {
|
||||||
|
for (let field in entrySorting) entrySorting[field].active = false;
|
||||||
|
s.active = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
entrySorting = { ...entrySorting };
|
||||||
|
stats = stats.sort((a, b) => {
|
||||||
|
if (a == null || b == null) return -1;
|
||||||
|
|
||||||
|
if (field === "id") {
|
||||||
|
return s.dir != "up" ? a[field].localeCompare(b[field]) : b[field].localeCompare(a[field])
|
||||||
|
} else {
|
||||||
|
return s.dir != "up"
|
||||||
|
? a.data[field] - b.data[field]
|
||||||
|
: b.data[field] - a.data[field];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$: stats = data
|
||||||
|
?.find((d) => d.name == metric && d.scope == scope)
|
||||||
|
?.stats.filter((s) => s.hostname == host && s.data != null)
|
||||||
|
?.sort(compareNumbers) || [];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if series == null || series.length == 0}
|
{#if stats == null || stats.length == 0}
|
||||||
<td colspan={scope == "node" ? 3 : 4}><i>No data</i></td>
|
<td colspan={scope == "node" ? 3 : 4}><i>No data</i></td>
|
||||||
{:else if series.length == 1 && scope == "node"}
|
{:else if stats.length == 1 && scope == "node"}
|
||||||
<td>
|
<td>
|
||||||
{series[0].statistics.min}
|
{stats[0].data.min}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{series[0].statistics.avg}
|
{stats[0].data.avg}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{series[0].statistics.max}
|
{stats[0].data.max}
|
||||||
</td>
|
</td>
|
||||||
{:else}
|
{:else}
|
||||||
<td colspan="4">
|
<td colspan="4">
|
||||||
@ -76,19 +76,19 @@
|
|||||||
<th on:click={() => sortByField(field)}>
|
<th on:click={() => sortByField(field)}>
|
||||||
Sort
|
Sort
|
||||||
<Icon
|
<Icon
|
||||||
name="caret-{sorting[field].dir}{sorting[field].active
|
name="caret-{entrySorting[field].dir}{entrySorting[field].active
|
||||||
? '-fill'
|
? '-fill'
|
||||||
: ''}"
|
: ''}"
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
{/each}
|
{/each}
|
||||||
</tr>
|
</tr>
|
||||||
{#each series as s, i}
|
{#each stats as s, i}
|
||||||
<tr>
|
<tr>
|
||||||
<th>{s.id ?? i}</th>
|
<th>{s.id ?? i}</th>
|
||||||
<td>{s.statistics.min}</td>
|
<td>{s.data.min}</td>
|
||||||
<td>{s.statistics.avg}</td>
|
<td>{s.data.avg}</td>
|
||||||
<td>{s.statistics.max}</td>
|
<td>{s.data.max}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</table>
|
</table>
|
Loading…
x
Reference in New Issue
Block a user