From 349922b38c285a2188378904717dc1201dd221a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carvalho?= Date: Wed, 19 Apr 2017 18:24:14 -0300 Subject: [PATCH] api: creates versioned package for prometheus v1 api This commit creates a new package to hold the prometheus v1 API interface. This interface will contain all the funcionality exposed by Prometheus v1 HTTP API. The underlying http client is kept on the api package since it may be reused across diferent API versions and also by the Alertmanager api package (to come.) --- api/client.go | 136 +++++++++ api/client_test.go | 112 ++++++++ api/prometheus/{ => v1}/api.go | 293 ++++++------------- api/prometheus/{ => v1}/api_test.go | 432 +++++++++++----------------- 4 files changed, 505 insertions(+), 468 deletions(-) create mode 100644 api/client.go create mode 100644 api/client_test.go rename api/prometheus/{ => v1}/api.go (57%) rename api/prometheus/{ => v1}/api_test.go (73%) diff --git a/api/client.go b/api/client.go new file mode 100644 index 0000000..c3f6cc8 --- /dev/null +++ b/api/client.go @@ -0,0 +1,136 @@ +// Copyright 2015 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package api provides clients the HTTP API's. +package api + +import ( + "io/ioutil" + "net" + "net/http" + "net/url" + "path" + "strings" + "time" + + "golang.org/x/net/context" + "golang.org/x/net/context/ctxhttp" +) + +// CancelableTransport is like net.Transport but provides +// per-request cancelation functionality. +type CancelableTransport interface { + http.RoundTripper + CancelRequest(req *http.Request) +} + +// DefaultTransport is used if no Transport is set in Config. +var DefaultTransport CancelableTransport = &http.Transport{ + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, +} + +// Config defines configuration parameters for a new client. +type Config struct { + // The address of the Prometheus to connect to. + Address string + + // Transport is used by the Client to drive HTTP requests. If not + // provided, DefaultTransport will be used. + Transport CancelableTransport +} + +func (cfg *Config) transport() CancelableTransport { + if cfg.Transport == nil { + return DefaultTransport + } + return cfg.Transport +} + +// Client is the interface for an API client. +type Client interface { + URL(ep string, args map[string]string) *url.URL + Do(context.Context, *http.Request) (*http.Response, []byte, error) +} + +// New returns a new Client. +// +// It is safe to use the returned Client from multiple goroutines. +func New(cfg Config) (Client, error) { + u, err := url.Parse(cfg.Address) + if err != nil { + return nil, err + } + u.Path = strings.TrimRight(u.Path, "/") + + return &httpClient{ + endpoint: u, + transport: cfg.transport(), + }, nil +} + +type httpClient struct { + endpoint *url.URL + transport CancelableTransport +} + +func (c *httpClient) URL(ep string, args map[string]string) *url.URL { + p := path.Join(c.endpoint.Path, ep) + + for arg, val := range args { + arg = ":" + arg + p = strings.Replace(p, arg, val, -1) + } + + u := *c.endpoint + u.Path = p + + return &u +} + +func (c *httpClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) { + resp, err := ctxhttp.Do(ctx, &http.Client{Transport: c.transport}, req) + + defer func() { + if resp != nil { + resp.Body.Close() + } + }() + + if err != nil { + return nil, nil, err + } + + var body []byte + done := make(chan struct{}) + go func() { + body, err = ioutil.ReadAll(resp.Body) + close(done) + }() + + select { + case <-ctx.Done(): + err = resp.Body.Close() + <-done + if err == nil { + err = ctx.Err() + } + case <-done: + } + + return resp, body, err +} diff --git a/api/client_test.go b/api/client_test.go new file mode 100644 index 0000000..a068e2f --- /dev/null +++ b/api/client_test.go @@ -0,0 +1,112 @@ +// Copyright 2015 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "net/url" + "testing" +) + +func TestConfig(t *testing.T) { + c := Config{} + if c.transport() != DefaultTransport { + t.Fatalf("expected default transport for nil Transport field") + } +} + +func TestClientURL(t *testing.T) { + tests := []struct { + address string + endpoint string + args map[string]string + expected string + }{ + { + address: "http://localhost:9090", + endpoint: "/test", + expected: "http://localhost:9090/test", + }, + { + address: "http://localhost", + endpoint: "/test", + expected: "http://localhost/test", + }, + { + address: "http://localhost:9090", + endpoint: "test", + expected: "http://localhost:9090/test", + }, + { + address: "http://localhost:9090/prefix", + endpoint: "/test", + expected: "http://localhost:9090/prefix/test", + }, + { + address: "https://localhost:9090/", + endpoint: "/test/", + expected: "https://localhost:9090/test", + }, + { + address: "http://localhost:9090", + endpoint: "/test/:param", + args: map[string]string{ + "param": "content", + }, + expected: "http://localhost:9090/test/content", + }, + { + address: "http://localhost:9090", + endpoint: "/test/:param/more/:param", + args: map[string]string{ + "param": "content", + }, + expected: "http://localhost:9090/test/content/more/content", + }, + { + address: "http://localhost:9090", + endpoint: "/test/:param/more/:foo", + args: map[string]string{ + "param": "content", + "foo": "bar", + }, + expected: "http://localhost:9090/test/content/more/bar", + }, + { + address: "http://localhost:9090", + endpoint: "/test/:param", + args: map[string]string{ + "nonexistant": "content", + }, + expected: "http://localhost:9090/test/:param", + }, + } + + for _, test := range tests { + ep, err := url.Parse(test.address) + if err != nil { + t.Fatal(err) + } + + hclient := &httpClient{ + endpoint: ep, + transport: DefaultTransport, + } + + u := hclient.URL(test.endpoint, test.args) + if u.String() != test.expected { + t.Errorf("unexpected result: got %s, want %s", u, test.expected) + continue + } + } +} diff --git a/api/prometheus/api.go b/api/prometheus/v1/api.go similarity index 57% rename from api/prometheus/api.go rename to api/prometheus/v1/api.go index 61c47ad..99bc0cd 100644 --- a/api/prometheus/api.go +++ b/api/prometheus/v1/api.go @@ -1,4 +1,4 @@ -// Copyright 2015 The Prometheus Authors +// Copyright 2017 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -11,35 +11,32 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package prometheus provides bindings to the Prometheus HTTP API: +// Package v1 provides bindings to the Prometheus HTTP API v1: // http://prometheus.io/docs/querying/api/ -package prometheus +package v1 import ( "encoding/json" "fmt" - "io/ioutil" - "net" "net/http" - "net/url" - "path" "strconv" - "strings" "time" - "github.com/prometheus/common/model" "golang.org/x/net/context" - "golang.org/x/net/context/ctxhttp" + + "github.com/prometheus/client_golang/api" + "github.com/prometheus/common/model" ) const ( statusAPIError = 422 - apiPrefix = "/api/v1" - epQuery = "/query" - epQueryRange = "/query_range" - epLabelValues = "/label/:name/values" - epSeries = "/series" + apiPrefix = "/api/v1" + + epQuery = apiPrefix + "/query" + epQueryRange = apiPrefix + "/query_range" + epLabelValues = apiPrefix + "/label/:name/values" + epSeries = apiPrefix + "/series" ) // ErrorType models the different API error types. @@ -64,168 +61,6 @@ func (e *Error) Error() string { return fmt.Sprintf("%s: %s", e.Type, e.Msg) } -// CancelableTransport is like net.Transport but provides -// per-request cancelation functionality. -type CancelableTransport interface { - http.RoundTripper - CancelRequest(req *http.Request) -} - -// DefaultTransport is used if no Transport is set in Config. -var DefaultTransport CancelableTransport = &http.Transport{ - Proxy: http.ProxyFromEnvironment, - Dial: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - }).Dial, - TLSHandshakeTimeout: 10 * time.Second, -} - -// Config defines configuration parameters for a new client. -type Config struct { - // The address of the Prometheus to connect to. - Address string - - // Transport is used by the Client to drive HTTP requests. If not - // provided, DefaultTransport will be used. - Transport CancelableTransport -} - -func (cfg *Config) transport() CancelableTransport { - if cfg.Transport == nil { - return DefaultTransport - } - return cfg.Transport -} - -// Client is the interface for an API client. -type Client interface { - url(ep string, args map[string]string) *url.URL - do(context.Context, *http.Request) (*http.Response, []byte, error) -} - -// New returns a new Client. -// -// It is safe to use the returned Client from multiple goroutines. -func New(cfg Config) (Client, error) { - u, err := url.Parse(cfg.Address) - if err != nil { - return nil, err - } - u.Path = strings.TrimRight(u.Path, "/") + apiPrefix - - return &httpClient{ - endpoint: u, - transport: cfg.transport(), - }, nil -} - -type httpClient struct { - endpoint *url.URL - transport CancelableTransport -} - -func (c *httpClient) url(ep string, args map[string]string) *url.URL { - p := path.Join(c.endpoint.Path, ep) - - for arg, val := range args { - arg = ":" + arg - p = strings.Replace(p, arg, val, -1) - } - - u := *c.endpoint - u.Path = p - - return &u -} - -func (c *httpClient) do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) { - resp, err := ctxhttp.Do(ctx, &http.Client{Transport: c.transport}, req) - - defer func() { - if resp != nil { - resp.Body.Close() - } - }() - - if err != nil { - return nil, nil, err - } - - var body []byte - done := make(chan struct{}) - go func() { - body, err = ioutil.ReadAll(resp.Body) - close(done) - }() - - select { - case <-ctx.Done(): - err = resp.Body.Close() - <-done - if err == nil { - err = ctx.Err() - } - case <-done: - } - - return resp, body, err -} - -// apiClient wraps a regular client and processes successful API responses. -// Successful also includes responses that errored at the API level. -type apiClient struct { - Client -} - -type apiResponse struct { - Status string `json:"status"` - Data json.RawMessage `json:"data"` - ErrorType ErrorType `json:"errorType"` - Error string `json:"error"` -} - -func (c apiClient) do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) { - resp, body, err := c.Client.do(ctx, req) - if err != nil { - return resp, body, err - } - - code := resp.StatusCode - - if code/100 != 2 && code != statusAPIError { - return resp, body, &Error{ - Type: ErrBadResponse, - Msg: fmt.Sprintf("bad response code %d", resp.StatusCode), - } - } - - var result apiResponse - - if err = json.Unmarshal(body, &result); err != nil { - return resp, body, &Error{ - Type: ErrBadResponse, - Msg: err.Error(), - } - } - - if (code == statusAPIError) != (result.Status == "error") { - err = &Error{ - Type: ErrBadResponse, - Msg: "inconsistent body for response code", - } - } - - if code == statusAPIError && result.Status == "error" { - err = &Error{ - Type: result.ErrorType, - Msg: result.Error, - } - } - - return resp, []byte(result.Data), err -} - // Range represents a sliced time range. type Range struct { // The boundaries of the time range. @@ -234,6 +69,16 @@ type Range struct { Step time.Duration } +// API provides bindings the Prometheus's v1 API. +type API interface { + // Query performs a query for the given time. + Query(ctx context.Context, query string, ts time.Time) (model.Value, error) + // QueryRange performs a query for the given range. + QueryRange(ctx context.Context, query string, r Range) (model.Value, error) + // LabelValues performs a query for the values of the given label. + LabelValues(ctx context.Context, label string) (model.LabelValues, error) +} + // queryResult contains result data for a query. type queryResult struct { Type model.ValueType `json:"resultType"` @@ -276,29 +121,19 @@ func (qr *queryResult) UnmarshalJSON(b []byte) error { return err } -// QueryAPI provides bindings the Prometheus's query API. -type QueryAPI interface { - // Query performs a query for the given time. - Query(ctx context.Context, query string, ts time.Time) (model.Value, error) - // QueryRange performs a query for the given range. - QueryRange(ctx context.Context, query string, r Range) (model.Value, error) - // LabelValues performs a query for the values of the given label. - LabelValues(ctx context.Context, label string) (model.LabelValues, error) -} - -// NewQueryAPI returns a new QueryAPI for the client. +// NewAPI returns a new API for the client. // -// It is safe to use the returned QueryAPI from multiple goroutines. -func NewQueryAPI(c Client) QueryAPI { - return &httpQueryAPI{client: apiClient{c}} +// It is safe to use the returned API from multiple goroutines. +func NewAPI(c api.Client) API { + return &httpAPI{client: apiClient{c}} } -type httpQueryAPI struct { - client Client +type httpAPI struct { + client api.Client } -func (h *httpQueryAPI) Query(ctx context.Context, query string, ts time.Time) (model.Value, error) { - u := h.client.url(epQuery, nil) +func (h *httpAPI) Query(ctx context.Context, query string, ts time.Time) (model.Value, error) { + u := h.client.URL(epQuery, nil) q := u.Query() q.Set("query", query) @@ -311,7 +146,7 @@ func (h *httpQueryAPI) Query(ctx context.Context, query string, ts time.Time) (m return nil, err } - _, body, err := h.client.do(ctx, req) + _, body, err := h.client.Do(ctx, req) if err != nil { return nil, err } @@ -322,8 +157,8 @@ func (h *httpQueryAPI) Query(ctx context.Context, query string, ts time.Time) (m return model.Value(qres.v), err } -func (h *httpQueryAPI) QueryRange(ctx context.Context, query string, r Range) (model.Value, error) { - u := h.client.url(epQueryRange, nil) +func (h *httpAPI) QueryRange(ctx context.Context, query string, r Range) (model.Value, error) { + u := h.client.URL(epQueryRange, nil) q := u.Query() var ( @@ -344,7 +179,7 @@ func (h *httpQueryAPI) QueryRange(ctx context.Context, query string, r Range) (m return nil, err } - _, body, err := h.client.do(ctx, req) + _, body, err := h.client.Do(ctx, req) if err != nil { return nil, err } @@ -355,13 +190,13 @@ func (h *httpQueryAPI) QueryRange(ctx context.Context, query string, r Range) (m return model.Value(qres.v), err } -func (h *httpQueryAPI) LabelValues(ctx context.Context, label string) (model.LabelValues, error) { - u := h.client.url(epLabelValues, map[string]string{"name": label}) +func (h *httpAPI) LabelValues(ctx context.Context, label string) (model.LabelValues, error) { + u := h.client.URL(epLabelValues, map[string]string{"name": label}) req, err := http.NewRequest(http.MethodGet, u.String(), nil) if err != nil { return nil, err } - _, body, err := h.client.do(ctx, req) + _, body, err := h.client.Do(ctx, req) if err != nil { return nil, err } @@ -369,3 +204,57 @@ func (h *httpQueryAPI) LabelValues(ctx context.Context, label string) (model.Lab err = json.Unmarshal(body, &labelValues) return labelValues, err } + +// apiClient wraps a regular client and processes successful API responses. +// Successful also includes responses that errored at the API level. +type apiClient struct { + api.Client +} + +type apiResponse struct { + Status string `json:"status"` + Data json.RawMessage `json:"data"` + ErrorType ErrorType `json:"errorType"` + Error string `json:"error"` +} + +func (c apiClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) { + resp, body, err := c.Client.Do(ctx, req) + if err != nil { + return resp, body, err + } + + code := resp.StatusCode + + if code/100 != 2 && code != statusAPIError { + return resp, body, &Error{ + Type: ErrBadResponse, + Msg: fmt.Sprintf("bad response code %d", resp.StatusCode), + } + } + + var result apiResponse + + if err = json.Unmarshal(body, &result); err != nil { + return resp, body, &Error{ + Type: ErrBadResponse, + Msg: err.Error(), + } + } + + if (code == statusAPIError) != (result.Status == "error") { + err = &Error{ + Type: ErrBadResponse, + Msg: "inconsistent body for response code", + } + } + + if code == statusAPIError && result.Status == "error" { + err = &Error{ + Type: result.ErrorType, + Msg: result.Error, + } + } + + return resp, []byte(result.Data), err +} diff --git a/api/prometheus/api_test.go b/api/prometheus/v1/api_test.go similarity index 73% rename from api/prometheus/api_test.go rename to api/prometheus/v1/api_test.go index f7ffc7c..57a9192 100644 --- a/api/prometheus/api_test.go +++ b/api/prometheus/v1/api_test.go @@ -1,4 +1,4 @@ -// Copyright 2015 The Prometheus Authors +// Copyright 2017 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package prometheus +package v1 import ( "encoding/json" @@ -23,107 +23,190 @@ import ( "testing" "time" - "github.com/prometheus/common/model" "golang.org/x/net/context" + + "github.com/prometheus/common/model" ) -func TestConfig(t *testing.T) { - c := Config{} - if c.transport() != DefaultTransport { - t.Fatalf("expected default transport for nil Transport field") - } +type apiTest struct { + do func() (interface{}, error) + inErr error + inRes interface{} + + reqPath string + reqParam url.Values + reqMethod string + res interface{} + err error } -func TestClientURL(t *testing.T) { - tests := []struct { - address string - endpoint string - args map[string]string - expected string - }{ +type apiTestClient struct { + *testing.T + curTest apiTest +} + +func (c *apiTestClient) URL(ep string, args map[string]string) *url.URL { + path := ep + for k, v := range args { + path = strings.Replace(path, ":"+k, v, -1) + } + u := &url.URL{ + Host: "test:9090", + Path: path, + } + return u +} + +func (c *apiTestClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) { + + test := c.curTest + + if req.URL.Path != test.reqPath { + c.Errorf("unexpected request path: want %s, got %s", test.reqPath, req.URL.Path) + } + if req.Method != test.reqMethod { + c.Errorf("unexpected request method: want %s, got %s", test.reqMethod, req.Method) + } + + b, err := json.Marshal(test.inRes) + if err != nil { + c.Fatal(err) + } + + resp := &http.Response{} + if test.inErr != nil { + resp.StatusCode = statusAPIError + } else { + resp.StatusCode = http.StatusOK + } + + return resp, b, test.inErr +} + +func TestAPIs(t *testing.T) { + + testTime := time.Now() + + client := &apiTestClient{T: t} + + queryAPI := &httpAPI{ + client: client, + } + + doQuery := func(q string, ts time.Time) func() (interface{}, error) { + return func() (interface{}, error) { + return queryAPI.Query(context.Background(), q, ts) + } + } + + doQueryRange := func(q string, rng Range) func() (interface{}, error) { + return func() (interface{}, error) { + return queryAPI.QueryRange(context.Background(), q, rng) + } + } + + doLabelValues := func(label string) func() (interface{}, error) { + return func() (interface{}, error) { + return queryAPI.LabelValues(context.Background(), label) + } + } + + queryTests := []apiTest{ { - address: "http://localhost:9090", - endpoint: "/test", - expected: "http://localhost:9090/test", - }, - { - address: "http://localhost", - endpoint: "/test", - expected: "http://localhost/test", - }, - { - address: "http://localhost:9090", - endpoint: "test", - expected: "http://localhost:9090/test", - }, - { - address: "http://localhost:9090/prefix", - endpoint: "/test", - expected: "http://localhost:9090/prefix/test", - }, - { - address: "https://localhost:9090/", - endpoint: "/test/", - expected: "https://localhost:9090/test", - }, - { - address: "http://localhost:9090", - endpoint: "/test/:param", - args: map[string]string{ - "param": "content", + do: doQuery("2", testTime), + inRes: &queryResult{ + Type: model.ValScalar, + Result: &model.Scalar{ + Value: 2, + Timestamp: model.TimeFromUnix(testTime.Unix()), + }, + }, + + reqMethod: "GET", + reqPath: "/api/v1/query", + reqParam: url.Values{ + "query": []string{"2"}, + "time": []string{testTime.Format(time.RFC3339Nano)}, + }, + res: &model.Scalar{ + Value: 2, + Timestamp: model.TimeFromUnix(testTime.Unix()), }, - expected: "http://localhost:9090/test/content", }, { - address: "http://localhost:9090", - endpoint: "/test/:param/more/:param", - args: map[string]string{ - "param": "content", + do: doQuery("2", testTime), + inErr: fmt.Errorf("some error"), + + reqMethod: "GET", + reqPath: "/api/v1/query", + reqParam: url.Values{ + "query": []string{"2"}, + "time": []string{testTime.Format(time.RFC3339Nano)}, }, - expected: "http://localhost:9090/test/content/more/content", + err: fmt.Errorf("some error"), }, + { - address: "http://localhost:9090", - endpoint: "/test/:param/more/:foo", - args: map[string]string{ - "param": "content", - "foo": "bar", + do: doQueryRange("2", Range{ + Start: testTime.Add(-time.Minute), + End: testTime, + Step: time.Minute, + }), + inErr: fmt.Errorf("some error"), + + reqMethod: "GET", + reqPath: "/api/v1/query_range", + reqParam: url.Values{ + "query": []string{"2"}, + "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, + "end": []string{testTime.Format(time.RFC3339Nano)}, + "step": []string{time.Minute.String()}, }, - expected: "http://localhost:9090/test/content/more/bar", + err: fmt.Errorf("some error"), }, + { - address: "http://localhost:9090", - endpoint: "/test/:param", - args: map[string]string{ - "nonexistant": "content", - }, - expected: "http://localhost:9090/test/:param", + do: doLabelValues("mylabel"), + inRes: []string{"val1", "val2"}, + reqMethod: "GET", + reqPath: "/api/v1/label/mylabel/values", + res: model.LabelValues{"val1", "val2"}, + }, + + { + do: doLabelValues("mylabel"), + inErr: fmt.Errorf("some error"), + reqMethod: "GET", + reqPath: "/api/v1/label/mylabel/values", + err: fmt.Errorf("some error"), }, } + var tests []apiTest + tests = append(tests, queryTests...) + for _, test := range tests { - ep, err := url.Parse(test.address) + client.curTest = test + + res, err := test.do() + + if test.err != nil { + if err == nil { + t.Errorf("expected error %q but got none", test.err) + continue + } + if err.Error() != test.err.Error() { + t.Errorf("unexpected error: want %s, got %s", test.err, err) + } + continue + } if err != nil { - t.Fatal(err) - } - - hclient := &httpClient{ - endpoint: ep, - transport: DefaultTransport, - } - - u := hclient.url(test.endpoint, test.args) - if u.String() != test.expected { - t.Errorf("unexpected result: got %s, want %s", u, test.expected) + t.Errorf("unexpected error: %s", err) continue } - // The apiClient must return exactly the same result as the httpClient. - aclient := &apiClient{hclient} - - u = aclient.url(test.endpoint, test.args) - if u.String() != test.expected { - t.Errorf("unexpected result: got %s, want %s", u, test.expected) + if !reflect.DeepEqual(res, test.res) { + t.Errorf("unexpected result: want %v, got %v", test.res, res) } } } @@ -142,11 +225,11 @@ type apiClientTest struct { err *Error } -func (c *testClient) url(ep string, args map[string]string) *url.URL { +func (c *testClient) URL(ep string, args map[string]string) *url.URL { return nil } -func (c *testClient) do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) { +func (c *testClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) { if ctx == nil { c.Fatalf("context was not passed down") } @@ -272,7 +355,7 @@ func TestAPIClientDo(t *testing.T) { tc.ch <- test - _, body, err := client.do(context.Background(), tc.req) + _, body, err := client.Do(context.Background(), tc.req) if test.err != nil { if err == nil { @@ -295,186 +378,3 @@ func TestAPIClientDo(t *testing.T) { } } } - -type apiTestClient struct { - *testing.T - curTest apiTest -} - -type apiTest struct { - do func() (interface{}, error) - inErr error - inRes interface{} - - reqPath string - reqParam url.Values - reqMethod string - res interface{} - err error -} - -func (c *apiTestClient) url(ep string, args map[string]string) *url.URL { - path := apiPrefix + ep - for k, v := range args { - path = strings.Replace(path, ":"+k, v, -1) - } - u := &url.URL{ - Host: "test:9090", - Path: path, - } - return u -} - -func (c *apiTestClient) do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) { - - test := c.curTest - - if req.URL.Path != test.reqPath { - c.Errorf("unexpected request path: want %s, got %s", test.reqPath, req.URL.Path) - } - if req.Method != test.reqMethod { - c.Errorf("unexpected request method: want %s, got %s", test.reqMethod, req.Method) - } - - b, err := json.Marshal(test.inRes) - if err != nil { - c.Fatal(err) - } - - resp := &http.Response{} - if test.inErr != nil { - resp.StatusCode = statusAPIError - } else { - resp.StatusCode = http.StatusOK - } - - return resp, b, test.inErr -} - -func TestAPIs(t *testing.T) { - - testTime := time.Now() - - client := &apiTestClient{T: t} - - queryAPI := &httpQueryAPI{ - client: client, - } - - doQuery := func(q string, ts time.Time) func() (interface{}, error) { - return func() (interface{}, error) { - return queryAPI.Query(context.Background(), q, ts) - } - } - - doQueryRange := func(q string, rng Range) func() (interface{}, error) { - return func() (interface{}, error) { - return queryAPI.QueryRange(context.Background(), q, rng) - } - } - - doLabelValues := func(label string) func() (interface{}, error) { - return func() (interface{}, error) { - return queryAPI.LabelValues(context.Background(), label) - } - } - - queryTests := []apiTest{ - { - do: doQuery("2", testTime), - inRes: &queryResult{ - Type: model.ValScalar, - Result: &model.Scalar{ - Value: 2, - Timestamp: model.TimeFromUnix(testTime.Unix()), - }, - }, - - reqMethod: "GET", - reqPath: "/api/v1/query", - reqParam: url.Values{ - "query": []string{"2"}, - "time": []string{testTime.Format(time.RFC3339Nano)}, - }, - res: &model.Scalar{ - Value: 2, - Timestamp: model.TimeFromUnix(testTime.Unix()), - }, - }, - { - do: doQuery("2", testTime), - inErr: fmt.Errorf("some error"), - - reqMethod: "GET", - reqPath: "/api/v1/query", - reqParam: url.Values{ - "query": []string{"2"}, - "time": []string{testTime.Format(time.RFC3339Nano)}, - }, - err: fmt.Errorf("some error"), - }, - - { - do: doQueryRange("2", Range{ - Start: testTime.Add(-time.Minute), - End: testTime, - Step: time.Minute, - }), - inErr: fmt.Errorf("some error"), - - reqMethod: "GET", - reqPath: "/api/v1/query_range", - reqParam: url.Values{ - "query": []string{"2"}, - "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, - "end": []string{testTime.Format(time.RFC3339Nano)}, - "step": []string{time.Minute.String()}, - }, - err: fmt.Errorf("some error"), - }, - - { - do: doLabelValues("mylabel"), - inRes: []string{"val1", "val2"}, - reqMethod: "GET", - reqPath: "/api/v1/label/mylabel/values", - res: model.LabelValues{"val1", "val2"}, - }, - - { - do: doLabelValues("mylabel"), - inErr: fmt.Errorf("some error"), - reqMethod: "GET", - reqPath: "/api/v1/label/mylabel/values", - err: fmt.Errorf("some error"), - }, - } - - var tests []apiTest - tests = append(tests, queryTests...) - - for _, test := range tests { - client.curTest = test - - res, err := test.do() - - if test.err != nil { - if err == nil { - t.Errorf("expected error %q but got none", test.err) - continue - } - if err.Error() != test.err.Error() { - t.Errorf("unexpected error: want %s, got %s", test.err, err) - } - continue - } - if err != nil { - t.Errorf("unexpected error: %s", err) - continue - } - - if !reflect.DeepEqual(res, test.res) { - t.Errorf("unexpected result: want %v, got %v", test.res, res) - } - } -}