diff --git a/buffer.go b/buffer.go index a7d9270..45207b8 100644 --- a/buffer.go +++ b/buffer.go @@ -62,16 +62,6 @@ func (bb *byteBuffer) AppendFloat(f float64, bitSize int) { bb.B = strconv.AppendFloat(bb.B, f, 'f', -1, bitSize) } -// Len returns the length of the underlying buffer. -func (bb *byteBuffer) Len() int { - return len(bb.B) -} - -// Cap returns the capacity of the underlying buffer. -func (bb *byteBuffer) Cap() int { - return cap(bb.B) -} - // Bytes returns a mutable reference to the underlying buffer. func (bb *byteBuffer) Bytes() []byte { return bb.B @@ -81,21 +71,3 @@ func (bb *byteBuffer) Bytes() []byte { func (bb *byteBuffer) Reset() { bb.B = bb.B[:0] } - -// Write implements io.Writer. -func (bb *byteBuffer) Write(bs []byte) (int, error) { - bb.B = append(bb.B, bs...) - return len(bs), nil -} - -// WriteByte writes a single byte to the buffer -func (bb *byteBuffer) WriteByte(v byte) error { - bb.B = append(bb.B, v) - return nil -} - -// WriteString writes a string to the buffer. -func (bb *byteBuffer) WriteString(s string) (int, error) { - bb.B = append(bb.B, s...) - return len(s), nil -} diff --git a/log.go b/log.go index 53ee191..dd71437 100644 --- a/log.go +++ b/log.go @@ -20,6 +20,7 @@ const ( var ( hex = "0123456789abcdef" bufPool byteBufferPool + exit = func() { os.Exit(1) } ) type Opts struct { @@ -82,6 +83,9 @@ func New(opts Opts) Logger { if opts.Level == 0 { opts.Level = InfoLevel } + if opts.CallerSkipFrameCount == 0 { + opts.CallerSkipFrameCount = 3 + } return Logger{ out: newSyncWriter(opts.Writer), @@ -156,7 +160,7 @@ func (l Logger) Error(msg string, fields ...any) { // It aborts the current program with an exit code of 1. func (l Logger) Fatal(msg string, fields ...any) { l.handleLog(msg, FatalLevel, fields...) - os.Exit(1) + exit() } // handleLog emits the log after filtering log level @@ -295,10 +299,16 @@ func writeToBuf(buf *byteBuffer, key string, val any, lvl Level, color, space bo buf.AppendByte('=') switch v := val.(type) { + case nil: + buf.AppendString("null") + case []byte: + escapeAndWriteString(buf, string(v)) case string: escapeAndWriteString(buf, v) case int: buf.AppendInt(int64(v)) + case int8: + buf.AppendInt(int64(v)) case int16: buf.AppendInt(int64(v)) case int32: @@ -309,6 +319,8 @@ func writeToBuf(buf *byteBuffer, key string, val any, lvl Level, color, space bo buf.AppendFloat(float64(v), 32) case float64: buf.AppendFloat(float64(v), 64) + case bool: + buf.AppendBool(v) case error: escapeAndWriteString(buf, v.Error()) case fmt.Stringer: @@ -325,7 +337,7 @@ func writeToBuf(buf *byteBuffer, key string, val any, lvl Level, color, space bo // escapeAndWriteString escapes the string if any unwanted chars are there. func escapeAndWriteString(buf *byteBuffer, s string) { idx := strings.IndexFunc(s, checkEscapingRune) - if idx != -1 { + if idx != -1 || s == "null" { writeQuotedString(buf, s) return } diff --git a/log_test.go b/log_test.go index aeaf389..a5f1257 100644 --- a/log_test.go +++ b/log_test.go @@ -3,13 +3,29 @@ package logf import ( "bytes" "errors" + "log" "strconv" "sync" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +func TestLogFormatWithEnableCaller(t *testing.T) { + buf := &bytes.Buffer{} + l := New(Opts{Writer: buf, EnableCaller: true}) + + l.Info("hello world") + require.Contains(t, buf.String(), `level=info message="hello world" caller=`) + require.Contains(t, buf.String(), `logf/log_test.go:18`) + buf.Reset() + + lC := New(Opts{Writer: buf, EnableCaller: true, EnableColor: true}) + lC.Info("hello world") + require.Contains(t, buf.String(), `logf/log_test.go:24`) + buf.Reset() +} + func TestLevelParsing(t *testing.T) { cases := []struct { String string @@ -25,67 +41,185 @@ func TestLevelParsing(t *testing.T) { for _, c := range cases { t.Run(c.String, func(t *testing.T) { - assert.Equal(t, c.Lvl.String(), c.String, "level should be equal") + require.Equal(t, c.Lvl.String(), c.String, "level should be equal") }) } // Check for an invalid case. t.Run("invalid", func(t *testing.T) { var invalidLvl Level = 10 - assert.Equal(t, invalidLvl.String(), "invalid lvl", "invalid level") + require.Equal(t, invalidLvl.String(), "invalid lvl", "invalid level") }) } func TestNewLoggerDefault(t *testing.T) { l := New(Opts{}) - assert.Equal(t, l.Opts.Level, InfoLevel, "level is info") - assert.Equal(t, l.Opts.EnableColor, false, "color output is disabled") - assert.Equal(t, l.Opts.EnableCaller, false, "caller is disabled") - assert.Equal(t, l.Opts.CallerSkipFrameCount, 0, "skip frame count is 0") - assert.Equal(t, l.Opts.TimestampFormat, defaultTSFormat, "timestamp format is default") + require.Equal(t, l.Opts.Level, InfoLevel, "level is info") + require.Equal(t, l.Opts.EnableColor, false, "color output is disabled") + require.Equal(t, l.Opts.EnableCaller, false, "caller is disabled") + require.Equal(t, l.Opts.CallerSkipFrameCount, 3, "skip frame count is 3") + require.Equal(t, l.Opts.TimestampFormat, defaultTSFormat, "timestamp format is default") +} + +func TestNewSyncWriterWithNil(t *testing.T) { + w := newSyncWriter(nil) + require.NotNil(t, w.w, "writer should not be nil") } func TestLogFormat(t *testing.T) { buf := &bytes.Buffer{} - l := New(Opts{Writer: buf}) + + l := New(Opts{Writer: buf, Level: DebugLevel}) + // Debug log. + l.Debug("debug log") + require.Contains(t, buf.String(), `level=debug message="debug log"`) + buf.Reset() + + l = New(Opts{Writer: buf}) + + // Debug log but with defualt level set to info. + l.Debug("debug log") + require.NotContains(t, buf.String(), `level=debug message="debug log"`) + buf.Reset() // Info log. l.Info("hello world") - assert.Contains(t, buf.String(), `level=info message="hello world"`, "info log") + require.Contains(t, buf.String(), `level=info message="hello world"`, "info log") buf.Reset() // Log with field. l.Warn("testing fields", "stack", "testing") - assert.Contains(t, buf.String(), `level=warn message="testing fields" stack=testing`, "warning log") + require.Contains(t, buf.String(), `level=warn message="testing fields" stack=testing`, "warning log") buf.Reset() // Log with error. fakeErr := errors.New("this is a fake error") l.Error("testing error", "error", fakeErr) - assert.Contains(t, buf.String(), `level=error message="testing error" error="this is a fake error"`, "error log") + require.Contains(t, buf.String(), `level=error message="testing error" error="this is a fake error"`, "error log") + buf.Reset() + + // Fatal log + var hadExit = false + exit = func() { + hadExit = true + } + + l.Fatal("fatal log") + require.True(t, hadExit, "exit should have been called") + require.Contains(t, buf.String(), `level=fatal message="fatal log"`, "fatal log") buf.Reset() } +func TestLogFormatWithColor(t *testing.T) { + buf := &bytes.Buffer{} + l := New(Opts{Writer: buf, EnableColor: true}) + + // Info log. + l.Info("hello world") + require.Contains(t, buf.String(), "\x1b[36mlevel\x1b[0m=info \x1b[36mmessage\x1b[0m=\"hello world\" \n") + buf.Reset() +} + +func TestLoggerTypes(t *testing.T) { + buf := &bytes.Buffer{} + l := New(Opts{Writer: buf, Level: DebugLevel}) + type foo struct { + A int + } + l.Info("hello world", + "string", "foo", + "int", 1, + "int8", int8(1), + "int16", int16(1), + "int32", int32(1), + "int64", int64(1), + "float32", float32(1.0), + "float64", float64(1.0), + "struct", foo{A: 1}, + "bool", true, + ) + + require.Contains(t, buf.String(), "level=info message=\"hello world\" string=foo int=1 int8=1 int16=1 int32=1 int64=1 float32=1 float64=1 struct={1} bool=true \n") +} + func TestLogFormatWithDefaultFields(t *testing.T) { buf := &bytes.Buffer{} l := New(Opts{Writer: buf, DefaultFields: []any{"defaultkey", "defaultvalue"}}) l.Info("hello world") - assert.Contains(t, buf.String(), `level=info message="hello world" defaultkey=defaultvalue`) + require.Contains(t, buf.String(), `level=info message="hello world" defaultkey=defaultvalue`) buf.Reset() l.Info("hello world", "component", "logf") - assert.Contains(t, buf.String(), `level=info message="hello world" defaultkey=defaultvalue component=logf`) + require.Contains(t, buf.String(), `level=info message="hello world" defaultkey=defaultvalue component=logf`) buf.Reset() } +type errWriter struct{} + +func (w *errWriter) Write(p []byte) (int, error) { + return 0, errors.New("dummy error") +} + +func TestIoWriterError(t *testing.T) { + w := &errWriter{} + l := New(Opts{Writer: w}) + buf := &bytes.Buffer{} + log.SetOutput(buf) + log.SetFlags(0) + l.Info("hello world") + require.Contains(t, buf.String(), "error logging: dummy error\n") +} + +func TestWriteQuotedStringCases(t *testing.T) { + buf := &bytes.Buffer{} + l := New(Opts{Writer: buf}) + + // cases from + // https://github.com/go-logfmt/logfmt/blob/99455b83edb21b32a1f1c0a32f5001b77487b721/encode_test.go + data := []struct { + key, value interface{} + want string + }{ + {key: "k", value: "v", want: "k=v"}, + {key: "k", value: nil, want: "k=null"}, + {key: `\`, value: "v", want: `\=v`}, + {key: "k", value: "", want: "k="}, + {key: "k", value: "null", want: `k="null"`}, + {key: "k", value: "", want: `k=`}, + {key: "k", value: true, want: "k=true"}, + {key: "k", value: 1, want: "k=1"}, + {key: "k", value: 1.025, want: "k=1.025"}, + {key: "k", value: 1e-3, want: "k=0.001"}, + {key: "k", value: 3.5 + 2i, want: "k=(3.5+2i)"}, + {key: "k", value: "v v", want: `k="v v"`}, + {key: "k", value: " ", want: `k=" "`}, + {key: "k", value: `"`, want: `k="\""`}, + {key: "k", value: `=`, want: `k="="`}, + {key: "k", value: `\`, want: `k=\`}, + {key: "k", value: `=\`, want: `k="=\\"`}, + {key: "k", value: `\"`, want: `k="\\\""`}, + {key: "k", value: "\xbd", want: `k="\ufffd"`}, + {key: "k", value: "\ufffd\x00", want: `k="\ufffd\u0000"`}, + {key: "k", value: "\ufffd", want: `k="\ufffd"`}, + {key: "k", value: []byte("\ufffd\x00"), want: `k="\ufffd\u0000"`}, + {key: "k", value: []byte("\ufffd"), want: `k="\ufffd"`}, + } + + for _, d := range data { + l.Info("hello world", d.key, d.value) + require.Contains(t, buf.String(), d.want) + buf.Reset() + } +} + func TestOddNumberedFields(t *testing.T) { buf := &bytes.Buffer{} l := New(Opts{Writer: buf}) // Give a odd number of fields. l.Info("hello world", "key1", "val1", "key2") - assert.Contains(t, buf.String(), `level=info message="hello world" key1=val1`) + require.Contains(t, buf.String(), `level=info message="hello world" key1=val1`) buf.Reset() }