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/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() -} diff --git a/web/api/api.go b/web/api/api.go index 44d599d1d..0ab6576c9 100644 --- a/web/api/api.go +++ b/web/api/api.go @@ -18,12 +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/web/httputils" ) // MetricsService manages the /api HTTP endpoint. type MetricsService struct { + Now func() clientmodel.Timestamp Storage local.Storage } diff --git a/web/api/api_test.go b/web/api/api_test.go new file mode 100644 index 000000000..d76f9f7f1 --- /dev/null +++ b/web/api/api_test.go @@ -0,0 +1,134 @@ +// 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" +) + +// 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) + +func testNow() clientmodel.Timestamp { + return testTimestamp +} + +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{ + Now: testNow, + 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..c4e8f4c81 100644 --- a/web/api/query.go +++ b/web/api/query.go @@ -46,9 +46,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, now clientmodel.Timestamp) (clientmodel.Timestamp, error) { if t == "" { - return clientmodel.Now(), nil + return now, nil } tFloat, err := strconv.ParseFloat(t, 64) @@ -74,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")) + timestamp, err := parseTimestampOrNow(params.Get("timestamp"), serv.Now()) if err != nil { httpJSONError(w, fmt.Errorf("invalid query timestamp %s", err), http.StatusBadRequest) return @@ -112,7 +112,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.Now()) if err != nil { httpJSONError(w, fmt.Errorf("invalid query timestamp: %s", err), http.StatusBadRequest) return @@ -122,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.Now() + end = serv.Now() } exprNode, err := rules.LoadExprFromString(expr) diff --git a/web/api/query_test.go b/web/api/query_test.go index b311180df..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("") + ts, err := parseTimestampOrNow("", testNow()) 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) + if !testNow().Equal(ts) { + t.Fatalf("ts = %v; want ts = %v", ts, testNow) } - ts, err = parseTimestampOrNow("1426956073.12345") + 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") + _, err = parseTimestampOrNow("123.45foo", testNow()) if err == nil { t.Fatalf("err = nil; want %s", err) }