Browse Source

Support matchers for Labels API (#8301)

Signed-off-by: Ben Ye <yb532204897@gmail.com>
Co-authored-by: Erik Klockare <eklockare@gmail.com>
pull/7956/head^2
Ben Ye 4 years ago committed by GitHub
parent
commit
caa173d2aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      docs/querying/api.md
  2. 2
      storage/interface.go
  3. 142
      web/api/v1/api.go
  4. 133
      web/api/v1/api_test.go

4
docs/querying/api.md

@ -271,6 +271,8 @@ URL query parameters:
- `start=<rfc3339 | unix_timestamp>`: Start timestamp. Optional. - `start=<rfc3339 | unix_timestamp>`: Start timestamp. Optional.
- `end=<rfc3339 | unix_timestamp>`: End timestamp. Optional. - `end=<rfc3339 | unix_timestamp>`: End timestamp. Optional.
- `match[]=<series_selector>`: 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. The `data` section of the JSON response is a list of string label names.
@ -319,6 +321,8 @@ URL query parameters:
- `start=<rfc3339 | unix_timestamp>`: Start timestamp. Optional. - `start=<rfc3339 | unix_timestamp>`: Start timestamp. Optional.
- `end=<rfc3339 | unix_timestamp>`: End timestamp. Optional. - `end=<rfc3339 | unix_timestamp>`: End timestamp. Optional.
- `match[]=<series_selector>`: 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. The `data` section of the JSON response is a list of string label values.

2
storage/interface.go

@ -95,9 +95,11 @@ type ChunkQuerier interface {
type LabelQuerier interface { type LabelQuerier interface {
// LabelValues returns all potential values for a label name. // LabelValues returns all potential values for a label name.
// It is not safe to use the strings beyond the lifefime of the querier. // 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) LabelValues(name string) ([]string, Warnings, error)
// LabelNames returns all the unique label names present in the block in sorted order. // LabelNames returns all the unique label names present in the block in sorted order.
// TODO(yeya24): support matchers or hints.
LabelNames() ([]string, Warnings, error) LabelNames() ([]string, Warnings, error)
// Close releases the resources of the Querier. // Close releases the resources of the Querier.

142
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} 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)) q, err := api.Queryable.Querier(r.Context(), timestamp.FromTime(start), timestamp.FromTime(end))
if err != nil { if err != nil {
return apiFuncResult{nil, &apiError{errorExec, err}, nil, nil} return apiFuncResult{nil, &apiError{errorExec, err}, nil, nil}
} }
defer q.Close() defer q.Close()
names, warnings, err := q.LabelNames() var (
if err != nil { names []string
return apiFuncResult{nil, &apiError{errorExec, err}, warnings, nil} 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 { if names == nil {
names = []string{} 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} 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)) q, err := api.Queryable.Querier(r.Context(), timestamp.FromTime(start), timestamp.FromTime(end))
if err != nil { if err != nil {
return apiFuncResult{nil, &apiError{errorExec, err}, nil, nil} return apiFuncResult{nil, &apiError{errorExec, err}, nil, nil}
@ -542,10 +588,49 @@ func (api *API) labelValues(r *http.Request) (result apiFuncResult) {
q.Close() q.Close()
} }
vals, warnings, err := q.LabelValues(name) var (
if err != nil { vals []string
return apiFuncResult{nil, &apiError{errorExec, err}, warnings, closer} 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 { if vals == nil {
vals = []string{} vals = []string{}
} }
@ -578,26 +663,9 @@ func (api *API) series(r *http.Request) (result apiFuncResult) {
return apiFuncResult{nil, &apiError{errorBadData, err}, nil, nil} return apiFuncResult{nil, &apiError{errorBadData, err}, nil, nil}
} }
var matcherSets [][]*labels.Matcher matcherSets, err := parseMatchersParam(r.Form["match[]"])
for _, s := range r.Form["match[]"] { if err != nil {
matchers, err := parser.ParseMetricSelector(s) return apiFuncResult{nil, &apiError{errorBadData, err}, nil, nil}
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}
}
} }
q, err := api.Queryable.Querier(r.Context(), timestamp.FromTime(start), timestamp.FromTime(end)) 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) 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) { func marshalPointJSON(ptr unsafe.Pointer, stream *jsoniter.Stream) {
p := *((*promql.Point)(ptr)) p := *((*promql.Point)(ptr))
stream.WriteArrayStart() stream.WriteArrayStart()

133
web/api/v1/api_test.go

@ -1622,7 +1622,84 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, testLabelAPI
"boo", "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. // Label names.
{ {
endpoint: api.labelNames, endpoint: api.labelNames,
@ -1708,6 +1785,60 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, testLabelAPI
}, },
response: []string{"__name__", "dup", "foo"}, 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"},
},
}...) }...)
} }

Loading…
Cancel
Save