Repository: rs/zerolog
Pull request #665 (2024-04-09 16:17:56)

Resolve panic in the console writer for short `level` field values
This PR fixes an issue in the `zerolog` console writer. Specifying a non-typical value for the `level` field, resulting in a serialized value of less than 3 bytes, causes a panic. For instance:

```golang
log := zerolog.New(zerolog.NewConsoleWriter())
log.Debug().Str("level", "-").Msg("Hello World")
```


Patch:
diff --git a/console.go b/console.go
index 10c1a1c..23b3a04 100644
--- a/console.go
+++ b/console.go
@@ -28,6 +28,8 @@ const (
 
 	colorBold     = 1
 	colorDarkGray = 90
+
+	unknownLevel = "???"
 )
 
 var (
@@ -400,25 +402,30 @@ func consoleDefaultFormatTimestamp(timeFormat string, location *time.Location, n
 	}
 }
 
+func stripLevel(ll string) string {
+	if len(ll) == 0 {
+		return unknownLevel
+	}
+	if len(ll) > 3 {
+		ll = ll[:3]
+	}
+	return strings.ToUpper(ll)
+}
+
 func consoleDefaultFormatLevel(noColor bool) Formatter {
 	return func(i interface{}) string {
-		var l string
 		if ll, ok := i.(string); ok {
 			level, _ := ParseLevel(ll)
 			fl, ok := FormattedLevels[level]
 			if ok {
-				l = colorize(fl, LevelColors[level], noColor)
-			} else {
-				l = strings.ToUpper(ll)[0:3]
-			}
-		} else {
-			if i == nil {
-				l = "???"
-			} else {
-				l = strings.ToUpper(fmt.Sprintf("%s", i))[0:3]
+				return colorize(fl, LevelColors[level], noColor)
 			}
+			return stripLevel(ll)
+		}
+		if i == nil {
+			return unknownLevel
 		}
-		return l
+		return stripLevel(fmt.Sprintf("%s", i))
 	}
 }
 


Test patch:
diff --git a/console_test.go b/console_test.go
index 08121d0..5b2983d 100644
--- a/console_test.go
+++ b/console_test.go
@@ -320,6 +320,85 @@ func TestConsoleWriter(t *testing.T) {
 			t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput)
 		}
 	})
+
+	t.Run("With an extra 'level' field", func(t *testing.T) {
+		t.Run("malformed string", func(t *testing.T) {
+			cases := []struct {
+				field  string
+				output string
+			}{
+				{"", "<nil> ??? Hello World foo=bar\n"},
+				{"-", "<nil> - Hello World foo=bar\n"},
+				{"1", "<nil> " + zerolog.FormattedLevels[1] + " Hello World foo=bar\n"},
+				{"a", "<nil> A Hello World foo=bar\n"},
+				{"12", "<nil> 12 Hello World foo=bar\n"},
+				{"a2", "<nil> A2 Hello World foo=bar\n"},
+				{"2a", "<nil> 2A Hello World foo=bar\n"},
+				{"ab", "<nil> AB Hello World foo=bar\n"},
+				{"12a", "<nil> 12A Hello World foo=bar\n"},
+				{"a12", "<nil> A12 Hello World foo=bar\n"},
+				{"abc", "<nil> ABC Hello World foo=bar\n"},
+				{"123", "<nil> 123 Hello World foo=bar\n"},
+				{"abcd", "<nil> ABC Hello World foo=bar\n"},
+				{"1234", "<nil> 123 Hello World foo=bar\n"},
+				{"123d", "<nil> 123 Hello World foo=bar\n"},
+				{"01", "<nil> " + zerolog.FormattedLevels[1] + " Hello World foo=bar\n"},
+				{"001", "<nil> " + zerolog.FormattedLevels[1] + " Hello World foo=bar\n"},
+				{"0001", "<nil> " + zerolog.FormattedLevels[1] + " Hello World foo=bar\n"},
+			}
+			for i, c := range cases {
+				c := c
+				t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
+					buf := &bytes.Buffer{}
+					out := zerolog.NewConsoleWriter()
+					out.NoColor = true
+					out.Out = buf
+					log := zerolog.New(out)
+
+					log.Debug().Str("level", c.field).Str("foo", "bar").Msg("Hello World")
+
+					actualOutput := buf.String()
+					if actualOutput != c.output {
+						t.Errorf("Unexpected output %q, want: %q", actualOutput, c.output)
+					}
+				})
+			}
+		})
+
+		t.Run("weird value", func(t *testing.T) {
+			cases := []struct {
+				field  interface{}
+				output string
+			}{
+				{0, "<nil> 0 Hello World foo=bar\n"},
+				{1, "<nil> 1 Hello World foo=bar\n"},
+				{-1, "<nil> -1 Hello World foo=bar\n"},
+				{-3, "<nil> -3 Hello World foo=bar\n"},
+				{-32, "<nil> -32 Hello World foo=bar\n"},
+				{-321, "<nil> -32 Hello World foo=bar\n"},
+				{12, "<nil> 12 Hello World foo=bar\n"},
+				{123, "<nil> 123 Hello World foo=bar\n"},
+				{1234, "<nil> 123 Hello World foo=bar\n"},
+			}
+			for i, c := range cases {
+				c := c
+				t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
+					buf := &bytes.Buffer{}
+					out := zerolog.NewConsoleWriter()
+					out.NoColor = true
+					out.Out = buf
+					log := zerolog.New(out)
+
+					log.Debug().Interface("level", c.field).Str("foo", "bar").Msg("Hello World")
+
+					actualOutput := buf.String()
+					if actualOutput != c.output {
+						t.Errorf("Unexpected output %q, want: %q", actualOutput, c.output)
+					}
+				})
+			}
+		})
+	})
 }
 
 func TestConsoleWriterConfiguration(t *testing.T) {