diff --git a/api/rest.go b/api/rest.go index f7baf05..69cfb77 100644 --- a/api/rest.go +++ b/api/rest.go @@ -43,6 +43,8 @@ func (api *RestApi) MountRoutes(r *mux.Router) { r.HandleFunc("/jobs/{id}", api.getJob).Methods(http.MethodGet) r.HandleFunc("/jobs/tag_job/{id}", api.tagJob).Methods(http.MethodPost, http.MethodPatch) + r.HandleFunc("/jobs/metrics/{id}", api.getJobMetrics).Methods(http.MethodGet) + if api.MachineStateDir != "" { r.HandleFunc("/machine_state/{cluster}/{host}", api.getMachineState).Methods(http.MethodGet) r.HandleFunc("/machine_state/{cluster}/{host}", api.putMachineState).Methods(http.MethodPut, http.MethodPost) @@ -370,6 +372,52 @@ func (api *RestApi) stopJob(rw http.ResponseWriter, r *http.Request) { } } +func (api *RestApi) getJobMetrics(rw http.ResponseWriter, r *http.Request) { + id := mux.Vars(r)["id"] + metrics := r.URL.Query()["metric"] + var scopes []schema.MetricScope + for _, scope := range r.URL.Query()["scope"] { + var s schema.MetricScope + if err := s.UnmarshalGQL(scope); err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + scopes = append(scopes, s) + } + + rw.Header().Add("Content-Type", "application/json") + rw.WriteHeader(http.StatusOK) + + type Respone struct { + Data *struct { + JobMetrics []*model.JobMetricWithName `json:"jobMetrics"` + } `json:"data"` + Error *struct { + Message string `json:"message"` + } `json:"error"` + } + + data, err := api.Resolver.Query().JobMetrics(r.Context(), id, metrics, scopes) + if err != nil { + if err := json.NewEncoder(rw).Encode(Respone{ + Error: &struct { + Message string "json:\"message\"" + }{Message: err.Error()}, + }); err != nil { + log.Println(err.Error()) + } + return + } + + if err := json.NewEncoder(rw).Encode(Respone{ + Data: &struct { + JobMetrics []*model.JobMetricWithName "json:\"jobMetrics\"" + }{JobMetrics: data}, + }); err != nil { + log.Println(err.Error()) + } +} + func (api *RestApi) putMachineState(rw http.ResponseWriter, r *http.Request) { if api.MachineStateDir == "" { http.Error(rw, "not enabled", http.StatusNotFound) diff --git a/graph/schema.graphqls b/graph/schema.graphqls index dc3456f..26a8821 100644 --- a/graph/schema.graphqls +++ b/graph/schema.graphqls @@ -103,9 +103,9 @@ type Series { } type MetricStatistics { - avg: NullableFloat! - min: NullableFloat! - max: NullableFloat! + avg: Float! + min: Float! + max: Float! } type StatsSeries { diff --git a/schema/float.go b/schema/float.go index 5aeb82e..a24a98b 100644 --- a/schema/float.go +++ b/schema/float.go @@ -65,3 +65,41 @@ func (f Float) MarshalGQL(w io.Writer) { w.Write(strconv.AppendFloat(make([]byte, 0, 10), float64(f), 'f', 2, 64)) } } + +// Only used via REST-API, not via GraphQL. +// This uses a lot less allocations per series, +// but it turns out that the performance increase +// from using this is not that big. +func (s *Series) MarshalJSON() ([]byte, error) { + buf := make([]byte, 0, 512+len(s.Data)*8) + buf = append(buf, `{"hostname":"`...) + buf = append(buf, s.Hostname...) + buf = append(buf, '"') + if s.Id != nil { + buf = append(buf, `,"id":`...) + buf = strconv.AppendInt(buf, int64(*s.Id), 10) + } + if s.Statistics != nil { + buf = append(buf, `,"statistics":{"min":`...) + buf = strconv.AppendFloat(buf, s.Statistics.Min, 'f', 2, 64) + buf = append(buf, `,"avg":`...) + buf = strconv.AppendFloat(buf, s.Statistics.Avg, 'f', 2, 64) + buf = append(buf, `,"max":`...) + buf = strconv.AppendFloat(buf, s.Statistics.Max, 'f', 2, 64) + buf = append(buf, '}') + } + buf = append(buf, `,"data":[`...) + for i := 0; i < len(s.Data); i++ { + if i != 0 { + buf = append(buf, ',') + } + + if s.Data[i].IsNaN() { + buf = append(buf, `null`...) + } else { + buf = strconv.AppendFloat(buf, float64(s.Data[i]), 'f', 2, 32) + } + } + buf = append(buf, ']', '}') + return buf, nil +}