github.com/m3db/m3@v1.5.0/scripts/comparator/compare.go (about)

     1  // Copyright (c) 2019 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package main
    22  
    23  import (
    24  	"bytes"
    25  	"encoding/json"
    26  	"errors"
    27  	"flag"
    28  	"fmt"
    29  	"io/ioutil"
    30  	"net/http"
    31  	"os"
    32  	"time"
    33  
    34  	"github.com/m3db/m3/scripts/comparator/utils"
    35  	"github.com/m3db/m3/src/query/api/v1/handler/prometheus"
    36  	xerrors "github.com/m3db/m3/src/x/errors"
    37  	"github.com/m3db/m3/src/x/instrument"
    38  
    39  	"go.uber.org/zap"
    40  )
    41  
    42  func paramError(err string, log *zap.Logger) {
    43  	log.Error(err)
    44  	flag.Usage()
    45  }
    46  
    47  func main() {
    48  	var (
    49  		iOpts = instrument.NewOptions()
    50  		log   = iOpts.Logger()
    51  
    52  		now = time.Now()
    53  
    54  		pQueryFile    = flag.String("input", "", "the query file")
    55  		pPromAddress  = flag.String("promAdress", "0.0.0.0:9090", "prom address")
    56  		pQueryAddress = flag.String("queryAddress", "0.0.0.0:7201/m3query", "M3 query address")
    57  
    58  		pComparatorAddress = flag.String("comparator", "", "comparator address")
    59  		pRegressionDir     = flag.String("regressionDir", "", "optional directory for regression tests")
    60  
    61  		pStart = flag.Int64("s", now.Add(time.Hour*-3).Unix(), "start time")
    62  		pEnd   = flag.Int64("e", now.Unix(), "start end")
    63  	)
    64  
    65  	flag.Parse()
    66  	var (
    67  		queryFile    = *pQueryFile
    68  		promAddress  = *pPromAddress
    69  		queryAddress = *pQueryAddress
    70  
    71  		regressionDir     = *pRegressionDir
    72  		comparatorAddress = *pComparatorAddress
    73  
    74  		start = *pStart
    75  		end   = *pEnd
    76  	)
    77  
    78  	fmt.Println(queryFile, start, end)
    79  
    80  	if len(queryFile) == 0 {
    81  		paramError("No query found", log)
    82  		os.Exit(1)
    83  	}
    84  
    85  	if len(promAddress) == 0 {
    86  		paramError("No prom address found", log)
    87  		os.Exit(1)
    88  	}
    89  
    90  	if len(queryAddress) == 0 {
    91  		paramError("No query server address found", log)
    92  		os.Exit(1)
    93  	}
    94  
    95  	if end < start {
    96  		paramError(fmt.Sprintf("start(%d) is before end (%d)", start, end), log)
    97  		os.Exit(1)
    98  	}
    99  
   100  	queries, err := utils.ParseFileToPromQLQueryGroup(queryFile, start, end, log)
   101  	if err != nil {
   102  		log.Error("could not parse file to PromQL queries", zap.Error(err))
   103  		os.Exit(1)
   104  	}
   105  
   106  	var multiErr xerrors.MultiError
   107  	for _, queryGroup := range queries {
   108  		runs := 1
   109  		if queryGroup.Reruns > 1 {
   110  			runs = queryGroup.Reruns
   111  		}
   112  
   113  		for i := 0; i < runs; i++ {
   114  			log.Info("running query group",
   115  				zap.String("group", queryGroup.QueryGroup),
   116  				zap.Int("run", i+1))
   117  			if err := runQueryGroup(
   118  				queryGroup,
   119  				promAddress,
   120  				queryAddress,
   121  				log,
   122  			); err != nil {
   123  				multiErr = multiErr.Add(err)
   124  				log.Error("query group encountered failure",
   125  					zap.String("group", queryGroup.QueryGroup),
   126  					zap.Int("run", i+1),
   127  					zap.Error(err))
   128  			}
   129  		}
   130  	}
   131  
   132  	if !multiErr.Empty() {
   133  		log.Fatal("mismatched queries detected in base queries")
   134  	}
   135  	log.Info("base queries success")
   136  
   137  	if err := runRegressionSuite(regressionDir, comparatorAddress,
   138  		promAddress, queryAddress, log); err != nil {
   139  		log.Fatal("failure or mismatched queries detected in regression suite", zap.Error(err))
   140  	}
   141  	log.Info("regression success")
   142  }
   143  
   144  func runRegressionSuite(
   145  	regressionDir string,
   146  	comparatorAddress string,
   147  	promAddress string,
   148  	queryAddress string,
   149  	log *zap.Logger,
   150  ) error {
   151  	fmt.Println("dir", regressionDir, "add", comparatorAddress)
   152  	if len(regressionDir) == 0 {
   153  		log.Info("no regression directory supplied.")
   154  		return nil
   155  	}
   156  
   157  	if len(comparatorAddress) == 0 {
   158  		err := errors.New("no comparator address")
   159  		log.Error("regression turned on but no comparator address", zap.Error(err))
   160  		return err
   161  	}
   162  
   163  	regressions, err := utils.ParseRegressionFilesToPromQLQueryGroup(regressionDir, log)
   164  	if err != nil {
   165  		log.Error("could not parse regressions to PromQL queries", zap.Error(err))
   166  		return err
   167  	}
   168  
   169  	var multiErr xerrors.MultiError
   170  	for _, regressionGroup := range regressions {
   171  		runs := 1
   172  		if regressionGroup.Reruns > 1 {
   173  			runs = regressionGroup.Reruns
   174  		}
   175  
   176  		for i := 0; i < runs; i++ {
   177  			log.Info("running query group",
   178  				zap.String("group", regressionGroup.QueryGroup),
   179  				zap.Int("run", i+1))
   180  
   181  			if err := runRegression(
   182  				regressionGroup,
   183  				comparatorAddress,
   184  				promAddress,
   185  				queryAddress,
   186  				log,
   187  			); err != nil {
   188  				multiErr = multiErr.Add(err)
   189  			}
   190  		}
   191  	}
   192  
   193  	if err := multiErr.LastError(); err != nil {
   194  		log.Error("mismatched queries detected in regression queries", zap.Error(err))
   195  		return err
   196  	}
   197  
   198  	return nil
   199  }
   200  
   201  func runRegression(
   202  	queryGroup utils.PromQLRegressionQueryGroup,
   203  	comparatorAddress string,
   204  	promAddress string,
   205  	queryAddress string,
   206  	log *zap.Logger,
   207  ) error {
   208  	data, err := json.Marshal(queryGroup.Data)
   209  	if err != nil {
   210  		log.Error("could not marshall data", zap.Error(err))
   211  		return err
   212  	}
   213  
   214  	comparatorURL := fmt.Sprintf("http://%s", comparatorAddress)
   215  	resp, err := http.Post(comparatorURL, "application/json", bytes.NewReader(data))
   216  	if err != nil {
   217  		log.Error("could not seed regression data", zap.Error(err))
   218  		return err
   219  	}
   220  
   221  	if resp.StatusCode/200 != 1 {
   222  		log.Error("seed status code not 2XX",
   223  			zap.Int("code", resp.StatusCode),
   224  			zap.String("status", resp.Status))
   225  		return fmt.Errorf("status code %d", resp.StatusCode)
   226  	}
   227  
   228  	var multiErr xerrors.MultiError
   229  	for _, query := range queryGroup.Queries {
   230  		promURL := fmt.Sprintf("http://%s%s", promAddress, query)
   231  		queryURL := fmt.Sprintf("http://%s%s", queryAddress, query)
   232  		if err := runComparison(promURL, queryURL, log); err != nil {
   233  			multiErr = multiErr.Add(err)
   234  			log.Error(
   235  				"mismatched query",
   236  				zap.String("promURL", promURL),
   237  				zap.String("queryURL", queryURL),
   238  			)
   239  		}
   240  	}
   241  
   242  	return multiErr.FinalError()
   243  }
   244  
   245  func runQueryGroup(
   246  	queryGroup utils.PromQLQueryGroup,
   247  	promAddress string,
   248  	queryAddress string,
   249  	log *zap.Logger,
   250  ) error {
   251  	var multiErr xerrors.MultiError
   252  	for _, query := range queryGroup.Queries {
   253  		promURL := fmt.Sprintf("http://%s%s", promAddress, query)
   254  		queryURL := fmt.Sprintf("http://%s%s", queryAddress, query)
   255  		if err := runComparison(promURL, queryURL, log); err != nil {
   256  			multiErr = multiErr.Add(err)
   257  			log.Error(
   258  				"mismatched query",
   259  				zap.String("promURL", promURL),
   260  				zap.String("queryURL", queryURL),
   261  			)
   262  		}
   263  	}
   264  
   265  	return multiErr.FinalError()
   266  }
   267  
   268  func runComparison(
   269  	promURL string,
   270  	queryURL string,
   271  	log *zap.Logger,
   272  ) error {
   273  	promResult, err := parseResult(promURL)
   274  	if err != nil {
   275  		log.Error("failed to parse Prometheus result", zap.Error(err))
   276  		return err
   277  	}
   278  
   279  	queryResult, err := parseResult(queryURL)
   280  	if err != nil {
   281  		log.Error("failed to parse M3Query result", zap.Error(err))
   282  		return err
   283  	}
   284  
   285  	_, err = promResult.Matches(queryResult)
   286  	if err != nil {
   287  		log.Error("mismatch", zap.Error(err))
   288  		return err
   289  	}
   290  
   291  	return nil
   292  }
   293  
   294  func parseResult(endpoint string) (prometheus.Response, error) {
   295  	var result prometheus.Response
   296  	response, err := http.Get(endpoint)
   297  	if err != nil {
   298  		return result, err
   299  	}
   300  
   301  	if response.StatusCode != http.StatusOK {
   302  		return result, fmt.Errorf("response failed with code %s", response.Status)
   303  	}
   304  
   305  	body := response.Body
   306  	defer func() {
   307  		body.Close()
   308  	}()
   309  
   310  	data, err := ioutil.ReadAll(body)
   311  	if err != nil {
   312  		return result, err
   313  	}
   314  
   315  	if err = json.Unmarshal(data, &result); err != nil {
   316  		return result, err
   317  	}
   318  
   319  	return result, nil
   320  }