Like Prometheus, but for logs.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
loki/pkg/querier/queryrange/volume_test.go

352 lines
8.7 KiB

package queryrange
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/grafana/dskit/user"
"github.com/grafana/loki/pkg/push"
"github.com/grafana/loki/v3/pkg/loghttp"
"github.com/grafana/loki/v3/pkg/logproto"
"github.com/grafana/loki/v3/pkg/querier/queryrange/queryrangebase"
"github.com/grafana/loki/v3/pkg/storage/stores/index/seriesvolume"
)
const forRangeQuery = false
const forInstantQuery = true
const aggregateBySeries = true
const aggregateByLabels = false
func Test_toPrometheusResponse(t *testing.T) {
t2 := time.Now()
t1 := t2.Add(-1 * time.Minute)
setup := func(instant bool, nameOne, nameTwo string) chan *bucketedVolumeResponse {
collector := make(chan *bucketedVolumeResponse, 2)
defer close(collector)
collector <- &bucketedVolumeResponse{
t1, &VolumeResponse{
Response: &logproto.VolumeResponse{
Volumes: []logproto.Volume{
{
Name: nameOne,
Volume: 100,
},
{
Name: nameTwo,
Volume: 50,
},
},
Limit: 10,
},
},
}
if !instant {
collector <- &bucketedVolumeResponse{
t2, &VolumeResponse{
Response: &logproto.VolumeResponse{
Volumes: []logproto.Volume{
{
Name: nameOne,
Volume: 50,
},
{
Name: nameTwo,
Volume: 25,
},
},
Limit: 10,
},
},
}
}
return collector
}
setupSeries := func(instant bool) chan *bucketedVolumeResponse {
return setup(instant, `{foo="baz"}`, `{foo="bar", fizz="buzz"}`)
}
setupLabels := func(instant bool) chan *bucketedVolumeResponse {
return setup(instant, `foo`, `fizz`)
}
t.Run("it converts series volumes with multiple timestamps into a prometheus timeseries matix response", func(t *testing.T) {
collector := setupSeries(forRangeQuery)
promResp := ToPrometheusResponse(collector, aggregateBySeries)
require.Equal(t, queryrangebase.PrometheusData{
ResultType: loghttp.ResultTypeMatrix,
Result: []queryrangebase.SampleStream{
{
Labels: []push.LabelAdapter{
{
Name: "foo",
Value: "baz",
},
},
Samples: []logproto.LegacySample{
{
Value: 100,
TimestampMs: t1.UnixNano() / 1e6,
},
{
Value: 50,
TimestampMs: t2.UnixNano() / 1e6,
},
},
},
{
Labels: []push.LabelAdapter{
{
Name: "fizz",
Value: "buzz",
},
{
Name: "foo",
Value: "bar",
},
},
Samples: []logproto.LegacySample{
{
Value: 50,
TimestampMs: t1.UnixNano() / 1e6,
},
{
Value: 25,
TimestampMs: t2.UnixNano() / 1e6,
},
},
},
},
}, promResp.Response.Data)
})
t.Run("it converts series volumes with a single timestamp into a prometheus timeseries vector response", func(t *testing.T) {
collector := setupSeries(forInstantQuery)
promResp := ToPrometheusResponse(collector, aggregateBySeries)
require.Equal(t, queryrangebase.PrometheusData{
ResultType: loghttp.ResultTypeVector,
Result: []queryrangebase.SampleStream{
{
Labels: []push.LabelAdapter{
{
Name: "foo",
Value: "baz",
},
},
Samples: []logproto.LegacySample{
{
Value: 100,
TimestampMs: t1.UnixNano() / 1e6,
},
},
},
{
Labels: []push.LabelAdapter{
{
Name: "fizz",
Value: "buzz",
},
{
Name: "foo",
Value: "bar",
},
},
Samples: []logproto.LegacySample{
{
Value: 50,
TimestampMs: t1.UnixNano() / 1e6,
},
},
},
},
}, promResp.Response.Data)
})
t.Run("it converts label volumes with multiple timestamps into a prometheus timeseries matrix response", func(t *testing.T) {
collector := setupLabels(forRangeQuery)
promResp := ToPrometheusResponse(collector, aggregateByLabels)
require.Equal(t, queryrangebase.PrometheusData{
ResultType: loghttp.ResultTypeMatrix,
Result: []queryrangebase.SampleStream{
{
Labels: []push.LabelAdapter{
{
Name: "foo",
Value: "",
},
},
Samples: []logproto.LegacySample{
{
Value: 100,
TimestampMs: t1.UnixNano() / 1e6,
},
{
Value: 50,
TimestampMs: t2.UnixNano() / 1e6,
},
},
},
{
Labels: []push.LabelAdapter{
{
Name: "fizz",
Value: "",
},
},
Samples: []logproto.LegacySample{
{
Value: 50,
TimestampMs: t1.UnixNano() / 1e6,
},
{
Value: 25,
TimestampMs: t2.UnixNano() / 1e6,
},
},
},
},
}, promResp.Response.Data)
})
t.Run("it converts label volumes with a single timestamp into a prometheus timeseries vector response", func(t *testing.T) {
collector := setupLabels(forInstantQuery)
promResp := ToPrometheusResponse(collector, aggregateByLabels)
require.Equal(t, queryrangebase.PrometheusData{
ResultType: loghttp.ResultTypeVector,
Result: []queryrangebase.SampleStream{
{
Labels: []push.LabelAdapter{
{
Name: "foo",
Value: "",
},
},
Samples: []logproto.LegacySample{
{
Value: 100,
TimestampMs: t1.UnixNano() / 1e6,
},
},
},
{
Labels: []push.LabelAdapter{
{
Name: "fizz",
Value: "",
},
},
Samples: []logproto.LegacySample{
{
Value: 50,
TimestampMs: t1.UnixNano() / 1e6,
},
},
},
},
}, promResp.Response.Data)
})
}
func Test_VolumeMiddleware(t *testing.T) {
makeVolumeRequest := func(req *logproto.VolumeRequest) *queryrangebase.PrometheusResponse {
nextHandler := queryrangebase.HandlerFunc(func(_ context.Context, _ queryrangebase.Request) (queryrangebase.Response, error) {
return &VolumeResponse{
Response: &logproto.VolumeResponse{
Volumes: []logproto.Volume{
{
Name: `{foo="bar"}`,
Volume: 42,
},
},
},
}, nil
})
m := NewVolumeMiddleware()
wrapped := m.Wrap(nextHandler)
ctx := user.InjectOrgID(context.Background(), "fake")
resp, err := wrapped.Do(ctx, req)
require.NoError(t, err)
require.NotNil(t, resp)
return resp.(*LokiPromResponse).Response
}
t.Run("it breaks query up into subqueries according to step", func(t *testing.T) {
volumeReq := &logproto.VolumeRequest{
From: 10,
Through: 20,
Matchers: `{foo="bar"}`,
Limit: seriesvolume.DefaultLimit,
Step: 1,
AggregateBy: seriesvolume.Series,
}
promResp := makeVolumeRequest(volumeReq)
require.Equal(t, promResp.Data.ResultType, loghttp.ResultTypeMatrix)
require.Equal(t, len(promResp.Data.Result), 1)
require.Equal(t, len(promResp.Data.Result[0].Samples), 10)
})
t.Run("only returns one datapoint when step is > than time range", func(t *testing.T) {
volumeReq := &logproto.VolumeRequest{
From: 10,
Through: 20,
Matchers: `{foo="bar"}`,
Limit: seriesvolume.DefaultLimit,
Step: 20,
AggregateBy: seriesvolume.Series,
}
promResp := makeVolumeRequest(volumeReq)
require.Equal(t, promResp.Data.ResultType, loghttp.ResultTypeVector)
require.Equal(t, len(promResp.Data.Result), 1)
require.Equal(t, len(promResp.Data.Result[0].Samples), 1)
})
t.Run("when requested time range is not evenly divisible by step, an extra datpoint is added", func(t *testing.T) {
volumeReq := &logproto.VolumeRequest{
From: 1698830441000, // 2023-11-01T09:20:41Z
Through: 1698830498000, // 2023-11-01T09:21:38Z, difference is 57s
Matchers: `{foo="bar"}`,
Limit: seriesvolume.DefaultLimit,
Step: 60000, // 60s
AggregateBy: seriesvolume.Series,
}
promResp := makeVolumeRequest(volumeReq)
require.Equal(t, promResp.Data.ResultType, loghttp.ResultTypeMatrix)
require.Equal(t, 1, len(promResp.Data.Result))
require.Equal(t, 2, len(promResp.Data.Result[0].Samples))
})
t.Run("timestamps are aligned with the end of steps", func(t *testing.T) {
volumeReq := &logproto.VolumeRequest{
From: 1000000000000,
Through: 1000000005000, // 5s range
Matchers: `{foo="bar"}`,
Limit: seriesvolume.DefaultLimit,
Step: 1000, // 1s
AggregateBy: seriesvolume.Series,
}
promResp := makeVolumeRequest(volumeReq)
require.Equal(t, int64(1000000000999),
promResp.Data.Result[0].Samples[0].TimestampMs,
"first timestamp should be one millisecond before the end of the first step")
require.Equal(t,
int64(1000000005000),
promResp.Data.Result[0].Samples[4].TimestampMs,
"last timestamp should be equal to the end of the requested query range")
})
}