Skip to content

Commit

Permalink
Implement /api/v1/metadata (#2549)
Browse files Browse the repository at this point in the history
* Handle /api/v1/metadata in the queriers

This commit handles the call to /api/v1/metadata from Prometheus. It looks for all metric metadata of a given metadata and exposes it as a map of slices.

Signed-off-by: gotjosh <josue@grafana.com>

* Add changelog entry

Signed-off-by: gotjosh <josue@grafana.com>

* Wordsmithing on comments

Signed-off-by: gotjosh <josue@grafana.com>

* Update CHANGELOG.md

Signed-off-by: gotjosh <josue@grafana.com>

* Add a note about this custom handler being temporary

Signed-off-by: gotjosh <josue@grafana.com>
  • Loading branch information
gotjosh committed Apr 30, 2020
1 parent 8a797c9 commit 3462eb6
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 8 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
* [FEATURE] Ruler: The `-ruler.evaluation-delay` flag was added to allow users to configure a default evaluation delay for all rules in cortex. The default value is 0 which is the current behavior. #2423
* [FEATURE] Experimental: Added a new object storage client for OpenStack Swift. #2440
* [FEATURE] Update in dependency `weaveworks/common`. TLS config options added to the Server. #2535
* [FEATURE] Experimental: Added support for `/api/v1/metadata` Prometheus-based endpoint. #2549
* [ENHANCEMENT] Experimental TSDB: sample ingestion errors are now reported via existing `cortex_discarded_samples_total` metric. #2370
* [ENHANCEMENT] Failures on samples at distributors and ingesters return the first validation error as opposed to the last. #2383
* [ENHANCEMENT] Experimental TSDB: Added `cortex_querier_blocks_meta_synced`, which reflects current state of synced blocks over all tenants. #2392
Expand Down
8 changes: 6 additions & 2 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,9 @@ func (a *API) RegisterQuerier(queryable storage.Queryable, engine *promql.Engine
a.registerRouteWithRouter(router, a.cfg.PrometheusHTTPPrefix+"/api/v1/labels", promHandler, true, "GET", "POST")
a.registerRouteWithRouter(router, a.cfg.PrometheusHTTPPrefix+"/api/v1/label/{name}/values", promHandler, true, "GET")
a.registerRouteWithRouter(router, a.cfg.PrometheusHTTPPrefix+"/api/v1/series", promHandler, true, "GET", "POST", "DELETE")
a.registerRouteWithRouter(router, a.cfg.PrometheusHTTPPrefix+"/api/v1/metadata", promHandler, true, "GET")
//TODO(gotjosh): This custom handler is temporary until we're able to vendor the changes in:
// https://github.com/prometheus/prometheus/pull/7125/files
a.registerRouteWithRouter(router, a.cfg.PrometheusHTTPPrefix+"/api/v1/metadata", querier.MetadataHandler(distributor), true, "GET")

legacyPromRouter := route.New().WithPrefix(a.cfg.ServerPrefix + a.cfg.LegacyHTTPPrefix + "/api/v1")
api.Register(legacyPromRouter)
Expand All @@ -306,7 +308,9 @@ func (a *API) RegisterQuerier(queryable storage.Queryable, engine *promql.Engine
a.registerRouteWithRouter(router, a.cfg.LegacyHTTPPrefix+"/api/v1/labels", legacyPromHandler, true, "GET", "POST")
a.registerRouteWithRouter(router, a.cfg.LegacyHTTPPrefix+"/api/v1/label/{name}/values", legacyPromHandler, true, "GET")
a.registerRouteWithRouter(router, a.cfg.LegacyHTTPPrefix+"/api/v1/series", legacyPromHandler, true, "GET", "POST", "DELETE")
a.registerRouteWithRouter(router, a.cfg.LegacyHTTPPrefix+"/api/v1/metadata", legacyPromHandler, true, "GET")
//TODO(gotjosh): This custom handler is temporary until we're able to vendor the changes in:
// https://github.com/prometheus/prometheus/pull/7125/files
a.registerRouteWithRouter(router, a.cfg.LegacyHTTPPrefix+"/api/v1/metadata", querier.MetadataHandler(distributor), true, "GET")

// if we have externally registered routes then we need to return the server handler
// so that we continue to use all standard middleware
Expand Down
5 changes: 2 additions & 3 deletions pkg/distributor/distributor.go
Original file line number Diff line number Diff line change
Expand Up @@ -702,11 +702,10 @@ func (d *Distributor) MetricsForLabelMatchers(ctx context.Context, from, through
return result, nil
}

// MetricMetadata returns all metric metadata of a user.
// MetricsMetadata returns all metric metadata of a user.
func (d *Distributor) MetricsMetadata(ctx context.Context) ([]scrape.MetricMetadata, error) {
req := &ingester_client.MetricsMetadataRequest{}
// TODO: We only need to look in all the ingesters if we're shardByAllLabels is enabled.
// Look into distributor/query.go
// TODO(gotjosh): We only need to look in all the ingesters if shardByAllLabels is enabled.
resps, err := d.forAllIngesters(ctx, false, func(client client.IngesterClient) (interface{}, error) {
return client.MetricsMetadata(ctx, req)
})
Expand Down
12 changes: 9 additions & 3 deletions pkg/querier/distributor_queryable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,10 @@ func TestIngesterStreaming(t *testing.T) {
}

type mockDistributor struct {
m model.Matrix
r *client.QueryStreamResponse
metadata []scrape.MetricMetadata
metadataError error
m model.Matrix
r *client.QueryStreamResponse
}

func (m *mockDistributor) Query(ctx context.Context, from, to model.Time, matchers ...*labels.Matcher) (model.Matrix, error) {
Expand All @@ -128,5 +130,9 @@ func (m *mockDistributor) MetricsForLabelMatchers(ctx context.Context, from, thr
}

func (m *mockDistributor) MetricsMetadata(ctx context.Context) ([]scrape.MetricMetadata, error) {
return nil, nil
if m.metadataError != nil {
return nil, m.metadataError
}

return m.metadata, nil
}
51 changes: 51 additions & 0 deletions pkg/querier/metadata_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package querier

import (
"net/http"

"github.com/cortexproject/cortex/pkg/util"
)

type metricMetadata struct {
Type string `json:"type"`
Help string `json:"help"`
Unit string `json:"unit"`
}

const (
statusSuccess = "success"
statusError = "error"
)

type metadataResult struct {
Status string `json:"status"`
Data map[string][]metricMetadata `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}

// MetadataHandler returns metric metadata held by Cortex for a given tenant.
// It is kept and returned as a set.
func MetadataHandler(d Distributor) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp, err := d.MetricsMetadata(r.Context())
if err != nil {
w.WriteHeader(http.StatusBadRequest)
util.WriteJSONResponse(w, metadataResult{Status: statusError, Error: err.Error()})
return
}

// Put all the elements of the pseudo-set into a map of slices for marshalling.
metrics := map[string][]metricMetadata{}
for _, m := range resp {
ms, ok := metrics[m.Metric]
if !ok {
// Most metrics will only hold 1 copy of the same metadata.
ms = make([]metricMetadata, 0, 1)
metrics[m.Metric] = ms
}
metrics[m.Metric] = append(ms, metricMetadata{Type: string(m.Type), Help: m.Help, Unit: m.Unit})
}

util.WriteJSONResponse(w, metadataResult{Status: statusSuccess, Data: metrics})
})
}
76 changes: 76 additions & 0 deletions pkg/querier/metadata_handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package querier

import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"

"github.com/prometheus/prometheus/scrape"
"github.com/stretchr/testify/require"
)

func TestMetadataHandler_Success(t *testing.T) {
d := &mockDistributor{
metadata: []scrape.MetricMetadata{
{Metric: "alertmanager_dispatcher_aggregation_groups", Help: "Number of active aggregation groups", Type: "gauge", Unit: ""},
},
}

handler := MetadataHandler(d)

request, err := http.NewRequest("GET", "/metadata", nil)
require.NoError(t, err)

recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, request)

require.Equal(t, http.StatusOK, recorder.Result().StatusCode)
responseBody, err := ioutil.ReadAll(recorder.Result().Body)
require.NoError(t, err)

expectedJSON := `
{
"status": "success",
"data": {
"alertmanager_dispatcher_aggregation_groups": [
{
"help": "Number of active aggregation groups",
"type": "gauge",
"unit": ""
}
]
}
}
`

require.JSONEq(t, expectedJSON, string(responseBody))
}

func TestMetadataHandler_Error(t *testing.T) {
d := &mockDistributor{
metadataError: fmt.Errorf("no user id"),
}

handler := MetadataHandler(d)

request, err := http.NewRequest("GET", "/metadata", nil)
require.NoError(t, err)

recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, request)

require.Equal(t, http.StatusBadRequest, recorder.Result().StatusCode)
responseBody, err := ioutil.ReadAll(recorder.Result().Body)
require.NoError(t, err)

expectedJSON := `
{
"status": "error",
"error": "no user id"
}
`

require.JSONEq(t, expectedJSON, string(responseBody))
}

0 comments on commit 3462eb6

Please sign in to comment.