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 }