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/iter/iterator_test.go

359 lines
9.3 KiB

package iter
import (
"fmt"
"sort"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/grafana/loki/pkg/logproto"
"github.com/stretchr/testify/assert"
)
const testSize = 10
const defaultLabels = "{foo=\"baz\"}"
func TestIterator(t *testing.T) {
for i, tc := range []struct {
iterator EntryIterator
generator generator
length int64
labels string
}{
// Test basic identity.
{
iterator: mkStreamIterator(identity, defaultLabels),
generator: identity,
length: testSize,
labels: defaultLabels,
},
// Test basic identity (backwards).
{
iterator: mkStreamIterator(inverse(identity), defaultLabels),
generator: inverse(identity),
length: testSize,
labels: defaultLabels,
},
// Test dedupe of overlapping iterators with the heap iterator.
{
iterator: NewHeapIterator([]EntryIterator{
mkStreamIterator(offset(0, identity), defaultLabels),
mkStreamIterator(offset(testSize/2, identity), defaultLabels),
mkStreamIterator(offset(testSize, identity), defaultLabels),
}, logproto.FORWARD),
generator: identity,
length: 2 * testSize,
labels: defaultLabels,
},
// Test dedupe of overlapping iterators with the heap iterator (backward).
{
iterator: NewHeapIterator([]EntryIterator{
mkStreamIterator(inverse(offset(0, identity)), defaultLabels),
mkStreamIterator(inverse(offset(-testSize/2, identity)), defaultLabels),
mkStreamIterator(inverse(offset(-testSize, identity)), defaultLabels),
}, logproto.BACKWARD),
generator: inverse(identity),
length: 2 * testSize,
labels: defaultLabels,
},
// Test dedupe of entries with the same timestamp but different entries.
{
iterator: NewHeapIterator([]EntryIterator{
mkStreamIterator(offset(0, constant(0)), defaultLabels),
mkStreamIterator(offset(0, constant(0)), defaultLabels),
mkStreamIterator(offset(testSize, constant(0)), defaultLabels),
}, logproto.FORWARD),
generator: constant(0),
length: 2 * testSize,
labels: defaultLabels,
},
// Test basic identity with non-default labels.
{
iterator: mkStreamIterator(identity, "{foobar: \"bazbar\"}"),
generator: identity,
length: testSize,
labels: "{foobar: \"bazbar\"}",
},
} {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
for i := int64(0); i < tc.length; i++ {
assert.Equal(t, true, tc.iterator.Next())
assert.Equal(t, tc.generator(i), tc.iterator.Entry(), fmt.Sprintln("iteration", i))
assert.Equal(t, tc.labels, tc.iterator.Labels(), fmt.Sprintln("iteration", i))
}
assert.Equal(t, false, tc.iterator.Next())
assert.Equal(t, nil, tc.iterator.Error())
assert.NoError(t, tc.iterator.Close())
})
}
}
func TestIteratorMultipleLabels(t *testing.T) {
for i, tc := range []struct {
iterator EntryIterator
generator generator
length int64
labels func(int64) string
}{
// Test merging with differing labels but same timestamps and values.
{
iterator: NewHeapIterator([]EntryIterator{
mkStreamIterator(identity, "{foobar: \"baz1\"}"),
mkStreamIterator(identity, "{foobar: \"baz2\"}"),
}, logproto.FORWARD),
generator: func(i int64) logproto.Entry {
return identity(i / 2)
},
length: testSize * 2,
labels: func(i int64) string {
if i%2 == 0 {
return "{foobar: \"baz1\"}"
}
return "{foobar: \"baz2\"}"
},
},
// Test merging with differing labels but all the same timestamps and different values.
{
iterator: NewHeapIterator([]EntryIterator{
mkStreamIterator(constant(0), "{foobar: \"baz1\"}"),
mkStreamIterator(constant(0), "{foobar: \"baz2\"}"),
}, logproto.FORWARD),
generator: func(i int64) logproto.Entry {
return constant(0)(i % testSize)
},
length: testSize * 2,
labels: func(i int64) string {
if i/testSize == 0 {
return "{foobar: \"baz1\"}"
}
return "{foobar: \"baz2\"}"
},
},
} {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
for i := int64(0); i < tc.length; i++ {
assert.Equal(t, true, tc.iterator.Next())
assert.Equal(t, tc.generator(i), tc.iterator.Entry(), fmt.Sprintln("iteration", i))
assert.Equal(t, tc.labels(i), tc.iterator.Labels(), fmt.Sprintln("iteration", i))
}
assert.Equal(t, false, tc.iterator.Next())
assert.Equal(t, nil, tc.iterator.Error())
assert.NoError(t, tc.iterator.Close())
})
}
}
func TestHeapIteratorPrefetch(t *testing.T) {
t.Parallel()
type tester func(t *testing.T, i HeapIterator)
tests := map[string]tester{
"prefetch on Len() when called as first method": func(t *testing.T, i HeapIterator) {
assert.Equal(t, 2, i.Len())
},
"prefetch on Peek() when called as first method": func(t *testing.T, i HeapIterator) {
assert.Equal(t, time.Unix(0, 0), i.Peek())
},
"prefetch on Next() when called as first method": func(t *testing.T, i HeapIterator) {
assert.True(t, i.Next())
assert.Equal(t, logproto.Entry{Timestamp: time.Unix(0, 0), Line: "0"}, i.Entry())
},
}
for testName, testFunc := range tests {
testFunc := testFunc
t.Run(testName, func(t *testing.T) {
t.Parallel()
i := NewHeapIterator([]EntryIterator{
mkStreamIterator(identity, "{foobar: \"baz1\"}"),
mkStreamIterator(identity, "{foobar: \"baz2\"}"),
}, logproto.FORWARD)
testFunc(t, i)
})
}
}
type generator func(i int64) logproto.Entry
func mkStreamIterator(f generator, labels string) EntryIterator {
entries := []logproto.Entry{}
for i := int64(0); i < testSize; i++ {
entries = append(entries, f(i))
}
return NewStreamIterator(&logproto.Stream{
Entries: entries,
Labels: labels,
})
}
func identity(i int64) logproto.Entry {
return logproto.Entry{
Timestamp: time.Unix(i, 0),
Line: fmt.Sprintf("%d", i),
}
}
func offset(j int64, g generator) generator {
return func(i int64) logproto.Entry {
return g(i + j)
}
}
// nolint
func constant(t int64) generator {
return func(i int64) logproto.Entry {
return logproto.Entry{
Timestamp: time.Unix(t, 0),
Line: fmt.Sprintf("%d", i),
}
}
}
func inverse(g generator) generator {
return func(i int64) logproto.Entry {
return g(-i)
}
}
func TestMostCommon(t *testing.T) {
// First is most common.
tuples := []tuple{
{Entry: logproto.Entry{Line: "a"}},
{Entry: logproto.Entry{Line: "b"}},
{Entry: logproto.Entry{Line: "c"}},
{Entry: logproto.Entry{Line: "a"}},
{Entry: logproto.Entry{Line: "b"}},
{Entry: logproto.Entry{Line: "c"}},
{Entry: logproto.Entry{Line: "a"}},
}
require.Equal(t, "a", mostCommon(tuples).Entry.Line)
tuples = []tuple{
{Entry: logproto.Entry{Line: "a"}},
{Entry: logproto.Entry{Line: "b"}},
{Entry: logproto.Entry{Line: "b"}},
{Entry: logproto.Entry{Line: "c"}},
{Entry: logproto.Entry{Line: "c"}},
{Entry: logproto.Entry{Line: "c"}},
{Entry: logproto.Entry{Line: "d"}},
}
require.Equal(t, "c", mostCommon(tuples).Entry.Line)
}
func TestInsert(t *testing.T) {
toInsert := []tuple{
{Entry: logproto.Entry{Line: "a"}},
{Entry: logproto.Entry{Line: "e"}},
{Entry: logproto.Entry{Line: "c"}},
{Entry: logproto.Entry{Line: "b"}},
{Entry: logproto.Entry{Line: "d"}},
{Entry: logproto.Entry{Line: "a"}},
{Entry: logproto.Entry{Line: "c"}},
}
var ts []tuple
for _, e := range toInsert {
ts = insert(ts, e)
}
require.True(t, sort.SliceIsSorted(ts, func(i, j int) bool {
return ts[i].Line < ts[j].Line
}))
}
func TestEntryIteratorForward(t *testing.T) {
itr1 := mkStreamIterator(inverse(offset(testSize, identity)), defaultLabels)
itr2 := mkStreamIterator(inverse(offset(testSize, identity)), "{foobar: \"bazbar\"}")
heapIterator := NewHeapIterator([]EntryIterator{itr1, itr2}, logproto.BACKWARD)
forwardIterator, err := NewEntryIteratorForward(heapIterator, testSize, false)
require.NoError(t, err)
for i := int64((testSize / 2) + 1); i <= testSize; i++ {
assert.Equal(t, true, forwardIterator.Next())
assert.Equal(t, identity(i), forwardIterator.Entry(), fmt.Sprintln("iteration", i))
assert.Equal(t, true, forwardIterator.Next())
assert.Equal(t, identity(i), forwardIterator.Entry(), fmt.Sprintln("iteration", i))
}
assert.Equal(t, false, forwardIterator.Next())
assert.Equal(t, nil, forwardIterator.Error())
assert.NoError(t, forwardIterator.Close())
}
func Test_PeekingIterator(t *testing.T) {
iter := NewPeekingIterator(NewStreamIterator(&logproto.Stream{
Entries: []logproto.Entry{
{
Timestamp: time.Unix(0, 1),
},
{
Timestamp: time.Unix(0, 2),
},
{
Timestamp: time.Unix(0, 3),
},
},
}))
_, peek, ok := iter.Peek()
if peek.Timestamp.UnixNano() != 1 {
t.Fatal("wrong peeked time.")
}
if !ok {
t.Fatal("should be ok.")
}
hasNext := iter.Next()
if !hasNext {
t.Fatal("should have next.")
}
if iter.Entry().Timestamp.UnixNano() != 1 {
t.Fatal("wrong peeked time.")
}
_, peek, ok = iter.Peek()
if peek.Timestamp.UnixNano() != 2 {
t.Fatal("wrong peeked time.")
}
if !ok {
t.Fatal("should be ok.")
}
hasNext = iter.Next()
if !hasNext {
t.Fatal("should have next.")
}
if iter.Entry().Timestamp.UnixNano() != 2 {
t.Fatal("wrong peeked time.")
}
_, peek, ok = iter.Peek()
if peek.Timestamp.UnixNano() != 3 {
t.Fatal("wrong peeked time.")
}
if !ok {
t.Fatal("should be ok.")
}
hasNext = iter.Next()
if !hasNext {
t.Fatal("should have next.")
}
if iter.Entry().Timestamp.UnixNano() != 3 {
t.Fatal("wrong peeked time.")
}
_, _, ok = iter.Peek()
if ok {
t.Fatal("should not be ok.")
}
}