Browse Source

Merge pull request #977 from lynncyrin/pad-option

Add an pad option log level text
pull/984/head
David Bariod 4 months ago
parent
commit
07a84ee741
No account linked to committer's email address
3 changed files with 118 additions and 1 deletions
  1. 1
    0
      README.md
  2. 26
    1
      text_formatter.go
  3. 91
    0
      text_formatter_test.go

+ 1
- 0
README.md View File

@@ -354,6 +354,7 @@ The built-in logging formatters are:
354 354
     [github.com/mattn/go-colorable](https://github.com/mattn/go-colorable).
355 355
   * When colors are enabled, levels are truncated to 4 characters by default. To disable
356 356
     truncation set the `DisableLevelTruncation` field to `true`.
357
+  * 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.
357 358
   * All options are listed in the [generated docs](https://godoc.org/github.com/sirupsen/logrus#TextFormatter).
358 359
 * `logrus.JSONFormatter`. Logs fields as JSON.
359 360
   * All options are listed in the [generated docs](https://godoc.org/github.com/sirupsen/logrus#JSONFormatter).

+ 26
- 1
text_formatter.go View File

@@ -6,9 +6,11 @@ import (
6 6
 	"os"
7 7
 	"runtime"
8 8
 	"sort"
9
+	"strconv"
9 10
 	"strings"
10 11
 	"sync"
11 12
 	"time"
13
+	"unicode/utf8"
12 14
 )
13 15
 
14 16
 const (
@@ -57,6 +59,10 @@ type TextFormatter struct {
57 59
 	// Disables the truncation of the level text to 4 characters.
58 60
 	DisableLevelTruncation bool
59 61
 
62
+	// PadLevelText Adds padding the level text so that all the levels output at the same length
63
+	// PadLevelText is a superset of the DisableLevelTruncation option
64
+	PadLevelText bool
65
+
60 66
 	// QuoteEmptyFields will wrap empty fields in quotes if true
61 67
 	QuoteEmptyFields bool
62 68
 
@@ -79,12 +85,22 @@ type TextFormatter struct {
79 85
 	CallerPrettyfier func(*runtime.Frame) (function string, file string)
80 86
 
81 87
 	terminalInitOnce sync.Once
88
+
89
+	// The max length of the level text, generated dynamically on init
90
+	levelTextMaxLength int
82 91
 }
83 92
 
84 93
 func (f *TextFormatter) init(entry *Entry) {
85 94
 	if entry.Logger != nil {
86 95
 		f.isTerminal = checkIfTerminal(entry.Logger.Out)
87 96
 	}
97
+	// Get the max length of the level text
98
+	for _, level := range AllLevels {
99
+		levelTextLength := utf8.RuneCount([]byte(level.String()))
100
+		if levelTextLength > f.levelTextMaxLength {
101
+			f.levelTextMaxLength = levelTextLength
102
+		}
103
+	}
88 104
 }
89 105
 
90 106
 func (f *TextFormatter) isColored() bool {
@@ -217,9 +233,18 @@ func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []strin
217 233
 	}
218 234
 
219 235
 	levelText := strings.ToUpper(entry.Level.String())
220
-	if !f.DisableLevelTruncation {
236
+	if !f.DisableLevelTruncation && !f.PadLevelText {
221 237
 		levelText = levelText[0:4]
222 238
 	}
239
+	if f.PadLevelText {
240
+		// Generates the format string used in the next line, for example "%-6s" or "%-7s".
241
+		// Based on the max level text length.
242
+		formatString := "%-" + strconv.Itoa(f.levelTextMaxLength) + "s"
243
+		// Formats the level text by appending spaces up to the max length, for example:
244
+		// 	- "INFO   "
245
+		//	- "WARNING"
246
+		levelText = fmt.Sprintf(formatString, levelText)
247
+	}
223 248
 
224 249
 	// Remove a single newline if it already exists in the message to keep
225 250
 	// the behavior of logrus text_formatter the same as the stdlib log package

+ 91
- 0
text_formatter_test.go View File

@@ -172,6 +172,97 @@ func TestDisableLevelTruncation(t *testing.T) {
172 172
 	checkDisableTruncation(false, InfoLevel)
173 173
 }
174 174
 
175
+func TestPadLevelText(t *testing.T) {
176
+	// A note for future maintainers / committers:
177
+	//
178
+	// This test denormalizes the level text as a part of its assertions.
179
+	// Because of that, its not really a "unit test" of the PadLevelText functionality.
180
+	// So! Many apologies to the potential future person who has to rewrite this test
181
+	// when they are changing some completely unrelated functionality.
182
+	params := []struct {
183
+		name            string
184
+		level           Level
185
+		paddedLevelText string
186
+	}{
187
+		{
188
+			name:            "PanicLevel",
189
+			level:           PanicLevel,
190
+			paddedLevelText: "PANIC  ", // 2 extra spaces
191
+		},
192
+		{
193
+			name:            "FatalLevel",
194
+			level:           FatalLevel,
195
+			paddedLevelText: "FATAL  ", // 2 extra spaces
196
+		},
197
+		{
198
+			name:            "ErrorLevel",
199
+			level:           ErrorLevel,
200
+			paddedLevelText: "ERROR  ", // 2 extra spaces
201
+		},
202
+		{
203
+			name:  "WarnLevel",
204
+			level: WarnLevel,
205
+			// WARNING is already the max length, so we don't need to assert a paddedLevelText
206
+		},
207
+		{
208
+			name:            "DebugLevel",
209
+			level:           DebugLevel,
210
+			paddedLevelText: "DEBUG  ", // 2 extra spaces
211
+		},
212
+		{
213
+			name:            "TraceLevel",
214
+			level:           TraceLevel,
215
+			paddedLevelText: "TRACE  ", // 2 extra spaces
216
+		},
217
+		{
218
+			name:            "InfoLevel",
219
+			level:           InfoLevel,
220
+			paddedLevelText: "INFO   ", // 3 extra spaces
221
+		},
222
+	}
223
+
224
+	// We create a "default" TextFormatter to do a control test.
225
+	// We also create a TextFormatter with PadLevelText, which is the parameter we want to do our most relevant assertions against.
226
+	tfDefault := TextFormatter{}
227
+	tfWithPadding := TextFormatter{PadLevelText: true}
228
+
229
+	for _, val := range params {
230
+		t.Run(val.name, func(t *testing.T) {
231
+			// TextFormatter writes into these bytes.Buffers, and we make assertions about their contents later
232
+			var bytesDefault bytes.Buffer
233
+			var bytesWithPadding bytes.Buffer
234
+
235
+			// The TextFormatter instance and the bytes.Buffer instance are different here
236
+			// all the other arguments are the same. We also initialize them so that they
237
+			// fill in the value of levelTextMaxLength.
238
+			tfDefault.init(&Entry{})
239
+			tfDefault.printColored(&bytesDefault, &Entry{Level: val.level}, []string{}, nil, "")
240
+			tfWithPadding.init(&Entry{})
241
+			tfWithPadding.printColored(&bytesWithPadding, &Entry{Level: val.level}, []string{}, nil, "")
242
+
243
+			// turn the bytes back into a string so that we can actually work with the data
244
+			logLineDefault := (&bytesDefault).String()
245
+			logLineWithPadding := (&bytesWithPadding).String()
246
+
247
+			// Control: the level text should not be padded by default
248
+			if val.paddedLevelText != "" && strings.Contains(logLineDefault, val.paddedLevelText) {
249
+				t.Errorf("log line %q should not contain the padded level text %q by default", logLineDefault, val.paddedLevelText)
250
+			}
251
+
252
+			// Assertion: the level text should still contain the string representation of the level
253
+			if !strings.Contains(strings.ToLower(logLineWithPadding), val.level.String()) {
254
+				t.Errorf("log line %q should contain the level text %q when padding is enabled", logLineWithPadding, val.level.String())
255
+			}
256
+
257
+			// Assertion: the level text should be in its padded form now
258
+			if val.paddedLevelText != "" && !strings.Contains(logLineWithPadding, val.paddedLevelText) {
259
+				t.Errorf("log line %q should contain the padded level text %q when padding is enabled", logLineWithPadding, val.paddedLevelText)
260
+			}
261
+
262
+		})
263
+	}
264
+}
265
+
175 266
 func TestDisableTimestampWithColoredOutput(t *testing.T) {
176 267
 	tf := &TextFormatter{DisableTimestamp: true, ForceColors: true}
177 268
 

Loading…
Cancel
Save