diff --git a/api/prometheus/v1/api.go b/api/prometheus/v1/api.go index a9b9ce4..3fc58d8 100644 --- a/api/prometheus/v1/api.go +++ b/api/prometheus/v1/api.go @@ -20,13 +20,16 @@ package v1 import ( "context" "encoding/json" + "errors" "fmt" "net/http" + "net/url" "strconv" "time" "github.com/prometheus/client_golang/api" "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/scrape" ) const ( @@ -34,13 +37,17 @@ const ( apiPrefix = "/api/v1" + epAlertManagers = apiPrefix + "/alertmanagers" epQuery = apiPrefix + "/query" epQueryRange = apiPrefix + "/query_range" epLabelValues = apiPrefix + "/label/:name/values" epSeries = apiPrefix + "/series" + epTargets = apiPrefix + "/targets" epSnapshot = apiPrefix + "/admin/tsdb/snapshot" epDeleteSeries = apiPrefix + "/admin/tsdb/delete_series" epCleanTombstones = apiPrefix + "/admin/tsdb/clean_tombstones" + epConfig = apiPrefix + "/status/config" + epFlags = apiPrefix + "/status/flags" ) // ErrorType models the different API error types. @@ -75,21 +82,79 @@ type Range struct { // API provides bindings for Prometheus's v1 API. type API interface { + // AlertManagers returns an overview of the current state of the Prometheus alert manager discovery. + AlertManagers(ctx context.Context) (AlertManagersResult, error) + // CleanTombstones removes the deleted data from disk and cleans up the existing tombstones. + CleanTombstones(ctx context.Context) error + // Config returns the current Prometheus configuration. + Config(ctx context.Context) (ConfigResult, 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 + // Flags returns the flag values that Prometheus was launched with. + Flags(ctx context.Context) (FlagsResult, error) + // LabelValues performs a query for the values of the given label. + LabelValues(ctx context.Context, label string) (model.LabelValues, error) // 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) // 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 + // Targets returns an overview of the current state of the Prometheus target discovery. + Targets(ctx context.Context) (TargetsResult, error) +} + +// AlertManagersResult contains the result from querying the alertmanagers endpoint. +type AlertManagersResult struct { + Active []AlertManager `json:"activeAlertManagers"` + Dropped []AlertManager `json:"droppedAlertManagers"` +} + +// AlertManager models a configured Alert Manager. +type AlertManager struct { + URL *url.URL `json:"url"` +} + +// ConfigResult contains the result from querying the config endpoint. +type ConfigResult struct { + YAML string `json:"yaml"` +} + +// FlagsResult contains the result from querying the flag endpoint. +type FlagsResult struct { + Flags Flags +} + +// Flags models a set of flags that Prometheus is configured with. +type Flags map[string]string + +// SnapshotResult contains the result from querying the snapshot endpoint. +type SnapshotResult struct { + Name string `json:"name"` +} + +// TargetsResult contains the result from querying the targets endpoint. +type TargetsResult struct { + Active []ActiveTarget `json:"activeTargets"` + Dropped []DroppedTarget `json:"droppedTargets"` +} + +// ActiveTarget models an active Prometheus scrape target. +type ActiveTarget struct { + DiscoveredLabels model.LabelSet `json:"discoveredLabels"` + Labels model.LabelSet `json:"labels"` + ScrapeURL *url.URL `json:"scrapeUrl"` + LastError error `json:"lastError"` + LastScrape time.Time `json:"lastScrape"` + Health scrape.TargetHealth `json:"health"` +} + +// DroppedTarget models a dropped Prometheus scrape target. +type DroppedTarget struct { + DiscoveredLabels model.LabelSet `json:"discoveredLabels"` } // queryResult contains result data for a query. @@ -101,9 +166,56 @@ type queryResult struct { v model.Value } -// SnapshotResult contains result data for a snapshot. -type SnapshotResult struct { - Name string +func (a *ActiveTarget) UnmarshalJSON(b []byte) error { + v := struct { + DiscoveredLabels model.LabelSet `json:"discoveredLabels"` + Labels model.LabelSet `json:"labels"` + ScrapeURL string `json:"scrapeUrl"` + LastError string `json:"lastError"` + LastScrape time.Time `json:"lastScrape"` + Health scrape.TargetHealth `json:"health"` + }{} + + err := json.Unmarshal(b, &v) + if err != nil { + return err + } + + url, err := url.Parse(v.ScrapeURL) + + a.DiscoveredLabels = v.DiscoveredLabels + a.Labels = v.Labels + a.ScrapeURL = url + a.LastScrape = v.LastScrape + a.Health = v.Health + if v.LastError != "" { + a.LastError = errors.New(v.LastError) + } else { + a.LastError = nil + } + + return err +} + +func (a *AlertManager) UnmarshalJSON(b []byte) error { + var v map[string]string + + err := json.Unmarshal(b, &v) + if err != nil { + return err + } + + url, err := url.Parse(v["url"]) + a.URL = url + return err +} + +func (f *FlagsResult) UnmarshalJSON(b []byte) error { + var v map[string]string + + err := json.Unmarshal(b, &v) + f.Flags = v + return err } func (qr *queryResult) UnmarshalJSON(b []byte) error { @@ -150,6 +262,109 @@ type httpAPI struct { client api.Client } +func (h *httpAPI) AlertManagers(ctx context.Context) (AlertManagersResult, error) { + u := h.client.URL(epAlertManagers, nil) + + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + if err != nil { + return AlertManagersResult{}, err + } + + _, body, err := h.client.Do(ctx, req) + if err != nil { + return AlertManagersResult{}, err + } + + var res AlertManagersResult + err = json.Unmarshal(body, &res) + return res, 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 +} + +func (h *httpAPI) Config(ctx context.Context) (ConfigResult, error) { + u := h.client.URL(epConfig, nil) + + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + if err != nil { + return ConfigResult{}, err + } + + _, body, err := h.client.Do(ctx, req) + if err != nil { + return ConfigResult{}, err + } + + var res ConfigResult + 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) Flags(ctx context.Context) (FlagsResult, error) { + u := h.client.URL(epFlags, nil) + + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + if err != nil { + return FlagsResult{}, err + } + + _, body, err := h.client.Do(ctx, req) + if err != nil { + return FlagsResult{}, err + } + + var res FlagsResult + err = json.Unmarshal(body, &res) + return res, 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 +} + func (h *httpAPI) Query(ctx context.Context, query string, ts time.Time) (model.Value, error) { u := h.client.URL(epQuery, nil) q := u.Query() @@ -210,21 +425,6 @@ func (h *httpAPI) QueryRange(ctx context.Context, query string, r Range) (model. 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 -} - func (h *httpAPI) Series(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) ([]model.LabelSet, error) { u := h.client.URL(epSeries, nil) q := u.Query() @@ -276,38 +476,22 @@ func (h *httpAPI) Snapshot(ctx context.Context, skipHead bool) (SnapshotResult, 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() +func (h *httpAPI) Targets(ctx context.Context) (TargetsResult, error) { + u := h.client.URL(epTargets, nil) - 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) + req, err := http.NewRequest(http.MethodGet, u.String(), nil) if err != nil { - return err + return TargetsResult{}, 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) + _, body, err := h.client.Do(ctx, req) if err != nil { - return err + return TargetsResult{}, err } - _, _, err = h.client.Do(ctx, req) - return err + var res TargetsResult + err = json.Unmarshal(body, &res) + return res, err } // apiClient wraps a regular client and processes successful API responses. diff --git a/api/prometheus/v1/api_test.go b/api/prometheus/v1/api_test.go index 5c1402b..7a42e17 100644 --- a/api/prometheus/v1/api_test.go +++ b/api/prometheus/v1/api_test.go @@ -18,6 +18,7 @@ package v1 import ( "context" "encoding/json" + "errors" "fmt" "net/http" "net/url" @@ -27,6 +28,7 @@ import ( "time" "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/scrape" ) type apiTest struct { @@ -88,12 +90,53 @@ func TestAPIs(t *testing.T) { testTime := time.Now() + testURL, err := url.Parse("http://127.0.0.1:9090/api/v1/alerts") + if err != nil { + t.Errorf("Failed to initialize test URL.") + } + client := &apiTestClient{T: t} promAPI := &httpAPI{ client: client, } + doAlertManagers := func() func() (interface{}, error) { + return func() (interface{}, error) { + return promAPI.AlertManagers(context.Background()) + } + } + + doCleanTombstones := func() func() (interface{}, error) { + return func() (interface{}, error) { + return nil, promAPI.CleanTombstones(context.Background()) + } + } + + doConfig := func() func() (interface{}, error) { + return func() (interface{}, error) { + return promAPI.Config(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) + } + } + + doFlags := func() func() (interface{}, error) { + return func() (interface{}, error) { + return promAPI.Flags(context.Background()) + } + } + + doLabelValues := func(label string) func() (interface{}, error) { + return func() (interface{}, error) { + return promAPI.LabelValues(context.Background(), label) + } + } + doQuery := func(q string, ts time.Time) func() (interface{}, error) { return func() (interface{}, error) { return promAPI.Query(context.Background(), q, ts) @@ -106,12 +149,6 @@ func TestAPIs(t *testing.T) { } } - doLabelValues := func(label string) func() (interface{}, error) { - return func() (interface{}, error) { - return promAPI.LabelValues(context.Background(), label) - } - } - doSeries := func(matcher string, startTime time.Time, endTime time.Time) func() (interface{}, error) { return func() (interface{}, error) { return promAPI.Series(context.Background(), []string{matcher}, startTime, endTime) @@ -124,15 +161,9 @@ func TestAPIs(t *testing.T) { } } - doCleanTombstones := func() func() (interface{}, error) { + doTargets := 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) + return promAPI.Targets(context.Background()) } } @@ -309,6 +340,168 @@ func TestAPIs(t *testing.T) { }, err: fmt.Errorf("some error"), }, + + { + do: doConfig(), + reqMethod: "GET", + reqPath: "/api/v1/status/config", + inRes: map[string]string{ + "yaml": "", + }, + res: ConfigResult{ + YAML: "", + }, + }, + + { + do: doConfig(), + reqMethod: "GET", + reqPath: "/api/v1/status/config", + inErr: fmt.Errorf("some error"), + err: fmt.Errorf("some error"), + }, + + { + do: doFlags(), + reqMethod: "GET", + reqPath: "/api/v1/status/flags", + inRes: map[string]string{ + "alertmanager.notification-queue-capacity": "10000", + "alertmanager.timeout": "10s", + "log.level": "info", + "query.lookback-delta": "5m", + "query.max-concurrency": "20", + }, + res: FlagsResult{ + Flags{ + "alertmanager.notification-queue-capacity": "10000", + "alertmanager.timeout": "10s", + "log.level": "info", + "query.lookback-delta": "5m", + "query.max-concurrency": "20", + }, + }, + }, + + { + do: doFlags(), + reqMethod: "GET", + reqPath: "/api/v1/status/flags", + inErr: fmt.Errorf("some error"), + err: fmt.Errorf("some error"), + }, + + { + do: doAlertManagers(), + reqMethod: "GET", + reqPath: "/api/v1/alertmanagers", + inRes: map[string]interface{}{ + "activeAlertManagers": []map[string]string{ + { + "url": testURL.String(), + }, + }, + "droppedAlertManagers": []map[string]string{ + { + "url": testURL.String(), + }, + }, + }, + res: AlertManagersResult{ + Active: []AlertManager{ + { + URL: testURL, + }, + }, + Dropped: []AlertManager{ + { + URL: testURL, + }, + }, + }, + }, + + { + do: doAlertManagers(), + reqMethod: "GET", + reqPath: "/api/v1/alertmanagers", + inErr: fmt.Errorf("some error"), + err: fmt.Errorf("some error"), + }, + + { + do: doTargets(), + reqMethod: "GET", + reqPath: "/api/v1/targets", + inRes: map[string]interface{}{ + "activeTargets": []map[string]interface{}{ + { + "discoveredLabels": map[string]string{ + "__address__": "127.0.0.1:9090", + "__metrics_path__": "/metrics", + "__scheme__": "http", + "job": "prometheus", + }, + "labels": map[string]string{ + "instance": "127.0.0.1:9090", + "job": "prometheus", + }, + "scrapeUrl": testURL.String(), + "lastError": "error while scraping target", + "lastScrape": testTime.Format(time.RFC3339Nano), + "health": "up", + }, + }, + "droppedTargets": []map[string]interface{}{ + { + "discoveredLabels": map[string]string{ + "__address__": "127.0.0.1:9100", + "__metrics_path__": "/metrics", + "__scheme__": "http", + "job": "node", + }, + }, + }, + }, + res: TargetsResult{ + Active: []ActiveTarget{ + { + DiscoveredLabels: model.LabelSet{ + "__address__": "127.0.0.1:9090", + "__metrics_path__": "/metrics", + "__scheme__": "http", + "job": "prometheus", + }, + Labels: model.LabelSet{ + "instance": "127.0.0.1:9090", + "job": "prometheus", + }, + ScrapeURL: testURL, + LastError: errors.New("error while scraping target"), + LastScrape: testTime.Round(0), + Health: scrape.HealthGood, + }, + }, + Dropped: []DroppedTarget{ + { + DiscoveredLabels: model.LabelSet{ + "__address__": "127.0.0.1:9100", + "__metrics_path__": "/metrics", + "__scheme__": "http", + "job": "node", + }, + }, + }, + }, + }, + + { + do: doTargets(), + reqMethod: "GET", + reqPath: "/api/v1/targets", + inErr: fmt.Errorf("some error"), + err: fmt.Errorf("some error"), + }, } var tests []apiTest