diff --git a/docs/querying/api.md b/docs/querying/api.md index f1e7129303..e3f97886dc 100644 --- a/docs/querying/api.md +++ b/docs/querying/api.md @@ -86,6 +86,7 @@ URL query parameters: - `time=`: Evaluation timestamp. Optional. - `timeout=`: Evaluation timeout. Optional. Defaults to and is capped by the value of the `-query.timeout` flag. +- `limit=`: Maximum number of returned series. Doesn’t affect scalars or strings but truncates the number of series for matrices and vectors. Optional. 0 means disabled. The current server time is used if the `time` parameter is omitted. @@ -154,6 +155,7 @@ URL query parameters: - `step=`: Query resolution step width in `duration` format or float number of seconds. - `timeout=`: Evaluation timeout. Optional. Defaults to and is capped by the value of the `-query.timeout` flag. +- `limit=`: Maximum number of returned series. Optional. 0 means disabled. You can URL-encode these parameters directly in the request body by using the `POST` method and `Content-Type: application/x-www-form-urlencoded` header. This is useful when specifying a large diff --git a/web/api/v1/api.go b/web/api/v1/api.go index 6e9c589087..4903f925cc 100644 --- a/web/api/v1/api.go +++ b/web/api/v1/api.go @@ -438,6 +438,10 @@ func (api *API) options(*http.Request) apiFuncResult { } func (api *API) query(r *http.Request) (result apiFuncResult) { + limit, err := parseLimitParam(r.FormValue("limit")) + if err != nil { + return invalidParamError(err, "limit") + } ts, err := parseTimeParam(r, "time", api.now()) if err != nil { return invalidParamError(err, "time") @@ -479,6 +483,15 @@ func (api *API) query(r *http.Request) (result apiFuncResult) { return apiFuncResult{nil, returnAPIError(res.Err), res.Warnings, qry.Close} } + warnings := res.Warnings + if limit > 0 { + var isTruncated bool + + res, isTruncated = truncateResults(res, limit) + if isTruncated { + warnings = warnings.Add(errors.New("results truncated due to limit")) + } + } // Optional stats field in response if parameter "stats" is not empty. sr := api.statsRenderer if sr == nil { @@ -490,7 +503,7 @@ func (api *API) query(r *http.Request) (result apiFuncResult) { ResultType: res.Value.Type(), Result: res.Value, Stats: qs, - }, nil, res.Warnings, qry.Close} + }, nil, warnings, qry.Close} } func (api *API) formatQuery(r *http.Request) (result apiFuncResult) { @@ -526,6 +539,10 @@ func extractQueryOpts(r *http.Request) (promql.QueryOpts, error) { } func (api *API) queryRange(r *http.Request) (result apiFuncResult) { + limit, err := parseLimitParam(r.FormValue("limit")) + if err != nil { + return invalidParamError(err, "limit") + } start, err := parseTime(r.FormValue("start")) if err != nil { return invalidParamError(err, "start") @@ -590,6 +607,16 @@ func (api *API) queryRange(r *http.Request) (result apiFuncResult) { return apiFuncResult{nil, returnAPIError(res.Err), res.Warnings, qry.Close} } + warnings := res.Warnings + if limit > 0 { + var isTruncated bool + + res, isTruncated = truncateResults(res, limit) + if isTruncated { + warnings = warnings.Add(errors.New("results truncated due to limit")) + } + } + // Optional stats field in response if parameter "stats" is not empty. sr := api.statsRenderer if sr == nil { @@ -601,7 +628,7 @@ func (api *API) queryRange(r *http.Request) (result apiFuncResult) { ResultType: res.Value.Type(), Result: res.Value, Stats: qs, - }, nil, res.Warnings, qry.Close} + }, nil, warnings, qry.Close} } func (api *API) queryExemplars(r *http.Request) apiFuncResult { @@ -2102,3 +2129,25 @@ func toHintLimit(limit int) int { } return limit } + +// truncateResults truncates result for queryRange() and query(). +// No truncation for other types(Scalars or Strings). +func truncateResults(result *promql.Result, limit int) (*promql.Result, bool) { + isTruncated := false + + switch v := result.Value.(type) { + case promql.Matrix: + if len(v) > limit { + result.Value = v[:limit] + isTruncated = true + } + case promql.Vector: + if len(v) > limit { + result.Value = v[:limit] + isTruncated = true + } + } + + // Return the modified result. Unchanged for other types. + return result, isTruncated +} diff --git a/web/api/v1/api_test.go b/web/api/v1/api_test.go index 175ed2e0f0..e6ca43508b 100644 --- a/web/api/v1/api_test.go +++ b/web/api/v1/api_test.go @@ -1164,6 +1164,49 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E }, }, }, + // Only matrix and vector responses are limited/truncated. String and scalar responses aren't truncated. + { + endpoint: api.query, + query: url.Values{ + "query": []string{"2"}, + "time": []string{"123.4"}, + "limit": []string{"1"}, + }, + response: &QueryData{ + ResultType: parser.ValueTypeScalar, + Result: promql.Scalar{ + V: 2, + T: timestamp.FromTime(start.Add(123*time.Second + 400*time.Millisecond)), + }, + }, + warningsCount: 0, + }, + // When limit = 0, limit is disabled. + { + endpoint: api.query, + query: url.Values{ + "query": []string{"2"}, + "time": []string{"123.4"}, + "limit": []string{"0"}, + }, + response: &QueryData{ + ResultType: parser.ValueTypeScalar, + Result: promql.Scalar{ + V: 2, + T: timestamp.FromTime(start.Add(123*time.Second + 400*time.Millisecond)), + }, + }, + warningsCount: 0, + }, + { + endpoint: api.query, + query: url.Values{ + "query": []string{"2"}, + "time": []string{"123.4"}, + "limit": []string{"-1"}, + }, + errType: errorBadData, + }, { endpoint: api.query, query: url.Values{ @@ -1205,6 +1248,179 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E }, }, }, + { + endpoint: api.query, + query: url.Values{ + "query": []string{ + `label_replace(vector(42), "foo", "bar", "", "") or label_replace(vector(3.1415), "dings", "bums", "", "")`, + }, + "time": []string{"123.4"}, + "limit": []string{"2"}, + }, + warningsCount: 0, + responseAsJSON: `{ + "resultType": "vector", + "result": [ + { + "metric": { + "foo": "bar" + }, + "value": [123.4, "42"] + }, + { + "metric": { + "dings": "bums" + }, + "value": [123.4, "3.1415"] + } + ] + }`, + }, + { + endpoint: api.query, + query: url.Values{ + "query": []string{ + `label_replace(vector(42), "foo", "bar", "", "") or label_replace(vector(3.1415), "dings", "bums", "", "")`, + }, + "time": []string{"123.4"}, + "limit": []string{"1"}, + }, + warningsCount: 1, + responseAsJSON: `{ + "resultType": "vector", + "result": [ + { + "metric": { + "foo": "bar" + }, + "value": [123.4, "42"] + } + ] + }`, + }, + { + endpoint: api.query, + query: url.Values{ + "query": []string{ + `label_replace(vector(42), "foo", "bar", "", "") or label_replace(vector(3.1415), "dings", "bums", "", "")`, + }, + "time": []string{"123.4"}, + "limit": []string{"0"}, + }, + responseAsJSON: `{ + "resultType": "vector", + "result": [ + { + "metric": { + "foo": "bar" + }, + "value": [123.4, "42"] + }, + { + "metric": { + "dings": "bums" + }, + "value": [123.4, "3.1415"] + } + ] + }`, + warningsCount: 0, + }, + // limit=0 means no limit. + { + endpoint: api.queryRange, + query: url.Values{ + "query": []string{ + `label_replace(vector(42), "foo", "bar", "", "") or label_replace(vector(3.1415), "dings", "bums", "", "")`, + }, + "start": []string{"0"}, + "end": []string{"2"}, + "step": []string{"1"}, + "limit": []string{"0"}, + }, + response: &QueryData{ + ResultType: parser.ValueTypeMatrix, + Result: promql.Matrix{ + promql.Series{ + Metric: labels.FromMap(map[string]string{"dings": "bums"}), + Floats: []promql.FPoint{ + {F: 3.1415, T: timestamp.FromTime(start)}, + {F: 3.1415, T: timestamp.FromTime(start.Add(1 * time.Second))}, + {F: 3.1415, T: timestamp.FromTime(start.Add(2 * time.Second))}, + }, + }, + promql.Series{ + Metric: labels.FromMap(map[string]string{"foo": "bar"}), + Floats: []promql.FPoint{ + {F: 42, T: timestamp.FromTime(start)}, + {F: 42, T: timestamp.FromTime(start.Add(1 * time.Second))}, + {F: 42, T: timestamp.FromTime(start.Add(2 * time.Second))}, + }, + }, + }, + }, + warningsCount: 0, + }, + { + endpoint: api.queryRange, + query: url.Values{ + "query": []string{ + `label_replace(vector(42), "foo", "bar", "", "") or label_replace(vector(3.1415), "dings", "bums", "", "")`, + }, + "start": []string{"0"}, + "end": []string{"2"}, + "step": []string{"1"}, + "limit": []string{"1"}, + }, + response: &QueryData{ + ResultType: parser.ValueTypeMatrix, + Result: promql.Matrix{ + promql.Series{ + Metric: labels.FromMap(map[string]string{"dings": "bums"}), + Floats: []promql.FPoint{ + {F: 3.1415, T: timestamp.FromTime(start)}, + {F: 3.1415, T: timestamp.FromTime(start.Add(1 * time.Second))}, + {F: 3.1415, T: timestamp.FromTime(start.Add(2 * time.Second))}, + }, + }, + }, + }, + warningsCount: 1, + }, + { + endpoint: api.queryRange, + query: url.Values{ + "query": []string{ + `label_replace(vector(42), "foo", "bar", "", "") or label_replace(vector(3.1415), "dings", "bums", "", "")`, + }, + "start": []string{"0"}, + "end": []string{"2"}, + "step": []string{"1"}, + "limit": []string{"2"}, + }, + response: &QueryData{ + ResultType: parser.ValueTypeMatrix, + Result: promql.Matrix{ + promql.Series{ + Metric: labels.FromMap(map[string]string{"dings": "bums"}), + Floats: []promql.FPoint{ + {F: 3.1415, T: timestamp.FromTime(start)}, + {F: 3.1415, T: timestamp.FromTime(start.Add(1 * time.Second))}, + {F: 3.1415, T: timestamp.FromTime(start.Add(2 * time.Second))}, + }, + }, + promql.Series{ + Metric: labels.FromMap(map[string]string{"foo": "bar"}), + Floats: []promql.FPoint{ + {F: 42, T: timestamp.FromTime(start)}, + {F: 42, T: timestamp.FromTime(start.Add(1 * time.Second))}, + {F: 42, T: timestamp.FromTime(start.Add(2 * time.Second))}, + }, + }, + }, + }, + warningsCount: 0, + }, { endpoint: api.queryRange, query: url.Values{ @@ -1222,7 +1438,6 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E {F: 1, T: timestamp.FromTime(start.Add(1 * time.Second))}, {F: 2, T: timestamp.FromTime(start.Add(2 * time.Second))}, }, - // No Metric returned - use zero value for comparison. }, }, }, @@ -1235,6 +1450,17 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E }, responseAsJSON: `{"resultType":"vector","result":[]}`, }, + { + endpoint: api.queryRange, + query: url.Values{ + "query": []string{"bottomk(2, notExists)"}, + "start": []string{"0"}, + "end": []string{"2"}, + "step": []string{"1"}, + "limit": []string{"-1"}, + }, + errType: errorBadData, + }, // Test empty matrix result { endpoint: api.queryRange,