mirror of
https://github.com/prometheus/prometheus.git
synced 2024-11-09 23:24:05 -08:00
Support matchers for Labels API (#8301)
Signed-off-by: Ben Ye <yb532204897@gmail.com> Co-authored-by: Erik Klockare <eklockare@gmail.com>
This commit is contained in:
parent
e5478983c4
commit
caa173d2aa
|
@ -271,6 +271,8 @@ URL query parameters:
|
|||
|
||||
- `start=<rfc3339 | unix_timestamp>`: Start 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.
|
||||
|
@ -319,6 +321,8 @@ URL query parameters:
|
|||
|
||||
- `start=<rfc3339 | unix_timestamp>`: Start 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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"},
|
||||
},
|
||||
}...)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue