From cfca98e6d9c0082625feae2fe6bde8cb955c370d Mon Sep 17 00:00:00 2001 From: Ben Brooks Date: Tue, 14 Feb 2017 10:53:03 +0000 Subject: [PATCH 1/3] Add 'QuoteEmptyFields' option to TextFormatter --- text_formatter.go | 12 +++++++++--- text_formatter_test.go | 6 +++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/text_formatter.go b/text_formatter.go index b4dffa1..51397cc 100644 --- a/text_formatter.go +++ b/text_formatter.go @@ -49,6 +49,9 @@ type TextFormatter struct { // be desired. DisableSorting bool + // QuoteEmptyFields will wrap empty fields in quotes if true + QuoteEmptyFields bool + // Whether the logger's out is to a terminal isTerminal bool terminalOnce sync.Once @@ -132,7 +135,10 @@ func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []strin } } -func needsQuoting(text string) bool { +func (f *TextFormatter) needsQuoting(text string) bool { + if f.QuoteEmptyFields && len(text) == 0 { + return true + } for _, ch := range text { if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || @@ -155,14 +161,14 @@ func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interf func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) { switch value := value.(type) { case string: - if !needsQuoting(value) { + if !f.needsQuoting(value) { b.WriteString(value) } else { fmt.Fprintf(b, "%q", value) } case error: errmsg := value.Error() - if !needsQuoting(errmsg) { + if !f.needsQuoting(errmsg) { b.WriteString(errmsg) } else { fmt.Fprintf(b, "%q", errmsg) diff --git a/text_formatter_test.go b/text_formatter_test.go index 107703f..54fc8fe 100644 --- a/text_formatter_test.go +++ b/text_formatter_test.go @@ -3,9 +3,9 @@ package logrus import ( "bytes" "errors" + "strings" "testing" "time" - "strings" ) func TestQuoting(t *testing.T) { @@ -24,6 +24,7 @@ func TestQuoting(t *testing.T) { } } + checkQuoting(false, "") checkQuoting(false, "abcd") checkQuoting(false, "v1.0") checkQuoting(false, "1234567890") @@ -32,6 +33,9 @@ func TestQuoting(t *testing.T) { checkQuoting(true, "x,y") checkQuoting(false, errors.New("invalid")) checkQuoting(true, errors.New("invalid argument")) + + tf.QuoteEmptyFields = true + checkQuoting(true, "") } func TestTimestampFormat(t *testing.T) { From b545aee819549403a5074d8ce7590c5867253409 Mon Sep 17 00:00:00 2001 From: Ben Brooks Date: Tue, 14 Feb 2017 11:51:23 +0000 Subject: [PATCH 2/3] Add TextFormatter config for custom quote runes --- text_formatter.go | 18 +++++++++++++----- text_formatter_test.go | 13 +++++++++++-- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/text_formatter.go b/text_formatter.go index 51397cc..ef799f1 100644 --- a/text_formatter.go +++ b/text_formatter.go @@ -7,6 +7,7 @@ import ( "strings" "sync" "time" + "unicode/utf8" ) const ( @@ -52,9 +53,13 @@ type TextFormatter struct { // QuoteEmptyFields will wrap empty fields in quotes if true QuoteEmptyFields bool + // QuoteRune can be set to override the default quote style + QuoteRune rune + // Whether the logger's out is to a terminal - isTerminal bool - terminalOnce sync.Once + isTerminal bool + + sync.Once } func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { @@ -75,7 +80,10 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { prefixFieldClashes(entry.Data) - f.terminalOnce.Do(func() { + f.Do(func() { + if f.QuoteRune == 0 || !utf8.ValidRune(f.QuoteRune) { + f.QuoteRune = '"' + } if entry.Logger != nil { f.isTerminal = IsTerminal(entry.Logger.Out) } @@ -164,14 +172,14 @@ func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) { if !f.needsQuoting(value) { b.WriteString(value) } else { - fmt.Fprintf(b, "%q", value) + fmt.Fprintf(b, "%c%v%c", f.QuoteRune, value, f.QuoteRune) } case error: errmsg := value.Error() if !f.needsQuoting(errmsg) { b.WriteString(errmsg) } else { - fmt.Fprintf(b, "%q", errmsg) + fmt.Fprintf(b, "%c%v%c", f.QuoteRune, errmsg, f.QuoteRune) } default: fmt.Fprint(b, value) diff --git a/text_formatter_test.go b/text_formatter_test.go index 54fc8fe..fce4cfe 100644 --- a/text_formatter_test.go +++ b/text_formatter_test.go @@ -14,7 +14,7 @@ func TestQuoting(t *testing.T) { checkQuoting := func(q bool, value interface{}) { b, _ := tf.Format(WithField("test", value)) idx := bytes.Index(b, ([]byte)("test=")) - cont := bytes.Contains(b[idx+5:], []byte{'"'}) + cont := bytes.ContainsRune(b[idx+5:], tf.QuoteRune) if cont != q { if q { t.Errorf("quoting expected for: %#v", value) @@ -34,6 +34,14 @@ func TestQuoting(t *testing.T) { checkQuoting(false, errors.New("invalid")) checkQuoting(true, errors.New("invalid argument")) + // Test for custom quote rune. + tf.QuoteRune = '`' + checkQuoting(false, "") + checkQuoting(false, "abcd") + checkQuoting(true, "/foobar") + checkQuoting(true, errors.New("invalid argument")) + + // Test for quoting empty fields. tf.QuoteEmptyFields = true checkQuoting(true, "") } @@ -45,7 +53,8 @@ func TestTimestampFormat(t *testing.T) { timeStart := bytes.Index(customStr, ([]byte)("time=")) timeEnd := bytes.Index(customStr, ([]byte)("level=")) timeStr := customStr[timeStart+5 : timeEnd-1] - if timeStr[0] == '"' && timeStr[len(timeStr)-1] == '"' { + if timeStr[0] == byte(customFormatter.QuoteRune) && + timeStr[len(timeStr)-1] == byte(customFormatter.QuoteRune) { timeStr = timeStr[1 : len(timeStr)-1] } if format == "" { From e98cd92ccfea08009a411ce5310e1797f84e3b23 Mon Sep 17 00:00:00 2001 From: Ben Brooks Date: Wed, 15 Feb 2017 13:08:26 +0000 Subject: [PATCH 3/3] Address PR comments --- text_formatter.go | 28 +++++++++++++++------------- text_formatter_test.go | 19 +++++++++++-------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/text_formatter.go b/text_formatter.go index ef799f1..ba88854 100644 --- a/text_formatter.go +++ b/text_formatter.go @@ -7,7 +7,6 @@ import ( "strings" "sync" "time" - "unicode/utf8" ) const ( @@ -53,8 +52,9 @@ type TextFormatter struct { // QuoteEmptyFields will wrap empty fields in quotes if true QuoteEmptyFields bool - // QuoteRune can be set to override the default quote style - QuoteRune rune + // QuoteCharacter can be set to the override the default quoting character " + // with something else. For example: ', or `. + QuoteCharacter string // Whether the logger's out is to a terminal isTerminal bool @@ -62,6 +62,15 @@ type TextFormatter struct { sync.Once } +func (f *TextFormatter) init(entry *Entry) { + if len(f.QuoteCharacter) == 0 { + f.QuoteCharacter = "\"" + } + if entry.Logger != nil { + f.isTerminal = IsTerminal(entry.Logger.Out) + } +} + func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { var b *bytes.Buffer keys := make([]string, 0, len(entry.Data)) @@ -80,14 +89,7 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { prefixFieldClashes(entry.Data) - f.Do(func() { - if f.QuoteRune == 0 || !utf8.ValidRune(f.QuoteRune) { - f.QuoteRune = '"' - } - if entry.Logger != nil { - f.isTerminal = IsTerminal(entry.Logger.Out) - } - }) + f.Do(func() { f.init(entry) }) isColored := (f.ForceColors || f.isTerminal) && !f.DisableColors @@ -172,14 +174,14 @@ func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) { if !f.needsQuoting(value) { b.WriteString(value) } else { - fmt.Fprintf(b, "%c%v%c", f.QuoteRune, value, f.QuoteRune) + fmt.Fprintf(b, "%s%v%s", f.QuoteCharacter, value, f.QuoteCharacter) } case error: errmsg := value.Error() if !f.needsQuoting(errmsg) { b.WriteString(errmsg) } else { - fmt.Fprintf(b, "%c%v%c", f.QuoteRune, errmsg, f.QuoteRune) + fmt.Fprintf(b, "%s%v%s", f.QuoteCharacter, errmsg, f.QuoteCharacter) } default: fmt.Fprint(b, value) diff --git a/text_formatter_test.go b/text_formatter_test.go index fce4cfe..9793b5f 100644 --- a/text_formatter_test.go +++ b/text_formatter_test.go @@ -14,7 +14,7 @@ func TestQuoting(t *testing.T) { checkQuoting := func(q bool, value interface{}) { b, _ := tf.Format(WithField("test", value)) idx := bytes.Index(b, ([]byte)("test=")) - cont := bytes.ContainsRune(b[idx+5:], tf.QuoteRune) + cont := bytes.Contains(b[idx+5:], []byte(tf.QuoteCharacter)) if cont != q { if q { t.Errorf("quoting expected for: %#v", value) @@ -34,16 +34,23 @@ func TestQuoting(t *testing.T) { checkQuoting(false, errors.New("invalid")) checkQuoting(true, errors.New("invalid argument")) - // Test for custom quote rune. - tf.QuoteRune = '`' + // Test for custom quote character. + tf.QuoteCharacter = "`" checkQuoting(false, "") checkQuoting(false, "abcd") checkQuoting(true, "/foobar") checkQuoting(true, errors.New("invalid argument")) + // Test for multi-character quotes. + tf.QuoteCharacter = "§~±" + checkQuoting(false, "abcd") + checkQuoting(true, errors.New("invalid argument")) + // Test for quoting empty fields. tf.QuoteEmptyFields = true checkQuoting(true, "") + checkQuoting(false, "abcd") + checkQuoting(true, errors.New("invalid argument")) } func TestTimestampFormat(t *testing.T) { @@ -52,11 +59,7 @@ func TestTimestampFormat(t *testing.T) { customStr, _ := customFormatter.Format(WithField("test", "test")) timeStart := bytes.Index(customStr, ([]byte)("time=")) timeEnd := bytes.Index(customStr, ([]byte)("level=")) - timeStr := customStr[timeStart+5 : timeEnd-1] - if timeStr[0] == byte(customFormatter.QuoteRune) && - timeStr[len(timeStr)-1] == byte(customFormatter.QuoteRune) { - timeStr = timeStr[1 : len(timeStr)-1] - } + timeStr := customStr[timeStart+5+len(customFormatter.QuoteCharacter) : timeEnd-1-len(customFormatter.QuoteCharacter)] if format == "" { format = time.RFC3339 }