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  }