1
0
mirror of https://github.com/amir20/dozzle.git synced 2025-12-24 06:28:42 +01:00

fix: implements logfmt with more strict rules. fixes #3006 (#3007)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
This commit is contained in:
Amir Raminfar
2024-06-03 08:52:40 -07:00
committed by GitHub
parent f2e29dadf1
commit a0d60357b8
9 changed files with 198 additions and 35 deletions

View File

@@ -8,7 +8,6 @@ import (
"fmt"
"hash/fnv"
"io"
"regexp"
"strings"
"sync"
"time"
@@ -17,7 +16,6 @@ import (
orderedmap "github.com/wk8/go-ordered-map/v2"
"github.com/go-logfmt/logfmt"
log "github.com/sirupsen/logrus"
)
@@ -160,9 +158,6 @@ func readEvent(reader *bufio.Reader, tty bool) (string, StdType, error) {
}
}
var validLogFmtMessage = regexp.MustCompile(`([a-zA-Z0-9_.-]+)=(?:(?:"(.*)")|(?:(?:([^\s]+)[\s])))`)
var validLogFmtKey = regexp.MustCompile(`^[a-zA-Z0-9_.-]+$`)
func createEvent(message string, streamType StdType) *LogEvent {
h := fnv.New32a()
h.Write([]byte(message))
@@ -185,27 +180,8 @@ func createEvent(message string, streamType StdType) *LogEvent {
} else {
logEvent.Message = data
}
} else if validLogFmtMessage.MatchString(message) {
buffer := bufPool.Get().(*bytes.Buffer)
buffer.Reset()
defer bufPool.Put(buffer)
buffer.WriteString(message)
decoder := logfmt.NewDecoder(buffer)
data := make(map[string]string)
decoder.ScanRecord()
allValid := true
for decoder.ScanKeyval() {
key := decoder.Key()
value := decoder.Value()
if !validLogFmtKey.Match(key) {
allValid = false
break
}
data[string(key)] = string(value)
}
if allValid && len(data) > 1 {
logEvent.Message = data
}
} else if data, err := ParseLogFmt(message); err == nil {
logEvent.Message = data
}
}
}

View File

