mirror of https://github.com/grafana/loki
Bytes aggregations (#2150)
* Add bytes_rate and bytes_over_time. Still need to works on test. Signed-off-by: Cyril Tovena <cyril.tovena@gmail.com> * Add more tests. Signed-off-by: Cyril Tovena <cyril.tovena@gmail.com> * lint. Signed-off-by: Cyril Tovena <cyril.tovena@gmail.com> * More lint fixes. Signed-off-by: Cyril Tovena <cyril.tovena@gmail.com> * More comments. Signed-off-by: Cyril Tovena <cyril.tovena@gmail.com> * Add documentation. Signed-off-by: Cyril Tovena <cyril.tovena@gmail.com> * Review feedbacks. Signed-off-by: Cyril Tovena <cyril.tovena@gmail.com> * Fix bad conflic resolution. Signed-off-by: Cyril Tovena <cyril.tovena@gmail.com>k20
parent
635dd0add5
commit
18f1b0640d
@ -0,0 +1,63 @@ |
|||||||
|
package logql |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/prometheus/prometheus/promql" |
||||||
|
) |
||||||
|
|
||||||
|
const unsupportedErr = "unsupported range vector aggregation operation: %s" |
||||||
|
|
||||||
|
func (r rangeAggregationExpr) extractor() (SampleExtractor, error) { |
||||||
|
switch r.operation { |
||||||
|
case OpRangeTypeRate, OpRangeTypeCount: |
||||||
|
return extractCount, nil |
||||||
|
case OpRangeTypeBytes, OpRangeTypeBytesRate: |
||||||
|
return extractBytes, nil |
||||||
|
default: |
||||||
|
return nil, fmt.Errorf(unsupportedErr, r.operation) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (r rangeAggregationExpr) aggregator() (RangeVectorAggregator, error) { |
||||||
|
switch r.operation { |
||||||
|
case OpRangeTypeRate: |
||||||
|
return rateLogs(r.left.interval), nil |
||||||
|
case OpRangeTypeCount: |
||||||
|
return countOverTime, nil |
||||||
|
case OpRangeTypeBytesRate: |
||||||
|
return rateLogBytes(r.left.interval), nil |
||||||
|
case OpRangeTypeBytes: |
||||||
|
return sumOverTime, nil |
||||||
|
default: |
||||||
|
return nil, fmt.Errorf(unsupportedErr, r.operation) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// rateLogs calculates the per-second rate of log lines.
|
||||||
|
func rateLogs(selRange time.Duration) func(samples []promql.Point) float64 { |
||||||
|
return func(samples []promql.Point) float64 { |
||||||
|
return float64(len(samples)) / selRange.Seconds() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// rateLogBytes calculates the per-second rate of log bytes.
|
||||||
|
func rateLogBytes(selRange time.Duration) func(samples []promql.Point) float64 { |
||||||
|
return func(samples []promql.Point) float64 { |
||||||
|
return sumOverTime(samples) / selRange.Seconds() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// countOverTime counts the amount of log lines.
|
||||||
|
func countOverTime(samples []promql.Point) float64 { |
||||||
|
return float64(len(samples)) |
||||||
|
} |
||||||
|
|
||||||
|
func sumOverTime(samples []promql.Point) float64 { |
||||||
|
var sum float64 |
||||||
|
for _, v := range samples { |
||||||
|
sum += v.V |
||||||
|
} |
||||||
|
return sum |
||||||
|
} |
@ -0,0 +1,99 @@ |
|||||||
|
package logql |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/grafana/loki/pkg/iter" |
||||||
|
"github.com/grafana/loki/pkg/logproto" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
extractBytes = bytesSampleExtractor{} |
||||||
|
extractCount = countSampleExtractor{} |
||||||
|
) |
||||||
|
|
||||||
|
// SeriesIterator is an iterator that iterate over a stream of logs and returns sample.
|
||||||
|
type SeriesIterator interface { |
||||||
|
Close() error |
||||||
|
Next() bool |
||||||
|
Peek() (Sample, bool) |
||||||
|
} |
||||||
|
|
||||||
|
// Sample is a series sample
|
||||||
|
type Sample struct { |
||||||
|
Labels string |
||||||
|
Value float64 |
||||||
|
TimestampNano int64 |
||||||
|
} |
||||||
|
|
||||||
|
type seriesIterator struct { |
||||||
|
iter iter.PeekingEntryIterator |
||||||
|
sampler SampleExtractor |
||||||
|
|
||||||
|
updated bool |
||||||
|
cur Sample |
||||||
|
} |
||||||
|
|
||||||
|
func newSeriesIterator(it iter.EntryIterator, sampler SampleExtractor) SeriesIterator { |
||||||
|
return &seriesIterator{ |
||||||
|
iter: iter.NewPeekingIterator(it), |
||||||
|
sampler: sampler, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (e *seriesIterator) Close() error { |
||||||
|
return e.iter.Close() |
||||||
|
} |
||||||
|
|
||||||
|
func (e *seriesIterator) Next() bool { |
||||||
|
e.updated = false |
||||||
|
return e.iter.Next() |
||||||
|
} |
||||||
|
|
||||||
|
func (e *seriesIterator) Peek() (Sample, bool) { |
||||||
|
if e.updated { |
||||||
|
return e.cur, true |
||||||
|
} |
||||||
|
|
||||||
|
for { |
||||||
|
lbs, entry, ok := e.iter.Peek() |
||||||
|
if !ok { |
||||||
|
return Sample{}, false |
||||||
|
} |
||||||
|
|
||||||
|
// transform
|
||||||
|
e.cur, ok = e.sampler.From(lbs, entry) |
||||||
|
if ok { |
||||||
|
break |
||||||
|
} |
||||||
|
if !e.iter.Next() { |
||||||
|
return Sample{}, false |
||||||
|
} |
||||||
|
} |
||||||
|
e.updated = true |
||||||
|
return e.cur, true |
||||||
|
} |
||||||
|
|
||||||
|
// SampleExtractor transforms a log entry into a sample.
|
||||||
|
// In case of failure the second return value will be false.
|
||||||
|
type SampleExtractor interface { |
||||||
|
From(labels string, e logproto.Entry) (Sample, bool) |
||||||
|
} |
||||||
|
|
||||||
|
type countSampleExtractor struct{} |
||||||
|
|
||||||
|
func (countSampleExtractor) From(lbs string, entry logproto.Entry) (Sample, bool) { |
||||||
|
return Sample{ |
||||||
|
Labels: lbs, |
||||||
|
TimestampNano: entry.Timestamp.UnixNano(), |
||||||
|
Value: 1., |
||||||
|
}, true |
||||||
|
} |
||||||
|
|
||||||
|
type bytesSampleExtractor struct{} |
||||||
|
|
||||||
|
func (bytesSampleExtractor) From(lbs string, entry logproto.Entry) (Sample, bool) { |
||||||
|
return Sample{ |
||||||
|
Labels: lbs, |
||||||
|
TimestampNano: entry.Timestamp.UnixNano(), |
||||||
|
Value: float64(len(entry.Line)), |
||||||
|
}, true |
||||||
|
} |
@ -0,0 +1,159 @@ |
|||||||
|
package logql |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"testing" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/require" |
||||||
|
|
||||||
|
"github.com/grafana/loki/pkg/iter" |
||||||
|
"github.com/grafana/loki/pkg/logproto" |
||||||
|
) |
||||||
|
|
||||||
|
func Test_seriesIterator_Peek(t *testing.T) { |
||||||
|
type expectation struct { |
||||||
|
ok bool |
||||||
|
sample Sample |
||||||
|
} |
||||||
|
for _, test := range []struct { |
||||||
|
name string |
||||||
|
it SeriesIterator |
||||||
|
expectations []expectation |
||||||
|
}{ |
||||||
|
{ |
||||||
|
"count", |
||||||
|
newSeriesIterator(iter.NewStreamIterator(newStream(5, identity, `{app="foo"}`)), extractCount), |
||||||
|
[]expectation{ |
||||||
|
{true, Sample{Labels: `{app="foo"}`, TimestampNano: 0, Value: 1}}, |
||||||
|
{true, Sample{Labels: `{app="foo"}`, TimestampNano: time.Unix(1, 0).UnixNano(), Value: 1}}, |
||||||
|
{true, Sample{Labels: `{app="foo"}`, TimestampNano: time.Unix(2, 0).UnixNano(), Value: 1}}, |
||||||
|
{true, Sample{Labels: `{app="foo"}`, TimestampNano: time.Unix(3, 0).UnixNano(), Value: 1}}, |
||||||
|
{true, Sample{Labels: `{app="foo"}`, TimestampNano: time.Unix(4, 0).UnixNano(), Value: 1}}, |
||||||
|
{false, Sample{}}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
"bytes empty", |
||||||
|
newSeriesIterator( |
||||||
|
iter.NewStreamIterator( |
||||||
|
newStream( |
||||||
|
3, |
||||||
|
func(i int64) logproto.Entry { |
||||||
|
return logproto.Entry{ |
||||||
|
Timestamp: time.Unix(i, 0), |
||||||
|
} |
||||||
|
}, |
||||||
|
`{app="foo"}`, |
||||||
|
), |
||||||
|
), |
||||||
|
extractBytes, |
||||||
|
), |
||||||
|
[]expectation{ |
||||||
|
{true, Sample{Labels: `{app="foo"}`, TimestampNano: 0, Value: 0}}, |
||||||
|
{true, Sample{Labels: `{app="foo"}`, TimestampNano: time.Unix(1, 0).UnixNano(), Value: 0}}, |
||||||
|
{true, Sample{Labels: `{app="foo"}`, TimestampNano: time.Unix(2, 0).UnixNano(), Value: 0}}, |
||||||
|
{false, Sample{}}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
"bytes", |
||||||
|
newSeriesIterator( |
||||||
|
iter.NewStreamIterator( |
||||||
|
newStream( |
||||||
|
3, |
||||||
|
func(i int64) logproto.Entry { |
||||||
|
return logproto.Entry{ |
||||||
|
Timestamp: time.Unix(i, 0), |
||||||
|
Line: "foo", |
||||||
|
} |
||||||
|
}, |
||||||
|
`{app="foo"}`, |
||||||
|
), |
||||||
|
), |
||||||
|
extractBytes, |
||||||
|
), |
||||||
|
[]expectation{ |
||||||
|
{true, Sample{Labels: `{app="foo"}`, TimestampNano: 0, Value: 3}}, |
||||||
|
{true, Sample{Labels: `{app="foo"}`, TimestampNano: time.Unix(1, 0).UnixNano(), Value: 3}}, |
||||||
|
{true, Sample{Labels: `{app="foo"}`, TimestampNano: time.Unix(2, 0).UnixNano(), Value: 3}}, |
||||||
|
{false, Sample{}}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
"bytes backward", |
||||||
|
newSeriesIterator( |
||||||
|
iter.NewStreamsIterator(context.Background(), |
||||||
|
[]logproto.Stream{ |
||||||
|
newStream( |
||||||
|
3, |
||||||
|
func(i int64) logproto.Entry { |
||||||
|
return logproto.Entry{ |
||||||
|
Timestamp: time.Unix(i, 0), |
||||||
|
Line: "foo", |
||||||
|
} |
||||||
|
}, |
||||||
|
`{app="foo"}`, |
||||||
|
), |
||||||
|
newStream( |
||||||
|
3, |
||||||
|
func(i int64) logproto.Entry { |
||||||
|
return logproto.Entry{ |
||||||
|
Timestamp: time.Unix(i, 0), |
||||||
|
Line: "barr", |
||||||
|
} |
||||||
|
}, |
||||||
|
`{app="barr"}`, |
||||||
|
), |
||||||
|
}, |
||||||
|
logproto.BACKWARD, |
||||||
|
), |
||||||
|
extractBytes, |
||||||
|
), |
||||||
|
[]expectation{ |
||||||
|
{true, Sample{Labels: `{app="foo"}`, TimestampNano: 0, Value: 3}}, |
||||||
|
{true, Sample{Labels: `{app="foo"}`, TimestampNano: time.Unix(1, 0).UnixNano(), Value: 3}}, |
||||||
|
{true, Sample{Labels: `{app="foo"}`, TimestampNano: time.Unix(2, 0).UnixNano(), Value: 3}}, |
||||||
|
{true, Sample{Labels: `{app="barr"}`, TimestampNano: 0, Value: 4}}, |
||||||
|
{true, Sample{Labels: `{app="barr"}`, TimestampNano: time.Unix(1, 0).UnixNano(), Value: 4}}, |
||||||
|
{true, Sample{Labels: `{app="barr"}`, TimestampNano: time.Unix(2, 0).UnixNano(), Value: 4}}, |
||||||
|
{false, Sample{}}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
"skip first", |
||||||
|
newSeriesIterator(iter.NewStreamIterator(newStream(2, identity, `{app="foo"}`)), fakeSampler{}), |
||||||
|
[]expectation{ |
||||||
|
{true, Sample{Labels: `{app="foo"}`, TimestampNano: time.Unix(1, 0).UnixNano(), Value: 10}}, |
||||||
|
{false, Sample{}}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
} { |
||||||
|
t.Run(test.name, func(t *testing.T) { |
||||||
|
for _, e := range test.expectations { |
||||||
|
sample, ok := test.it.Peek() |
||||||
|
require.Equal(t, e.ok, ok) |
||||||
|
if !e.ok { |
||||||
|
continue |
||||||
|
} |
||||||
|
require.Equal(t, e.sample, sample) |
||||||
|
test.it.Next() |
||||||
|
} |
||||||
|
require.NoError(t, test.it.Close()) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// fakeSampler is a Sampler that returns no value for 0 timestamp otherwise always 10
|
||||||
|
type fakeSampler struct{} |
||||||
|
|
||||||
|
func (fakeSampler) From(lbs string, entry logproto.Entry) (Sample, bool) { |
||||||
|
if entry.Timestamp.UnixNano() == 0 { |
||||||
|
return Sample{}, false |
||||||
|
} |
||||||
|
return Sample{ |
||||||
|
Labels: lbs, |
||||||
|
TimestampNano: entry.Timestamp.UnixNano(), |
||||||
|
Value: 10, |
||||||
|
}, true |
||||||
|
} |
Loading…
Reference in new issue