From a5a553f1dac028065e27f4de4e435f0d925c4143 Mon Sep 17 00:00:00 2001 From: Julius Volz Date: Fri, 27 Mar 2015 21:27:27 +0100 Subject: [PATCH 1/3] Add initial HTTP API tests. This covers the /query (instant query) endpoint for now. Others to follow. --- web/api/api.go | 2 + web/api/api_test.go | 144 ++++++++++++++++++++++++++++++++++++++++++ web/api/query.go | 11 ++-- web/api/query_test.go | 12 ++-- 4 files changed, 158 insertions(+), 11 deletions(-) create mode 100644 web/api/api_test.go diff --git a/web/api/api.go b/web/api/api.go index 44d599d1d..32c3c375a 100644 --- a/web/api/api.go +++ b/web/api/api.go @@ -19,11 +19,13 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/prometheus/storage/local" + "github.com/prometheus/prometheus/utility" "github.com/prometheus/prometheus/web/httputils" ) // MetricsService manages the /api HTTP endpoint. type MetricsService struct { + nower utility.Time Storage local.Storage } diff --git a/web/api/api_test.go b/web/api/api_test.go new file mode 100644 index 000000000..bfcf598cf --- /dev/null +++ b/web/api/api_test.go @@ -0,0 +1,144 @@ +// 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 ( + "io/ioutil" + "net/http" + "net/http/httptest" + "regexp" + "testing" + "time" + + clientmodel "github.com/prometheus/client_golang/model" + + "github.com/prometheus/prometheus/storage/local" + "github.com/prometheus/prometheus/utility" +) + +type testInstantProvider struct { + now time.Time +} + +func (p testInstantProvider) Now() time.Time { + return p.now +} + +// This is a bit annoying. On one hand, we have to choose a current timestamp +// because the storage doesn't have a mocked-out time yet and would otherwise +// immediately throw away "old" samples. On the other hand, we have to make +// sure that the float value survives the parsing and re-formatting in the +// query layer precisely without any change. Thus we round to seconds and then +// add known-good digits after the decimal point which behave well in +// parsing/re-formatting. +var testTimestamp = clientmodel.TimestampFromTime(time.Now().Round(time.Second)).Add(124 * time.Millisecond) + +var testNower = utility.Time{ + Provider: testInstantProvider{ + now: testTimestamp.Time(), + }, +} + +func TestQuery(t *testing.T) { + scenarios := []struct { + // URL query string. + queryStr string + // Expected HTTP response status code. + status int + // Regex to match against response body. + bodyRe string + }{ + { + queryStr: "", + status: http.StatusOK, + bodyRe: "syntax error", + }, + { + queryStr: "expr=testmetric", + status: http.StatusOK, + bodyRe: `{"type":"vector","value":\[\{"metric":{"__name__":"testmetric"},"value":"0","timestamp":\d+\.\d+}\],"version":1\}`, + }, + { + queryStr: "expr=testmetric×tamp=" + testTimestamp.String(), + status: http.StatusOK, + bodyRe: `{"type":"vector","value":\[\{"metric":{"__name__":"testmetric"},"value":"0","timestamp":` + testTimestamp.String() + `}\],"version":1\}`, + }, + { + queryStr: "expr=testmetric×tamp=" + testTimestamp.Add(-time.Hour).String(), + status: http.StatusOK, + bodyRe: `{"type":"vector","value":\[\],"version":1\}`, + }, + { + queryStr: "timestamp=invalid", + status: http.StatusBadRequest, + bodyRe: "invalid query timestamp", + }, + { + queryStr: "expr=(badexpression", + status: http.StatusOK, + bodyRe: "syntax error", + }, + } + + storage, closer := local.NewTestStorage(t, 1) + defer closer.Close() + storage.Append(&clientmodel.Sample{ + Metric: clientmodel.Metric{ + clientmodel.MetricNameLabel: "testmetric", + }, + Timestamp: testTimestamp, + Value: 0, + }) + storage.WaitForIndexing() + + api := MetricsService{ + Storage: storage, + } + api.RegisterHandler() + + server := httptest.NewServer(http.DefaultServeMux) + defer server.Close() + + for i, s := range scenarios { + // Do query. + resp, err := http.Get(server.URL + "/api/query?" + s.queryStr) + if err != nil { + t.Fatalf("%d. Error querying API: %s", i, err) + } + + // Check status code. + if resp.StatusCode != s.status { + t.Fatalf("%d. Unexpected status code; got %d, want %d", i, resp.StatusCode, s.status) + } + + // Check response headers. + ct := resp.Header["Content-Type"] + if len(ct) != 1 { + t.Fatalf("%d. Unexpected number of 'Content-Type' headers; got %d, want 1", i, len(ct)) + } + if ct[0] != "application/json" { + t.Fatalf("%d. Unexpected 'Content-Type' header; got %s; want %s", i, ct[0], "application/json") + } + + // Check body. + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf("%d. Error reading response body: %s", i, err) + } + re := regexp.MustCompile(s.bodyRe) + if !re.Match(b) { + t.Fatalf("%d. Body didn't match '%s'. Body: %s", i, s.bodyRe, string(b)) + } + } +} diff --git a/web/api/query.go b/web/api/query.go index ae539dee8..8cae6884c 100644 --- a/web/api/query.go +++ b/web/api/query.go @@ -29,6 +29,7 @@ import ( "github.com/prometheus/prometheus/rules" "github.com/prometheus/prometheus/rules/ast" "github.com/prometheus/prometheus/stats" + "github.com/prometheus/prometheus/utility" "github.com/prometheus/prometheus/web/httputils" ) @@ -46,9 +47,9 @@ func httpJSONError(w http.ResponseWriter, err error, code int) { fmt.Fprintln(w, ast.ErrorToJSON(err)) } -func parseTimestampOrNow(t string) (clientmodel.Timestamp, error) { +func parseTimestampOrNow(t string, nower utility.Time) (clientmodel.Timestamp, error) { if t == "" { - return clientmodel.Now(), nil + return clientmodel.TimestampFromTime(nower.Now()), nil } tFloat, err := strconv.ParseFloat(t, 64) @@ -74,7 +75,7 @@ func (serv MetricsService) Query(w http.ResponseWriter, r *http.Request) { params := httputils.GetQueryParams(r) expr := params.Get("expr") - timestamp, err := parseTimestampOrNow(params.Get("timestamp")) + timestamp, err := parseTimestampOrNow(params.Get("timestamp"), serv.nower) if err != nil { httpJSONError(w, fmt.Errorf("invalid query timestamp %s", err), http.StatusBadRequest) return @@ -112,7 +113,7 @@ func (serv MetricsService) QueryRange(w http.ResponseWriter, r *http.Request) { return } - end, err := parseTimestampOrNow(params.Get("end")) + end, err := parseTimestampOrNow(params.Get("end"), serv.nower) if err != nil { httpJSONError(w, fmt.Errorf("invalid query timestamp: %s", err), http.StatusBadRequest) return @@ -122,7 +123,7 @@ func (serv MetricsService) QueryRange(w http.ResponseWriter, r *http.Request) { // the current time as the end time. Instead, the "end" parameter should // simply be omitted or set to an empty string for that case. if end == 0 { - end = clientmodel.Now() + end = clientmodel.TimestampFromTime(serv.nower.Now()) } exprNode, err := rules.LoadExprFromString(expr) diff --git a/web/api/query_test.go b/web/api/query_test.go index b311180df..ce691eda0 100644 --- a/web/api/query_test.go +++ b/web/api/query_test.go @@ -21,16 +21,16 @@ import ( ) func TestParseTimestampOrNow(t *testing.T) { - ts, err := parseTimestampOrNow("") + ts, err := parseTimestampOrNow("", testNower) if err != nil { t.Fatalf("err = %s; want nil", err) } - now := clientmodel.Now() - if now.Sub(ts) > time.Second || now.Sub(ts) < 0 { - t.Fatalf("ts = %v; want %v <= ts <= %v", ts, now.Sub(ts), now) + now := clientmodel.TimestampFromTime(testNower.Now()) + if !now.Equal(ts) { + t.Fatalf("ts = %v; want ts = %v", ts, now) } - ts, err = parseTimestampOrNow("1426956073.12345") + ts, err = parseTimestampOrNow("1426956073.12345", testNower) if err != nil { t.Fatalf("err = %s; want nil", err) } @@ -39,7 +39,7 @@ func TestParseTimestampOrNow(t *testing.T) { t.Fatalf("ts = %v; want %v", ts, expTS) } - _, err = parseTimestampOrNow("123.45foo") + _, err = parseTimestampOrNow("123.45foo", testNower) if err == nil { t.Fatalf("err = nil; want %s", err) } From 33702da8a8f28a4738e92c5ae97b1f749ac8def2 Mon Sep 17 00:00:00 2001 From: Julius Volz Date: Fri, 27 Mar 2015 23:43:47 +0100 Subject: [PATCH 2/3] Use simple Now() func in API instead of utility.Time. --- main.go | 2 ++ web/api/api.go | 5 +++-- web/api/api_test.go | 16 +++------------- web/api/query.go | 11 +++++------ web/api/query_test.go | 11 +++++------ 5 files changed, 18 insertions(+), 27 deletions(-) diff --git a/main.go b/main.go index cbc756d39..19e355744 100644 --- a/main.go +++ b/main.go @@ -24,6 +24,7 @@ import ( "github.com/golang/glog" + clientmodel "github.com/prometheus/client_golang/model" registry "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/prometheus/config" @@ -167,6 +168,7 @@ func NewPrometheus() *prometheus { } metricsService := &api.MetricsService{ + Now: clientmodel.Now, Storage: memStorage, } diff --git a/web/api/api.go b/web/api/api.go index 32c3c375a..0ab6576c9 100644 --- a/web/api/api.go +++ b/web/api/api.go @@ -18,14 +18,15 @@ import ( "github.com/prometheus/client_golang/prometheus" + clientmodel "github.com/prometheus/client_golang/model" + "github.com/prometheus/prometheus/storage/local" - "github.com/prometheus/prometheus/utility" "github.com/prometheus/prometheus/web/httputils" ) // MetricsService manages the /api HTTP endpoint. type MetricsService struct { - nower utility.Time + Now func() clientmodel.Timestamp Storage local.Storage } diff --git a/web/api/api_test.go b/web/api/api_test.go index bfcf598cf..d76f9f7f1 100644 --- a/web/api/api_test.go +++ b/web/api/api_test.go @@ -24,17 +24,8 @@ import ( clientmodel "github.com/prometheus/client_golang/model" "github.com/prometheus/prometheus/storage/local" - "github.com/prometheus/prometheus/utility" ) -type testInstantProvider struct { - now time.Time -} - -func (p testInstantProvider) Now() time.Time { - return p.now -} - // This is a bit annoying. On one hand, we have to choose a current timestamp // because the storage doesn't have a mocked-out time yet and would otherwise // immediately throw away "old" samples. On the other hand, we have to make @@ -44,10 +35,8 @@ func (p testInstantProvider) Now() time.Time { // parsing/re-formatting. var testTimestamp = clientmodel.TimestampFromTime(time.Now().Round(time.Second)).Add(124 * time.Millisecond) -var testNower = utility.Time{ - Provider: testInstantProvider{ - now: testTimestamp.Time(), - }, +func testNow() clientmodel.Timestamp { + return testTimestamp } func TestQuery(t *testing.T) { @@ -103,6 +92,7 @@ func TestQuery(t *testing.T) { storage.WaitForIndexing() api := MetricsService{ + Now: testNow, Storage: storage, } api.RegisterHandler() diff --git a/web/api/query.go b/web/api/query.go index 8cae6884c..c4e8f4c81 100644 --- a/web/api/query.go +++ b/web/api/query.go @@ -29,7 +29,6 @@ import ( "github.com/prometheus/prometheus/rules" "github.com/prometheus/prometheus/rules/ast" "github.com/prometheus/prometheus/stats" - "github.com/prometheus/prometheus/utility" "github.com/prometheus/prometheus/web/httputils" ) @@ -47,9 +46,9 @@ func httpJSONError(w http.ResponseWriter, err error, code int) { fmt.Fprintln(w, ast.ErrorToJSON(err)) } -func parseTimestampOrNow(t string, nower utility.Time) (clientmodel.Timestamp, error) { +func parseTimestampOrNow(t string, now clientmodel.Timestamp) (clientmodel.Timestamp, error) { if t == "" { - return clientmodel.TimestampFromTime(nower.Now()), nil + return now, nil } tFloat, err := strconv.ParseFloat(t, 64) @@ -75,7 +74,7 @@ func (serv MetricsService) Query(w http.ResponseWriter, r *http.Request) { params := httputils.GetQueryParams(r) expr := params.Get("expr") - timestamp, err := parseTimestampOrNow(params.Get("timestamp"), serv.nower) + timestamp, err := parseTimestampOrNow(params.Get("timestamp"), serv.Now()) if err != nil { httpJSONError(w, fmt.Errorf("invalid query timestamp %s", err), http.StatusBadRequest) return @@ -113,7 +112,7 @@ func (serv MetricsService) QueryRange(w http.ResponseWriter, r *http.Request) { return } - end, err := parseTimestampOrNow(params.Get("end"), serv.nower) + end, err := parseTimestampOrNow(params.Get("end"), serv.Now()) if err != nil { httpJSONError(w, fmt.Errorf("invalid query timestamp: %s", err), http.StatusBadRequest) return @@ -123,7 +122,7 @@ func (serv MetricsService) QueryRange(w http.ResponseWriter, r *http.Request) { // the current time as the end time. Instead, the "end" parameter should // simply be omitted or set to an empty string for that case. if end == 0 { - end = clientmodel.TimestampFromTime(serv.nower.Now()) + end = serv.Now() } exprNode, err := rules.LoadExprFromString(expr) diff --git a/web/api/query_test.go b/web/api/query_test.go index ce691eda0..8ee334e58 100644 --- a/web/api/query_test.go +++ b/web/api/query_test.go @@ -21,16 +21,15 @@ import ( ) func TestParseTimestampOrNow(t *testing.T) { - ts, err := parseTimestampOrNow("", testNower) + ts, err := parseTimestampOrNow("", testNow()) if err != nil { t.Fatalf("err = %s; want nil", err) } - now := clientmodel.TimestampFromTime(testNower.Now()) - if !now.Equal(ts) { - t.Fatalf("ts = %v; want ts = %v", ts, now) + if !testNow().Equal(ts) { + t.Fatalf("ts = %v; want ts = %v", ts, testNow) } - ts, err = parseTimestampOrNow("1426956073.12345", testNower) + ts, err = parseTimestampOrNow("1426956073.12345", testNow()) if err != nil { t.Fatalf("err = %s; want nil", err) } @@ -39,7 +38,7 @@ func TestParseTimestampOrNow(t *testing.T) { t.Fatalf("ts = %v; want %v", ts, expTS) } - _, err = parseTimestampOrNow("123.45foo", testNower) + _, err = parseTimestampOrNow("123.45foo", testNow()) if err == nil { t.Fatalf("err = nil; want %s", err) } From 188aec0e6d94e7d6c407cf00ec48da14b2bcfe0b Mon Sep 17 00:00:00 2001 From: Julius Volz Date: Fri, 27 Mar 2015 23:45:11 +0100 Subject: [PATCH 3/3] Remove now-unused utility.Time type. --- utility/test/time.go | 42 ----------------------------------------- utility/time.go | 45 -------------------------------------------- 2 files changed, 87 deletions(-) delete mode 100644 utility/test/time.go delete mode 100644 utility/time.go diff --git a/utility/test/time.go b/utility/test/time.go deleted file mode 100644 index 54aaeb28f..000000000 --- a/utility/test/time.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2013 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 test - -import ( - "github.com/prometheus/prometheus/utility" - "time" -) - -type instantProvider struct { - index int - timeQueue []time.Time -} - -func (t *instantProvider) Now() (time time.Time) { - time = t.timeQueue[t.index] - - t.index++ - - return -} - -// NewInstantProvider furnishes an InstantProvider with prerecorded responses -// for calls made against it. It has no validation behaviors of its own and -// will panic if times are requested more than available pre-recorded behaviors. -func NewInstantProvider(times []time.Time) utility.InstantProvider { - return &instantProvider{ - index: 0, - timeQueue: times, - } -} diff --git a/utility/time.go b/utility/time.go deleted file mode 100644 index 3fbe54fd6..000000000 --- a/utility/time.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2013 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 utility - -import ( - "time" -) - -// InstantProvider is a basic interface only useful in testing contexts for -// dispensing the time in a controlled manner. -type InstantProvider interface { - // The current instant. - Now() time.Time -} - -// Time is a simple means for fluently wrapping around standard Go timekeeping -// mechanisms to enhance testability without compromising code readability. -// -// It is sufficient for use on bare initialization. A provider should be -// set only for test contexts. When not provided, it emits the current -// system time. -type Time struct { - // The underlying means through which time is provided, if supplied. - Provider InstantProvider -} - -// Now emits the current instant. -func (t Time) Now() time.Time { - if t.Provider == nil { - return time.Now() - } - - return t.Provider.Now() -}