web/api: Add a limit parameter to /query and /query_range (#15552)

add limit param to query and rangeQuery

---------

Signed-off-by: Vandit Singh <vanditsinghkv@gmail.com>
Signed-off-by: Vandit Singh <107131545+Vandit1604@users.noreply.github.com>
Co-authored-by: Björn Rabenstein <github@rabenste.in>
This commit is contained in:
Vandit Singh 2025-01-09 21:57:39 +05:30 committed by GitHub
parent b3e30d52ce
commit 6339989e25
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 280 additions and 3 deletions

View file

@ -86,6 +86,7 @@ URL query parameters:
- `time=<rfc3339 | unix_timestamp>`: Evaluation timestamp. Optional.
- `timeout=<duration>`: Evaluation timeout. Optional. Defaults to and
is capped by the value of the `-query.timeout` flag.
- `limit=<number>`: Maximum number of returned series. Doesnt 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=<duration | float>`: Query resolution step width in `duration` format or float number of seconds.
- `timeout=<duration>`: Evaluation timeout. Optional. Defaults to and
is capped by the value of the `-query.timeout` flag.
- `limit=<number>`: 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

View file

@ -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
}

View file

@ -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,