From 535c002f79185b282da5fe0929338ee7c53865d0 Mon Sep 17 00:00:00 2001 From: Fabian Reinartz Date: Mon, 8 Jun 2015 21:08:06 +0200 Subject: [PATCH 1/2] util/route: add WithParam function. --- util/route/route.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/util/route/route.go b/util/route/route.go index 96063ea25..8d6c2ed71 100644 --- a/util/route/route.go +++ b/util/route/route.go @@ -27,6 +27,11 @@ func Param(ctx context.Context, p string) string { return ctx.Value(param(p)).(string) } +// WithParam returns a new context with param p set to v. +func WithParam(ctx context.Context, p, v string) context.Context { + return context.WithValue(ctx, param(p), v) +} + // handle turns a Handle into httprouter.Handle func handle(h http.HandlerFunc) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, params httprouter.Params) { From 75b0b7420ec095a8c95f639f5a53d42ed923bd6b Mon Sep 17 00:00:00 2001 From: Fabian Reinartz Date: Mon, 8 Jun 2015 21:19:52 +0200 Subject: [PATCH 2/2] web/api: replace /metrics/names with /label/:name/values endpoint. --- web/api/v1/api.go | 25 +++++++++++++++------ web/api/v1/api_test.go | 49 +++++++++++++++++++++++++++++++++++------- 2 files changed, 60 insertions(+), 14 deletions(-) diff --git a/web/api/v1/api.go b/web/api/v1/api.go index b813e5f7b..7a430b732 100644 --- a/web/api/v1/api.go +++ b/web/api/v1/api.go @@ -10,6 +10,7 @@ import ( "time" "github.com/prometheus/client_golang/prometheus" + "golang.org/x/net/context" clientmodel "github.com/prometheus/client_golang/model" @@ -29,7 +30,8 @@ const ( type errorType string const ( - errorTimeout errorType = "timeout" + errorNone errorType = "" + errorTimeout = "timeout" errorCanceled = "canceled" errorExec = "execution" errorBadData = "bad_data" @@ -56,6 +58,8 @@ type response struct { type API struct { Storage local.Storage QueryEngine *promql.Engine + + context func(r *http.Request) context.Context } // Enables cross-site script calls. @@ -70,6 +74,10 @@ type apiFunc func(r *http.Request) (interface{}, *apiError) // Register the API's endpoints in the given router. func (api *API) Register(r *route.Router) { + if api.context == nil { + api.context = route.Context + } + instr := func(name string, f apiFunc) http.HandlerFunc { return prometheus.InstrumentHandlerFunc(name, func(w http.ResponseWriter, r *http.Request) { setCORS(w) @@ -84,7 +92,7 @@ func (api *API) Register(r *route.Router) { r.Get("/query", instr("query", api.query)) r.Get("/query_range", instr("query_range", api.queryRange)) - r.Get("/metrics/names", instr("metric_names", api.metricNames)) + r.Get("/label/:name/values", instr("label_values", api.labelValues)) } type queryData struct { @@ -154,11 +162,16 @@ func (api *API) queryRange(r *http.Request) (interface{}, *apiError) { }, nil } -func (api *API) metricNames(r *http.Request) (interface{}, *apiError) { - metricNames := api.Storage.LabelValuesForLabelName(clientmodel.MetricNameLabel) - sort.Sort(metricNames) +func (api *API) labelValues(r *http.Request) (interface{}, *apiError) { + name := route.Param(api.context(r), "name") - return metricNames, nil + if !clientmodel.LabelNameRE.MatchString(name) { + return nil, &apiError{errorBadData, fmt.Errorf("invalid label name: %q", name)} + } + vals := api.Storage.LabelValuesForLabelName(clientmodel.LabelName(name)) + sort.Sort(vals) + + return vals, nil } func respond(w http.ResponseWriter, data interface{}) { diff --git a/web/api/v1/api_test.go b/web/api/v1/api_test.go index de1c37d51..4c808634f 100644 --- a/web/api/v1/api_test.go +++ b/web/api/v1/api_test.go @@ -11,9 +11,12 @@ import ( "testing" "time" + "golang.org/x/net/context" + clientmodel "github.com/prometheus/client_golang/model" "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/util/route" ) func TestEndpoints(t *testing.T) { @@ -40,6 +43,7 @@ func TestEndpoints(t *testing.T) { start := clientmodel.Timestamp(0) var tests = []struct { endpoint apiFunc + params map[string]string query url.Values response interface{} errType errorType @@ -87,30 +91,59 @@ func TestEndpoints(t *testing.T) { }, }, { - endpoint: api.metricNames, + endpoint: api.labelValues, + params: map[string]string{ + "name": "__name__", + }, response: clientmodel.LabelValues{ "test_metric1", "test_metric2", }, }, + { + endpoint: api.labelValues, + params: map[string]string{ + "name": "not!!!allowed", + }, + errType: errorBadData, + }, + { + endpoint: api.labelValues, + params: map[string]string{ + "name": "foo", + }, + response: clientmodel.LabelValues{ + "bar", + "boo", + }, + }, } for _, test := range tests { + // Build a context with the correct request params. + ctx := context.Background() + for p, v := range test.params { + ctx = route.WithParam(ctx, p, v) + } + api.context = func(r *http.Request) context.Context { + return ctx + } + req, err := http.NewRequest("ANY", fmt.Sprintf("http://example.com?%s", test.query.Encode()), nil) if err != nil { t.Fatal(err) } - resp, apierr := test.endpoint(req) - if apierr != nil { - if test.errType == "" { - t.Fatalf("Unexpected error: %s", apierr) + resp, apiErr := test.endpoint(req) + if apiErr != nil { + if test.errType == errorNone { + t.Fatalf("Unexpected error: %s", apiErr) } - if test.errType != apierr.typ { - t.Fatalf("Expected error of type %q but got type %q", test.errType, apierr.typ) + if test.errType != apiErr.typ { + t.Fatalf("Expected error of type %q but got type %q", test.errType, apiErr.typ) } continue } - if apierr == nil && test.errType != "" { + if apiErr == nil && test.errType != errorNone { t.Fatalf("Expected error of type %q but got none", test.errType) } if !reflect.DeepEqual(resp, test.response) {