From 10dfec77bf569feea09d2baf665ed0f015d41bb3 Mon Sep 17 00:00:00 2001 From: Bob Shannon Date: Thu, 5 Apr 2018 19:47:48 -0400 Subject: [PATCH] Implement admin methods for Prometheus API Signed-off-by: Bob Shannon --- api/prometheus/v1/api.go | 80 +++++++++++++++++++++++++++-- api/prometheus/v1/api_test.go | 95 +++++++++++++++++++++++++++++++++-- 2 files changed, 166 insertions(+), 9 deletions(-) diff --git a/api/prometheus/v1/api.go b/api/prometheus/v1/api.go index d9e1e8a..a9b9ce4 100644 --- a/api/prometheus/v1/api.go +++ b/api/prometheus/v1/api.go @@ -34,10 +34,13 @@ const ( apiPrefix = "/api/v1" - epQuery = apiPrefix + "/query" - epQueryRange = apiPrefix + "/query_range" - epLabelValues = apiPrefix + "/label/:name/values" - epSeries = apiPrefix + "/series" + epQuery = apiPrefix + "/query" + epQueryRange = apiPrefix + "/query_range" + epLabelValues = apiPrefix + "/label/:name/values" + epSeries = apiPrefix + "/series" + epSnapshot = apiPrefix + "/admin/tsdb/snapshot" + epDeleteSeries = apiPrefix + "/admin/tsdb/delete_series" + epCleanTombstones = apiPrefix + "/admin/tsdb/clean_tombstones" ) // ErrorType models the different API error types. @@ -80,6 +83,13 @@ type API interface { LabelValues(ctx context.Context, label string) (model.LabelValues, error) // Series finds series by label matchers. Series(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) ([]model.LabelSet, error) + // Snapshot creates a snapshot of all current data into snapshots/- + // under the TSDB's data directory and returns the directory as response. + Snapshot(ctx context.Context, skipHead bool) (SnapshotResult, error) + // DeleteSeries deletes data for a selection of series in a time range. + DeleteSeries(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) error + // CleanTombstones removes the deleted data from disk and cleans up the existing tombstones. + CleanTombstones(ctx context.Context) error } // queryResult contains result data for a query. @@ -91,6 +101,11 @@ type queryResult struct { v model.Value } +// SnapshotResult contains result data for a snapshot. +type SnapshotResult struct { + Name string +} + func (qr *queryResult) UnmarshalJSON(b []byte) error { v := struct { Type model.ValueType `json:"resultType"` @@ -238,6 +253,63 @@ func (h *httpAPI) Series(ctx context.Context, matches []string, startTime time.T return mset, err } +func (h *httpAPI) Snapshot(ctx context.Context, skipHead bool) (SnapshotResult, error) { + u := h.client.URL(epSnapshot, nil) + q := u.Query() + + q.Set("skip_head", strconv.FormatBool(skipHead)) + + u.RawQuery = q.Encode() + + req, err := http.NewRequest(http.MethodPost, u.String(), nil) + if err != nil { + return SnapshotResult{}, err + } + + _, body, err := h.client.Do(ctx, req) + if err != nil { + return SnapshotResult{}, err + } + + var res SnapshotResult + err = json.Unmarshal(body, &res) + return res, err +} + +func (h *httpAPI) DeleteSeries(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) error { + u := h.client.URL(epDeleteSeries, nil) + q := u.Query() + + for _, m := range matches { + q.Add("match[]", m) + } + + q.Set("start", startTime.Format(time.RFC3339Nano)) + q.Set("end", endTime.Format(time.RFC3339Nano)) + + u.RawQuery = q.Encode() + + req, err := http.NewRequest(http.MethodPost, u.String(), nil) + if err != nil { + return err + } + + _, _, err = h.client.Do(ctx, req) + return err +} + +func (h *httpAPI) CleanTombstones(ctx context.Context) error { + u := h.client.URL(epCleanTombstones, nil) + + req, err := http.NewRequest(http.MethodPost, u.String(), nil) + if err != nil { + return err + } + + _, _, err = h.client.Do(ctx, req) + return err +} + // apiClient wraps a regular client and processes successful API responses. // Successful also includes responses that errored at the API level. type apiClient struct { diff --git a/api/prometheus/v1/api_test.go b/api/prometheus/v1/api_test.go index e557d68..5c1402b 100644 --- a/api/prometheus/v1/api_test.go +++ b/api/prometheus/v1/api_test.go @@ -90,31 +90,49 @@ func TestAPIs(t *testing.T) { client := &apiTestClient{T: t} - queryAPI := &httpAPI{ + promAPI := &httpAPI{ client: client, } doQuery := func(q string, ts time.Time) func() (interface{}, error) { return func() (interface{}, error) { - return queryAPI.Query(context.Background(), q, ts) + return promAPI.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) + return promAPI.QueryRange(context.Background(), q, rng) } } doLabelValues := func(label string) func() (interface{}, error) { return func() (interface{}, error) { - return queryAPI.LabelValues(context.Background(), label) + return promAPI.LabelValues(context.Background(), label) } } doSeries := func(matcher string, startTime time.Time, endTime time.Time) func() (interface{}, error) { return func() (interface{}, error) { - return queryAPI.Series(context.Background(), []string{matcher}, startTime, endTime) + return promAPI.Series(context.Background(), []string{matcher}, startTime, endTime) + } + } + + doSnapshot := func(skipHead bool) func() (interface{}, error) { + return func() (interface{}, error) { + return promAPI.Snapshot(context.Background(), skipHead) + } + } + + doCleanTombstones := func() func() (interface{}, error) { + return func() (interface{}, error) { + return nil, promAPI.CleanTombstones(context.Background()) + } + } + + doDeleteSeries := func(matcher string, startTime time.Time, endTime time.Time) func() (interface{}, error) { + return func() (interface{}, error) { + return nil, promAPI.DeleteSeries(context.Background(), []string{matcher}, startTime, endTime) } } @@ -224,6 +242,73 @@ func TestAPIs(t *testing.T) { }, err: fmt.Errorf("some error"), }, + + { + do: doSnapshot(true), + inRes: map[string]string{ + "name": "20171210T211224Z-2be650b6d019eb54", + }, + reqMethod: "POST", + reqPath: "/api/v1/admin/tsdb/snapshot", + reqParam: url.Values{ + "skip_head": []string{"true"}, + }, + res: SnapshotResult{ + Name: "20171210T211224Z-2be650b6d019eb54", + }, + }, + + { + do: doSnapshot(true), + inErr: fmt.Errorf("some error"), + reqMethod: "POST", + reqPath: "/api/v1/admin/tsdb/snapshot", + err: fmt.Errorf("some error"), + }, + + { + do: doCleanTombstones(), + reqMethod: "POST", + reqPath: "/api/v1/admin/tsdb/clean_tombstones", + }, + + { + do: doCleanTombstones(), + inErr: fmt.Errorf("some error"), + reqMethod: "POST", + reqPath: "/api/v1/admin/tsdb/clean_tombstones", + err: fmt.Errorf("some error"), + }, + + { + do: doDeleteSeries("up", testTime.Add(-time.Minute), testTime), + inRes: []map[string]string{ + { + "__name__": "up", + "job": "prometheus", + "instance": "localhost:9090"}, + }, + reqMethod: "POST", + reqPath: "/api/v1/admin/tsdb/delete_series", + reqParam: url.Values{ + "match": []string{"up"}, + "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, + "end": []string{testTime.Format(time.RFC3339Nano)}, + }, + }, + + { + do: doDeleteSeries("up", testTime.Add(-time.Minute), testTime), + inErr: fmt.Errorf("some error"), + reqMethod: "POST", + reqPath: "/api/v1/admin/tsdb/delete_series", + reqParam: url.Values{ + "match": []string{"up"}, + "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, + "end": []string{testTime.Format(time.RFC3339Nano)}, + }, + err: fmt.Errorf("some error"), + }, } var tests []apiTest