package datasource import ( "context" "fmt" "net/http" "net/http/httptest" "net/url" "reflect" "sort" "strings" "testing" "time" "github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils" "github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth" "github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal" ) var ( ctx = context.Background() basicAuthName = "foo" basicAuthPass = "bar" baCfg = &promauth.BasicAuthConfig{ Username: basicAuthName, Password: promauth.NewSecret(basicAuthPass), } vmQuery = "vm_rows" queryRender = "constantLine(10)" vlogsQuery = "_time: 5m | stats by (foo) count() total" vlogsRangeQuery = "* | stats by (foo) count() total" ) func TestVMInstantQuery(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/", func(_ http.ResponseWriter, _ *http.Request) { t.Fatalf("should not be called") }) c := -1 mux.HandleFunc("/api/v1/query", func(w http.ResponseWriter, r *http.Request) { c++ if r.Method != http.MethodPost { t.Fatalf("expected POST method got %s", r.Method) } if name, pass, _ := r.BasicAuth(); name != basicAuthName || pass != basicAuthPass { t.Fatalf("expected %s:%s as basic auth got %s:%s", basicAuthName, basicAuthPass, name, pass) } if r.URL.Query().Get("query") != vmQuery { t.Fatalf("expected %s in query param, got %s", vmQuery, r.URL.Query().Get("query")) } timeParam := r.URL.Query().Get("time") if timeParam == "" { t.Fatalf("expected 'time' in query param, got nil instead") } if _, err := time.Parse(time.RFC3339, timeParam); err != nil { t.Fatalf("failed to parse 'time' query param %q: %s", timeParam, err) } switch c { case 0: w.WriteHeader(500) case 1: w.Write([]byte("[]")) case 2: w.Write([]byte(`{"status":"error", "errorType":"type:", "error":"some error msg"}`)) case 3: w.Write([]byte(`{"status":"unknown"}`)) case 4: w.Write([]byte(`{"status":"success","data":{"resultType":"matrix"}}`)) case 5: w.Write([]byte(`{"status":"success","data":{"resultType":"vector","result":[{"metric":{"__name__":"vm_rows","foo":"bar"},"value":[1583786142,"13763"]},{"metric":{"__name__":"vm_requests","foo":"baz"},"value":[1583786140,"2000"]}]}}`)) case 6: w.Write([]byte(`{"status":"success","data":{"resultType":"scalar","result":[1583786142, "1"]}}`)) case 7: w.Write([]byte(`{"status":"success","data":{"resultType":"scalar","result":[1583786142, "1"]},"stats":{"seriesFetched": "42"}}`)) } }) mux.HandleFunc("/render", func(w http.ResponseWriter, _ *http.Request) { c++ switch c { case 8: w.Write([]byte(`[{"target":"constantLine(10)","tags":{"name":"constantLine(10)"},"datapoints":[[10,1611758343],[10,1611758373],[10,1611758403]]}]`)) } }) mux.HandleFunc("/select/logsql/stats_query", func(w http.ResponseWriter, r *http.Request) { c++ if r.Method != http.MethodPost { t.Fatalf("expected POST method got %s", r.Method) } if name, pass, _ := r.BasicAuth(); name != basicAuthName || pass != basicAuthPass { t.Fatalf("expected %s:%s as basic auth got %s:%s", basicAuthName, basicAuthPass, name, pass) } if r.URL.Query().Get("query") != vlogsQuery { t.Fatalf("expected %s in query param, got %s", vlogsQuery, r.URL.Query().Get("query")) } timeParam := r.URL.Query().Get("time") if timeParam == "" { t.Fatalf("expected 'time' in query param, got nil instead") } if _, err := time.Parse(time.RFC3339, timeParam); err != nil { t.Fatalf("failed to parse 'time' query param %q: %s", timeParam, err) } switch c { case 9: w.Write([]byte("[]")) case 10: w.Write([]byte(`{"status":"success","data":{"resultType":"vector","result":[{"metric":{"__name__":"total","foo":"bar"},"value":[1583786142,"13763"]},{"metric":{"__name__":"total","foo":"baz"},"value":[1583786140,"2000"]}]}}`)) } }) srv := httptest.NewServer(mux) defer srv.Close() authCfg, err := baCfg.NewConfig(".") if err != nil { t.Fatalf("unexpected: %s", err) } s := NewPrometheusClient(srv.URL, authCfg, false, srv.Client()) p := datasourcePrometheus pq := s.BuildWithParams(QuerierParams{DataSourceType: string(p), EvaluationInterval: 15 * time.Second}) ts := time.Now() expErr := func(query, err string) { _, _, gotErr := pq.Query(ctx, query, ts) if gotErr == nil { t.Fatalf("expected %q got nil", err) } if !strings.Contains(gotErr.Error(), err) { t.Fatalf("expected err %q; got %q", err, gotErr) } } expErr(vmQuery, "500") // 0 expErr(vmQuery, "error parsing response") // 1 expErr(vmQuery, "response error") // 2 expErr(vmQuery, "unknown status") // 3 expErr(vmQuery, "unexpected end of JSON input") // 4 res, _, err := pq.Query(ctx, vmQuery, ts) // 5 - vector if err != nil { t.Fatalf("unexpected %s", err) } if len(res.Data) != 2 { t.Fatalf("expected 2 metrics got %d in %+v", len(res.Data), res.Data) } expected := []Metric{ { Labels: []prompbmarshal.Label{{Value: "vm_rows", Name: "__name__"}, {Value: "bar", Name: "foo"}}, Timestamps: []int64{1583786142}, Values: []float64{13763}, }, { Labels: []prompbmarshal.Label{{Value: "vm_requests", Name: "__name__"}, {Value: "baz", Name: "foo"}}, Timestamps: []int64{1583786140}, Values: []float64{2000}, }, } metricsEqual(t, res.Data, expected) res, req, err := pq.Query(ctx, vmQuery, ts) // 6 - scalar if err != nil { t.Fatalf("unexpected %s", err) } if req == nil { t.Fatalf("expected request to be non-nil") } if len(res.Data) != 1 { t.Fatalf("expected 1 metrics got %d in %+v", len(res.Data), res.Data) } expected = []Metric{ { Timestamps: []int64{1583786142}, Values: []float64{1}, }, } if !reflect.DeepEqual(res.Data, expected) { t.Fatalf("unexpected metric %+v want %+v", res.Data, expected) } if res.SeriesFetched != nil { t.Fatalf("expected `seriesFetched` field to be nil when it is missing in datasource response; got %v instead", res.SeriesFetched) } res, _, err = pq.Query(ctx, vmQuery, ts) // 7 - scalar with stats if err != nil { t.Fatalf("unexpected %s", err) } if len(res.Data) != 1 { t.Fatalf("expected 1 metrics got %d in %+v", len(res.Data), res) } expected = []Metric{ { Timestamps: []int64{1583786142}, Values: []float64{1}, }, } if !reflect.DeepEqual(res.Data, expected) { t.Fatalf("unexpected metric %+v want %+v", res.Data, expected) } if *res.SeriesFetched != 42 { t.Fatalf("expected `seriesFetched` field to be 42; got %d instead", *res.SeriesFetched) } // test graphite gq := s.BuildWithParams(QuerierParams{DataSourceType: string(datasourceGraphite)}) res, _, err = gq.Query(ctx, queryRender, ts) // 8 - graphite if err != nil { t.Fatalf("unexpected %s", err) } if len(res.Data) != 1 { t.Fatalf("expected 1 metric got %d in %+v", len(res.Data), res.Data) } exp := []Metric{ { Labels: []prompbmarshal.Label{{Value: "constantLine(10)", Name: "name"}}, Timestamps: []int64{1611758403}, Values: []float64{10}, }, } metricsEqual(t, res.Data, exp) // test victorialogs vlogs := datasourceVLogs pq = s.BuildWithParams(QuerierParams{DataSourceType: string(vlogs), EvaluationInterval: 15 * time.Second}) expErr(vlogsQuery, "error parsing response") // 9 res, _, err = pq.Query(ctx, vlogsQuery, ts) // 10 if err != nil { t.Fatalf("unexpected %s", err) } if len(res.Data) != 2 { t.Fatalf("expected 2 metrics got %d in %+v", len(res.Data), res.Data) } expected = []Metric{ { Labels: []prompbmarshal.Label{{Value: "total", Name: "stats_result"}, {Value: "bar", Name: "foo"}}, Timestamps: []int64{1583786142}, Values: []float64{13763}, }, { Labels: []prompbmarshal.Label{{Value: "total", Name: "stats_result"}, {Value: "baz", Name: "foo"}}, Timestamps: []int64{1583786140}, Values: []float64{2000}, }, } metricsEqual(t, res.Data, expected) } func TestVMInstantQueryWithRetry(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/", func(_ http.ResponseWriter, _ *http.Request) { t.Fatalf("should not be called") }) c := -1 mux.HandleFunc("/api/v1/query", func(w http.ResponseWriter, r *http.Request) { c++ if r.URL.Query().Get("query") != vmQuery { t.Fatalf("expected %s in query param, got %s", vmQuery, r.URL.Query().Get("query")) } switch c { case 0: w.Write([]byte(`{"status":"success","data":{"resultType":"scalar","result":[1583786142, "1"]}}`)) case 1: conn, _, _ := w.(http.Hijacker).Hijack() _ = conn.Close() case 2: w.Write([]byte(`{"status":"success","data":{"resultType":"scalar","result":[1583786142, "2"]}}`)) case 3: conn, _, _ := w.(http.Hijacker).Hijack() _ = conn.Close() case 4: conn, _, _ := w.(http.Hijacker).Hijack() _ = conn.Close() } }) srv := httptest.NewServer(mux) defer srv.Close() s := NewPrometheusClient(srv.URL, nil, false, srv.Client()) pq := s.BuildWithParams(QuerierParams{DataSourceType: string(datasourcePrometheus)}) expErr := func(err string) { _, _, gotErr := pq.Query(ctx, vmQuery, time.Now()) if gotErr == nil { t.Fatalf("expected %q got nil", err) } if !strings.Contains(gotErr.Error(), err) { t.Fatalf("expected err %q; got %q", err, gotErr) } } expValue := func(v float64) { res, _, err := pq.Query(ctx, vmQuery, time.Now()) if err != nil { t.Fatalf("unexpected %s", err) } m := res.Data if len(m) != 1 { t.Fatalf("expected 1 metrics got %d in %+v", len(m), m) } expected := []Metric{ { Timestamps: []int64{1583786142}, Values: []float64{v}, }, } if !reflect.DeepEqual(m, expected) { t.Fatalf("unexpected metric %+v want %+v", m, expected) } } expValue(1) // 0 expValue(2) // 1 - fail, 2 - retry expErr("EOF") // 3, 4 - retries } func metricsEqual(t *testing.T, gotM, expectedM []Metric) { for i, exp := range expectedM { got := gotM[i] gotTS, expTS := got.Timestamps, exp.Timestamps if !reflect.DeepEqual(gotTS, expTS) { t.Fatalf("unexpected timestamps %+v want %+v", gotTS, expTS) } gotV, expV := got.Values, exp.Values if !reflect.DeepEqual(gotV, expV) { t.Fatalf("unexpected values %+v want %+v", gotV, expV) } sort.Slice(got.Labels, func(i, j int) bool { return got.Labels[i].Name < got.Labels[j].Name }) sort.Slice(exp.Labels, func(i, j int) bool { return exp.Labels[i].Name < exp.Labels[j].Name }) if !reflect.DeepEqual(exp.Labels, got.Labels) { t.Fatalf("unexpected labels %+v want %+v", got.Labels, exp.Labels) } } } func TestVMRangeQuery(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/", func(_ http.ResponseWriter, _ *http.Request) { t.Fatalf("should not be called") }) c := -1 mux.HandleFunc("/api/v1/query_range", func(w http.ResponseWriter, r *http.Request) { c++ if r.Method != http.MethodPost { t.Fatalf("expected POST method got %s", r.Method) } if name, pass, _ := r.BasicAuth(); name != basicAuthName || pass != basicAuthPass { t.Fatalf("expected %s:%s as basic auth got %s:%s", basicAuthName, basicAuthPass, name, pass) } if r.URL.Query().Get("query") != vmQuery { t.Fatalf("expected %s in query param, got %s", vmQuery, r.URL.Query().Get("query")) } startTS := r.URL.Query().Get("start") if startTS == "" { t.Fatalf("expected 'start' in query param, got nil instead") } if _, err := time.Parse(time.RFC3339, startTS); err != nil { t.Fatalf("failed to parse 'start' query param: %s", err) } endTS := r.URL.Query().Get("end") if endTS == "" { t.Fatalf("expected 'end' in query param, got nil instead") } if _, err := time.Parse(time.RFC3339, endTS); err != nil { t.Fatalf("failed to parse 'end' query param: %s", err) } step := r.URL.Query().Get("step") if step != "15s" { t.Fatalf("expected 'step' query param to be 15s; got %q instead", step) } switch c { case 0: w.Write([]byte(`{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"__name__":"vm_rows"},"values":[[1583786142,"13763"]]}]}}`)) } }) mux.HandleFunc("/select/logsql/stats_query_range", func(w http.ResponseWriter, r *http.Request) { c++ if r.Method != http.MethodPost { t.Fatalf("expected POST method got %s", r.Method) } if name, pass, _ := r.BasicAuth(); name != basicAuthName || pass != basicAuthPass { t.Fatalf("expected %s:%s as basic auth got %s:%s", basicAuthName, basicAuthPass, name, pass) } if r.URL.Query().Get("query") != vlogsRangeQuery { t.Fatalf("expected %s in query param, got %s", vmQuery, r.URL.Query().Get("query")) } startTS := r.URL.Query().Get("start") if startTS == "" { t.Fatalf("expected 'start' in query param, got nil instead") } if _, err := time.Parse(time.RFC3339, startTS); err != nil { t.Fatalf("failed to parse 'start' query param: %s", err) } endTS := r.URL.Query().Get("end") if endTS == "" { t.Fatalf("expected 'end' in query param, got nil instead") } if _, err := time.Parse(time.RFC3339, endTS); err != nil { t.Fatalf("failed to parse 'end' query param: %s", err) } step := r.URL.Query().Get("step") if step != "60s" { t.Fatalf("expected 'step' query param to be 60s; got %q instead", step) } switch c { case 1: w.Write([]byte(`{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"__name__":"total"},"values":[[1583786142,"10"]]}]}}`)) } }) srv := httptest.NewServer(mux) defer srv.Close() authCfg, err := baCfg.NewConfig(".") if err != nil { t.Fatalf("unexpected: %s", err) } s := NewPrometheusClient(srv.URL, authCfg, false, srv.Client()) pq := s.BuildWithParams(QuerierParams{DataSourceType: string(datasourcePrometheus), EvaluationInterval: 15 * time.Second}) _, err = pq.QueryRange(ctx, vmQuery, time.Now(), time.Time{}) expectError(t, err, "is missing") _, err = pq.QueryRange(ctx, vmQuery, time.Time{}, time.Now()) expectError(t, err, "is missing") start, end := time.Now().Add(-time.Minute), time.Now() res, err := pq.QueryRange(ctx, vmQuery, start, end) if err != nil { t.Fatalf("unexpected %s", err) } m := res.Data if len(m) != 1 { t.Fatalf("expected 1 metric got %d in %+v", len(m), m) } expected := Metric{ Labels: []prompbmarshal.Label{{Value: "vm_rows", Name: "__name__"}}, Timestamps: []int64{1583786142}, Values: []float64{13763}, } if !reflect.DeepEqual(m[0], expected) { t.Fatalf("unexpected metric %+v want %+v", m[0], expected) } // test unsupported graphite gq := s.BuildWithParams(QuerierParams{DataSourceType: string(datasourceGraphite)}) _, err = gq.QueryRange(ctx, queryRender, start, end) expectError(t, err, "is not supported") // unsupported logsql gq = s.BuildWithParams(QuerierParams{DataSourceType: string(datasourceVLogs), EvaluationInterval: 60 * time.Second}) res, err = gq.QueryRange(ctx, vlogsRangeQuery, start, end) expectError(t, err, "is not supported") // supported logsql gq = s.BuildWithParams(QuerierParams{DataSourceType: string(datasourceVLogs), EvaluationInterval: 60 * time.Second, ApplyIntervalAsTimeFilter: true}) res, err = gq.QueryRange(ctx, vlogsRangeQuery, start, end) if err != nil { t.Fatalf("unexpected %s", err) } m = res.Data if len(m) != 1 { t.Fatalf("expected 1 metric got %d in %+v", len(m), m) } expected = Metric{ Labels: []prompbmarshal.Label{{Value: "total", Name: "stats_result"}}, Timestamps: []int64{1583786142}, Values: []float64{10}, } if !reflect.DeepEqual(m[0], expected) { t.Fatalf("unexpected metric %+v want %+v", m[0], expected) } } func TestRequestParams(t *testing.T) { query := "up" vlogsQuery := "_time: 5m | stats count() total" timestamp := time.Date(2001, 2, 3, 4, 5, 6, 0, time.UTC) f := func(isQueryRange bool, c *Client, checkFn func(t *testing.T, r *http.Request)) { t.Helper() req, err := c.newRequest(ctx) if err != nil { t.Fatalf("error in newRequest: %s", err) } switch c.dataSourceType { case datasourcePrometheus: if isQueryRange { c.setPrometheusRangeReqParams(req, query, timestamp, timestamp) } else { c.setPrometheusInstantReqParams(req, query, timestamp) } case datasourceGraphite: c.setGraphiteReqParams(req, query) case datasourceVLogs: if isQueryRange { c.setVLogsRangeReqParams(req, vlogsQuery, timestamp, timestamp) } else { c.setVLogsInstantReqParams(req, vlogsQuery, timestamp) } } checkFn(t, req) } authCfg, err := baCfg.NewConfig(".") if err != nil { t.Fatalf("unexpected error: %s", err) } storage := Client{ extraParams: url.Values{"round_digits": {"10"}}, } // prometheus path f(false, &Client{ dataSourceType: datasourcePrometheus, }, func(t *testing.T, r *http.Request) { checkEqualString(t, "/api/v1/query", r.URL.Path) }) // prometheus prefix f(false, &Client{ dataSourceType: datasourcePrometheus, appendTypePrefix: true, }, func(t *testing.T, r *http.Request) { checkEqualString(t, "/prometheus/api/v1/query", r.URL.Path) }) // prometheus range path f(true, &Client{ dataSourceType: datasourcePrometheus, }, func(t *testing.T, r *http.Request) { checkEqualString(t, "/api/v1/query_range", r.URL.Path) }) // prometheus range prefix f(true, &Client{ dataSourceType: datasourcePrometheus, appendTypePrefix: true, }, func(t *testing.T, r *http.Request) { checkEqualString(t, "/prometheus/api/v1/query_range", r.URL.Path) }) // graphite path f(false, &Client{ dataSourceType: datasourceGraphite, }, func(t *testing.T, r *http.Request) { checkEqualString(t, graphitePath, r.URL.Path) }) // graphite prefix f(false, &Client{ dataSourceType: datasourceGraphite, appendTypePrefix: true, }, func(t *testing.T, r *http.Request) { checkEqualString(t, graphitePrefix+graphitePath, r.URL.Path) }) // default params f(false, &Client{dataSourceType: datasourcePrometheus}, func(t *testing.T, r *http.Request) { exp := url.Values{"query": {query}, "time": {timestamp.Format(time.RFC3339)}} checkEqualString(t, exp.Encode(), r.URL.RawQuery) }) f(false, &Client{dataSourceType: datasourcePrometheus, applyIntervalAsTimeFilter: true}, func(t *testing.T, r *http.Request) { exp := url.Values{"query": {query}, "time": {timestamp.Format(time.RFC3339)}} checkEqualString(t, exp.Encode(), r.URL.RawQuery) }) // default range params f(true, &Client{dataSourceType: datasourcePrometheus}, func(t *testing.T, r *http.Request) { ts := timestamp.Format(time.RFC3339) exp := url.Values{"query": {query}, "start": {ts}, "end": {ts}} checkEqualString(t, exp.Encode(), r.URL.RawQuery) }) // basic auth f(false, &Client{ dataSourceType: datasourcePrometheus, authCfg: authCfg, }, func(t *testing.T, r *http.Request) { u, p, _ := r.BasicAuth() checkEqualString(t, "foo", u) checkEqualString(t, "bar", p) }) // basic auth range f(true, &Client{ dataSourceType: datasourcePrometheus, authCfg: authCfg, }, func(t *testing.T, r *http.Request) { u, p, _ := r.BasicAuth() checkEqualString(t, "foo", u) checkEqualString(t, "bar", p) }) // evaluation interval f(false, &Client{ dataSourceType: datasourcePrometheus, evaluationInterval: 15 * time.Second, }, func(t *testing.T, r *http.Request) { evalInterval := 15 * time.Second exp := url.Values{"query": {query}, "step": {evalInterval.String()}, "time": {timestamp.Format(time.RFC3339)}} checkEqualString(t, exp.Encode(), r.URL.RawQuery) }) // step override f(false, &Client{ dataSourceType: datasourcePrometheus, queryStep: time.Minute, }, func(t *testing.T, r *http.Request) { exp := url.Values{ "query": {query}, "step": {fmt.Sprintf("%ds", int(time.Minute.Seconds()))}, "time": {timestamp.Format(time.RFC3339)}, } checkEqualString(t, exp.Encode(), r.URL.RawQuery) }) // step to seconds f(false, &Client{ dataSourceType: datasourcePrometheus, evaluationInterval: 3 * time.Hour, }, func(t *testing.T, r *http.Request) { evalInterval := 3 * time.Hour exp := url.Values{"query": {query}, "step": {fmt.Sprintf("%ds", int(evalInterval.Seconds()))}, "time": {timestamp.Format(time.RFC3339)}} checkEqualString(t, exp.Encode(), r.URL.RawQuery) }) // prometheus extra params f(false, &Client{ dataSourceType: datasourcePrometheus, extraParams: url.Values{"round_digits": {"10"}}, }, func(t *testing.T, r *http.Request) { exp := url.Values{"query": {query}, "round_digits": {"10"}, "time": {timestamp.Format(time.RFC3339)}} checkEqualString(t, exp.Encode(), r.URL.RawQuery) }) // prometheus extra params range f(true, &Client{ dataSourceType: datasourcePrometheus, extraParams: url.Values{ "nocache": {"1"}, "max_lookback": {"1h"}, }, }, func(t *testing.T, r *http.Request) { exp := url.Values{ "query": {query}, "end": {timestamp.Format(time.RFC3339)}, "start": {timestamp.Format(time.RFC3339)}, "nocache": {"1"}, "max_lookback": {"1h"}, } checkEqualString(t, exp.Encode(), r.URL.RawQuery) }) // custom params overrides the original params f(false, storage.Clone().ApplyParams(QuerierParams{ DataSourceType: string(datasourcePrometheus), QueryParams: url.Values{"round_digits": {"2"}}, }), func(t *testing.T, r *http.Request) { exp := url.Values{"query": {query}, "round_digits": {"2"}, "time": {timestamp.Format(time.RFC3339)}} checkEqualString(t, exp.Encode(), r.URL.RawQuery) }) // allow duplicates in query params f(false, storage.Clone().ApplyParams(QuerierParams{ DataSourceType: string(datasourcePrometheus), QueryParams: url.Values{"extra_labels": {"env=dev", "foo=bar"}}, }), func(t *testing.T, r *http.Request) { exp := url.Values{"query": {query}, "round_digits": {"10"}, "extra_labels": {"env=dev", "foo=bar"}, "time": {timestamp.Format(time.RFC3339)}} checkEqualString(t, exp.Encode(), r.URL.RawQuery) }) // graphite extra params f(false, &Client{ dataSourceType: datasourceGraphite, extraParams: url.Values{ "nocache": {"1"}, "max_lookback": {"1h"}, }, }, func(t *testing.T, r *http.Request) { exp := fmt.Sprintf("format=json&from=-5min&max_lookback=1h&nocache=1&target=%s&until=now", query) checkEqualString(t, exp, r.URL.RawQuery) }) // graphite extra params allows to override from f(false, &Client{ dataSourceType: datasourceGraphite, extraParams: url.Values{ "from": {"-10m"}, }, }, func(t *testing.T, r *http.Request) { exp := fmt.Sprintf("format=json&from=-10m&target=%s&until=now", query) checkEqualString(t, exp, r.URL.RawQuery) }) // test vlogs f(false, &Client{ dataSourceType: datasourceVLogs, evaluationInterval: time.Minute, }, func(t *testing.T, r *http.Request) { exp := url.Values{"query": {vlogsQuery}, "time": {timestamp.Format(time.RFC3339)}} checkEqualString(t, exp.Encode(), r.URL.RawQuery) }) f(false, &Client{ dataSourceType: datasourceVLogs, evaluationInterval: time.Minute, applyIntervalAsTimeFilter: true, }, func(t *testing.T, r *http.Request) { ts := timestamp.Format(time.RFC3339) exp := url.Values{"query": {vlogsQuery}, "time": {ts}, "start": {timestamp.Add(-time.Minute).Format(time.RFC3339)}, "end": {ts}} checkEqualString(t, exp.Encode(), r.URL.RawQuery) }) f(true, &Client{ dataSourceType: datasourceVLogs, evaluationInterval: time.Minute, }, func(t *testing.T, r *http.Request) { ts := timestamp.Format(time.RFC3339) exp := url.Values{"query": {vlogsQuery}, "start": {ts}, "end": {ts}, "step": {"60s"}} checkEqualString(t, exp.Encode(), r.URL.RawQuery) }) } func TestHeaders(t *testing.T) { f := func(vmFn func() *Client, checkFn func(t *testing.T, r *http.Request)) { t.Helper() vm := vmFn() req, err := vm.newQueryRequest(ctx, "foo", time.Now()) if err != nil { t.Fatalf("error in newQueryRequest: %s", err) } checkFn(t, req) } // basic auth f(func() *Client { cfg, err := utils.AuthConfig(utils.WithBasicAuth("foo", "bar", "")) if err != nil { t.Fatalf("Error get auth config: %s", err) } return NewPrometheusClient("", cfg, false, nil) }, func(t *testing.T, r *http.Request) { u, p, _ := r.BasicAuth() checkEqualString(t, "foo", u) checkEqualString(t, "bar", p) }) // bearer auth f(func() *Client { cfg, err := utils.AuthConfig(utils.WithBearer("foo", "")) if err != nil { t.Fatalf("Error get auth config: %s", err) } return NewPrometheusClient("", cfg, false, nil) }, func(t *testing.T, r *http.Request) { reqToken := r.Header.Get("Authorization") splitToken := strings.Split(reqToken, "Bearer ") if len(splitToken) != 2 { t.Fatalf("expected two items got %d", len(splitToken)) } token := splitToken[1] checkEqualString(t, "foo", token) }) // custom extraHeaders f(func() *Client { c := NewPrometheusClient("", nil, false, nil) c.extraHeaders = []keyValue{ {key: "Foo", value: "bar"}, {key: "Baz", value: "qux"}, } return c }, func(t *testing.T, r *http.Request) { h1 := r.Header.Get("Foo") checkEqualString(t, "bar", h1) h2 := r.Header.Get("Baz") checkEqualString(t, "qux", h2) }) // custom header overrides basic auth f(func() *Client { cfg, err := utils.AuthConfig(utils.WithBasicAuth("foo", "bar", "")) if err != nil { t.Fatalf("Error get auth config: %s", err) } c := NewPrometheusClient("", cfg, false, nil) c.extraHeaders = []keyValue{ {key: "Authorization", value: "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="}, } return c }, func(t *testing.T, r *http.Request) { u, p, _ := r.BasicAuth() checkEqualString(t, "Aladdin", u) checkEqualString(t, "open sesame", p) }) } func checkEqualString(t *testing.T, exp, got string) { t.Helper() if got != exp { t.Fatalf("expected to get: \n%q; \ngot: \n%q", exp, got) } } func expectError(t *testing.T, err error, exp string) { t.Helper() if err == nil { t.Fatalf("expected non-nil error") } if !strings.Contains(err.Error(), exp) { t.Fatalf("expected error %q to contain %q", err, exp) } }