mirror of https://github.com/grafana/loki
Split by range of instant queries (#5662)
* Split by range on Instant queries POC v3 Co-authored-by: Christian Haudum <christian.haudum@gmail.com> * Handle uneven split by duration * Register SplitByRangeMiddleware in roundtripper Signed-off-by: Christian Haudum <christian.haudum@gmail.com> * fixup! Register SplitByRangeMiddleware in roundtripper Signed-off-by: Christian Haudum <christian.haudum@gmail.com> * fixup! fixup! Register SplitByRangeMiddleware in roundtripper Signed-off-by: Christian Haudum <christian.haudum@gmail.com> * Remove rewrite if range aggr has label extraction stage In case a range aggregation has a generic label extraction stage, such as `| json` or `| logfmt` and no group by, we cannot split it, because otherwise the downstream queries would result in too many series. Signed-off-by: Christian Haudum <christian.haudum@gmail.com> * Fix linting * Implement range splitting for rate() and bytes_rate() Signed-off-by: Christian Haudum <christian.haudum@gmail.com> * Fix linting * Calculate offset of downstream queries correctly if the outer query range contains an offset as well. Signed-off-by: Christian Haudum <christian.haudum@gmail.com> * Fix linting * Add optimization by moving the outer label grouping downstream * Add label grouping downstream optimization to rate and bytes_rate expressions * Add changelog entry Signed-off-by: Christian Haudum <christian.haudum@gmail.com> * Simplify types in rangemapper Signed-off-by: Christian Haudum <christian.haudum@gmail.com> * fixup! Simplify types in rangemapper Signed-off-by: Christian Haudum <christian.haudum@gmail.com> * Check in Map function if query is splittable by range Since this is the main function of the mapper, we can ensure here that only supported vector/range aggregations are handled. Signed-off-by: Christian Haudum <christian.haudum@gmail.com> * Some code cleanups and variable renaming Signed-off-by: Christian Haudum <christian.haudum@gmail.com> * Extract duplicate code in range aggr mapping into function Signed-off-by: Christian Haudum <christian.haudum@gmail.com> * Add topk to supported splittable vector aggregations Signed-off-by: Christian Haudum <christian.haudum@gmail.com> * Check if query is splittable by range before calling Map() Signed-off-by: Christian Haudum <christian.haudum@gmail.com> * Add more function comments Signed-off-by: Christian Haudum <christian.haudum@gmail.com> * Rename RangeVectorMapper to RangeMapper Signed-off-by: Christian Haudum <christian.haudum@gmail.com> * Fix incorrect import due to rebase Signed-off-by: Christian Haudum <christian.haudum@gmail.com> * Add equivalence test cases with `logfmt` pipeline stage Signed-off-by: Christian Haudum <christian.haudum@gmail.com> * Remove limitation of pushing down vector aggr only if grouping is present Signed-off-by: Christian Haudum <christian.haudum@gmail.com> * Remove TestRangeMappingEquivalenceMockMapper test This test is essentially the same as the test Test_SplitRangeVectorMapping, just using a different representation of the result. Signed-off-by: Christian Haudum <christian.haudum@gmail.com> * fixup! Remove limitation of pushing down vector aggr only if grouping is present Signed-off-by: Christian Haudum <christian.haudum@gmail.com> * fixup! fixup! Remove limitation of pushing down vector aggr only if grouping is present Signed-off-by: Christian Haudum <christian.haudum@gmail.com> * Fix linter errors Signed-off-by: Christian Haudum <christian.haudum@gmail.com> * Better naming of variable Signed-off-by: Christian Haudum <christian.haudum@gmail.com> * Split SplitRangeVectorMapping test into two to have the test for noop queries separated Signed-off-by: Christian Haudum <christian.haudum@gmail.com> Co-authored-by: Christian Haudum <christian.haudum@gmail.com> Co-authored-by: Owen Diehl <ow.diehl@gmail.com>pull/5828/head
parent
cc3a8e4415
commit
0bce8d95a2
@ -0,0 +1,332 @@ |
||||
package logql |
||||
|
||||
import ( |
||||
"fmt" |
||||
"time" |
||||
|
||||
"github.com/go-kit/log/level" |
||||
"github.com/pkg/errors" |
||||
|
||||
"github.com/grafana/loki/pkg/logql/syntax" |
||||
util_log "github.com/grafana/loki/pkg/util/log" |
||||
) |
||||
|
||||
var splittableVectorOp = map[string]struct{}{ |
||||
syntax.OpTypeSum: {}, |
||||
syntax.OpTypeCount: {}, |
||||
syntax.OpTypeMax: {}, |
||||
syntax.OpTypeMin: {}, |
||||
syntax.OpTypeAvg: {}, |
||||
syntax.OpTypeTopK: {}, |
||||
} |
||||
|
||||
var splittableRangeVectorOp = map[string]struct{}{ |
||||
syntax.OpRangeTypeRate: {}, |
||||
syntax.OpRangeTypeBytesRate: {}, |
||||
syntax.OpRangeTypeBytes: {}, |
||||
syntax.OpRangeTypeCount: {}, |
||||
syntax.OpRangeTypeSum: {}, |
||||
syntax.OpRangeTypeMax: {}, |
||||
syntax.OpRangeTypeMin: {}, |
||||
} |
||||
|
||||
type RangeMapper struct { |
||||
splitByInterval time.Duration |
||||
} |
||||
|
||||
func NewRangeMapper(interval time.Duration) (RangeMapper, error) { |
||||
if interval <= 0 { |
||||
return RangeMapper{}, fmt.Errorf("cannot create RangeMapper with splitByInterval <= 0; got %s", interval) |
||||
} |
||||
return RangeMapper{ |
||||
splitByInterval: interval, |
||||
}, nil |
||||
} |
||||
|
||||
// Parse returns (noop, parsed expression, error)
|
||||
func (m RangeMapper) Parse(query string) (bool, syntax.Expr, error) { |
||||
origExpr, err := syntax.ParseSampleExpr(query) |
||||
if err != nil { |
||||
return true, nil, err |
||||
} |
||||
|
||||
if !isSplittableByRange(origExpr) { |
||||
return true, origExpr, nil |
||||
} |
||||
|
||||
modExpr, err := m.Map(origExpr, nil) |
||||
if err != nil { |
||||
return true, nil, err |
||||
} |
||||
|
||||
return origExpr.String() == modExpr.String(), modExpr, err |
||||
} |
||||
|
||||
func (m RangeMapper) Map(expr syntax.SampleExpr, vectorAggrPushdown *syntax.VectorAggregationExpr) (syntax.SampleExpr, error) { |
||||
// immediately clone the passed expr to avoid mutating the original
|
||||
expr, err := clone(expr) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
switch e := expr.(type) { |
||||
case *syntax.VectorAggregationExpr: |
||||
return m.mapVectorAggregationExpr(e) |
||||
case *syntax.RangeAggregationExpr: |
||||
return m.mapRangeAggregationExpr(e, vectorAggrPushdown), nil |
||||
case *syntax.BinOpExpr: |
||||
lhsMapped, err := m.Map(e.SampleExpr, vectorAggrPushdown) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
rhsMapped, err := m.Map(e.RHS, vectorAggrPushdown) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
e.SampleExpr = lhsMapped |
||||
e.RHS = rhsMapped |
||||
return e, nil |
||||
default: |
||||
return nil, errors.Errorf("unexpected expr type (%T) for ASTMapper type (%T) ", expr, m) |
||||
} |
||||
} |
||||
|
||||
// getRangeInterval returns the interval in the range vector
|
||||
// Note that this function must not be called with a BinOpExpr as argument
|
||||
// as it returns only the range of the RHS.
|
||||
// Example: expression `count_over_time({app="foo"}[10m])` returns 10m
|
||||
func getRangeInterval(expr syntax.SampleExpr) time.Duration { |
||||
var rangeInterval time.Duration |
||||
expr.Walk(func(e interface{}) { |
||||
switch concrete := e.(type) { |
||||
case *syntax.RangeAggregationExpr: |
||||
rangeInterval = concrete.Left.Interval |
||||
} |
||||
}) |
||||
return rangeInterval |
||||
} |
||||
|
||||
// hasLabelExtractionStage returns true if an expression contains a stage for label extraction,
|
||||
// such as `| json` or `| logfmt`, that would result in an exploding amount of series in downstream queries.
|
||||
func hasLabelExtractionStage(expr syntax.SampleExpr) bool { |
||||
found := false |
||||
expr.Walk(func(e interface{}) { |
||||
switch concrete := e.(type) { |
||||
case *syntax.LabelParserExpr: |
||||
// It will **not** return true for `regexp`, `unpack` and `pattern`, since these label extraction
|
||||
// stages can control how many labels, and therefore the resulting amount of series, are extracted.
|
||||
if concrete.Op == syntax.OpParserTypeJSON || concrete.Op == syntax.OpParserTypeLogfmt { |
||||
found = true |
||||
} |
||||
} |
||||
}) |
||||
return found |
||||
} |
||||
|
||||
// sumOverFullRange returns an expression that sums up individual downstream queries (with preserving labels)
|
||||
// and dividing it by the full range in seconds to calculate a rate value.
|
||||
// The operation defines the range aggregation operation of the downstream queries.
|
||||
// Example:
|
||||
// rate({app="foo"}[2m])
|
||||
// => (sum without (count_over_time({app="foo"}[1m]) ++ count_over_time({app="foo"}[1m]) offset 1m) / 120)
|
||||
func (m RangeMapper) sumOverFullRange(expr *syntax.RangeAggregationExpr, overrideDownstream *syntax.VectorAggregationExpr, operation string, rangeInterval time.Duration) syntax.SampleExpr { |
||||
var downstreamExpr syntax.SampleExpr = &syntax.RangeAggregationExpr{ |
||||
Left: expr.Left, |
||||
Operation: operation, |
||||
} |
||||
// Optimization: in case overrideDownstream exists, the downstream expression can be optimized with the grouping
|
||||
// and operation of the overrideDownstream expression in order to reduce the returned streams' label set.
|
||||
if overrideDownstream != nil { |
||||
downstreamExpr = &syntax.VectorAggregationExpr{ |
||||
Left: downstreamExpr, |
||||
Grouping: overrideDownstream.Grouping, |
||||
Operation: overrideDownstream.Operation, |
||||
} |
||||
} |
||||
return &syntax.BinOpExpr{ |
||||
SampleExpr: &syntax.VectorAggregationExpr{ |
||||
Left: m.mapConcatSampleExpr(downstreamExpr, rangeInterval), |
||||
Grouping: &syntax.Grouping{ |
||||
Without: true, |
||||
}, |
||||
Operation: syntax.OpTypeSum, |
||||
}, |
||||
RHS: &syntax.LiteralExpr{Val: rangeInterval.Seconds()}, |
||||
Op: syntax.OpTypeDiv, |
||||
Opts: &syntax.BinOpOptions{}, |
||||
} |
||||
} |
||||
|
||||
// vectorAggrWithRangeDownstreams returns an expression that aggregates a concat sample expression of multiple range
|
||||
// aggregations. If a vector aggregation is pushed down, the downstream queries of the concat sample expression are
|
||||
// wrapped in the vector aggregation of the parent node.
|
||||
// Example:
|
||||
// min(bytes_over_time({job="bar"} [2m])
|
||||
// => min without (bytes_over_time({job="bar"} [1m]) ++ bytes_over_time({job="bar"} [1m] offset 1m))
|
||||
// min by (app) (bytes_over_time({job="bar"} [2m])
|
||||
// => min without (min by (app) (bytes_over_time({job="bar"} [1m])) ++ min by (app) (bytes_over_time({job="bar"} [1m] offset 1m)))
|
||||
func (m RangeMapper) vectorAggrWithRangeDownstreams(expr *syntax.RangeAggregationExpr, vectorAggrPushdown *syntax.VectorAggregationExpr, op string, rangeInterval time.Duration) syntax.SampleExpr { |
||||
grouping := expr.Grouping |
||||
if expr.Grouping == nil { |
||||
grouping = &syntax.Grouping{ |
||||
Without: true, |
||||
} |
||||
} |
||||
var downstream syntax.SampleExpr = expr |
||||
if vectorAggrPushdown != nil { |
||||
downstream = vectorAggrPushdown |
||||
} |
||||
return &syntax.VectorAggregationExpr{ |
||||
Left: m.mapConcatSampleExpr(downstream, rangeInterval), |
||||
Grouping: grouping, |
||||
Operation: op, |
||||
} |
||||
} |
||||
|
||||
// appendDownstream adds expression expr with a range interval 'interval' and offset 'offset' to the downstreams list.
|
||||
// Returns the updated downstream ConcatSampleExpr.
|
||||
func appendDownstream(downstreams *ConcatSampleExpr, expr syntax.SampleExpr, interval time.Duration, offset time.Duration) *ConcatSampleExpr { |
||||
sampleExpr, _ := clone(expr) |
||||
sampleExpr.Walk(func(e interface{}) { |
||||
switch concrete := e.(type) { |
||||
case *syntax.RangeAggregationExpr: |
||||
concrete.Left.Interval = interval |
||||
if offset != 0 { |
||||
concrete.Left.Offset += offset |
||||
} |
||||
} |
||||
}) |
||||
downstreams = &ConcatSampleExpr{ |
||||
DownstreamSampleExpr: DownstreamSampleExpr{ |
||||
SampleExpr: sampleExpr, |
||||
}, |
||||
next: downstreams, |
||||
} |
||||
return downstreams |
||||
} |
||||
|
||||
// mapConcatSampleExpr transform expr in multiple downstream subexpressions split by offset range interval
|
||||
// rangeInterval should be greater than m.splitByInterval, otherwise the resultant expression
|
||||
// will have an unnecessary aggregation operation
|
||||
func (m RangeMapper) mapConcatSampleExpr(expr syntax.SampleExpr, rangeInterval time.Duration) syntax.SampleExpr { |
||||
splitCount := int(rangeInterval / m.splitByInterval) |
||||
|
||||
if splitCount == 0 { |
||||
return expr |
||||
} |
||||
|
||||
var split int |
||||
var downstreams *ConcatSampleExpr |
||||
for split = 0; split < splitCount; split++ { |
||||
downstreams = appendDownstream(downstreams, expr, m.splitByInterval, time.Duration(split)*m.splitByInterval) |
||||
} |
||||
// Add the remainder offset interval
|
||||
if rangeInterval%m.splitByInterval != 0 { |
||||
offset := time.Duration(split) * m.splitByInterval |
||||
downstreams = appendDownstream(downstreams, expr, rangeInterval-offset, offset) |
||||
} |
||||
|
||||
return downstreams |
||||
} |
||||
|
||||
func (m RangeMapper) mapVectorAggregationExpr(expr *syntax.VectorAggregationExpr) (syntax.SampleExpr, error) { |
||||
rangeInterval := getRangeInterval(expr) |
||||
|
||||
// in case the interval is smaller than the configured split interval,
|
||||
// don't split it.
|
||||
// TODO: what if there is another internal expr with an interval that can be split?
|
||||
if rangeInterval <= m.splitByInterval { |
||||
return expr, nil |
||||
} |
||||
|
||||
// In order to minimize the amount of streams on the downstream query,
|
||||
// we can push down the outer vector aggregation to the downstream query.
|
||||
// This does not work for `count()` and `topk()`, though.
|
||||
// We also do not want to push down, if the inner expression is a binary operation.
|
||||
// TODO: Currently, it is sending the last inner expression grouping dowstream.
|
||||
// Which grouping should be sent downstream?
|
||||
var vectorAggrPushdown *syntax.VectorAggregationExpr |
||||
if _, ok := expr.Left.(*syntax.BinOpExpr); !ok && expr.Operation != syntax.OpTypeCount && expr.Operation != syntax.OpTypeTopK { |
||||
vectorAggrPushdown = expr |
||||
} |
||||
|
||||
// Split the vector aggregation's inner expression
|
||||
lhsMapped, err := m.Map(expr.Left, vectorAggrPushdown) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return &syntax.VectorAggregationExpr{ |
||||
Left: lhsMapped, |
||||
Grouping: expr.Grouping, |
||||
Params: expr.Params, |
||||
Operation: expr.Operation, |
||||
}, nil |
||||
} |
||||
|
||||
// mapRangeAggregationExpr maps expr into a new SampleExpr with multiple downstream subqueries split by range interval
|
||||
// Optimization: in order to reduce the returned stream from the inner downstream functions, in case a range aggregation
|
||||
// expression is aggregated by a vector aggregation expression with a label grouping, the downstream expression can be
|
||||
// exactly the same as the initial query concatenated by a `sum` operation. If this is the case, overrideDownstream
|
||||
// contains the initial query which will be the downstream expression with a split range interval.
|
||||
// Example: `sum by (a) (bytes_over_time)`
|
||||
// Is mapped to `sum by (a) (sum without downstream<sum by (a) (bytes_over_time)>++downstream<sum by (a) (bytes_over_time)>++...)`
|
||||
func (m RangeMapper) mapRangeAggregationExpr(expr *syntax.RangeAggregationExpr, vectorAggrPushdown *syntax.VectorAggregationExpr) syntax.SampleExpr { |
||||
rangeInterval := getRangeInterval(expr) |
||||
|
||||
// in case the interval is smaller than the configured split interval,
|
||||
// don't split it.
|
||||
if rangeInterval <= m.splitByInterval { |
||||
return expr |
||||
} |
||||
|
||||
// We cannot execute downstream queries that would potentially produce a huge amount of series
|
||||
// and therefore would very likely fail.
|
||||
if expr.Grouping == nil && hasLabelExtractionStage(expr) { |
||||
return expr |
||||
} |
||||
switch expr.Operation { |
||||
case syntax.OpRangeTypeBytes, syntax.OpRangeTypeCount, syntax.OpRangeTypeSum: |
||||
return m.vectorAggrWithRangeDownstreams(expr, vectorAggrPushdown, syntax.OpTypeSum, rangeInterval) |
||||
case syntax.OpRangeTypeMax: |
||||
return m.vectorAggrWithRangeDownstreams(expr, vectorAggrPushdown, syntax.OpTypeMax, rangeInterval) |
||||
case syntax.OpRangeTypeMin: |
||||
return m.vectorAggrWithRangeDownstreams(expr, vectorAggrPushdown, syntax.OpTypeMin, rangeInterval) |
||||
case syntax.OpRangeTypeRate: |
||||
return m.sumOverFullRange(expr, vectorAggrPushdown, syntax.OpRangeTypeCount, rangeInterval) |
||||
case syntax.OpRangeTypeBytesRate: |
||||
return m.sumOverFullRange(expr, vectorAggrPushdown, syntax.OpRangeTypeBytes, rangeInterval) |
||||
default: |
||||
// this should not be reachable.
|
||||
// If an operation is splittable it should have an optimization listed.
|
||||
level.Warn(util_log.Logger).Log( |
||||
"msg", "unexpected range aggregation expression", |
||||
"operation", expr.Operation, |
||||
) |
||||
return expr |
||||
} |
||||
} |
||||
|
||||
// isSplittableByRange returns whether it is possible to optimize the given sample expression
|
||||
func isSplittableByRange(expr syntax.SampleExpr) bool { |
||||
switch e := expr.(type) { |
||||
case *syntax.VectorAggregationExpr: |
||||
_, ok := splittableVectorOp[e.Operation] |
||||
return ok && isSplittableByRange(e.Left) |
||||
case *syntax.BinOpExpr: |
||||
return isSplittableByRange(e.SampleExpr) && isSplittableByRange(e.RHS) |
||||
case *syntax.LabelReplaceExpr: |
||||
return isSplittableByRange(e.Left) |
||||
case *syntax.RangeAggregationExpr: |
||||
_, ok := splittableRangeVectorOp[e.Operation] |
||||
return ok |
||||
default: |
||||
return false |
||||
} |
||||
} |
||||
|
||||
// clone returns a copy of the given sample expression
|
||||
// This is needed whenever we want to modify the existing query tree.
|
||||
func clone(expr syntax.SampleExpr) (syntax.SampleExpr, error) { |
||||
return syntax.ParseSampleExpr(expr.String()) |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,120 @@ |
||||
package queryrange |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"net/http" |
||||
|
||||
"github.com/go-kit/log" |
||||
"github.com/go-kit/log/level" |
||||
"github.com/grafana/dskit/tenant" |
||||
"github.com/prometheus/prometheus/promql/parser" |
||||
"github.com/weaveworks/common/httpgrpc" |
||||
|
||||
"github.com/grafana/loki/pkg/loghttp" |
||||
"github.com/grafana/loki/pkg/logql" |
||||
"github.com/grafana/loki/pkg/querier/queryrange/queryrangebase" |
||||
util_log "github.com/grafana/loki/pkg/util/log" |
||||
"github.com/grafana/loki/pkg/util/marshal" |
||||
"github.com/grafana/loki/pkg/util/validation" |
||||
) |
||||
|
||||
type splitByRange struct { |
||||
logger log.Logger |
||||
next queryrangebase.Handler |
||||
limits Limits |
||||
|
||||
ng *logql.DownstreamEngine |
||||
} |
||||
|
||||
// NewSplitByRangeMiddleware creates a new Middleware that splits log requests by the range interval.
|
||||
func NewSplitByRangeMiddleware(logger log.Logger, limits Limits, metrics *logql.ShardingMetrics) queryrangebase.Middleware { |
||||
return queryrangebase.MiddlewareFunc(func(next queryrangebase.Handler) queryrangebase.Handler { |
||||
return &splitByRange{ |
||||
logger: log.With(logger, "middleware", "InstantQuery.splitByRangeVector"), |
||||
next: next, |
||||
limits: limits, |
||||
ng: logql.NewDownstreamEngine(logql.EngineOpts{}, DownstreamHandler{next}, metrics, limits, logger), |
||||
} |
||||
}) |
||||
} |
||||
|
||||
func (s *splitByRange) Do(ctx context.Context, request queryrangebase.Request) (queryrangebase.Response, error) { |
||||
logger := util_log.WithContext(ctx, s.logger) |
||||
|
||||
tenants, err := tenant.TenantIDs(ctx) |
||||
if err != nil { |
||||
return nil, httpgrpc.Errorf(http.StatusBadRequest, err.Error()) |
||||
} |
||||
|
||||
interval := validation.SmallestPositiveNonZeroDurationPerTenant(tenants, s.limits.QuerySplitDuration) |
||||
// if no interval configured, continue to the next middleware
|
||||
if interval == 0 { |
||||
return s.next.Do(ctx, request) |
||||
} |
||||
|
||||
mapper, err := logql.NewRangeMapper(interval) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
noop, parsed, err := mapper.Parse(request.GetQuery()) |
||||
if err != nil { |
||||
level.Warn(logger).Log("msg", "failed mapping AST", "err", err.Error(), "query", request.GetQuery()) |
||||
return nil, err |
||||
} |
||||
level.Debug(logger).Log("msg", "mapped instant query", "interval", interval.String(), "noop", noop, "original", request.GetQuery(), "mapped", parsed.String()) |
||||
|
||||
if noop { |
||||
// the query cannot be split, so continue
|
||||
return s.next.Do(ctx, request) |
||||
} |
||||
|
||||
params, err := paramsFromRequest(request) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if _, ok := request.(*LokiInstantRequest); !ok { |
||||
return nil, fmt.Errorf("expected *LokiInstantRequest") |
||||
} |
||||
|
||||
query := s.ng.Query(params, parsed) |
||||
|
||||
res, err := query.Exec(ctx) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
value, err := marshal.NewResultValue(res.Data) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
switch res.Data.Type() { |
||||
case parser.ValueTypeMatrix: |
||||
return &LokiPromResponse{ |
||||
Response: &queryrangebase.PrometheusResponse{ |
||||
Status: loghttp.QueryStatusSuccess, |
||||
Data: queryrangebase.PrometheusData{ |
||||
ResultType: loghttp.ResultTypeMatrix, |
||||
Result: toProtoMatrix(value.(loghttp.Matrix)), |
||||
}, |
||||
}, |
||||
Statistics: res.Statistics, |
||||
}, nil |
||||
case parser.ValueTypeVector: |
||||
return &LokiPromResponse{ |
||||
Statistics: res.Statistics, |
||||
Response: &queryrangebase.PrometheusResponse{ |
||||
Status: loghttp.QueryStatusSuccess, |
||||
Data: queryrangebase.PrometheusData{ |
||||
ResultType: loghttp.ResultTypeVector, |
||||
Result: toProtoVector(value.(loghttp.Vector)), |
||||
}, |
||||
}, |
||||
}, nil |
||||
default: |
||||
return nil, fmt.Errorf("unexpected downstream response type (%T)", res.Data.Type()) |
||||
} |
||||
} |
@ -0,0 +1,181 @@ |
||||
package queryrange |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/grafana/loki/pkg/loghttp" |
||||
|
||||
"github.com/go-kit/log" |
||||
"github.com/stretchr/testify/require" |
||||
"github.com/weaveworks/common/user" |
||||
|
||||
"github.com/grafana/loki/pkg/logproto" |
||||
"github.com/grafana/loki/pkg/querier/queryrange/queryrangebase" |
||||
) |
||||
|
||||
func Test_RangeVectorSplit(t *testing.T) { |
||||
srm := NewSplitByRangeMiddleware(log.NewNopLogger(), fakeLimits{ |
||||
maxSeries: 10000, |
||||
splits: map[string]time.Duration{ |
||||
"tenant": time.Minute, |
||||
}, |
||||
}, nilShardingMetrics) |
||||
|
||||
ctx := user.InjectOrgID(context.TODO(), "tenant") |
||||
|
||||
for _, tc := range []struct { |
||||
in queryrangebase.Request |
||||
subQueries []queryrangebase.RequestResponse |
||||
expected queryrangebase.Response |
||||
}{ |
||||
{ |
||||
in: &LokiInstantRequest{ |
||||
Query: `sum(bytes_over_time({app="foo"}[3m]))`, |
||||
TimeTs: time.Unix(1, 0), |
||||
Path: "/loki/api/v1/query", |
||||
}, |
||||
subQueries: []queryrangebase.RequestResponse{ |
||||
subQueryRequestResponse(`sum(bytes_over_time({app="foo"}[1m]))`, 1), |
||||
subQueryRequestResponse(`sum(bytes_over_time({app="foo"}[1m] offset 1m0s))`, 2), |
||||
subQueryRequestResponse(`sum(bytes_over_time({app="foo"}[1m] offset 2m0s))`, 3), |
||||
}, |
||||
expected: expectedMergedResponse(1 + 2 + 3), |
||||
}, |
||||
{ |
||||
in: &LokiInstantRequest{ |
||||
Query: `sum by (bar) (bytes_over_time({app="foo"}[3m]))`, |
||||
TimeTs: time.Unix(1, 0), |
||||
Path: "/loki/api/v1/query", |
||||
}, |
||||
subQueries: []queryrangebase.RequestResponse{ |
||||
subQueryRequestResponse(`sum by(bar)(bytes_over_time({app="foo"}[1m]))`, 10), |
||||
subQueryRequestResponse(`sum by(bar)(bytes_over_time({app="foo"}[1m] offset 1m0s))`, 20), |
||||
subQueryRequestResponse(`sum by(bar)(bytes_over_time({app="foo"}[1m] offset 2m0s))`, 30), |
||||
}, |
||||
expected: expectedMergedResponse(10 + 20 + 30), |
||||
}, |
||||
{ |
||||
in: &LokiInstantRequest{ |
||||
Query: `sum(count_over_time({app="foo"}[3m]))`, |
||||
TimeTs: time.Unix(1, 0), |
||||
Path: "/loki/api/v1/query", |
||||
}, |
||||
subQueries: []queryrangebase.RequestResponse{ |
||||
subQueryRequestResponse(`sum(count_over_time({app="foo"}[1m]))`, 1), |
||||
subQueryRequestResponse(`sum(count_over_time({app="foo"}[1m] offset 1m0s))`, 1), |
||||
subQueryRequestResponse(`sum(count_over_time({app="foo"}[1m] offset 2m0s))`, 1), |
||||
}, |
||||
expected: expectedMergedResponse(1 + 1 + 1), |
||||
}, |
||||
{ |
||||
in: &LokiInstantRequest{ |
||||
Query: `sum by (bar) (count_over_time({app="foo"}[3m]))`, |
||||
TimeTs: time.Unix(1, 0), |
||||
Path: "/loki/api/v1/query", |
||||
}, |
||||
subQueries: []queryrangebase.RequestResponse{ |
||||
subQueryRequestResponse(`sum by(bar)(count_over_time({app="foo"}[1m]))`, 0), |
||||
subQueryRequestResponse(`sum by(bar)(count_over_time({app="foo"}[1m] offset 1m0s))`, 0), |
||||
subQueryRequestResponse(`sum by(bar)(count_over_time({app="foo"}[1m] offset 2m0s))`, 0), |
||||
}, |
||||
expected: expectedMergedResponse(0 + 0 + 0), |
||||
}, |
||||
{ |
||||
in: &LokiInstantRequest{ |
||||
Query: `sum(sum_over_time({app="foo"} | unwrap bar [3m]))`, |
||||
TimeTs: time.Unix(1, 0), |
||||
Path: "/loki/api/v1/query", |
||||
}, |
||||
subQueries: []queryrangebase.RequestResponse{ |
||||
subQueryRequestResponse(`sum(sum_over_time({app="foo"} | unwrap bar[1m]))`, 1), |
||||
subQueryRequestResponse(`sum(sum_over_time({app="foo"} | unwrap bar[1m] offset 1m0s))`, 2), |
||||
subQueryRequestResponse(`sum(sum_over_time({app="foo"} | unwrap bar[1m] offset 2m0s))`, 3), |
||||
}, |
||||
expected: expectedMergedResponse(1 + 2 + 3), |
||||
}, |
||||
{ |
||||
in: &LokiInstantRequest{ |
||||
Query: `sum by (bar) (sum_over_time({app="foo"} | unwrap bar [3m]))`, |
||||
TimeTs: time.Unix(1, 0), |
||||
Path: "/loki/api/v1/query", |
||||
}, |
||||
subQueries: []queryrangebase.RequestResponse{ |
||||
subQueryRequestResponse(`sum by(bar)(sum_over_time({app="foo"} | unwrap bar[1m]))`, 1), |
||||
subQueryRequestResponse(`sum by(bar)(sum_over_time({app="foo"} | unwrap bar[1m] offset 1m0s))`, 2), |
||||
subQueryRequestResponse(`sum by(bar)(sum_over_time({app="foo"} | unwrap bar[1m] offset 2m0s))`, 3), |
||||
}, |
||||
expected: expectedMergedResponse(1 + 2 + 3), |
||||
}, |
||||
} { |
||||
tc := tc |
||||
t.Run(tc.in.GetQuery(), func(t *testing.T) { |
||||
resp, err := srm.Wrap(queryrangebase.HandlerFunc( |
||||
func(ctx context.Context, req queryrangebase.Request) (queryrangebase.Response, error) { |
||||
// Assert subquery request
|
||||
for _, reqResp := range tc.subQueries { |
||||
if req.GetQuery() == reqResp.Request.GetQuery() { |
||||
require.Equal(t, reqResp.Request, req) |
||||
// return the test data subquery response
|
||||
return reqResp.Response, nil |
||||
} |
||||
} |
||||
|
||||
return nil, fmt.Errorf("subquery request '" + req.GetQuery() + "' not found") |
||||
})).Do(ctx, tc.in) |
||||
require.NoError(t, err) |
||||
require.Equal(t, tc.expected, resp.(*LokiPromResponse).Response) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
// subQueryRequestResponse returns a RequestResponse containing the expected subQuery instant request
|
||||
// and a response containing a sample value returned from the following wrapper
|
||||
func subQueryRequestResponse(expectedSubQuery string, sampleValue float64) queryrangebase.RequestResponse { |
||||
return queryrangebase.RequestResponse{ |
||||
Request: &LokiInstantRequest{ |
||||
Query: expectedSubQuery, |
||||
TimeTs: time.Unix(1, 0), |
||||
Path: "/loki/api/v1/query", |
||||
}, |
||||
Response: &LokiPromResponse{ |
||||
Response: &queryrangebase.PrometheusResponse{ |
||||
Status: loghttp.QueryStatusSuccess, |
||||
Data: queryrangebase.PrometheusData{ |
||||
ResultType: loghttp.ResultTypeVector, |
||||
Result: []queryrangebase.SampleStream{ |
||||
{ |
||||
Labels: []logproto.LabelAdapter{ |
||||
{Name: "app", Value: "foo"}, |
||||
}, |
||||
Samples: []logproto.LegacySample{ |
||||
{TimestampMs: 1000, Value: sampleValue}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
} |
||||
} |
||||
|
||||
// expectedMergedResponse returns the expected middleware Prometheus response with the samples
|
||||
// as the expectedSampleValue
|
||||
func expectedMergedResponse(expectedSampleValue float64) *queryrangebase.PrometheusResponse { |
||||
return &queryrangebase.PrometheusResponse{ |
||||
Status: loghttp.QueryStatusSuccess, |
||||
Data: queryrangebase.PrometheusData{ |
||||
ResultType: loghttp.ResultTypeVector, |
||||
Result: []queryrangebase.SampleStream{ |
||||
{ |
||||
Labels: []logproto.LabelAdapter{}, |
||||
Samples: []logproto.LegacySample{ |
||||
{TimestampMs: 1000, Value: expectedSampleValue}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
} |
||||
} |
Loading…
Reference in new issue