diff --git a/docs/querying/api.md b/docs/querying/api.md index bd1c67987..1e0e36ee9 100644 --- a/docs/querying/api.md +++ b/docs/querying/api.md @@ -268,6 +268,12 @@ GET /api/v1/labels POST /api/v1/labels ``` +URL query parameters: + +- `start=`: Start timestamp. Optional. +- `end=`: End timestamp. Optional. + + The `data` section of the JSON response is a list of string label names. Here is an example. @@ -310,6 +316,12 @@ The following endpoint returns a list of label values for a provided label name: GET /api/v1/label//values ``` +URL query parameters: + +- `start=`: Start timestamp. Optional. +- `end=`: End timestamp. Optional. + + The `data` section of the JSON response is a list of string label values. This example queries for all label values for the `job` label: diff --git a/tsdb/head.go b/tsdb/head.go index 358c3ed40..5cbaefc1e 100644 --- a/tsdb/head.go +++ b/tsdb/head.go @@ -1539,9 +1539,16 @@ func (h *headIndexReader) Symbols() index.StringIter { return index.NewStringListIter(res) } -// LabelValues returns the possible label values +// LabelValues returns label values present in the head for the +// specific label name that are within the time range mint to maxt. func (h *headIndexReader) LabelValues(name string) ([]string, error) { h.head.symMtx.RLock() + + if h.maxt < h.head.MinTime() || h.mint > h.head.MaxTime() { + h.head.symMtx.RUnlock() + return []string{}, nil + } + sl := make([]string, 0, len(h.head.values[name])) for s := range h.head.values[name] { sl = append(sl, s) @@ -1551,10 +1558,16 @@ func (h *headIndexReader) LabelValues(name string) ([]string, error) { return sl, nil } -// LabelNames returns all the unique label names present in the head. +// LabelNames returns all the unique label names present in the head +// that are within the time range mint to maxt. func (h *headIndexReader) LabelNames() ([]string, error) { h.head.symMtx.RLock() defer h.head.symMtx.RUnlock() + + if h.maxt < h.head.MinTime() || h.mint > h.head.MaxTime() { + return []string{}, nil + } + labelNames := make([]string, 0, len(h.head.values)) for name := range h.head.values { if name == "" { diff --git a/tsdb/head_test.go b/tsdb/head_test.go index b97932a30..f95c16a20 100644 --- a/tsdb/head_test.go +++ b/tsdb/head_test.go @@ -1811,3 +1811,63 @@ func newTestHead(t testing.TB, chunkRange int64, compressWAL bool) (*Head, *wal. testutil.Ok(t, os.RemoveAll(dir)) } } + +func TestHeadLabelNamesValuesWithMinMaxRange(t *testing.T) { + head, _, closer := newTestHead(t, 1000, false) + defer closer() + defer func() { + testutil.Ok(t, head.Close()) + }() + + const ( + firstSeriesTimestamp int64 = 100 + secondSeriesTimestamp int64 = 200 + lastSeriesTimestamp int64 = 300 + ) + var ( + seriesTimestamps = []int64{firstSeriesTimestamp, + secondSeriesTimestamp, + lastSeriesTimestamp, + } + expectedLabelNames = []string{"a", "b", "c"} + expectedLabelValues = []string{"d", "e", "f"} + ) + + app := head.Appender() + for i, name := range expectedLabelNames { + _, err := app.Add(labels.Labels{{Name: name, Value: expectedLabelValues[i]}}, seriesTimestamps[i], 0) + testutil.Ok(t, err) + } + testutil.Ok(t, app.Commit()) + testutil.Equals(t, head.MinTime(), firstSeriesTimestamp) + testutil.Equals(t, head.MaxTime(), lastSeriesTimestamp) + + var testCases = []struct { + name string + mint int64 + maxt int64 + expectedNames []string + expectedValues []string + }{ + {"maxt less than head min", head.MaxTime() - 10, head.MinTime() - 10, []string{}, []string{}}, + {"mint less than head max", head.MaxTime() + 10, head.MinTime() + 10, []string{}, []string{}}, + {"mint and maxt outside head", head.MaxTime() + 10, head.MinTime() - 10, []string{}, []string{}}, + {"mint and maxt within head", head.MaxTime() - 10, head.MinTime() + 10, expectedLabelNames, expectedLabelValues}, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + headIdxReader := head.indexRange(tt.mint, tt.maxt) + actualLabelNames, err := headIdxReader.LabelNames() + testutil.Ok(t, err) + testutil.Equals(t, tt.expectedNames, actualLabelNames) + if len(tt.expectedValues) > 0 { + for i, name := range expectedLabelNames { + actualLabelValue, err := headIdxReader.LabelValues(name) + testutil.Ok(t, err) + testutil.Equals(t, []string{tt.expectedValues[i]}, actualLabelValue) + } + } + }) + } +} diff --git a/tsdb/querier.go b/tsdb/querier.go index d9d6c62ee..5af688b26 100644 --- a/tsdb/querier.go +++ b/tsdb/querier.go @@ -64,7 +64,7 @@ func (q *querier) LabelNames() ([]string, storage.Warnings, error) { func (q *querier) lvals(qs []storage.Querier, n string) ([]string, storage.Warnings, error) { if len(qs) == 0 { - return nil, nil, nil + return []string{}, nil, nil } if len(qs) == 1 { return qs[0].LabelValues(n) @@ -75,12 +75,12 @@ func (q *querier) lvals(qs []storage.Querier, n string) ([]string, storage.Warni s1, w, err := q.lvals(qs[:l], n) ws = append(ws, w...) if err != nil { - return nil, ws, err + return []string{}, ws, err } s2, ws, err := q.lvals(qs[l:], n) ws = append(ws, w...) if err != nil { - return nil, ws, err + return []string{}, ws, err } return mergeStrings(s1, s2), ws, nil } diff --git a/web/api/v1/api.go b/web/api/v1/api.go index 62bca6d78..8a6e3a900 100644 --- a/web/api/v1/api.go +++ b/web/api/v1/api.go @@ -482,7 +482,16 @@ func returnAPIError(err error) *apiError { } func (api *API) labelNames(r *http.Request) apiFuncResult { - q, err := api.Queryable.Querier(r.Context(), math.MinInt64, math.MaxInt64) + start, err := parseTimeParam(r, "start", minTime) + if err != nil { + return apiFuncResult{nil, &apiError{errorBadData, errors.Wrap(err, "invalid parameter 'start'")}, nil, nil} + } + end, err := parseTimeParam(r, "end", maxTime) + if err != nil { + return apiFuncResult{nil, &apiError{errorBadData, errors.Wrap(err, "invalid parameter 'end'")}, 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} } @@ -502,7 +511,17 @@ func (api *API) labelValues(r *http.Request) (result apiFuncResult) { if !model.LabelNameRE.MatchString(name) { return apiFuncResult{nil, &apiError{errorBadData, errors.Errorf("invalid label name: %q", name)}, nil, nil} } - q, err := api.Queryable.Querier(ctx, math.MinInt64, math.MaxInt64) + + start, err := parseTimeParam(r, "start", minTime) + if err != nil { + return apiFuncResult{nil, &apiError{errorBadData, errors.Wrap(err, "invalid parameter 'start'")}, nil, nil} + } + end, err := parseTimeParam(r, "end", maxTime) + if err != nil { + return apiFuncResult{nil, &apiError{errorBadData, errors.Wrap(err, "invalid parameter 'end'")}, 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} } diff --git a/web/api/v1/api_test.go b/web/api/v1/api_test.go index 0aff0350f..aa86c4124 100644 --- a/web/api/v1/api_test.go +++ b/web/api/v1/api_test.go @@ -1474,11 +1474,216 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, testLabelAPI }, errType: errorBadData, }, + // Start and end before LabelValues starts. + { + endpoint: api.labelValues, + params: map[string]string{ + "name": "foo", + }, + query: url.Values{ + "start": []string{"-2"}, + "end": []string{"-1"}, + }, + response: []string{}, + }, + // Start and end within LabelValues. + { + endpoint: api.labelValues, + params: map[string]string{ + "name": "foo", + }, + query: url.Values{ + "start": []string{"1"}, + "end": []string{"100"}, + }, + response: []string{ + "bar", + "boo", + }, + }, + // Start before LabelValues, end within LabelValues. + { + endpoint: api.labelValues, + params: map[string]string{ + "name": "foo", + }, + query: url.Values{ + "start": []string{"-1"}, + "end": []string{"3"}, + }, + response: []string{ + "bar", + "boo", + }, + }, + // Start before LabelValues starts, end after LabelValues ends. + { + endpoint: api.labelValues, + params: map[string]string{ + "name": "foo", + }, + query: url.Values{ + "start": []string{"1969-12-31T00:00:00Z"}, + "end": []string{"1970-02-01T00:02:03Z"}, + }, + response: []string{ + "bar", + "boo", + }, + }, + // Start with bad data, end within LabelValues. + { + endpoint: api.labelValues, + params: map[string]string{ + "name": "foo", + }, + query: url.Values{ + "start": []string{"boop"}, + "end": []string{"1"}, + }, + errType: errorBadData, + }, + // Start within LabelValues, end after. + { + endpoint: api.labelValues, + params: map[string]string{ + "name": "foo", + }, + query: url.Values{ + "start": []string{"1"}, + "end": []string{"100000000"}, + }, + response: []string{ + "bar", + "boo", + }, + }, + // Start and end after LabelValues ends. + { + endpoint: api.labelValues, + params: map[string]string{ + "name": "foo", + }, + query: url.Values{ + "start": []string{"148966367200.372"}, + "end": []string{"148966367200.972"}, + }, + response: []string{}, + }, + // Only provide Start within LabelValues, don't provide an end time. + { + endpoint: api.labelValues, + params: map[string]string{ + "name": "foo", + }, + query: url.Values{ + "start": []string{"2"}, + }, + response: []string{ + "bar", + "boo", + }, + }, + // Only provide end within LabelValues, don't provide a start time. + { + endpoint: api.labelValues, + params: map[string]string{ + "name": "foo", + }, + query: url.Values{ + "end": []string{"100"}, + }, + response: []string{ + "bar", + "boo", + }, + }, + // Label names. { endpoint: api.labelNames, response: []string{"__name__", "foo"}, }, + // Start and end before Label names starts. + { + endpoint: api.labelNames, + query: url.Values{ + "start": []string{"-2"}, + "end": []string{"-1"}, + }, + response: []string{}, + }, + // Start and end within Label names. + { + endpoint: api.labelNames, + query: url.Values{ + "start": []string{"1"}, + "end": []string{"100"}, + }, + response: []string{"__name__", "foo"}, + }, + // Start before Label names, end within Label names. + { + endpoint: api.labelNames, + query: url.Values{ + "start": []string{"-1"}, + "end": []string{"10"}, + }, + response: []string{"__name__", "foo"}, + }, + + // Start before Label names starts, end after Label names ends. + { + endpoint: api.labelNames, + query: url.Values{ + "start": []string{"-1"}, + "end": []string{"100000"}, + }, + response: []string{"__name__", "foo"}, + }, + // Start with bad data for Label names, end within Label names. + { + endpoint: api.labelNames, + query: url.Values{ + "start": []string{"boop"}, + "end": []string{"1"}, + }, + errType: errorBadData, + }, + // Start within Label names, end after. + { + endpoint: api.labelNames, + query: url.Values{ + "start": []string{"1"}, + "end": []string{"1000000006"}, + }, + response: []string{"__name__", "foo"}, + }, + // Start and end after Label names ends. + { + endpoint: api.labelNames, + query: url.Values{ + "start": []string{"148966367200.372"}, + "end": []string{"148966367200.972"}, + }, + response: []string{}, + }, + // Only provide Start within Label names, don't provide an end time. + { + endpoint: api.labelNames, + query: url.Values{ + "start": []string{"4"}, + }, + response: []string{"__name__", "foo"}, + }, + // Only provide End within Label names, don't provide a start time. + { + endpoint: api.labelNames, + query: url.Values{ + "end": []string{"20"}, + }, + response: []string{"__name__", "foo"}, + }, }...) }