Merge pull request #977 from lynncyrin/pad-option

Add an pad option log level text
This commit is contained in:
David Bariod 2019-07-01 16:35:06 +02:00 committed by GitHub
commit 07a84ee741
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 118 additions and 1 deletions

View File

@ -354,6 +354,7 @@ The built-in logging formatters are:
[github.com/mattn/go-colorable](https://github.com/mattn/go-colorable). [github.com/mattn/go-colorable](https://github.com/mattn/go-colorable).
* When colors are enabled, levels are truncated to 4 characters by default. To disable * When colors are enabled, levels are truncated to 4 characters by default. To disable
truncation set the `DisableLevelTruncation` field to `true`. truncation set the `DisableLevelTruncation` field to `true`.
* When outputting to a TTY, it's often helpful to visually scan down a column where all the levels are the same width. Setting the `PadLevelText` field to `true` enables this behavior, by adding padding to the level text.
* All options are listed in the [generated docs](https://godoc.org/github.com/sirupsen/logrus#TextFormatter). * All options are listed in the [generated docs](https://godoc.org/github.com/sirupsen/logrus#TextFormatter).
* `logrus.JSONFormatter`. Logs fields as JSON. * `logrus.JSONFormatter`. Logs fields as JSON.
* All options are listed in the [generated docs](https://godoc.org/github.com/sirupsen/logrus#JSONFormatter). * All options are listed in the [generated docs](https://godoc.org/github.com/sirupsen/logrus#JSONFormatter).

View File

@ -6,9 +6,11 @@ import (
"os" "os"
"runtime" "runtime"
"sort" "sort"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
"unicode/utf8"
) )
const ( const (
@ -57,6 +59,10 @@ type TextFormatter struct {
// Disables the truncation of the level text to 4 characters. // Disables the truncation of the level text to 4 characters.
DisableLevelTruncation bool DisableLevelTruncation bool
// PadLevelText Adds padding the level text so that all the levels output at the same length
// PadLevelText is a superset of the DisableLevelTruncation option
PadLevelText bool
// QuoteEmptyFields will wrap empty fields in quotes if true // QuoteEmptyFields will wrap empty fields in quotes if true
QuoteEmptyFields bool QuoteEmptyFields bool
@ -79,12 +85,22 @@ type TextFormatter struct {
CallerPrettyfier func(*runtime.Frame) (function string, file string) CallerPrettyfier func(*runtime.Frame) (function string, file string)
terminalInitOnce sync.Once terminalInitOnce sync.Once
// The max length of the level text, generated dynamically on init
levelTextMaxLength int
} }
func (f *TextFormatter) init(entry *Entry) { func (f *TextFormatter) init(entry *Entry) {
if entry.Logger != nil { if entry.Logger != nil {
f.isTerminal = checkIfTerminal(entry.Logger.Out) f.isTerminal = checkIfTerminal(entry.Logger.Out)
} }
// Get the max length of the level text
for _, level := range AllLevels {
levelTextLength := utf8.RuneCount([]byte(level.String()))
if levelTextLength > f.levelTextMaxLength {
f.levelTextMaxLength = levelTextLength
}
}
} }
func (f *TextFormatter) isColored() bool { func (f *TextFormatter) isColored() bool {
@ -217,9 +233,18 @@ func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []strin
} }
levelText := strings.ToUpper(entry.Level.String()) levelText := strings.ToUpper(entry.Level.String())
if !f.DisableLevelTruncation { if !f.DisableLevelTruncation && !f.PadLevelText {
levelText = levelText[0:4] levelText = levelText[0:4]
} }
if f.PadLevelText {
// Generates the format string used in the next line, for example "%-6s" or "%-7s".
// Based on the max level text length.
formatString := "%-" + strconv.Itoa(f.levelTextMaxLength) + "s"
// Formats the level text by appending spaces up to the max length, for example:
// - "INFO "
// - "WARNING"
levelText = fmt.Sprintf(formatString, levelText)
}
// Remove a single newline if it already exists in the message to keep // Remove a single newline if it already exists in the message to keep
// the behavior of logrus text_formatter the same as the stdlib log package // the behavior of logrus text_formatter the same as the stdlib log package

View File

@ -172,6 +172,97 @@ func TestDisableLevelTruncation(t *testing.T) {
checkDisableTruncation(false, InfoLevel) checkDisableTruncation(false, InfoLevel)
} }
func TestPadLevelText(t *testing.T) {
// A note for future maintainers / committers:
//
// This test denormalizes the level text as a part of its assertions.
// Because of that, its not really a "unit test" of the PadLevelText functionality.
// So! Many apologies to the potential future person who has to rewrite this test
// when they are changing some completely unrelated functionality.
params := []struct {
name string
level Level
paddedLevelText string
}{
{
name: "PanicLevel",
level: PanicLevel,
paddedLevelText: "PANIC ", // 2 extra spaces
},
{
name: "FatalLevel",
level: FatalLevel,
paddedLevelText: "FATAL ", // 2 extra spaces
},
{
name: "ErrorLevel",
level: ErrorLevel,
paddedLevelText: "ERROR ", // 2 extra spaces
},
{
name: "WarnLevel",
level: WarnLevel,
// WARNING is already the max length, so we don't need to assert a paddedLevelText
},
{
name: "DebugLevel",
level: DebugLevel,
paddedLevelText: "DEBUG ", // 2 extra spaces
},
{
name: "TraceLevel",
level: TraceLevel,
paddedLevelText: "TRACE ", // 2 extra spaces
},
{
name: "InfoLevel",
level: InfoLevel,
paddedLevelText: "INFO ", // 3 extra spaces
},
}
// We create a "default" TextFormatter to do a control test.
// We also create a TextFormatter with PadLevelText, which is the parameter we want to do our most relevant assertions against.
tfDefault := TextFormatter{}
tfWithPadding := TextFormatter{PadLevelText: true}
for _, val := range params {
t.Run(val.name, func(t *testing.T) {
// TextFormatter writes into these bytes.Buffers, and we make assertions about their contents later
var bytesDefault bytes.Buffer
var bytesWithPadding bytes.Buffer
// The TextFormatter instance and the bytes.Buffer instance are different here
// all the other arguments are the same. We also initialize them so that they
// fill in the value of levelTextMaxLength.
tfDefault.init(&Entry{})
tfDefault.printColored(&bytesDefault, &Entry{Level: val.level}, []string{}, nil, "")
tfWithPadding.init(&Entry{})
tfWithPadding.printColored(&bytesWithPadding, &Entry{Level: val.level}, []string{}, nil, "")
// turn the bytes back into a string so that we can actually work with the data
logLineDefault := (&bytesDefault).String()
logLineWithPadding := (&bytesWithPadding).String()
// Control: the level text should not be padded by default
if val.paddedLevelText != "" && strings.Contains(logLineDefault, val.paddedLevelText) {
t.Errorf("log line %q should not contain the padded level text %q by default", logLineDefault, val.paddedLevelText)
}
// Assertion: the level text should still contain the string representation of the level
if !strings.Contains(strings.ToLower(logLineWithPadding), val.level.String()) {
t.Errorf("log line %q should contain the level text %q when padding is enabled", logLineWithPadding, val.level.String())
}
// Assertion: the level text should be in its padded form now
if val.paddedLevelText != "" && !strings.Contains(logLineWithPadding, val.paddedLevelText) {
t.Errorf("log line %q should contain the padded level text %q when padding is enabled", logLineWithPadding, val.paddedLevelText)
}
})
}
}
func TestDisableTimestampWithColoredOutput(t *testing.T) { func TestDisableTimestampWithColoredOutput(t *testing.T) {
tf := &TextFormatter{DisableTimestamp: true, ForceColors: true} tf := &TextFormatter{DisableTimestamp: true, ForceColors: true}