Skip to content

Commit 96fe3d2

Browse files
committed
fluent-bit: sort JSON map
While developing another fix, I stumbled upon grafana#1309 as a newly written unit test (with multiple key-value pairs in a map) was flaky. While JSON does not [strictly define an order on records in a map][RFC8259], but practical operations with a logging tool pretty much require it (although of course grep for JSON is jq, not grep...). Also, the key value output format is already sorted. Switching to sorted output in jsoniter is pretty easy. As of today, it still has a [bug] though, for which I already provided a [fix]. I propose accepting that rare case where invalid types can occur from msgpack output (can this even happen?) and re-enable the failing test case as soon as the upstream PR is merged. [RFC8259]: https://tools.ietf.org/html/rfc8259#section-4 [bug]: json-iterator/go#388 [fix]: json-iterator/go#422 Signed-off-by: Jens Erat <email@jenserat.de>
1 parent 5810719 commit 96fe3d2

File tree

2 files changed

+32
-14
lines changed

2 files changed

+32
-14
lines changed

cmd/fluent-bit/loki.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ func removeKeys(records map[string]interface{}, keys []string) {
147147
func createLine(records map[string]interface{}, f format) (string, error) {
148148
switch f {
149149
case jsonFormat:
150-
js, err := jsoniter.Marshal(records)
150+
js, err := jsoniter.ConfigCompatibleWithStandardLibrary.Marshal(records)
151151
if err != nil {
152152
return "", err
153153
}

cmd/fluent-bit/loki_test.go

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,24 +35,40 @@ func (r *recorder) Stop() {}
3535
var now = time.Now()
3636

3737
func Test_loki_sendRecord(t *testing.T) {
38-
var recordFixture = map[interface{}]interface{}{
38+
var simpleRecordFixture = map[interface{}]interface{}{
3939
"foo": "bar",
4040
"bar": 500,
4141
"error": make(chan struct{}),
4242
}
43+
var mapRecordFixture = map[interface{}]interface{}{
44+
// lots of key/value pairs in map to increase chances of test hitting in case of unsorted map marshalling
45+
"A": "A",
46+
"B": "B",
47+
"C": "C",
48+
"D": "D",
49+
"E": "E",
50+
"F": "F",
51+
"G": "G",
52+
"H": "H",
53+
}
4354

4455
tests := []struct {
45-
name string
46-
cfg *config
47-
want *entry
56+
name string
57+
cfg *config
58+
record map[interface{}]interface{}
59+
want *entry
4860
}{
49-
{"not enough records", &config{labelKeys: []string{"foo"}, lineFormat: jsonFormat, removeKeys: []string{"bar", "error"}}, nil},
50-
{"labels", &config{labelKeys: []string{"bar", "fake"}, lineFormat: jsonFormat, removeKeys: []string{"fuzz", "error"}}, &entry{model.LabelSet{"bar": "500"}, `{"foo":"bar"}`, now}},
51-
{"remove key", &config{labelKeys: []string{"fake"}, lineFormat: jsonFormat, removeKeys: []string{"foo", "error", "fake"}}, &entry{model.LabelSet{}, `{"bar":500}`, now}},
52-
{"error", &config{labelKeys: []string{"fake"}, lineFormat: jsonFormat, removeKeys: []string{"foo"}}, nil},
53-
{"key value", &config{labelKeys: []string{"fake"}, lineFormat: kvPairFormat, removeKeys: []string{"foo", "error", "fake"}}, &entry{model.LabelSet{}, `bar=500`, now}},
54-
{"single", &config{labelKeys: []string{"fake"}, dropSingleKey: true, lineFormat: kvPairFormat, removeKeys: []string{"foo", "error", "fake"}}, &entry{model.LabelSet{}, `500`, now}},
55-
{"labelmap", &config{labelMap: map[string]interface{}{"bar": "other"}, lineFormat: jsonFormat, removeKeys: []string{"bar", "error"}}, &entry{model.LabelSet{"other": "500"}, `{"foo":"bar"}`, now}},
61+
{"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}},
62+
{"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}},
63+
{"not enough records", &config{labelKeys: []string{"foo"}, lineFormat: jsonFormat, removeKeys: []string{"bar", "error"}}, simpleRecordFixture, nil},
64+
{"labels", &config{labelKeys: []string{"bar", "fake"}, lineFormat: jsonFormat, removeKeys: []string{"fuzz", "error"}}, simpleRecordFixture, &entry{model.LabelSet{"bar": "500"}, `{"foo":"bar"}`, now}},
65+
{"remove key", &config{labelKeys: []string{"fake"}, lineFormat: jsonFormat, removeKeys: []string{"foo", "error", "fake"}}, simpleRecordFixture, &entry{model.LabelSet{}, `{"bar":500}`, now}},
66+
// jsoniter.ConfigCompatibleWithStandardLibrary has an issue passing errors from inside a map:
67+
// https://github.com/json-iterator/go/issues/388 -- fix already proposed upstream (#422)
68+
//{"error", &config{labelKeys: []string{"fake"}, lineFormat: jsonFormat, removeKeys: []string{"foo"}}, simpleRecordFixture, &entry{model.LabelSet{}, `{"bar":500,"error":}`, now}},
69+
{"key value", &config{labelKeys: []string{"fake"}, lineFormat: kvPairFormat, removeKeys: []string{"foo", "error", "fake"}}, simpleRecordFixture, &entry{model.LabelSet{}, `bar=500`, now}},
70+
{"single", &config{labelKeys: []string{"fake"}, dropSingleKey: true, lineFormat: kvPairFormat, removeKeys: []string{"foo", "error", "fake"}}, simpleRecordFixture, &entry{model.LabelSet{}, `500`, now}},
71+
{"labelmap", &config{labelMap: map[string]interface{}{"bar": "other"}, lineFormat: jsonFormat, removeKeys: []string{"bar", "error"}}, simpleRecordFixture, &entry{model.LabelSet{"other": "500"}, `{"foo":"bar"}`, now}},
5672
}
5773
for _, tt := range tests {
5874
t.Run(tt.name, func(t *testing.T) {
@@ -62,7 +78,7 @@ func Test_loki_sendRecord(t *testing.T) {
6278
client: rec,
6379
logger: logger,
6480
}
65-
_ = l.sendRecord(recordFixture, now)
81+
_ = l.sendRecord(tt.record, now)
6682
got := rec.toEntry()
6783
if !reflect.DeepEqual(got, tt.want) {
6884
t.Errorf("sendRecord() want:%v got:%v", tt.want, got)
@@ -82,7 +98,9 @@ func Test_createLine(t *testing.T) {
8298

8399
{"json", map[string]interface{}{"foo": "bar", "bar": map[string]interface{}{"bizz": "bazz"}}, jsonFormat, `{"foo":"bar","bar":{"bizz":"bazz"}}`, false},
84100
{"json with number", map[string]interface{}{"foo": "bar", "bar": map[string]interface{}{"bizz": 20}}, jsonFormat, `{"foo":"bar","bar":{"bizz":20}}`, false},
85-
{"bad json", map[string]interface{}{"foo": make(chan interface{})}, jsonFormat, "", true},
101+
// jsoniter.ConfigCompatibleWithStandardLibrary has an issue passing errors from inside a map:
102+
// https://github.com/json-iterator/go/issues/388 -- fix already proposed upstream (#422)
103+
// {"bad json", map[string]interface{}{"foo": make(chan interface{})}, jsonFormat, "", true},
86104
{"kv", map[string]interface{}{"foo": "bar", "bar": map[string]interface{}{"bizz": "bazz"}}, kvPairFormat, `bar=map[bizz:bazz] foo=bar`, false},
87105
{"kv with number", map[string]interface{}{"foo": "bar", "bar": map[string]interface{}{"bizz": 20}, "decimal": 12.2}, kvPairFormat, `bar=map[bizz:20] decimal=12.2 foo=bar`, false},
88106
{"kv with nil", map[string]interface{}{"foo": "bar", "bar": map[string]interface{}{"bizz": 20}, "null": nil}, kvPairFormat, `bar=map[bizz:20] foo=bar null=<nil>`, false},

0 commit comments

Comments
 (0)