Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Early Request Validation in Query Frontend #10093

Merged
merged 7 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
* [ENHANCEMENT] Distributor: allow a different limit for info series (series ending in `_info`) label count, via `-validation.max-label-names-per-info-series`. #10028
* [ENHANCEMENT] Ingester: do not reuse labels, samples and histograms slices in the write request if there are more entries than 10x the pre-allocated size. This should help to reduce the in-use memory in case of few requests with a very large number of labels, samples or histograms. #10040
* [ENHANCEMENT] Query-Frontend: prune `<subquery> and on() (vector(x)==y)` style queries and stop pruning `<subquery> < -Inf`. Triggered by https://github.com/prometheus/prometheus/pull/15245. #10026
* [ENHANCEMENT] Query-Frontend: perform request format validation before processing the request. #10093
* [BUGFIX] Fix issue where functions such as `rate()` over native histograms could return incorrect values if a float stale marker was present in the selected range. #9508
* [BUGFIX] Fix issue where negation of native histograms (eg. `-some_native_histogram_series`) did nothing. #9508
* [BUGFIX] Fix issue where `metric might not be a counter, name does not end in _total/_sum/_count/_bucket` annotation would be emitted even if `rate` or `increase` did not have enough samples to compute a result. #9508
Expand Down
35 changes: 35 additions & 0 deletions pkg/frontend/querymiddleware/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package querymiddleware
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"math"
Expand All @@ -33,6 +34,7 @@ import (
"golang.org/x/exp/slices"

apierror "github.com/grafana/mimir/pkg/api/error"
"github.com/grafana/mimir/pkg/cardinality"
"github.com/grafana/mimir/pkg/mimirpb"
"github.com/grafana/mimir/pkg/querier/api"
"github.com/grafana/mimir/pkg/querier/stats"
Expand Down Expand Up @@ -535,6 +537,39 @@ func DecodeLabelsQueryTimeParams(reqValues *url.Values, usePromDefaults bool) (s
return start, end, err
}

// DecodeCardinalityQueryParams strictly handles validation for cardinality API endpoint parameters.
// The current decoding of the cardinality requests is handled in the cardinality package
// which is not yet compatible with the codec's approach of using interfaces
// and multiple concrete proto implementations to represent different query types.
func DecodeCardinalityQueryParams(r *http.Request) (any, error) {
var err error

reqValues, err := util.ParseRequestFormWithoutConsumingBody(r)
if err != nil {
return nil, apierror.New(apierror.TypeBadData, err.Error())
}

var parsedReq any
switch {
case strings.HasSuffix(r.URL.Path, cardinalityLabelNamesPathSuffix):
parsedReq, err = cardinality.DecodeLabelNamesRequestFromValues(reqValues)

case strings.HasSuffix(r.URL.Path, cardinalityLabelValuesPathSuffix):
parsedReq, err = cardinality.DecodeLabelValuesRequestFromValues(reqValues)

case strings.HasSuffix(r.URL.Path, cardinalityActiveSeriesPathSuffix):
parsedReq, err = cardinality.DecodeActiveSeriesRequestFromValues(reqValues)

default:
return nil, errors.New("unknown cardinality API endpoint")
}

if err != nil {
return nil, apierror.New(apierror.TypeBadData, err.Error())
}
return parsedReq, nil
}

func decodeQueryMinMaxTime(queryExpr parser.Expr, start, end, step int64, lookbackDelta time.Duration) (minTime, maxTime int64) {
evalStmt := &parser.EvalStmt{
Expr: queryExpr,
Expand Down
92 changes: 92 additions & 0 deletions pkg/frontend/querymiddleware/request_validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// SPDX-License-Identifier: AGPL-3.0-only

package querymiddleware

import (
"context"
"net/http"

"github.com/grafana/dskit/cancellation"
)

const requestValidationFailedFmt = "request validation failed for "

var errMetricsQueryRequestValidationFailed = cancellation.NewErrorf(
requestValidationFailedFmt + "metrics query",
)
var errLabelsQueryRequestValidationFailed = cancellation.NewErrorf(
requestValidationFailedFmt + "labels query",
)
var errCardinalityQueryRequestValidationFailed = cancellation.NewErrorf(
requestValidationFailedFmt + "cardinality query",
)

type MetricsQueryRequestValidationRoundTripper struct {
codec Codec
next http.RoundTripper
}

func NewMetricsQueryRequestValidationRoundTripper(codec Codec, next http.RoundTripper) http.RoundTripper {
return MetricsQueryRequestValidationRoundTripper{
codec: codec,
next: next,
}
}

func (rt MetricsQueryRequestValidationRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
ctx, cancel := context.WithCancelCause(r.Context())
defer cancel(errMetricsQueryRequestValidationFailed)
r = r.WithContext(ctx)

_, err := rt.codec.DecodeMetricsQueryRequest(ctx, r)
if err != nil {
return nil, err
}
return rt.next.RoundTrip(r)
}

type LabelsQueryRequestValidationRoundTripper struct {
codec Codec
next http.RoundTripper
}

func NewLabelsQueryRequestValidationRoundTripper(codec Codec, next http.RoundTripper) http.RoundTripper {
return LabelsQueryRequestValidationRoundTripper{
codec: codec,
next: next,
}
}

func (rt LabelsQueryRequestValidationRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
ctx, cancel := context.WithCancelCause(r.Context())
defer cancel(errLabelsQueryRequestValidationFailed)
r = r.WithContext(ctx)

_, err := rt.codec.DecodeLabelsQueryRequest(ctx, r)
if err != nil {
return nil, err
}
return rt.next.RoundTrip(r)
}

type CardinalityQueryRequestValidationRoundTripper struct {
next http.RoundTripper
}

func NewCardinalityQueryRequestValidationRoundTripper(next http.RoundTripper) http.RoundTripper {
return CardinalityQueryRequestValidationRoundTripper{
next: next,
}
}

func (rt CardinalityQueryRequestValidationRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
ctx, cancel := context.WithCancelCause(r.Context())
defer cancel(errCardinalityQueryRequestValidationFailed)
r = r.WithContext(ctx)

_, err := DecodeCardinalityQueryParams(r)
if err != nil {
return nil, err
}
return rt.next.RoundTrip(r)
}
Loading
Loading