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  }