diff --git a/Makefile b/Makefile index 69b905ba..bdaa9858 100644 --- a/Makefile +++ b/Makefile @@ -118,9 +118,9 @@ upload-msg-to-rosocp: get-recommendations: ifdef env $(eval APIPOD=$(shell oc get pods -o custom-columns=POD:.metadata.name --no-headers -n ${env} | grep ros-ocp-backend-api)) - oc exec ${APIPOD} -c ros-ocp-backend-api -n ${env} -- /bin/bash -c 'curl -s -H "X-Rh-Identity: ${b64_identity}" -H "x-rh-request_id: testtesttest" http://localhost:8000/api/cost-management/v1/recommendations/openshift' | python -m json.tool + oc exec ${APIPOD} -c ros-ocp-backend-api -n ${env} -- /bin/bash -c 'curl -s -H "X-Rh-Identity: ${b64_identity}" -H "x-rh-request_id: testtesttest" http://localhost:8000/api/cost-management/v1/recommendations/openshift?start_date=1992-12-26' | python -m json.tool else curl -s -H "x-rh-identity: ${b64_identity}" \ -H "x-rh-request_id: testtesttest" \ - http://localhost:8000/api/cost-management/v1/recommendations/openshift | python -m json.tool + http://localhost:8000/api/cost-management/v1/recommendations/openshift?start_date=1992-12-26 | python -m json.tool endif diff --git a/internal/api/utils.go b/internal/api/utils.go index d6381e03..4d5ed436 100644 --- a/internal/api/utils.go +++ b/internal/api/utils.go @@ -223,11 +223,6 @@ func TransformComponentUnits(jsonData datatypes.JSON) map[string]interface{} { return nil } - durationBased, ok := data["duration_based"].(map[string]interface{}) - if !ok { - fmt.Printf("duration_based not found in JSON") - } - convertMemory := func(memory map[string]interface{}) error { amount, ok := memory["amount"].(float64) if ok { @@ -276,45 +271,58 @@ func TransformComponentUnits(jsonData datatypes.JSON) map[string]interface{} { /* Recommendation data is available for three periods + for cost and performance For each of these actual values will be present in below mentioned dataBlocks > request and limits */ - for _, period := range []string{"long_term", "medium_term", "short_term"} { - intervalData, ok := durationBased[period].(map[string]interface{}) + recommendation_terms, ok := data["recommendation_terms"].(map[string]interface{}) + if !ok { + fmt.Printf("recommendation_terms not found in JSON") + } + + for _, period := range []string{"short_term", "medium_term", "long_term"} { + intervalData, ok := recommendation_terms[period].(map[string]interface{}) if !ok { continue } - for _, dataBlock := range []string{"current", "config", "variation"} { - recommendationSection, ok := intervalData[dataBlock].(map[string]interface{}) + for _, recommendationType := range []string{"cost", "performance"} { + engineData, ok := intervalData["recommendation_engines"].(map[string]interface{})[recommendationType].(map[string]interface{}) if !ok { continue } - for _, section := range []string{"limits", "requests"} { + for _, dataBlock := range []string{"current", "config", "variation"} { + recommendationSection, ok := engineData[dataBlock].(map[string]interface{}) + if !ok { + continue + } + + for _, section := range []string{"limits", "requests"} { - sectionObject, ok := recommendationSection[section].(map[string]interface{}) - if ok { - memory, ok := sectionObject["memory"].(map[string]interface{}) + sectionObject, ok := recommendationSection[section].(map[string]interface{}) if ok { - err := convertMemory(memory) - if err != nil { - fmt.Printf("error converting memory in %s: %v\n", period, err) - continue + memory, ok := sectionObject["memory"].(map[string]interface{}) + if ok { + err := convertMemory(memory) + if err != nil { + fmt.Printf("error converting memory in %s: %v\n", period, err) + continue + } } - } - cpu, ok := sectionObject["cpu"].(map[string]interface{}) - if ok { - err := convertCPU(cpu) - if err != nil { - fmt.Printf("error converting cpu in %s: %v\n", period, err) - continue + cpu, ok := sectionObject["cpu"].(map[string]interface{}) + if ok { + err := convertCPU(cpu) + if err != nil { + fmt.Printf("error converting cpu in %s: %v\n", period, err) + continue + } } } - } - } - + } + + } } } diff --git a/internal/services/report_processor.go b/internal/services/report_processor.go index 8fb1a649..62f3482b 100644 --- a/internal/services/report_processor.go +++ b/internal/services/report_processor.go @@ -98,7 +98,9 @@ func ProcessReport(msg *kafka.Message) { k8s_object_name, ) - container_names, err := kruize.Create_kruize_experiments(experiment_name, k8s_object) + cluster_identifier := kafkaMsg.Metadata.Cluster_alias + "; " + kafkaMsg.Metadata.Cluster_uuid + + container_names, err := kruize.Create_kruize_experiments(experiment_name, cluster_identifier, k8s_object) if err != nil { log.Error(err) continue @@ -199,7 +201,7 @@ func ProcessReport(msg *kafka.Message) { continue } - if kruize.Is_valid_recommendation(recommendation) { + if kruize.Is_valid_recommendation(recommendation, experiment_name, maxEndTime.String()) { containers := recommendation[0].Kubernetes_objects[0].Containers for _, container := range containers { for _, v := range container.Recommendations.Data { @@ -212,8 +214,8 @@ func ProcessReport(msg *kafka.Message) { recommendationSet := model.RecommendationSet{ WorkloadID: workload.ID, ContainerName: container.Container_name, - MonitoringStartTime: v.Duration_based.Short_term.Monitoring_start_time, - MonitoringEndTime: v.Duration_based.Short_term.Monitoring_end_time, + MonitoringStartTime: v.RecommendationTerms.Short_term.MonitoringStartTime, + MonitoringEndTime: v.MonitoringEndTime, Recommendations: marshalData, } if err := recommendationSet.CreateRecommendationSet(); err != nil { @@ -227,8 +229,8 @@ func ProcessReport(msg *kafka.Message) { historicalRecommendationSet := model.HistoricalRecommendationSet{ WorkloadID: workload.ID, ContainerName: container.Container_name, - MonitoringStartTime: v.Duration_based.Short_term.Monitoring_start_time, - MonitoringEndTime: v.Duration_based.Short_term.Monitoring_end_time, + MonitoringStartTime: v.RecommendationTerms.Short_term.MonitoringStartTime, + MonitoringEndTime: v.MonitoringEndTime, Recommendations: marshalData, } if err := historicalRecommendationSet.CreateHistoricalRecommendationSet(); err != nil { diff --git a/internal/types/kruizePayload/common.go b/internal/types/kruizePayload/common.go index 935ace90..0a9327d3 100644 --- a/internal/types/kruizePayload/common.go +++ b/internal/types/kruizePayload/common.go @@ -38,36 +38,47 @@ type aggregation_info struct { } type recommendation struct { - Data map[string]recommendationType `json:"data,omitempty"` - Notifications map[string]notification `json:"notifications,omitempty"` + Version string `json:"version,omitempty"` + Data map[string]RecommendationData `json:"data,omitempty"` + Notifications map[string]Notification `json:"notifications,omitempty"` } -type notification struct { + +type Notification struct { NotifyType string `json:"type,omitempty"` Message string `json:"message,omitempty"` Code int `json:"code,omitempty"` } -type recommendationType struct { - Duration_based termbased `json:"duration_based,omitempty"` +type RecommendationEngineObject struct { + PodsCount int `json:"pods_count,omitempty"` + ConfidenceLevel float64 `json:"confidence_level,omitempty"` + Config ConfigObject `json:"config,omitempty"` + Variation ConfigObject `json:"variation,omitempty"` + Notifications map[string]Notification `json:"notifications,omitempty"` +} + +type RecommendationData struct { + Notifications map[string]Notification `json:"notifications"` + MonitoringEndTime time.Time `json:"monitoring_end_time"` + Current ConfigObject `json:"current"` + RecommendationTerms TermBased `json:"recommendation_terms"` } -type termbased struct { - Short_term recommendationObject `json:"short_term,omitempty"` - Medium_term recommendationObject `json:"medium_term,omitempty"` - Long_term recommendationObject `json:"long_term,omitempty"` +type RecommendationTerm struct { + DurationInHours float64 `json:"duration_in_hours"` + Notifications map[string]Notification `json:"notifications"` + MonitoringStartTime time.Time + RecommendationEngines struct { + Cost RecommendationEngineObject `json:"cost"` + Performance RecommendationEngineObject `json:"performance"` + } `json:"recommendation_engines"` } -type recommendationObject struct { - Monitoring_start_time time.Time `json:"monitoring_start_time,omitempty"` - Monitoring_end_time time.Time `json:"monitoring_end_time,omitempty"` - Duration_in_hours float64 `json:"duration_in_hours,omitempty"` - Pods_count int `json:"pods_count,omitempty"` - Confidence_level float64 `json:"confidence_level,omitempty"` - Current ConfigObject `json:"current,omitempty"` - Config ConfigObject `json:"config,omitempty"` - Variation ConfigObject `json:"variation,omitempty"` - Notifications map[string]notification `json:"notifications,omitempty"` +type TermBased struct { + Short_term RecommendationTerm `json:"short_term,omitempty"` + Medium_term RecommendationTerm `json:"medium_term,omitempty"` + Long_term RecommendationTerm `json:"long_term,omitempty"` } type ConfigObject struct { diff --git a/internal/types/kruizePayload/createExperiment.go b/internal/types/kruizePayload/createExperiment.go index 2153b36a..037eacef 100644 --- a/internal/types/kruizePayload/createExperiment.go +++ b/internal/types/kruizePayload/createExperiment.go @@ -7,6 +7,7 @@ import ( type createExperiment struct { Version string `json:"version"` Experiment_name string `json:"experiment_name"` + Cluster_name string `json:"cluster_name"` Performance_profile string `json:"performance_profile"` Mode string `json:"mode"` Target_cluster string `json:"target_cluster"` @@ -23,7 +24,7 @@ type recommendation_settings struct { Threshold string `json:"threshold"` } -func GetCreateExperimentPayload(experiment_name string, containers []map[string]string, data map[string]string) ([]byte, error) { +func GetCreateExperimentPayload(experiment_name string, cluster_identifier string, containers []map[string]string, data map[string]string) ([]byte, error) { container_array := []container{} for _, c := range containers { container_array = append(container_array, container{ @@ -35,6 +36,7 @@ func GetCreateExperimentPayload(experiment_name string, containers []map[string] { Version: "1.0", Experiment_name: experiment_name, + Cluster_name: cluster_identifier, Performance_profile: "resource-optimization-openshift", Mode: "monitor", Target_cluster: "remote", diff --git a/internal/utils/kruize/kruize_api.go b/internal/utils/kruize/kruize_api.go index 39fd2ddc..aa6fbbe6 100644 --- a/internal/utils/kruize/kruize_api.go +++ b/internal/utils/kruize/kruize_api.go @@ -20,7 +20,7 @@ var log *logrus.Entry = logging.GetLogger() var cfg *config.Config = config.GetConfig() var experimentCreateAttempt bool = true -func Create_kruize_experiments(experiment_name string, k8s_object []map[string]interface{}) ([]string, error) { +func Create_kruize_experiments(experiment_name string, cluster_identifier string, k8s_object []map[string]interface{}) ([]string, error) { // k8s_object (can) contain multiple containers of same k8s object type. data := map[string]string{ "namespace": k8s_object[0]["namespace"].(string), @@ -39,7 +39,7 @@ func Create_kruize_experiments(experiment_name string, k8s_object []map[string]i }) } } - payload, err := kruizePayload.GetCreateExperimentPayload(experiment_name, containers, data) + payload, err := kruizePayload.GetCreateExperimentPayload(experiment_name, cluster_identifier, containers, data) if err != nil { return nil, fmt.Errorf("unable to create payload: %v", err) } @@ -69,7 +69,7 @@ func Create_kruize_experiments(experiment_name string, k8s_object []map[string]i log.Info("Tring to create resource_optimization_openshift performance profile") utils.Setup_kruize_performance_profile() experimentCreateAttempt = false // Attempting only once - container_names, err := Create_kruize_experiments(experiment_name, k8s_object) + container_names, err := Create_kruize_experiments(experiment_name, cluster_identifier, k8s_object) experimentCreateAttempt = true if err != nil { return nil, err @@ -176,15 +176,71 @@ func Update_recommendations(experiment_name string, interval_end_time time.Time) } -func Is_valid_recommendation(d []kruizePayload.ListRecommendations) bool { +func Is_valid_recommendation(d []kruizePayload.ListRecommendations, experiment_name string, maxEndTime string) bool { if len(d) > 0 { + + // To maintain a local reference the following map has been created from + // https://github.com/kruize/autotune/blob/master/design/NotificationCodes.md#detailed-codes + notificationCodeValidities := map[string]string{ + "111000": "INFO", + "120001": "INFO", + "221001": "ERROR", + "221002": "ERROR", + "221003": "ERROR", + "221004": "ERROR", + "223001": "ERROR", + "223002": "ERROR", + "223003": "ERROR", + "223004": "ERROR", + "224001": "ERROR", + "224002": "ERROR", + "224003": "ERROR", + "224004": "ERROR", + } + notifications := d[0].Kubernetes_objects[0].Containers[0].Recommendations.Notifications - // 112101 is notification code for "Duration Based Recommendations Available". - if _, ok := notifications["112101"]; ok { - return true - } else { - return false + + for key := range notifications{ + notificationType, keyExists := notificationCodeValidities[key] + if !keyExists { + return false + } + + if key == "111000" && notificationType == "INFO"{ + notificationsLevelTwo := d[0].Kubernetes_objects[0].Containers[0].Recommendations.Data[maxEndTime].Notifications + + for key := range notificationsLevelTwo{ + if notificationCodeValidities[key] == "ERROR"{ + kruizeInvalidRecommendationDetail.WithLabelValues(key, experiment_name).Set(1) + + notificationsLevelThreeShortTerm := d[0].Kubernetes_objects[0].Containers[0].Recommendations.Data[maxEndTime].RecommendationTerms.Short_term.Notifications + notificationsLevelThreeMediumTerm := d[0].Kubernetes_objects[0].Containers[0].Recommendations.Data[maxEndTime].RecommendationTerms.Medium_term.Notifications + notificationsLevelThreeLongTerm := d[0].Kubernetes_objects[0].Containers[0].Recommendations.Data[maxEndTime].RecommendationTerms.Long_term.Notifications + + notificationSections := []map[string]kruizePayload.Notification{ + notificationsLevelTwo, + notificationsLevelThreeShortTerm, + notificationsLevelThreeMediumTerm, + notificationsLevelThreeLongTerm, + } + + for _, notificationBody := range notificationSections{ + for key := range notificationBody{ + if notificationCodeValidities[key] == "ERROR"{ + kruizeInvalidRecommendationDetail.WithLabelValues(key, experiment_name).Set(1) + } + + } + } + return true + } else { + // Setting the metric counter to 1 as we expect a single metric + // for a combination of notification_code and experiment_name + kruizeInvalidRecommendationDetail.WithLabelValues(key, experiment_name).Set(1) + return false + } } - } + }}} + return false } diff --git a/internal/utils/kruize/metrics.go b/internal/utils/kruize/metrics.go index a6980148..eb3d17a3 100644 --- a/internal/utils/kruize/metrics.go +++ b/internal/utils/kruize/metrics.go @@ -13,3 +13,14 @@ var ( []string{"path"}, ) ) + + +var ( + kruizeInvalidRecommendationDetail = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "rosocp_kruize_invalid_recommendation_detail", + Help: "List of INFO/ERROR type recommendations from Kruize", + }, + []string{"notification_code", "experiment_name"}, + ) +) \ No newline at end of file diff --git a/openapi.json b/openapi.json index 0411151a..c2c9d4f2 100644 --- a/openapi.json +++ b/openapi.json @@ -27,7 +27,7 @@ { "name": "workload_type", "in": "query", - "description": "Workload type", + "description": "Options are daemonset, deployment, deploymentconfig, replicaset, replicationcontroller, statefulset", "required": false, "schema": { "type": "string" @@ -67,7 +67,8 @@ "required": false, "schema": { "type": "string" - } + }, + "example": "YYYY-MM-DD" }, { "name": "end_date", @@ -76,7 +77,8 @@ "required": false, "schema": { "type": "string" - } + }, + "example": "YYYY-MM-DD" }, { "name": "offset", @@ -202,12 +204,12 @@ "properties": { "count": { "type": "integer", - "minimum": 0 + "minimum": 1 }, "limit": { "type": "integer", "minimum": 1, - "maximum": 100 + "maximum": 10 }, "offset": { "type": "integer", @@ -236,24 +238,18 @@ }, "Notifications": { "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "NOTICE", - "WARNING", - "CRITICAL" - ] - }, - "message": { - "type": "string", - "example": "There is not enough data available to generate a recommendation." - }, - "code": { - "type": "number" - } + "properties": { + "type": { + "type": "string", + "example": "INFO" + }, + "code": { + "type": "string", + "example": 112101 + }, + "message": { + "type": "string", + "example": "Duration Based Recommendations Available" } } }, @@ -336,7 +332,7 @@ "properties": { "amount": { "type": "number", - "example": 1.91 + "example": 2 }, "format": { "type": "string", @@ -349,7 +345,7 @@ "properties": { "amount": { "type": "number", - "example": 16.391 + "example": 20.391 }, "format": { "type": "string", @@ -372,7 +368,7 @@ "properties": { "amount": { "type": "number", - "example": 2.11 + "example": 3.11 }, "format": { "type": "string", @@ -403,7 +399,7 @@ "properties": { "amount": { "type": "number", - "example": 1.92 + "example": 3 }, "format": { "type": "string", @@ -428,31 +424,6 @@ } } }, - "notifications": { - "$ref": "#/components/schemas/Notifications" - }, - "pods_count": { - "type": "integer", - "example": 1 - }, - "confidence_level": { - "type": "number", - "example": 0.5 - }, - "duration_in_hours": { - "type": "number", - "example": 361 - }, - "monitoring_end_time": { - "type": "string", - "format": "date-time", - "example": "2023-04-18T15:00:00.000Z" - }, - "monitoring_start_time": { - "type": "string", - "format": "date-time", - "example": "2023-04-03T15:00:00.000Z" - }, "variation": { "type": "object", "properties": { @@ -464,7 +435,7 @@ "properties": { "amount": { "type": "number", - "example": 3 + "example": 1 }, "format": { "type": "string", @@ -477,7 +448,7 @@ "properties": { "amount": { "type": "number", - "example": 344 + "example": 0.959 }, "format": { "type": "string", @@ -495,7 +466,7 @@ "properties": { "amount": { "type": "number", - "example": 4 + "example": 1 }, "format": { "type": "string", @@ -508,7 +479,7 @@ "properties": { "amount": { "type": "number", - "example": 500 + "example": 3.995 }, "format": { "type": "string", @@ -519,6 +490,31 @@ } } } + }, + "notifications": { + "$ref": "#/components/schemas/Notifications" + }, + "pods_count": { + "type": "integer", + "example": 1 + }, + "confidence_level": { + "type": "number", + "example": 0.5 + }, + "duration_in_hours": { + "type": "number", + "example": 361 + }, + "monitoring_end_time": { + "type": "string", + "format": "date-time", + "example": "2023-04-18T15:00:00.000Z" + }, + "monitoring_start_time": { + "type": "string", + "format": "date-time", + "example": "2023-04-03T15:00:00.000Z" } } }, @@ -549,7 +545,7 @@ "properties": { "amount": { "type": "number", - "example": 30.715 + "example": 300 }, "format": { "type": "string", @@ -580,11 +576,11 @@ "properties": { "amount": { "type": "number", - "example": 16.391 + "example": 5 }, "format": { "type": "string", - "example": "MiB" + "example": "GiB" } } } @@ -616,11 +612,11 @@ "properties": { "amount": { "type": "number", - "example": 31.674 + "example": 500 }, "format": { "type": "string", - "example": "GiB" + "example": "MiB" } } } @@ -634,7 +630,7 @@ "properties": { "amount": { "type": "number", - "example": 1.92 + "example": 3.92 }, "format": { "type": "string", @@ -647,11 +643,11 @@ "properties": { "amount": { "type": "number", - "example": 16.396 + "example": 6 }, "format": { "type": "string", - "example": "MiB" + "example": "GiB" } } } @@ -659,31 +655,6 @@ } } }, - "notifications": { - "$ref": "#/components/schemas/Notifications" - }, - "pods_count": { - "type": "integer", - "example": 1 - }, - "confidence_level": { - "type": "number", - "example": 0.5 - }, - "duration_in_hours": { - "type": "number", - "example": 169 - }, - "monitoring_end_time": { - "type": "string", - "format": "date-time", - "example": "2023-04-18T15:00:00.000Z" - }, - "monitoring_start_time": { - "type": "string", - "format": "date-time", - "example": "2023-04-11T15:00:00.000Z" - }, "variation": { "type": "object", "properties": { @@ -695,7 +666,7 @@ "properties": { "amount": { "type": "number", - "example": 2 + "example": -1.468 }, "format": { "type": "string", @@ -708,7 +679,7 @@ "properties": { "amount": { "type": "number", - "example": 959.4 + "example": 200 }, "format": { "type": "string", @@ -726,7 +697,7 @@ "properties": { "amount": { "type": "number", - "example": 5 + "example": 2 }, "format": { "type": "string", @@ -739,17 +710,42 @@ "properties": { "amount": { "type": "number", - "example": 200.333 + "example": 1 }, "format": { "type": "string", - "example": "MiB" + "example": "GiB" } } } } } } + }, + "notifications": { + "$ref": "#/components/schemas/Notifications" + }, + "pods_count": { + "type": "integer", + "example": 1 + }, + "confidence_level": { + "type": "number", + "example": 0.5 + }, + "duration_in_hours": { + "type": "number", + "example": 169 + }, + "monitoring_end_time": { + "type": "string", + "format": "date-time", + "example": "2023-04-18T15:00:00.000Z" + }, + "monitoring_start_time": { + "type": "string", + "format": "date-time", + "example": "2023-04-11T15:00:00.000Z" } } }, @@ -767,7 +763,7 @@ "properties": { "amount": { "type": "number", - "example": 2.09 + "example": 3.76 }, "format": { "type": "string", @@ -780,11 +776,11 @@ "properties": { "amount": { "type": "number", - "example": 30.715 + "example": 5 }, "format": { "type": "string", - "example": "MiB" + "example": "GiB" } } } @@ -811,7 +807,7 @@ "properties": { "amount": { "type": "number", - "example": 16.391 + "example": 400 }, "format": { "type": "string", @@ -834,7 +830,7 @@ "properties": { "amount": { "type": "number", - "example": 2.11 + "example": 5 }, "format": { "type": "string", @@ -847,11 +843,11 @@ "properties": { "amount": { "type": "number", - "example": 31.674 + "example": 6.7 }, "format": { "type": "string", - "example": "MiB" + "example": "GiB" } } } @@ -865,7 +861,7 @@ "properties": { "amount": { "type": "number", - "example": 1.92 + "example": 3 }, "format": { "type": "string", @@ -878,7 +874,7 @@ "properties": { "amount": { "type": "number", - "example": 16.396 + "example": 700 }, "format": { "type": "string", @@ -890,31 +886,6 @@ } } }, - "notifications": { - "$ref": "#/components/schemas/Notifications" - }, - "pods_count": { - "type": "integer", - "example": 1 - }, - "confidence_level": { - "type": "number", - "example": 0.5 - }, - "duration_in_hours": { - "type": "number", - "example": 25 - }, - "monitoring_end_time": { - "type": "string", - "format": "date-time", - "example": "2023-04-18T15:00:00.000Z" - }, - "monitoring_start_time": { - "type": "string", - "format": "date-time", - "example": "2023-04-17T15:00:00.000Z" - }, "variation": { "type": "object", "properties": { @@ -926,7 +897,7 @@ "properties": { "amount": { "type": "number", - "example": 5 + "example": 1.24 }, "format": { "type": "string", @@ -939,11 +910,11 @@ "properties": { "amount": { "type": "number", - "example": 929.111 + "example": 1.7 }, "format": { "type": "string", - "example": "MiB" + "example": "GiB" } } } @@ -957,7 +928,7 @@ "properties": { "amount": { "type": "number", - "example": 3 + "example": 1.08 }, "format": { "type": "string", @@ -970,7 +941,7 @@ "properties": { "amount": { "type": "number", - "example": 500 + "example": 300 }, "format": { "type": "string", @@ -981,6 +952,31 @@ } } } + }, + "notifications": { + "$ref": "#/components/schemas/Notifications" + }, + "pods_count": { + "type": "integer", + "example": 1 + }, + "confidence_level": { + "type": "number", + "example": 0.5 + }, + "duration_in_hours": { + "type": "number", + "example": 25 + }, + "monitoring_end_time": { + "type": "string", + "format": "date-time", + "example": "2023-04-18T15:00:00.000Z" + }, + "monitoring_start_time": { + "type": "string", + "format": "date-time", + "example": "2023-04-17T15:00:00.000Z" } } } @@ -1004,4 +1000,4 @@ } } } -} +} \ No newline at end of file