Merge pull request #291 from andrestc/query-label-values

api: reorganize API package and add label values endpoint implementation
This commit is contained in:
Julius Volz 2017-04-25 16:35:58 -05:00 committed by GitHub
commit 7d9484283e
4 changed files with 526 additions and 439 deletions

131
api/client.go Normal file
View File

@ -0,0 +1,131 @@
// 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.
// +build go1.7
// Package api provides clients for the HTTP APIs.
package api
import (
"context"
"io/ioutil"
"net"
"net/http"
"net/url"
"path"
"strings"
"time"
)
// DefaultRoundTripper is used if no RoundTripper is set in Config.
var DefaultRoundTripper http.RoundTripper = &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
// RoundTripper is used by the Client to drive HTTP requests. If not
// provided, DefaultRoundTripper will be used.
RoundTripper http.RoundTripper
}
func (cfg *Config) roundTripper() http.RoundTripper {
if cfg.RoundTripper == nil {
return DefaultRoundTripper
}
return cfg.RoundTripper
}
// 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)
}
// NewClient returns a new Client.
//
// It is safe to use the returned Client from multiple goroutines.
func NewClient(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,
client: http.Client{Transport: cfg.roundTripper()},
}, nil
}
type httpClient struct {
endpoint *url.URL
client http.Client
}
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) {
if ctx != nil {
req = req.WithContext(ctx)
}
resp, err := c.client.Do(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
}

115
api/client_test.go Normal file
View File

@ -0,0 +1,115 @@
// 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.
// +build go1.7
package api
import (
"net/http"
"net/url"
"testing"
)
func TestConfig(t *testing.T) {
c := Config{}
if c.roundTripper() != DefaultRoundTripper {
t.Fatalf("expected default roundtripper for nil RoundTripper 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,
client: http.Client{Transport: DefaultRoundTripper},
}
u := hclient.URL(test.endpoint, test.args)
if u.String() != test.expected {
t.Errorf("unexpected result: got %s, want %s", u, test.expected)
continue
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright 2015 The Prometheus Authors // Copyright 2017 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
// You may obtain a copy of the License at // You may obtain a copy of the License at
@ -11,35 +11,33 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// Package prometheus provides bindings to the Prometheus HTTP API: // +build go1.7
// Package v1 provides bindings to the Prometheus HTTP API v1:
// http://prometheus.io/docs/querying/api/ // http://prometheus.io/docs/querying/api/
package prometheus package v1
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil"
"net"
"net/http" "net/http"
"net/url"
"path"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/prometheus/client_golang/api"
"github.com/prometheus/common/model" "github.com/prometheus/common/model"
"golang.org/x/net/context"
"golang.org/x/net/context/ctxhttp"
) )
const ( const (
statusAPIError = 422 statusAPIError = 422
apiPrefix = "/api/v1" apiPrefix = "/api/v1"
epQuery = "/query" epQuery = apiPrefix + "/query"
epQueryRange = "/query_range" epQueryRange = apiPrefix + "/query_range"
epLabelValues = "/label/:name/values" epLabelValues = apiPrefix + "/label/:name/values"
epSeries = "/series" epSeries = apiPrefix + "/series"
) )
// ErrorType models the different API error types. // ErrorType models the different API error types.
@ -64,168 +62,6 @@ func (e *Error) Error() string {
return fmt.Sprintf("%s: %s", e.Type, e.Msg) 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. // Range represents a sliced time range.
type Range struct { type Range struct {
// The boundaries of the time range. // The boundaries of the time range.
@ -234,6 +70,16 @@ type Range struct {
Step time.Duration Step time.Duration
} }
// API provides bindings for 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. // queryResult contains result data for a query.
type queryResult struct { type queryResult struct {
Type model.ValueType `json:"resultType"` Type model.ValueType `json:"resultType"`
@ -276,27 +122,19 @@ func (qr *queryResult) UnmarshalJSON(b []byte) error {
return err return err
} }
// QueryAPI provides bindings the Prometheus's query API. // NewAPI returns a new API for the client.
type QueryAPI interface {
// Query performs a query for the given time.
Query(ctx context.Context, query string, ts time.Time) (model.Value, error)
// Query performs a query for the given range.
QueryRange(ctx context.Context, query string, r Range) (model.Value, error)
}
// NewQueryAPI returns a new QueryAPI for the client.
// //
// It is safe to use the returned QueryAPI from multiple goroutines. // It is safe to use the returned API from multiple goroutines.
func NewQueryAPI(c Client) QueryAPI { func NewAPI(c api.Client) API {
return &httpQueryAPI{client: apiClient{c}} return &httpAPI{client: apiClient{c}}
} }
type httpQueryAPI struct { type httpAPI struct {
client Client client api.Client
} }
func (h *httpQueryAPI) Query(ctx context.Context, query string, ts time.Time) (model.Value, error) { func (h *httpAPI) Query(ctx context.Context, query string, ts time.Time) (model.Value, error) {
u := h.client.url(epQuery, nil) u := h.client.URL(epQuery, nil)
q := u.Query() q := u.Query()
q.Set("query", query) q.Set("query", query)
@ -304,9 +142,12 @@ func (h *httpQueryAPI) Query(ctx context.Context, query string, ts time.Time) (m
u.RawQuery = q.Encode() u.RawQuery = q.Encode()
req, _ := http.NewRequest("GET", u.String(), nil) req, err := http.NewRequest("GET", 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 { if err != nil {
return nil, err return nil, err
} }
@ -317,8 +158,8 @@ func (h *httpQueryAPI) Query(ctx context.Context, query string, ts time.Time) (m
return model.Value(qres.v), err return model.Value(qres.v), err
} }
func (h *httpQueryAPI) QueryRange(ctx context.Context, query string, r Range) (model.Value, error) { func (h *httpAPI) QueryRange(ctx context.Context, query string, r Range) (model.Value, error) {
u := h.client.url(epQueryRange, nil) u := h.client.URL(epQueryRange, nil)
q := u.Query() q := u.Query()
var ( var (
@ -334,9 +175,12 @@ func (h *httpQueryAPI) QueryRange(ctx context.Context, query string, r Range) (m
u.RawQuery = q.Encode() u.RawQuery = q.Encode()
req, _ := http.NewRequest("GET", u.String(), nil) req, err := http.NewRequest("GET", 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 { if err != nil {
return nil, err return nil, err
} }
@ -346,3 +190,72 @@ func (h *httpQueryAPI) QueryRange(ctx context.Context, query string, r Range) (m
return model.Value(qres.v), err return model.Value(qres.v), err
} }
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)
if err != nil {
return nil, err
}
var labelValues model.LabelValues
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
}

View File

@ -1,4 +1,4 @@
// Copyright 2015 The Prometheus Authors // Copyright 2017 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
// You may obtain a copy of the License at // You may obtain a copy of the License at
@ -11,118 +11,203 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package prometheus // +build go1.7
package v1
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"reflect" "reflect"
"strings"
"testing" "testing"
"time" "time"
"github.com/prometheus/common/model" "github.com/prometheus/common/model"
"golang.org/x/net/context"
) )
func TestConfig(t *testing.T) { type apiTest struct {
c := Config{} do func() (interface{}, error)
if c.transport() != DefaultTransport { inErr error
t.Fatalf("expected default transport for nil Transport field") inRes interface{}
}
reqPath string
reqParam url.Values
reqMethod string
res interface{}
err error
} }
func TestClientURL(t *testing.T) { type apiTestClient struct {
tests := []struct { *testing.T
address string curTest apiTest
endpoint string }
args map[string]string
expected string 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", do: doQuery("2", testTime),
endpoint: "/test", inRes: &queryResult{
expected: "http://localhost:9090/test", 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()),
},
}, },
{ {
address: "http://localhost", do: doQuery("2", testTime),
endpoint: "/test", inErr: fmt.Errorf("some error"),
expected: "http://localhost/test",
reqMethod: "GET",
reqPath: "/api/v1/query",
reqParam: url.Values{
"query": []string{"2"},
"time": []string{testTime.Format(time.RFC3339Nano)},
}, },
err: fmt.Errorf("some error"),
},
{ {
address: "http://localhost:9090", do: doQueryRange("2", Range{
endpoint: "test", Start: testTime.Add(-time.Minute),
expected: "http://localhost:9090/test", 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"),
},
{ {
address: "http://localhost:9090/prefix", do: doLabelValues("mylabel"),
endpoint: "/test", inRes: []string{"val1", "val2"},
expected: "http://localhost:9090/prefix/test", reqMethod: "GET",
reqPath: "/api/v1/label/mylabel/values",
res: model.LabelValues{"val1", "val2"},
}, },
{ {
address: "https://localhost:9090/", do: doLabelValues("mylabel"),
endpoint: "/test/", inErr: fmt.Errorf("some error"),
expected: "https://localhost:9090/test", reqMethod: "GET",
}, reqPath: "/api/v1/label/mylabel/values",
{ err: fmt.Errorf("some error"),
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",
}, },
} }
var tests []apiTest
tests = append(tests, queryTests...)
for _, test := range tests { 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 { if err != nil {
t.Fatal(err) t.Errorf("unexpected error: %s", 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 continue
} }
// The apiClient must return exactly the same result as the httpClient. if !reflect.DeepEqual(res, test.res) {
aclient := &apiClient{hclient} t.Errorf("unexpected result: want %v, got %v", test.res, res)
u = aclient.url(test.endpoint, test.args)
if u.String() != test.expected {
t.Errorf("unexpected result: got %s, want %s", u, test.expected)
} }
} }
} }
@ -141,11 +226,11 @@ type apiClientTest struct {
err *Error 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 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 { if ctx == nil {
c.Fatalf("context was not passed down") c.Fatalf("context was not passed down")
} }
@ -271,7 +356,7 @@ func TestAPIClientDo(t *testing.T) {
tc.ch <- test tc.ch <- test
_, body, err := client.do(context.Background(), tc.req) _, body, err := client.Do(context.Background(), tc.req)
if test.err != nil { if test.err != nil {
if err == nil { if err == nil {
@ -294,160 +379,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 {
u := &url.URL{
Host: "test:9090",
Path: apiPrefix + ep,
}
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)
}
}
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"),
},
}
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)
}
}
}