@@ -127,7 +127,17 @@ func Test_createEvent(t *testing.T) {
Message: "123",
},
},
{
name: "invalid logfmt message",
args: args{
message: "2020-05-13T18:55:37.772853839Z sample text with=equal sign",
},
want: &LogEvent{
Message: "sample text with=equal sign",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := createEvent(tt.args.message, STDOUT); !reflect.DeepEqual(got.Message, tt.want.Message) {

View File

@@ -52,6 +52,11 @@ func guessLogLevel(logEvent *LogEvent) string {
}
}
case *orderedmap.OrderedMap[string, string]:
if level, ok := value.Get("level"); ok {
return strings.ToLower(level)
}
case map[string]interface{}:
if level, ok := value["level"].(string); ok {
return strings.ToLower(level)

View File

@@ -7,10 +7,6 @@ import (
)
func TestGuessLogLevel(t *testing.T) {
ordereddata := orderedmap.New[string, any]()
ordereddata.Set("key", "value")
ordereddata.Set("level", "info")
tests := []struct {
input any
expected string
@@ -35,7 +31,18 @@ func TestGuessLogLevel(t *testing.T) {
{map[string]interface{}{"level": "INFO"}, "info"},
{map[string]string{"level": "info"}, "info"},
{map[string]string{"level": "INFO"}, "info"},
{ordereddata, "info"},
{orderedmap.New[string, string](
orderedmap.WithInitialData(
orderedmap.Pair[string, string]{Key: "key", Value: "value"},
orderedmap.Pair[string, string]{Key: "level", Value: "info"},
),
), "info"},
{orderedmap.New[string, any](
orderedmap.WithInitialData(
orderedmap.Pair[string, any]{Key: "key", Value: "value"},
orderedmap.Pair[string, any]{Key: "level", Value: "info"},
),
), "info"},
}
for _, test := range tests {

86
internal/docker/logfmt.go Normal file
View File

@@ -0,0 +1,86 @@
package docker
import (
"errors"
orderedmap "github.com/wk8/go-ordered-map/v2"
)
// ParseLogFmt parses a log entry in logfmt format and returns a map of key-value pairs.
func ParseLogFmt(log string) (*orderedmap.OrderedMap[string, string], error) {
result := orderedmap.New[string, string]()
var key, value string
inQuotes, escaping, isKey := false, false, true
start := 0
for i := 0; i < len(log); i++ {
char := log[i]
if isKey {
if char == '=' {
if i == start {
return nil, errors.New("invalid format: key is empty")
}
key = log[start:i]
isKey = false
start = i + 1
} else if char == ' ' {
if i > start {
return nil, errors.New("invalid format: unexpected space in key")
}
}
} else {
if inQuotes {
if escaping {
escaping = false
} else if char == '\\' {
escaping = true
} else if char == '"' {
value = log[start:i]
result.Set(key, value)
inQuotes = false
isKey = true
start = i + 2
}
} else {
if char == '"' {
inQuotes = true
start = i + 1
} else if char == ' ' {
if i == start {
return nil, errors.New("invalid format: value is empty")
}
value = log[start:i]
result.Set(key, value)
isKey = true
start = i + 1
}
}
}
}
// Handle the last key-value pair if there is no trailing space
if !isKey && start < len(log) {
if inQuotes {
return nil, errors.New("invalid format: unclosed quotes")
}
value = log[start:]
result.Set(key, value)
} else if isKey && start < len(log) {
return nil, errors.New("invalid format: unexpected key without value")
}
if !isKey {
if inQuotes {
return nil, errors.New("invalid format: unclosed quotes")
}
if start >= len(log) {
return nil, errors.New("invalid format: value is empty")
}
value = log[start:]
result.Set(key, value)
}
return result, nil
}

View File

@@ -0,0 +1,82 @@
package docker
import (
"encoding/json"
"reflect"
"testing"
orderedmap "github.com/wk8/go-ordered-map/v2"
)
func TestParseLog(t *testing.T) {
tests := []struct {
name string
log string
want *orderedmap.OrderedMap[string, string]
wantErr bool
}{
{
name: "Valid logfmt log",
log: `time="2024-06-02T14:30:42Z" level=debug msg="container e23e04da2cb9 started"`,
want: orderedmap.New[string, string](
orderedmap.WithInitialData(
orderedmap.Pair[string, string]{Key: "time", Value: "2024-06-02T14:30:42Z"},
orderedmap.Pair[string, string]{Key: "level", Value: "debug"},
orderedmap.Pair[string, string]{Key: "msg", Value: "container e23e04da2cb9 started"},
),
),
wantErr: false,
},
{
name: "Random test with equal sign",
log: "foo bar=baz",
want: nil,
wantErr: true,
},
{
name: "Invalid log with key without value",
log: "key1=value1 key2=",
want: nil,
wantErr: true,
},
{
name: "Valid log",
log: "key1=value1 key2=value2",
want: orderedmap.New[string, string](
orderedmap.WithInitialData(
orderedmap.Pair[string, string]{Key: "key1", Value: "value1"},
orderedmap.Pair[string, string]{Key: "key2", Value: "value2"},
),
),
wantErr: false,
},
{
name: "Invalid log with unclosed quotes",
log: "key1=\"value1 key2=value2",
want: nil,
wantErr: true,
},
{
name: "Plain text log",
log: "foo bar baz",
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseLogFmt(tt.log)
if (err != nil) != tt.wantErr {
t.Errorf("ParseLogFmt() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
jsonGot, _ := json.MarshalIndent(got, "", " ")
jsonWant, _ := json.MarshalIndent(tt.want, "", " ")
t.Errorf("ParseLogFmt() = %v, want %v", string(jsonGot), string(jsonWant))
}
})
}
}