github.com/m3db/m3@v1.5.0/scripts/docker-integration-tests/query_fanout/restrict.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 "encoding/json" 25 "flag" 26 "fmt" 27 "io/ioutil" 28 "net/http" 29 "os" 30 "reflect" 31 "runtime" 32 "sort" 33 "strings" 34 ) 35 36 var ( 37 // name is global and set on startup. 38 name string 39 // clusters are constant, set by the test harness. 40 clusters = []string{"coordinator-cluster-a", "coordinator-cluster-b"} 41 ) 42 43 func main() { 44 var ts int 45 flag.IntVar(&ts, "t", -1, "metric name to search") 46 flag.Parse() 47 48 requireTrue(ts > 0, "no timestamp supplied") 49 name = fmt.Sprintf("foo_%d", ts) 50 instant := fmt.Sprintf("http://0.0.0.0:7201/m3query/api/v1/query?query=%s", name) 51 rnge := fmt.Sprintf("http://0.0.0.0:7201/m3query/api/v1/query_range?query=%s"+ 52 "&start=%d&end=%d&step=100", name, ts/100*100, (ts/100+1)*100) 53 54 for _, url := range []string{instant, rnge} { 55 singleClusterDefaultStrip(url) 56 bothClusterCustomStrip(url) 57 bothClusterDefaultStrip(url) 58 bothClusterNoStrip(url) 59 bothClusterMultiStrip(url) 60 } 61 } 62 63 func queryWithHeader(url string, h string) (response, error) { 64 var result response 65 req, err := http.NewRequest("GET", url, nil) 66 if err != nil { 67 return result, err 68 } 69 70 req.Header.Add(restrictByTagsJSONHeader, h) 71 client := http.DefaultClient 72 resp, err := client.Do(req) 73 if err != nil { 74 return result, err 75 } 76 77 if resp.StatusCode != http.StatusOK { 78 return result, fmt.Errorf("response failed with code %s", resp.Status) 79 } 80 81 defer resp.Body.Close() 82 data, err := ioutil.ReadAll(resp.Body) 83 if err != nil { 84 return result, err 85 } 86 87 json.Unmarshal(data, &result) 88 return result, err 89 } 90 91 func mustParseOpts(o stringTagOptions) string { 92 m, err := json.Marshal(o) 93 requireNoError(err, "cannot marshal to json") 94 return string(m) 95 } 96 97 func bothClusterDefaultStrip(url string) { 98 m := mustParseOpts(stringTagOptions{ 99 Restrict: []stringMatch{ 100 stringMatch{Name: "val", Type: "EQUAL", Value: "1"}, 101 }, 102 }) 103 104 resp, err := queryWithHeader(url, m) 105 requireNoError(err, "failed to query") 106 107 data := resp.Data.Result 108 data.Sort() 109 requireEqual(len(data), 2) 110 for i, d := range data { 111 requireEqual(2, len(d.Metric)) 112 requireEqual(name, d.Metric["__name__"]) 113 requireEqual(clusters[i], d.Metric["cluster"]) 114 } 115 } 116 117 func bothClusterCustomStrip(url string) { 118 m := mustParseOpts(stringTagOptions{ 119 Restrict: []stringMatch{ 120 stringMatch{Name: "val", Type: "EQUAL", Value: "1"}, 121 }, 122 Strip: []string{"__name__"}, 123 }) 124 125 resp, err := queryWithHeader(url, string(m)) 126 requireNoError(err, "failed to query") 127 128 data := resp.Data.Result 129 data.Sort() 130 requireEqual(len(data), 2) 131 for i, d := range data { 132 requireEqual(2, len(d.Metric)) 133 requireEqual(clusters[i], d.Metric["cluster"]) 134 requireEqual("1", d.Metric["val"]) 135 } 136 } 137 138 func bothClusterNoStrip(url string) { 139 m := mustParseOpts(stringTagOptions{ 140 Restrict: []stringMatch{ 141 stringMatch{Name: "val", Type: "EQUAL", Value: "1"}, 142 }, 143 Strip: []string{}, 144 }) 145 146 resp, err := queryWithHeader(url, string(m)) 147 requireNoError(err, "failed to query") 148 149 data := resp.Data.Result 150 data.Sort() 151 requireEqual(len(data), 2) 152 for i, d := range data { 153 requireEqual(3, len(d.Metric)) 154 requireEqual(name, d.Metric["__name__"]) 155 requireEqual(clusters[i], d.Metric["cluster"]) 156 requireEqual("1", d.Metric["val"]) 157 } 158 } 159 160 func bothClusterMultiStrip(url string) { 161 m := mustParseOpts(stringTagOptions{ 162 Restrict: []stringMatch{ 163 stringMatch{Name: "val", Type: "EQUAL", Value: "1"}, 164 }, 165 Strip: []string{"val", "__name__"}, 166 }) 167 168 resp, err := queryWithHeader(url, string(m)) 169 requireNoError(err, "failed to query") 170 171 data := resp.Data.Result 172 data.Sort() 173 requireEqual(len(data), 2) 174 for i, d := range data { 175 requireEqual(1, len(d.Metric)) 176 requireEqual(clusters[i], d.Metric["cluster"]) 177 } 178 } 179 180 // NB: cluster 1 is expected to have metrics with vals in range: [1,5] 181 // and cluster 2 is expected to have metrics with vals in range: [1,10] 182 // so setting the value to be in (5..10] should hit only a single metric. 183 func singleClusterDefaultStrip(url string) { 184 m := mustParseOpts(stringTagOptions{ 185 Restrict: []stringMatch{ 186 stringMatch{Name: "val", Type: "EQUAL", Value: "9"}, 187 }, 188 }) 189 190 resp, err := queryWithHeader(url, string(m)) 191 requireNoError(err, "failed to query") 192 193 data := resp.Data.Result 194 requireEqual(len(data), 1, url) 195 requireEqual(2, len(data[0].Metric)) 196 requireEqual(name, data[0].Metric["__name__"], "single") 197 requireEqual("coordinator-cluster-b", data[0].Metric["cluster"]) 198 } 199 200 /* 201 202 Helper functions below. This allows the test file to avoid importing any non 203 standard libraries. 204 205 */ 206 207 const restrictByTagsJSONHeader = "M3-Restrict-By-Tags-JSON" 208 209 // StringMatch is an easy to use JSON representation of models.Matcher that 210 // allows plaintext fields rather than forcing base64 encoded values. 211 type stringMatch struct { 212 Name string `json:"name"` 213 Type string `json:"type"` 214 Value string `json:"value"` 215 } 216 217 // stringTagOptions is an easy to use JSON representation of 218 // storage.RestrictByTag that allows plaintext string fields rather than 219 // forcing base64 encoded values. 220 type stringTagOptions struct { 221 Restrict []stringMatch `json:"match"` 222 Strip []string `json:"strip"` 223 } 224 225 func printMessage(msg ...interface{}) { 226 fmt.Println(msg...) 227 228 _, fn, line, _ := runtime.Caller(4) 229 fmt.Printf("\tin func: %v, line: %v\n", fn, line) 230 231 os.Exit(1) 232 } 233 234 func testEqual(ex interface{}, ac interface{}) bool { 235 if ex == nil || ac == nil { 236 return ex == ac 237 } 238 239 return reflect.DeepEqual(ex, ac) 240 } 241 242 func requireEqual(ex interface{}, ac interface{}, msg ...interface{}) { 243 if testEqual(ex, ac) { 244 return 245 } 246 247 fmt.Printf(""+ 248 "Not equal: %#v (expected)\n"+ 249 " %#v (actual)\n", ex, ac) 250 printMessage(msg...) 251 } 252 253 func requireNoError(err error, msg ...interface{}) { 254 if err == nil { 255 return 256 } 257 258 fmt.Printf("Received unexpected error %q\n", err) 259 printMessage(msg...) 260 } 261 262 func requireTrue(b bool, msg ...interface{}) { 263 if b { 264 return 265 } 266 267 fmt.Println("Expected true, got false") 268 printMessage(msg...) 269 } 270 271 // response represents Prometheus's query response. 272 type response struct { 273 // Status is the response status. 274 Status string `json:"status"` 275 // Data is the response data. 276 Data data `json:"data"` 277 } 278 279 type data struct { 280 // ResultType is the result type for the response. 281 ResultType string `json:"resultType"` 282 // Result is the list of results for the response. 283 Result results `json:"result"` 284 } 285 286 type results []result 287 288 // Len is the number of elements in the collection. 289 func (r results) Len() int { return len(r) } 290 291 // Less reports whether the element with 292 // index i should sort before the element with index j. 293 func (r results) Less(i, j int) bool { 294 return r[i].id < r[j].id 295 } 296 297 // Swap swaps the elements with indexes i and j. 298 func (r results) Swap(i, j int) { r[i], r[j] = r[j], r[i] } 299 300 // Sort sorts the results. 301 func (r results) Sort() { 302 for i, result := range r { 303 r[i] = result.genID() 304 } 305 306 sort.Sort(r) 307 } 308 309 // result is the result itself. 310 type result struct { 311 // Metric is the tags for the result. 312 Metric tags `json:"metric"` 313 // Values is the set of values for the result. 314 Values values `json:"values"` 315 id string 316 } 317 318 // tags is a simple representation of Prometheus tags. 319 type tags map[string]string 320 321 // Values is a list of values for the Prometheus result. 322 type values []value 323 324 // Value is a single value for Prometheus result. 325 type value []interface{} 326 327 func (r *result) genID() result { 328 tags := make(sort.StringSlice, len(r.Metric)) 329 for k, v := range r.Metric { 330 tags = append(tags, fmt.Sprintf("%s:%s,", k, v)) 331 } 332 333 sort.Sort(tags) 334 var sb strings.Builder 335 // NB: this may clash but exact tag values are also checked, and this is a 336 // validation endpoint so there's less concern over correctness. 337 for _, t := range tags { 338 sb.WriteString(t) 339 } 340 341 r.id = sb.String() 342 return *r 343 }