diff --git a/docs/querying/api.md b/docs/querying/api.md index 48794d98f..b767f04b8 100644 --- a/docs/querying/api.md +++ b/docs/querying/api.md @@ -271,6 +271,8 @@ URL query parameters: - `start=`: Start timestamp. Optional. - `end=`: End timestamp. Optional. +- `match[]=`: Repeated series selector argument that selects the + series from which to read the label names. Optional. The `data` section of the JSON response is a list of string label names. @@ -319,6 +321,8 @@ URL query parameters: - `start=`: Start timestamp. Optional. - `end=`: End timestamp. Optional. +- `match[]=`: Repeated series selector argument that selects the + series from which to read the label values. Optional. The `data` section of the JSON response is a list of string label values. diff --git a/storage/interface.go b/storage/interface.go index a10178aee..e697d57d1 100644 --- a/storage/interface.go +++ b/storage/interface.go @@ -95,9 +95,11 @@ type ChunkQuerier interface { type LabelQuerier interface { // LabelValues returns all potential values for a label name. // It is not safe to use the strings beyond the lifefime of the querier. + // TODO(yeya24): support matchers or hints. LabelValues(name string) ([]string, Warnings, error) // LabelNames returns all the unique label names present in the block in sorted order. + // TODO(yeya24): support matchers or hints. LabelNames() ([]string, Warnings, error) // Close releases the resources of the Querier. diff --git a/web/api/v1/api.go b/web/api/v1/api.go index 68bcc6d8e..2d0d442fb 100644 --- a/web/api/v1/api.go +++ b/web/api/v1/api.go @@ -493,16 +493,57 @@ func (api *API) labelNames(r *http.Request) apiFuncResult { return apiFuncResult{nil, &apiError{errorBadData, errors.Wrap(err, "invalid parameter 'end'")}, nil, nil} } + matcherSets, err := parseMatchersParam(r.Form["match[]"]) + if err != nil { + return apiFuncResult{nil, &apiError{errorBadData, err}, nil, nil} + } + q, err := api.Queryable.Querier(r.Context(), timestamp.FromTime(start), timestamp.FromTime(end)) if err != nil { return apiFuncResult{nil, &apiError{errorExec, err}, nil, nil} } defer q.Close() - names, warnings, err := q.LabelNames() - if err != nil { - return apiFuncResult{nil, &apiError{errorExec, err}, warnings, nil} + var ( + names []string + warnings storage.Warnings + ) + if len(matcherSets) > 0 { + hints := &storage.SelectHints{ + Start: timestamp.FromTime(start), + End: timestamp.FromTime(end), + Func: "series", // There is no series function, this token is used for lookups that don't need samples. + } + + labelNamesSet := make(map[string]struct{}) + // Get all series which match matchers. + for _, mset := range matcherSets { + s := q.Select(false, hints, mset...) + for s.Next() { + series := s.At() + for _, lb := range series.Labels() { + labelNamesSet[lb.Name] = struct{}{} + } + } + warnings = append(warnings, s.Warnings()...) + if err := s.Err(); err != nil { + return apiFuncResult{nil, &apiError{errorExec, err}, warnings, nil} + } + } + + // Convert the map to an array. + names = make([]string, 0, len(labelNamesSet)) + for key := range labelNamesSet { + names = append(names, key) + } + sort.Strings(names) + } else { + names, warnings, err = q.LabelNames() + if err != nil { + return apiFuncResult{nil, &apiError{errorExec, err}, warnings, nil} + } } + if names == nil { names = []string{} } @@ -526,6 +567,11 @@ func (api *API) labelValues(r *http.Request) (result apiFuncResult) { return apiFuncResult{nil, &apiError{errorBadData, errors.Wrap(err, "invalid parameter 'end'")}, nil, nil} } + matcherSets, err := parseMatchersParam(r.Form["match[]"]) + if err != nil { + return apiFuncResult{nil, &apiError{errorBadData, err}, nil, nil} + } + q, err := api.Queryable.Querier(r.Context(), timestamp.FromTime(start), timestamp.FromTime(end)) if err != nil { return apiFuncResult{nil, &apiError{errorExec, err}, nil, nil} @@ -542,10 +588,49 @@ func (api *API) labelValues(r *http.Request) (result apiFuncResult) { q.Close() } - vals, warnings, err := q.LabelValues(name) - if err != nil { - return apiFuncResult{nil, &apiError{errorExec, err}, warnings, closer} + var ( + vals []string + warnings storage.Warnings + ) + if len(matcherSets) > 0 { + hints := &storage.SelectHints{ + Start: timestamp.FromTime(start), + End: timestamp.FromTime(end), + Func: "series", // There is no series function, this token is used for lookups that don't need samples. + } + + labelValuesSet := make(map[string]struct{}) + // Get all series which match matchers. + for _, mset := range matcherSets { + s := q.Select(false, hints, mset...) + for s.Next() { + series := s.At() + labelValue := series.Labels().Get(name) + // Filter out empty value. + if labelValue == "" { + continue + } + labelValuesSet[labelValue] = struct{}{} + } + warnings = append(warnings, s.Warnings()...) + if err := s.Err(); err != nil { + return apiFuncResult{nil, &apiError{errorExec, err}, warnings, nil} + } + } + + // Convert the map to an array. + vals = make([]string, 0, len(labelValuesSet)) + for key := range labelValuesSet { + vals = append(vals, key) + } + sort.Strings(vals) + } else { + vals, warnings, err = q.LabelValues(name) + if err != nil { + return apiFuncResult{nil, &apiError{errorExec, err}, warnings, closer} + } } + if vals == nil { vals = []string{} } @@ -578,26 +663,9 @@ func (api *API) series(r *http.Request) (result apiFuncResult) { return apiFuncResult{nil, &apiError{errorBadData, err}, nil, nil} } - var matcherSets [][]*labels.Matcher - for _, s := range r.Form["match[]"] { - matchers, err := parser.ParseMetricSelector(s) - if err != nil { - return apiFuncResult{nil, &apiError{errorBadData, err}, nil, nil} - } - matcherSets = append(matcherSets, matchers) - } - - for _, ms := range matcherSets { - var nonEmpty bool - for _, lm := range ms { - if lm != nil && !lm.Matches("") { - nonEmpty = true - break - } - } - if !nonEmpty { - return apiFuncResult{nil, &apiError{errorBadData, errors.New("match[] must contain at least one non-empty matcher")}, nil, nil} - } + matcherSets, err := parseMatchersParam(r.Form["match[]"]) + if err != nil { + return apiFuncResult{nil, &apiError{errorBadData, err}, nil, nil} } q, err := api.Queryable.Querier(r.Context(), timestamp.FromTime(start), timestamp.FromTime(end)) @@ -1631,6 +1699,28 @@ func parseDuration(s string) (time.Duration, error) { return 0, errors.Errorf("cannot parse %q to a valid duration", s) } +func parseMatchersParam(matchers []string) ([][]*labels.Matcher, error) { + var matcherSets [][]*labels.Matcher + for _, s := range matchers { + matchers, err := parser.ParseMetricSelector(s) + if err != nil { + return nil, err + } + matcherSets = append(matcherSets, matchers) + } + +OUTER: + for _, ms := range matcherSets { + for _, lm := range ms { + if lm != nil && !lm.Matches("") { + continue OUTER + } + } + return nil, errors.New("match[] must contain at least one non-empty matcher") + } + return matcherSets, nil +} + func marshalPointJSON(ptr unsafe.Pointer, stream *jsoniter.Stream) { p := *((*promql.Point)(ptr)) stream.WriteArrayStart() diff --git a/web/api/v1/api_test.go b/web/api/v1/api_test.go index f2834ea11..2e63d9bb8 100644 --- a/web/api/v1/api_test.go +++ b/web/api/v1/api_test.go @@ -1622,7 +1622,84 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, testLabelAPI "boo", }, }, - + // Label values with bad matchers. + { + endpoint: api.labelValues, + params: map[string]string{ + "name": "foo", + }, + query: url.Values{ + "match[]": []string{`{foo=""`, `test_metric2`}, + }, + errType: errorBadData, + }, + // Label values with empty matchers. + { + endpoint: api.labelValues, + params: map[string]string{ + "name": "foo", + }, + query: url.Values{ + "match[]": []string{`{foo=""}`}, + }, + errType: errorBadData, + }, + // Label values with matcher. + { + endpoint: api.labelValues, + params: map[string]string{ + "name": "foo", + }, + query: url.Values{ + "match[]": []string{`test_metric2`}, + }, + response: []string{ + "boo", + }, + }, + // Label values with matcher. + { + endpoint: api.labelValues, + params: map[string]string{ + "name": "foo", + }, + query: url.Values{ + "match[]": []string{`test_metric1`}, + }, + response: []string{ + "bar", + "boo", + }, + }, + // Label values with matcher using label filter. + { + endpoint: api.labelValues, + params: map[string]string{ + "name": "foo", + }, + query: url.Values{ + "match[]": []string{`test_metric1{foo="bar"}`}, + }, + response: []string{ + "bar", + }, + }, + // Label values with matcher and time range. + { + endpoint: api.labelValues, + params: map[string]string{ + "name": "foo", + }, + query: url.Values{ + "match[]": []string{`test_metric1`}, + "start": []string{"1"}, + "end": []string{"100000000"}, + }, + response: []string{ + "bar", + "boo", + }, + }, // Label names. { endpoint: api.labelNames, @@ -1708,6 +1785,60 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, testLabelAPI }, response: []string{"__name__", "dup", "foo"}, }, + // Label names with bad matchers. + { + endpoint: api.labelNames, + query: url.Values{ + "match[]": []string{`{foo=""`, `test_metric2`}, + }, + errType: errorBadData, + }, + // Label values with empty matchers. + { + endpoint: api.labelNames, + params: map[string]string{ + "name": "foo", + }, + query: url.Values{ + "match[]": []string{`{foo=""}`}, + }, + errType: errorBadData, + }, + // Label names with matcher. + { + endpoint: api.labelNames, + query: url.Values{ + "match[]": []string{`test_metric2`}, + }, + response: []string{"__name__", "foo"}, + }, + // Label names with matcher. + { + endpoint: api.labelNames, + query: url.Values{ + "match[]": []string{`test_metric3`}, + }, + response: []string{"__name__", "dup", "foo"}, + }, + // Label names with matcher using label filter. + // There is no matching series. + { + endpoint: api.labelNames, + query: url.Values{ + "match[]": []string{`test_metric1{foo="test"}`}, + }, + response: []string{}, + }, + // Label names with matcher and time range. + { + endpoint: api.labelNames, + query: url.Values{ + "match[]": []string{`test_metric2`}, + "start": []string{"1"}, + "end": []string{"100000000"}, + }, + response: []string{"__name__", "foo"}, + }, }...) }