mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-07-23 12:51:40 +02:00
Merge pull request #143 from ClusterCockpit/102-add-rest-api-endpoint-to-query-full-job-data-including-all-metric-timeseries
102 add rest api endpoint to query full job data including all metric timeseries
This commit is contained in:
@@ -628,6 +628,91 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/jobs/{id}": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Job to get is specified by database ID\nReturns full job resource information according to 'JobMeta' scheme and all metrics according to 'JobData'.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"query"
|
||||
],
|
||||
"summary": "Get complete job meta and metric data",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Database ID of Job",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Array of metric names",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Job resource",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.GetJobApiResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Resource not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Entity: finding job failed: sql: no rows in result set",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
@@ -690,6 +775,20 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"api.GetJobApiResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/api.JobMetricWithName"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"$ref": "#/definitions/schema.Job"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api.GetJobsApiResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -710,6 +809,20 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"api.JobMetricWithName": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"metric": {
|
||||
"$ref": "#/definitions/schema.JobMetric"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"scope": {
|
||||
"$ref": "#/definitions/schema.MetricScope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api.StartJobApiResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -771,6 +884,9 @@ const docTemplate = `{
|
||||
"type": "string",
|
||||
"example": "fritz"
|
||||
},
|
||||
"concurrentJobs": {
|
||||
"$ref": "#/definitions/schema.JobLinkResultList"
|
||||
},
|
||||
"duration": {
|
||||
"description": "Duration of job in seconds (Min \u003e 0)",
|
||||
"type": "integer",
|
||||
@@ -795,6 +911,14 @@ const docTemplate = `{
|
||||
},
|
||||
"jobState": {
|
||||
"description": "Final state of job",
|
||||
"enum": [
|
||||
"completed",
|
||||
"failed",
|
||||
"cancelled",
|
||||
"stopped",
|
||||
"timeout",
|
||||
"out_of_memory"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/schema.JobState"
|
||||
@@ -823,7 +947,7 @@ const docTemplate = `{
|
||||
"example": 2
|
||||
},
|
||||
"numHwthreads": {
|
||||
"description": "Number of HWThreads used (Min \u003e 0)",
|
||||
"description": "NumCores int32 ` + "`" + `json:\"numCores\" db:\"num_cores\" example:\"20\" minimum:\"1\"` + "`" + ` // Number of HWThreads used (Min \u003e 0)",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"example": 20
|
||||
@@ -885,6 +1009,31 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.JobLink": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"jobId": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.JobLinkResultList": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/schema.JobLink"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.JobMeta": {
|
||||
"description": "Meta data information of a HPC job.",
|
||||
"type": "object",
|
||||
@@ -899,6 +1048,9 @@ const docTemplate = `{
|
||||
"type": "string",
|
||||
"example": "fritz"
|
||||
},
|
||||
"concurrentJobs": {
|
||||
"$ref": "#/definitions/schema.JobLinkResultList"
|
||||
},
|
||||
"duration": {
|
||||
"description": "Duration of job in seconds (Min \u003e 0)",
|
||||
"type": "integer",
|
||||
@@ -923,6 +1075,14 @@ const docTemplate = `{
|
||||
},
|
||||
"jobState": {
|
||||
"description": "Final state of job",
|
||||
"enum": [
|
||||
"completed",
|
||||
"failed",
|
||||
"cancelled",
|
||||
"stopped",
|
||||
"timeout",
|
||||
"out_of_memory"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/schema.JobState"
|
||||
@@ -951,7 +1111,7 @@ const docTemplate = `{
|
||||
"example": 2
|
||||
},
|
||||
"numHwthreads": {
|
||||
"description": "Number of HWThreads used (Min \u003e 0)",
|
||||
"description": "NumCores int32 ` + "`" + `json:\"numCores\" db:\"num_cores\" example:\"20\" minimum:\"1\"` + "`" + ` // Number of HWThreads used (Min \u003e 0)",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"example": 20
|
||||
@@ -1022,6 +1182,26 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.JobMetric": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"series": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/schema.Series"
|
||||
}
|
||||
},
|
||||
"statisticsSeries": {
|
||||
"$ref": "#/definitions/schema.StatsSeries"
|
||||
},
|
||||
"timestep": {
|
||||
"type": "integer"
|
||||
},
|
||||
"unit": {
|
||||
"$ref": "#/definitions/schema.Unit"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.JobState": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -1068,9 +1248,42 @@ const docTemplate = `{
|
||||
"example": 2000
|
||||
},
|
||||
"unit": {
|
||||
"description": "Metric unit (see schema/unit.schema.json)",
|
||||
"type": "string",
|
||||
"example": "GHz"
|
||||
"$ref": "#/definitions/schema.Unit"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.MetricScope": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"invalid_scope",
|
||||
"node",
|
||||
"socket",
|
||||
"memoryDomain",
|
||||
"core",
|
||||
"hwthread",
|
||||
"accelerator"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"MetricScopeInvalid",
|
||||
"MetricScopeNode",
|
||||
"MetricScopeSocket",
|
||||
"MetricScopeMemoryDomain",
|
||||
"MetricScopeCore",
|
||||
"MetricScopeHWThread",
|
||||
"MetricScopeAccelerator"
|
||||
]
|
||||
},
|
||||
"schema.MetricStatistics": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"avg": {
|
||||
"type": "number"
|
||||
},
|
||||
"max": {
|
||||
"type": "number"
|
||||
},
|
||||
"min": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1102,12 +1315,64 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.Series": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"hostname": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"statistics": {
|
||||
"$ref": "#/definitions/schema.MetricStatistics"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.StatsSeries": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"max": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"mean": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"min": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"percentiles": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.Tag": {
|
||||
"description": "Defines a tag using name and type.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "The unique DB identifier of a tag",
|
||||
"description": "The unique DB identifier of a tag\nThe unique DB identifier of a tag",
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
@@ -1121,6 +1386,17 @@ const docTemplate = `{
|
||||
"example": "Debug"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.Unit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"base": {
|
||||
"type": "string"
|
||||
},
|
||||
"prefix": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
@@ -1139,7 +1415,7 @@ const docTemplate = `{
|
||||
|
||||
// SwaggerInfo holds exported Swagger Info so clients can modify it
|
||||
var SwaggerInfo = &swag.Spec{
|
||||
Version: "0.2.0",
|
||||
Version: "1",
|
||||
Host: "localhost:8080",
|
||||
BasePath: "/api",
|
||||
Schemes: []string{},
|
||||
|
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/ClusterCockpit/cc-backend/internal/graph"
|
||||
"github.com/ClusterCockpit/cc-backend/internal/graph/model"
|
||||
"github.com/ClusterCockpit/cc-backend/internal/importer"
|
||||
"github.com/ClusterCockpit/cc-backend/internal/metricdata"
|
||||
"github.com/ClusterCockpit/cc-backend/internal/repository"
|
||||
"github.com/ClusterCockpit/cc-backend/pkg/archive"
|
||||
"github.com/ClusterCockpit/cc-backend/pkg/log"
|
||||
@@ -68,7 +69,7 @@ func (api *RestApi) MountRoutes(r *mux.Router) {
|
||||
// r.HandleFunc("/jobs/import/", api.importJob).Methods(http.MethodPost, http.MethodPut)
|
||||
|
||||
r.HandleFunc("/jobs/", api.getJobs).Methods(http.MethodGet)
|
||||
// r.HandleFunc("/jobs/{id}", api.getJob).Methods(http.MethodGet)
|
||||
r.HandleFunc("/jobs/{id}", api.getJobById).Methods(http.MethodPost)
|
||||
r.HandleFunc("/jobs/tag_job/{id}", api.tagJob).Methods(http.MethodPost, http.MethodPatch)
|
||||
r.HandleFunc("/jobs/metrics/{id}", api.getJobMetrics).Methods(http.MethodGet)
|
||||
r.HandleFunc("/jobs/delete_job/", api.deleteJobByRequest).Methods(http.MethodDelete)
|
||||
@@ -142,6 +143,19 @@ type ApiTag struct {
|
||||
|
||||
type TagJobApiRequest []*ApiTag
|
||||
|
||||
type GetJobApiRequest []string
|
||||
|
||||
type GetJobApiResponse struct {
|
||||
Meta *schema.Job
|
||||
Data []*JobMetricWithName
|
||||
}
|
||||
|
||||
type JobMetricWithName struct {
|
||||
Name string `json:"name"`
|
||||
Scope schema.MetricScope `json:"scope"`
|
||||
Metric *schema.JobMetric `json:"metric"`
|
||||
}
|
||||
|
||||
func handleError(err error, statusCode int, rw http.ResponseWriter) {
|
||||
log.Warnf("REST ERROR : %s", err.Error())
|
||||
rw.Header().Add("Content-Type", "application/json")
|
||||
@@ -301,6 +315,99 @@ func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// getJobById godoc
|
||||
// @summary Get complete job meta and metric data
|
||||
// @tags query
|
||||
// @description Job to get is specified by database ID
|
||||
// @description Returns full job resource information according to 'JobMeta' scheme and all metrics according to 'JobData'.
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "Database ID of Job"
|
||||
// @param request body api.GetJobApiRequest true "Array of metric names"
|
||||
// @success 200 {object} api.GetJobApiResponse "Job resource"
|
||||
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
||||
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
||||
// @failure 403 {object} api.ErrorResponse "Forbidden"
|
||||
// @failure 404 {object} api.ErrorResponse "Resource not found"
|
||||
// @failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set"
|
||||
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
|
||||
// @security ApiKeyAuth
|
||||
// @router /jobs/{id} [post]
|
||||
func (api *RestApi) getJobById(rw http.ResponseWriter, r *http.Request) {
|
||||
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
|
||||
handleError(fmt.Errorf("missing role: %v",
|
||||
auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch job from db
|
||||
id, ok := mux.Vars(r)["id"]
|
||||
var job *schema.Job
|
||||
var err error
|
||||
if ok {
|
||||
id, e := strconv.ParseInt(id, 10, 64)
|
||||
if e != nil {
|
||||
handleError(fmt.Errorf("integer expected in path for id: %w", e), http.StatusBadRequest, rw)
|
||||
return
|
||||
}
|
||||
|
||||
job, err = api.JobRepository.FindById(id)
|
||||
} else {
|
||||
handleError(errors.New("the parameter 'id' is required"), http.StatusBadRequest, rw)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
handleError(fmt.Errorf("finding job failed: %w", err), http.StatusUnprocessableEntity, rw)
|
||||
return
|
||||
}
|
||||
|
||||
var metrics GetJobApiRequest
|
||||
if err = decode(r.Body, &metrics); err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var scopes []schema.MetricScope
|
||||
|
||||
if job.NumNodes == 1 {
|
||||
scopes = []schema.MetricScope{"core"}
|
||||
} else {
|
||||
scopes = []schema.MetricScope{"node"}
|
||||
}
|
||||
|
||||
data, err := metricdata.LoadData(job, metrics, scopes, r.Context())
|
||||
if err != nil {
|
||||
log.Warn("Error while loading job data")
|
||||
return
|
||||
}
|
||||
|
||||
res := []*JobMetricWithName{}
|
||||
for name, md := range data {
|
||||
for scope, metric := range md {
|
||||
res = append(res, &JobMetricWithName{
|
||||
Name: name,
|
||||
Scope: scope,
|
||||
Metric: metric,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("/api/job/%s: get job %d", id, job.JobID)
|
||||
rw.Header().Add("Content-Type", "application/json")
|
||||
bw := bufio.NewWriter(rw)
|
||||
defer bw.Flush()
|
||||
|
||||
payload := GetJobApiResponse{
|
||||
Meta: job,
|
||||
Data: res,
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(bw).Encode(payload); err != nil {
|
||||
handleError(err, http.StatusInternalServerError, rw)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// tagJob godoc
|
||||
// @summary Adds one or more tags to a job
|
||||
// @tags add and modify
|
||||
|
Reference in New Issue
Block a user