k8s.io/perf-tests/clusterloader2@v0.0.0-20240304094227-64bdb12da87e/pkg/measurement/common/generic_query_measurement.go (about) 1 /* 2 Copyright 2021 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package common 18 19 import ( 20 goerrors "errors" 21 "fmt" 22 "strings" 23 "time" 24 25 "github.com/prometheus/common/model" 26 27 "k8s.io/klog/v2" 28 "k8s.io/perf-tests/clusterloader2/pkg/errors" 29 "k8s.io/perf-tests/clusterloader2/pkg/measurement" 30 measurementutil "k8s.io/perf-tests/clusterloader2/pkg/measurement/util" 31 "k8s.io/perf-tests/clusterloader2/pkg/util" 32 ) 33 34 const ( 35 genericPrometheusQueryMeasurementName = "GenericPrometheusQuery" 36 ) 37 38 func init() { 39 create := func() measurement.Measurement { 40 return CreatePrometheusMeasurement(&genericQueryGatherer{}) 41 } 42 if err := measurement.Register(genericPrometheusQueryMeasurementName, create); err != nil { 43 klog.Fatalf("Cannot register %s: %v", genericPrometheusQueryMeasurementName, err) 44 } 45 } 46 47 type genericQueryGatherer struct { 48 StartParams 49 } 50 51 // StartParams represents configuration that can be passed as params 52 // with action: start. 53 type StartParams struct { 54 MetricName string 55 MetricVersion string 56 Queries []GenericQuery 57 Unit string 58 Dimensions []string 59 } 60 61 // TODO(mborsz): github.com/go-playground/validator or similar project? 62 func (p *StartParams) Validate() error { 63 if p.MetricName == "" { 64 return goerrors.New("metricName is required") 65 } 66 if p.MetricVersion == "" { 67 return goerrors.New("metricVersion is required") 68 } 69 if p.Unit == "" { 70 return goerrors.New("unit is required") 71 } 72 73 for idx, query := range p.Queries { 74 if err := query.Validate(); err != nil { 75 return fmt.Errorf("params.queries[%d] validation failed: %v", idx, err) 76 } 77 } 78 79 return nil 80 } 81 82 type GenericQuery struct { 83 Name string 84 Query string 85 Threshold *float64 86 LowerBound bool 87 RequireSamples bool 88 } 89 90 func (q *GenericQuery) Validate() error { 91 if q.Name == "" { 92 return goerrors.New("name is required") 93 } 94 if q.Query == "" { 95 return goerrors.New("query is required") 96 } 97 return nil 98 } 99 100 func (g *genericQueryGatherer) Configure(config *measurement.Config) error { 101 if err := util.ToStruct(config.Params, &g.StartParams); err != nil { 102 return err 103 } 104 return g.StartParams.Validate() 105 } 106 107 func (g *genericQueryGatherer) IsEnabled(config *measurement.Config) bool { 108 return true 109 } 110 111 func key(metric model.Metric, dimensions []string) (string, map[string]string) { 112 s := make([]string, 0) 113 m := make(map[string]string) 114 for _, dimension := range dimensions { 115 val := string(metric[model.LabelName(dimension)]) 116 s = append(s, val) 117 m[dimension] = val 118 } 119 return fmt.Sprintf("%v", s), m 120 } 121 122 func getOrCreate(dataItems map[string]*measurementutil.DataItem, key, unit string, labels map[string]string) *measurementutil.DataItem { 123 dataItem, ok := dataItems[key] 124 if ok { 125 return dataItem 126 } 127 dataItem = &measurementutil.DataItem{ 128 Data: make(map[string]float64), 129 Unit: unit, 130 Labels: labels, 131 } 132 dataItems[key] = dataItem 133 return dataItem 134 } 135 136 func (g *genericQueryGatherer) validateSample(q GenericQuery, val float64) error { 137 thresholdMsg := "none" 138 if q.Threshold != nil { 139 thresholdMsg = fmt.Sprintf("%v (upper bound)", *q.Threshold) 140 if q.LowerBound { 141 thresholdMsg = fmt.Sprintf("%v (lower bound)", *q.Threshold) 142 } 143 } 144 klog.V(2).Infof("metric: %v: %v, value: %v, threshold: %v", g.MetricName, q.Name, val, thresholdMsg) 145 if q.Threshold != nil { 146 if q.LowerBound && val < *q.Threshold { 147 return errors.NewMetricViolationError(q.Name, fmt.Sprintf("sample below threshold: want: greater or equal than %v, got: %v", *q.Threshold, val)) 148 } 149 if !q.LowerBound && val > *q.Threshold { 150 return errors.NewMetricViolationError(q.Name, fmt.Sprintf("sample above threshold: want: less or equal than %v, got: %v", *q.Threshold, val)) 151 } 152 } 153 return nil 154 } 155 156 func (g *genericQueryGatherer) Gather(executor QueryExecutor, startTime, endTime time.Time, config *measurement.Config) ([]measurement.Summary, error) { 157 var errs []error 158 dataItems := map[string]*measurementutil.DataItem{} 159 for _, q := range g.Queries { 160 samples, err := g.query(q, executor, startTime, endTime) 161 if err != nil { 162 return nil, err 163 } 164 165 if len(samples) == 0 { 166 if q.RequireSamples { 167 errs = append(errs, errors.NewMetricViolationError(q.Name, fmt.Sprintf("query returned no samples for %v", g.MetricName))) 168 } 169 klog.Warningf("query returned no samples for %v: %v", g.MetricName, q.Name) 170 continue 171 } 172 173 for _, sample := range samples { 174 k, labels := key(sample.Metric, g.Dimensions) 175 dataItem := getOrCreate(dataItems, k, g.Unit, labels) 176 177 val := float64(sample.Value) 178 prevVal, ok := dataItem.Data[q.Name] 179 if ok { 180 errs = append(errs, errors.NewMetricViolationError(q.Name, fmt.Sprintf("too many samples for %s: query returned %v and %v, expected single value.", k, val, prevVal))) 181 } else { 182 dataItem.Data[q.Name] = val 183 } 184 185 if err := g.validateSample(q, val); err != nil { 186 errs = append(errs, err) 187 } 188 } 189 } 190 summary, err := g.createSummary(g.MetricName, dataItems) 191 if err != nil { 192 return nil, err 193 } 194 if len(errs) > 0 { 195 err = errors.NewMetricViolationError(g.MetricName, fmt.Sprintf("%v", errs)) 196 } 197 return []measurement.Summary{summary}, err 198 } 199 200 func (g *genericQueryGatherer) String() string { 201 return genericPrometheusQueryMeasurementName 202 } 203 204 func (g *genericQueryGatherer) query(q GenericQuery, executor QueryExecutor, startTime, endTime time.Time) ([]*model.Sample, error) { 205 duration := endTime.Sub(startTime) 206 // Replace all provided duration placeholders (%v) with the test duration. 207 boundedQuery := strings.ReplaceAll(q.Query, "%v", measurementutil.ToPrometheusTime(duration)) 208 klog.V(2).Infof("bounded query: %s, duration: %v", boundedQuery, duration) 209 return executor.Query(boundedQuery, endTime) 210 } 211 212 func (g *genericQueryGatherer) createSummary(metricName string, dataItems map[string]*measurementutil.DataItem) (measurement.Summary, error) { 213 perfData := &measurementutil.PerfData{ 214 Version: g.MetricVersion, 215 DataItems: nil, 216 } 217 218 for _, dataItem := range dataItems { 219 perfData.DataItems = append(perfData.DataItems, *dataItem) 220 } 221 222 content, err := util.PrettyPrintJSON(perfData) 223 if err != nil { 224 return nil, err 225 } 226 // Replace '_' by spaces as '_' is used as delimiter to extract metricName from file name 227 return measurement.CreateSummary(genericPrometheusQueryMeasurementName+" "+metricName, "json", content), nil 228 }