mirror of https://github.com/grafana/loki
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.
367 lines
12 KiB
367 lines
12 KiB
package main
|
|
|
|
import (
|
|
"errors"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
jsoniter "github.com/json-iterator/go"
|
|
"github.com/prometheus/common/model"
|
|
)
|
|
|
|
type entry struct {
|
|
lbs model.LabelSet
|
|
line string
|
|
ts time.Time
|
|
}
|
|
|
|
type recorder struct {
|
|
*entry
|
|
}
|
|
|
|
func (r *recorder) Handle(labels model.LabelSet, time time.Time, e string) error {
|
|
r.entry = &entry{
|
|
labels,
|
|
e,
|
|
time,
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *recorder) toEntry() *entry { return r.entry }
|
|
|
|
func (r *recorder) Stop() {}
|
|
|
|
var now = time.Now()
|
|
|
|
func Test_loki_sendRecord(t *testing.T) {
|
|
var simpleRecordFixture = map[interface{}]interface{}{
|
|
"foo": "bar",
|
|
"bar": 500,
|
|
"error": make(chan struct{}),
|
|
}
|
|
var mapRecordFixture = map[interface{}]interface{}{
|
|
// lots of key/value pairs in map to increase chances of test hitting in case of unsorted map marshalling
|
|
"A": "A",
|
|
"B": "B",
|
|
"C": "C",
|
|
"D": "D",
|
|
"E": "E",
|
|
"F": "F",
|
|
"G": "G",
|
|
"H": "H",
|
|
}
|
|
var byteArrayRecordFixture = map[interface{}]interface{}{
|
|
"label": "label",
|
|
"outer": []byte("foo"),
|
|
"map": map[interface{}]interface{}{
|
|
"inner": []byte("bar"),
|
|
},
|
|
}
|
|
var mixedTypesRecordFixture = map[interface{}]interface{}{
|
|
"label": "label",
|
|
"int": 42,
|
|
"float": 42.42,
|
|
"array": []interface{}{42, 42.42, "foo"},
|
|
"map": map[interface{}]interface{}{
|
|
"nested": map[interface{}]interface{}{
|
|
"foo": "bar",
|
|
"invalid": []byte("a\xc5z"),
|
|
},
|
|
},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
cfg *config
|
|
record map[interface{}]interface{}
|
|
want *entry
|
|
wantErr bool
|
|
}{
|
|
{"map to JSON", &config{labelKeys: []string{"A"}, lineFormat: jsonFormat}, mapRecordFixture, &entry{model.LabelSet{"A": "A"}, `{"B":"B","C":"C","D":"D","E":"E","F":"F","G":"G","H":"H"}`, now}, false},
|
|
{"map to kvPairFormat", &config{labelKeys: []string{"A"}, lineFormat: kvPairFormat}, mapRecordFixture, &entry{model.LabelSet{"A": "A"}, `B=B C=C D=D E=E F=F G=G H=H`, now}, false},
|
|
{"not enough records", &config{labelKeys: []string{"foo"}, lineFormat: jsonFormat, removeKeys: []string{"bar", "error"}}, simpleRecordFixture, nil, false},
|
|
{"labels", &config{labelKeys: []string{"bar", "fake"}, lineFormat: jsonFormat, removeKeys: []string{"fuzz", "error"}}, simpleRecordFixture, &entry{model.LabelSet{"bar": "500"}, `{"foo":"bar"}`, now}, false},
|
|
{"remove key", &config{labelKeys: []string{"fake"}, lineFormat: jsonFormat, removeKeys: []string{"foo", "error", "fake"}}, simpleRecordFixture, &entry{model.LabelSet{}, `{"bar":500}`, now}, false},
|
|
{"error", &config{labelKeys: []string{"fake"}, lineFormat: jsonFormat, removeKeys: []string{"foo"}}, simpleRecordFixture, nil, true},
|
|
{"key value", &config{labelKeys: []string{"fake"}, lineFormat: kvPairFormat, removeKeys: []string{"foo", "error", "fake"}}, simpleRecordFixture, &entry{model.LabelSet{}, `bar=500`, now}, false},
|
|
{"single", &config{labelKeys: []string{"fake"}, dropSingleKey: true, lineFormat: kvPairFormat, removeKeys: []string{"foo", "error", "fake"}}, simpleRecordFixture, &entry{model.LabelSet{}, `500`, now}, false},
|
|
{"labelmap", &config{labelMap: map[string]interface{}{"bar": "other"}, lineFormat: jsonFormat, removeKeys: []string{"bar", "error"}}, simpleRecordFixture, &entry{model.LabelSet{"other": "500"}, `{"foo":"bar"}`, now}, false},
|
|
{"byte array", &config{labelKeys: []string{"label"}, lineFormat: jsonFormat}, byteArrayRecordFixture, &entry{model.LabelSet{"label": "label"}, `{"map":{"inner":"bar"},"outer":"foo"}`, now}, false},
|
|
{"mixed types", &config{labelKeys: []string{"label"}, lineFormat: jsonFormat}, mixedTypesRecordFixture, &entry{model.LabelSet{"label": "label"}, `{"array":[42,42.42,"foo"],"float":42.42,"int":42,"map":{"nested":{"foo":"bar","invalid":"a\ufffdz"}}}`, now}, false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
rec := &recorder{}
|
|
l := &loki{
|
|
cfg: tt.cfg,
|
|
client: rec,
|
|
logger: logger,
|
|
}
|
|
err := l.sendRecord(tt.record, now)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("sendRecord() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
got := rec.toEntry()
|
|
if !reflect.DeepEqual(got, tt.want) {
|
|
t.Errorf("sendRecord() want:%v got:%v", tt.want, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_createLine(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
records map[string]interface{}
|
|
f format
|
|
want string
|
|
wantErr bool
|
|
}{
|
|
|
|
{"json", map[string]interface{}{"foo": "bar", "bar": map[string]interface{}{"bizz": "bazz"}}, jsonFormat, `{"foo":"bar","bar":{"bizz":"bazz"}}`, false},
|
|
{"json with number", map[string]interface{}{"foo": "bar", "bar": map[string]interface{}{"bizz": 20}}, jsonFormat, `{"foo":"bar","bar":{"bizz":20}}`, false},
|
|
{"bad json", map[string]interface{}{"foo": make(chan interface{})}, jsonFormat, "", true},
|
|
{"kv with space", map[string]interface{}{"foo": "bar", "bar": "foo foo"}, kvPairFormat, `bar="foo foo" foo=bar`, false},
|
|
{"kv with number", map[string]interface{}{"foo": "bar foo", "decimal": 12.2}, kvPairFormat, `decimal=12.2 foo="bar foo"`, false},
|
|
{"kv with nil", map[string]interface{}{"foo": "bar", "null": nil}, kvPairFormat, `foo=bar null=null`, false},
|
|
{"kv with array", map[string]interface{}{"foo": "bar", "array": []string{"foo", "bar"}}, kvPairFormat, `array="[foo bar]" foo=bar`, false},
|
|
{"kv with map", map[string]interface{}{"foo": "bar", "map": map[string]interface{}{"foo": "bar", "bar ": "foo "}}, kvPairFormat, `foo=bar map="map[bar :foo foo:bar]"`, false},
|
|
{"kv empty", map[string]interface{}{}, kvPairFormat, ``, false},
|
|
{"bad format", nil, format(3), "", true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := createLine(tt.records, tt.f)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("createLine() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if err != nil {
|
|
return
|
|
}
|
|
if tt.f == jsonFormat {
|
|
compareJSON(t, got, tt.want)
|
|
} else {
|
|
if got != tt.want {
|
|
t.Errorf("createLine() = %v, want %v", got, tt.want)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// compareJson unmarshal both string to map[string]interface compare json result.
|
|
// we can't compare string to string as jsoniter doesn't ensure field ordering.
|
|
func compareJSON(t *testing.T, got, want string) {
|
|
var w map[string]interface{}
|
|
err := jsoniter.Unmarshal([]byte(want), &w)
|
|
if err != nil {
|
|
t.Errorf("failed to unmarshal string: %s", err)
|
|
}
|
|
var g map[string]interface{}
|
|
err = jsoniter.Unmarshal([]byte(got), &g)
|
|
if err != nil {
|
|
t.Errorf("failed to unmarshal string: %s", err)
|
|
}
|
|
if !reflect.DeepEqual(g, w) {
|
|
t.Errorf("compareJson() = %v, want %v", g, w)
|
|
}
|
|
}
|
|
|
|
func Test_removeKeys(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
records map[string]interface{}
|
|
expected map[string]interface{}
|
|
keys []string
|
|
}{
|
|
{"remove all keys", map[string]interface{}{"foo": "bar", "bar": map[string]interface{}{"bizz": "bazz"}}, map[string]interface{}{}, []string{"foo", "bar"}},
|
|
{"remove none", map[string]interface{}{"foo": "bar"}, map[string]interface{}{"foo": "bar"}, []string{}},
|
|
{"remove not existing", map[string]interface{}{"foo": "bar"}, map[string]interface{}{"foo": "bar"}, []string{"bar"}},
|
|
{"remove one", map[string]interface{}{"foo": "bar", "bazz": "buzz"}, map[string]interface{}{"foo": "bar"}, []string{"bazz"}},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
removeKeys(tt.records, tt.keys)
|
|
if !reflect.DeepEqual(tt.expected, tt.records) {
|
|
t.Errorf("removeKeys() = %v, want %v", tt.records, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_extractLabels(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
records map[string]interface{}
|
|
keys []string
|
|
want model.LabelSet
|
|
}{
|
|
{"single string", map[string]interface{}{"foo": "bar", "bar": map[string]interface{}{"bizz": "bazz"}}, []string{"foo"}, model.LabelSet{"foo": "bar"}},
|
|
{"multiple", map[string]interface{}{"foo": "bar", "bar": map[string]interface{}{"bizz": "bazz"}}, []string{"foo", "bar"}, model.LabelSet{"foo": "bar", "bar": "map[bizz:bazz]"}},
|
|
{"nil", map[string]interface{}{"foo": nil}, []string{"foo"}, model.LabelSet{"foo": "<nil>"}},
|
|
{"none", map[string]interface{}{"foo": nil}, []string{}, model.LabelSet{}},
|
|
{"missing", map[string]interface{}{"foo": "bar"}, []string{"foo", "buzz"}, model.LabelSet{"foo": "bar"}},
|
|
{"skip invalid", map[string]interface{}{"foo.blah": "bar", "bar": "a\xc5z"}, []string{"foo.blah", "bar"}, model.LabelSet{}},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := extractLabels(tt.records, tt.keys); !reflect.DeepEqual(got, tt.want) {
|
|
t.Errorf("extractLabels() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_toStringMap(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
record map[interface{}]interface{}
|
|
want map[string]interface{}
|
|
}{
|
|
{"already string", map[interface{}]interface{}{"string": "foo", "bar": []byte("buzz")}, map[string]interface{}{"string": "foo", "bar": "buzz"}},
|
|
{"skip non string", map[interface{}]interface{}{"string": "foo", 1.0: []byte("buzz")}, map[string]interface{}{"string": "foo"}},
|
|
{"byteslice in array", map[interface{}]interface{}{"string": "foo", "bar": []interface{}{map[interface{}]interface{}{"baz": []byte("quux")}}},
|
|
map[string]interface{}{"string": "foo", "bar": []interface{}{map[string]interface{}{"baz": "quux"}}}},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := toStringMap(tt.record); !reflect.DeepEqual(got, tt.want) {
|
|
t.Errorf("toStringMap() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_labelMapping(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
records map[string]interface{}
|
|
mapping map[string]interface{}
|
|
want model.LabelSet
|
|
}{
|
|
{
|
|
"empty record",
|
|
map[string]interface{}{},
|
|
map[string]interface{}{},
|
|
model.LabelSet{},
|
|
},
|
|
{
|
|
"empty subrecord",
|
|
map[string]interface{}{
|
|
"kubernetes": map[interface{}]interface{}{
|
|
"foo": []byte("buzz"),
|
|
},
|
|
},
|
|
map[string]interface{}{},
|
|
model.LabelSet{},
|
|
},
|
|
{
|
|
"deep string",
|
|
map[string]interface{}{
|
|
"int": "42",
|
|
"float": "42.42",
|
|
"array": `[42,42.42,"foo"]`,
|
|
"kubernetes": map[string]interface{}{
|
|
"label": map[string]interface{}{
|
|
"component": map[string]interface{}{
|
|
"buzz": "value",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
map[string]interface{}{
|
|
"int": "int",
|
|
"float": "float",
|
|
"array": "array",
|
|
"kubernetes": map[string]interface{}{
|
|
"label": map[string]interface{}{
|
|
"component": map[string]interface{}{
|
|
"buzz": "label",
|
|
},
|
|
},
|
|
},
|
|
"stream": "output",
|
|
"nope": "nope",
|
|
},
|
|
model.LabelSet{
|
|
"int": "42",
|
|
"float": "42.42",
|
|
"array": `[42,42.42,"foo"]`,
|
|
"label": "value",
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := model.LabelSet{}
|
|
if mapLabels(tt.records, tt.mapping, got); !reflect.DeepEqual(got, tt.want) {
|
|
t.Errorf("mapLabels() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_AutoKubernetesLabels(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
records map[interface{}]interface{}
|
|
want model.LabelSet
|
|
err error
|
|
}{
|
|
{
|
|
"records without labels",
|
|
map[interface{}]interface{}{
|
|
"kubernetes": map[interface{}]interface{}{
|
|
"foo": []byte("buzz"),
|
|
},
|
|
},
|
|
model.LabelSet{
|
|
"foo": "buzz",
|
|
},
|
|
nil,
|
|
},
|
|
{
|
|
"records with labels",
|
|
map[interface{}]interface{}{
|
|
"kubernetes": map[string]interface{}{
|
|
"labels": map[string]interface{}{
|
|
"foo": "bar",
|
|
"buzz": "value",
|
|
},
|
|
},
|
|
},
|
|
model.LabelSet{
|
|
"foo": "bar",
|
|
"buzz": "value",
|
|
},
|
|
nil,
|
|
},
|
|
{
|
|
"records without kubernetes labels",
|
|
map[interface{}]interface{}{
|
|
"foo": "bar",
|
|
"label": "value",
|
|
},
|
|
model.LabelSet{},
|
|
errors.New("kubernetes labels not found, no labels will be added"),
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
m := toStringMap(tt.records)
|
|
lbs := model.LabelSet{}
|
|
err := autoLabels(m, lbs)
|
|
if err != nil && err.Error() != tt.err.Error() {
|
|
t.Errorf("error in autolabels, error = %v", err)
|
|
return
|
|
}
|
|
if !reflect.DeepEqual(lbs, tt.want) {
|
|
t.Errorf("mapLabels() = %v, want %v", lbs, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|