diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b9bcccf..2b9ae77 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v2 - - uses: actions/cache@v2 + - uses: actions/cache@v3.0.5 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} diff --git a/README.md b/README.md index c22ee2e..e8cbfc2 100644 --- a/README.md +++ b/README.md @@ -411,7 +411,7 @@ log.Info().Msg("hello world") Equivalent of `Lshortfile`: ```go -zerolog.CallerMarshalFunc = func(file string, line int) string { +zerolog.CallerMarshalFunc = func(pc uintptr, file string, line int) string { short := file for i := len(file) - 1; i > 0; i-- { if file[i] == '/' { @@ -625,7 +625,7 @@ Most fields are also available in the slice format (`Strs` for `[]string`, `Errs ## Binary Encoding -In addition to the default JSON encoding, `zerolog` can produce binary logs using [CBOR](http://cbor.io) encoding. The choice of encoding can be decided at compile time using the build tag `binary_log` as follows: +In addition to the default JSON encoding, `zerolog` can produce binary logs using [CBOR](https://cbor.io) encoding. The choice of encoding can be decided at compile time using the build tag `binary_log` as follows: ```bash go build -tags binary_log . diff --git a/cmd/prettylog/README.md b/cmd/prettylog/README.md new file mode 100644 index 0000000..2412915 --- /dev/null +++ b/cmd/prettylog/README.md @@ -0,0 +1,40 @@ +# Zerolog PrettyLog + +This is a basic CLI utility that will colorize and pretty print your structured JSON logs. + +## Usage + +You can compile it or run it directly. The only issue is that by default Zerolog does not output to `stdout` +but rather to `stderr` so we must pipe `stderr` stream to this CLI tool. + +### Linux + +These commands will redirect `stderr` to our `prettylog` tool and `stdout` will remain unaffected. + +1. Compiled version + +```shell +some_program_with_zerolog 2> >(prettylog) +``` + +2. Run it directly with `go run` + +```shell +some_program_with_zerolog 2> >(go run cmd/prettylog/prettylog.go) +``` + +### Windows + +These commands will redirect `stderr` to `stdout` and then pipe it to our `prettylog` tool. + +1. Compiled version + +```shell +some_program_with_zerolog 2>&1 | prettylog +``` + +2. Run it directly with `go run` + +```shell +some_program_with_zerolog 2>&1 | go run cmd/prettylog/prettylog.go +``` diff --git a/cmd/prettylog/prettylog.go b/cmd/prettylog/prettylog.go new file mode 100644 index 0000000..61bf8b8 --- /dev/null +++ b/cmd/prettylog/prettylog.go @@ -0,0 +1,26 @@ +package main + +import ( + "fmt" + "io" + "os" + + "github.com/rs/zerolog" +) + +func isInputFromPipe() bool { + fileInfo, _ := os.Stdin.Stat() + return fileInfo.Mode()&os.ModeCharDevice == 0 +} + +func main() { + if !isInputFromPipe() { + fmt.Println("The command is intended to work with pipes.") + fmt.Println("Usage: app_with_zerolog | 2> >(prettylog)") + os.Exit(1) + return + } + + writer := zerolog.NewConsoleWriter() + _, _ = io.Copy(writer, os.Stdin) +} diff --git a/console.go b/console.go index 25c178b..9f6ffbf 100644 --- a/console.go +++ b/console.go @@ -74,6 +74,8 @@ type ConsoleWriter struct { FormatFieldValue Formatter FormatErrFieldName Formatter FormatErrFieldValue Formatter + + FormatExtra func(map[string]interface{}, *bytes.Buffer) error } // NewConsoleWriter creates and initializes a new ConsoleWriter. @@ -128,10 +130,18 @@ func (w ConsoleWriter) Write(p []byte) (n int, err error) { w.writeFields(evt, buf) + if w.FormatExtra != nil { + err = w.FormatExtra(evt, buf) + if err != nil { + return n, err + } + } + err = buf.WriteByte('\n') if err != nil { return n, err } + _, err = buf.WriteTo(w.Out) return len(p), err } @@ -221,7 +231,7 @@ func (w ConsoleWriter) writeFields(evt map[string]interface{}, buf *bytes.Buffer case json.Number: buf.WriteString(fv(fValue)) default: - b, err := json.Marshal(fValue) + b, err := InterfaceMarshalFunc(fValue) if err != nil { fmt.Fprintf(buf, colorize("[error: %v]", colorRed, w.NoColor), err) } else { @@ -331,7 +341,7 @@ func consoleDefaultFormatTimestamp(timeFormat string, noColor bool) Formatter { if err != nil { t = tt } else { - t = ts.Format(timeFormat) + t = ts.Local().Format(timeFormat) } case json.Number: i, err := tt.Int64() @@ -347,7 +357,7 @@ func consoleDefaultFormatTimestamp(timeFormat string, noColor bool) Formatter { nsec = int64(time.Duration(i) * time.Microsecond) sec = 0 } - ts := time.Unix(sec, nsec).UTC() + ts := time.Unix(sec, nsec) t = ts.Format(timeFormat) } } @@ -375,7 +385,7 @@ func consoleDefaultFormatLevel(noColor bool) Formatter { case LevelPanicValue: l = colorize(colorize("PNC", colorRed, noColor), colorBold, noColor) default: - l = colorize("???", colorBold, noColor) + l = colorize(ll, colorBold, noColor) } } else { if i == nil { diff --git a/console_test.go b/console_test.go index bd79584..98a4e27 100644 --- a/console_test.go +++ b/console_test.go @@ -108,13 +108,14 @@ func TestConsoleWriter(t *testing.T) { buf := &bytes.Buffer{} w := zerolog.ConsoleWriter{Out: buf, NoColor: true} - d := time.Unix(0, 0).UTC().Format(time.RFC3339) + ts := time.Unix(0, 0) + d := ts.UTC().Format(time.RFC3339) _, err := w.Write([]byte(`{"time": "` + d + `", "level": "debug", "message": "Foobar", "foo": "bar"}`)) if err != nil { t.Errorf("Unexpected error when writing output: %s", err) } - expectedOutput := "12:00AM DBG Foobar foo=bar\n" + expectedOutput := ts.Format(time.Kitchen) + " DBG Foobar foo=bar\n" actualOutput := buf.String() if actualOutput != expectedOutput { t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput) @@ -136,7 +137,7 @@ func TestConsoleWriter(t *testing.T) { t.Errorf("Unexpected error when writing output: %s", err) } - expectedOutput := "Jan 1 00:20:34.000 DBG Foobar foo=bar\n" + expectedOutput := time.Unix(1234, 0).Format(time.StampMilli) + " DBG Foobar foo=bar\n" actualOutput := buf.String() if actualOutput != expectedOutput { t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput) @@ -158,7 +159,7 @@ func TestConsoleWriter(t *testing.T) { t.Errorf("Unexpected error when writing output: %s", err) } - expectedOutput := "Jan 1 00:20:34.567 DBG Foobar foo=bar\n" + expectedOutput := time.Unix(1234, 567000000).Format(time.StampMilli) + " DBG Foobar foo=bar\n" actualOutput := buf.String() if actualOutput != expectedOutput { t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput) @@ -180,7 +181,7 @@ func TestConsoleWriter(t *testing.T) { t.Errorf("Unexpected error when writing output: %s", err) } - expectedOutput := "Jan 1 00:20:34.567891 DBG Foobar foo=bar\n" + expectedOutput := time.Unix(1234, 567891000).Format(time.StampMicro) + " DBG Foobar foo=bar\n" actualOutput := buf.String() if actualOutput != expectedOutput { t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput) @@ -239,7 +240,8 @@ func TestConsoleWriter(t *testing.T) { buf := &bytes.Buffer{} w := zerolog.ConsoleWriter{Out: buf, NoColor: true} - d := time.Unix(0, 0).UTC().Format(time.RFC3339) + ts := time.Unix(0, 0) + d := ts.UTC().Format(time.RFC3339) evt := `{"time": "` + d + `", "level": "error", "message": "Foobar", "aaa": "bbb", "error": "Error"}` // t.Log(evt) @@ -248,7 +250,7 @@ func TestConsoleWriter(t *testing.T) { t.Errorf("Unexpected error when writing output: %s", err) } - expectedOutput := "12:00AM ERR Foobar error=Error aaa=bbb\n" + expectedOutput := ts.Format(time.Kitchen) + " ERR Foobar error=Error aaa=bbb\n" actualOutput := buf.String() if actualOutput != expectedOutput { t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput) @@ -264,7 +266,8 @@ func TestConsoleWriter(t *testing.T) { t.Fatalf("Cannot get working directory: %s", err) } - d := time.Unix(0, 0).UTC().Format(time.RFC3339) + ts := time.Unix(0, 0) + d := ts.UTC().Format(time.RFC3339) evt := `{"time": "` + d + `", "level": "debug", "message": "Foobar", "foo": "bar", "caller": "` + cwd + `/foo/bar.go"}` // t.Log(evt) @@ -273,7 +276,7 @@ func TestConsoleWriter(t *testing.T) { t.Errorf("Unexpected error when writing output: %s", err) } - expectedOutput := "12:00AM DBG foo/bar.go > Foobar foo=bar\n" + expectedOutput := ts.Format(time.Kitchen) + " DBG foo/bar.go > Foobar foo=bar\n" actualOutput := buf.String() if actualOutput != expectedOutput { t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput) @@ -305,7 +308,8 @@ func TestConsoleWriterConfiguration(t *testing.T) { buf := &bytes.Buffer{} w := zerolog.ConsoleWriter{Out: buf, NoColor: true, TimeFormat: time.RFC3339} - d := time.Unix(0, 0).UTC().Format(time.RFC3339) + ts := time.Unix(0, 0) + d := ts.UTC().Format(time.RFC3339) evt := `{"time": "` + d + `", "level": "info", "message": "Foobar"}` _, err := w.Write([]byte(evt)) @@ -313,7 +317,7 @@ func TestConsoleWriterConfiguration(t *testing.T) { t.Errorf("Unexpected error when writing output: %s", err) } - expectedOutput := "1970-01-01T00:00:00Z INF Foobar\n" + expectedOutput := ts.Format(time.RFC3339) + " INF Foobar\n" actualOutput := buf.String() if actualOutput != expectedOutput { t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput) @@ -371,6 +375,29 @@ func TestConsoleWriterConfiguration(t *testing.T) { t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput) } }) + + t.Run("Sets FormatExtra", func(t *testing.T) { + buf := &bytes.Buffer{} + w := zerolog.ConsoleWriter{ + Out: buf, NoColor: true, PartsOrder: []string{"level", "message"}, + FormatExtra: func(evt map[string]interface{}, buf *bytes.Buffer) error { + buf.WriteString("\nAdditional stacktrace") + return nil + }, + } + + evt := `{"level": "info", "message": "Foobar"}` + _, err := w.Write([]byte(evt)) + if err != nil { + t.Errorf("Unexpected error when writing output: %s", err) + } + + expectedOutput := "INF Foobar\nAdditional stacktrace\n" + actualOutput := buf.String() + if actualOutput != expectedOutput { + t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput) + } + }) } func BenchmarkConsoleWriter(b *testing.B) { diff --git a/diode/diode_test.go b/diode/diode_test.go index 098bd44..d0d0aff 100644 --- a/diode/diode_test.go +++ b/diode/diode_test.go @@ -30,6 +30,14 @@ func TestNewWriter(t *testing.T) { } } +func TestClose(t *testing.T) { + buf := bytes.Buffer{} + w := diode.NewWriter(&buf, 1000, 0, func(missed int) {}) + log := zerolog.New(w) + log.Print("test") + w.Close() +} + func Benchmark(b *testing.B) { log.SetOutput(ioutil.Discard) defer log.SetOutput(os.Stderr) diff --git a/diode/internal/diodes/waiter.go b/diode/internal/diodes/waiter.go index 7c9bb0c..a4a8e97 100644 --- a/diode/internal/diodes/waiter.go +++ b/diode/internal/diodes/waiter.go @@ -39,7 +39,12 @@ func NewWaiter(d Diode, opts ...WaiterConfigOption) *Waiter { go func() { <-w.ctx.Done() + + // Mutex is strictly necessary here to avoid a race in Next() (between + // w.isDone() and w.c.Wait()) and w.c.Broadcast() here. + w.mu.Lock() w.c.Broadcast() + w.mu.Unlock() }() return w diff --git a/event.go b/event.go index b9fa138..0e2eaa6 100644 --- a/event.go +++ b/event.go @@ -744,11 +744,11 @@ func (e *Event) caller(skip int) *Event { if e == nil { return e } - _, file, line, ok := runtime.Caller(skip + e.skipFrame) + pc, file, line, ok := runtime.Caller(skip + e.skipFrame) if !ok { return e } - e.buf = enc.AppendString(enc.AppendKey(e.buf, CallerFieldName), CallerMarshalFunc(file, line)) + e.buf = enc.AppendString(enc.AppendKey(e.buf, CallerFieldName), CallerMarshalFunc(pc, file, line)) return e } diff --git a/globals.go b/globals.go index 9da6ae0..e1067de 100644 --- a/globals.go +++ b/globals.go @@ -19,6 +19,10 @@ const ( // TimeFormatUnixMicro defines a time format that makes time fields to be // serialized as Unix timestamp integers in microseconds. TimeFormatUnixMicro = "UNIXMICRO" + + // TimeFormatUnixNano defines a time format that makes time fields to be + // serialized as Unix timestamp integers in nanoseconds. + TimeFormatUnixNano = "UNIXNANO" ) var ( @@ -61,7 +65,7 @@ var ( CallerSkipFrameCount = 2 // CallerMarshalFunc allows customization of global caller marshaling - CallerMarshalFunc = func(file string, line int) string { + CallerMarshalFunc = func(pc uintptr, file string, line int) string { return file + ":" + strconv.Itoa(line) } @@ -81,7 +85,7 @@ var ( InterfaceMarshalFunc = json.Marshal // TimeFieldFormat defines the time format of the Time field type. If set to - // TimeFormatUnix, TimeFormatUnixMs or TimeFormatUnixMicro, the time is formatted as a UNIX + // TimeFormatUnix, TimeFormatUnixMs, TimeFormatUnixMicro or TimeFormatUnixNano, the time is formatted as a UNIX // timestamp as integer. TimeFieldFormat = time.RFC3339 diff --git a/go.mod b/go.mod index 746596d..e5fcd6f 100644 --- a/go.mod +++ b/go.mod @@ -6,5 +6,5 @@ require ( github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 github.com/mattn/go-colorable v0.1.12 github.com/pkg/errors v0.9.1 - github.com/rs/xid v1.3.0 + github.com/rs/xid v1.4.0 ) diff --git a/go.sum b/go.sum index d8f860b..d32e91d 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 h1:rtAn27wIbmOGUs7RIbVgPEjb31ehTVniDwPGXyMxm5U= github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -9,8 +7,8 @@ github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4= -github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/hlog/hlog.go b/hlog/hlog.go index 4f19a08..3773400 100644 --- a/hlog/hlog.go +++ b/hlog/hlog.go @@ -121,6 +121,20 @@ func RefererHandler(fieldKey string) func(next http.Handler) http.Handler { } } +// ProtoHandler adds the requests protocol version as a field to the context logger +// using fieldKey as field Key. +func ProtoHandler(fieldKey string) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log := zerolog.Ctx(r.Context()) + log.UpdateContext(func(c zerolog.Context) zerolog.Context { + return c.Str(fieldKey, r.Proto) + }) + next.ServeHTTP(w, r) + }) + } +} + type idKey struct{} // IDFromRequest returns the unique id associated to the request if any. diff --git a/hlog/hlog_test.go b/hlog/hlog_test.go index 481525b..c584e98 100644 --- a/hlog/hlog_test.go +++ b/hlog/hlog_test.go @@ -1,3 +1,4 @@ +//go:build go1.7 // +build go1.7 package hlog @@ -200,6 +201,22 @@ func TestCustomHeaderHandler(t *testing.T) { } } +func TestProtoHandler(t *testing.T) { + out := &bytes.Buffer{} + r := &http.Request{ + Proto: "test", + } + h := ProtoHandler("proto")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + l := FromRequest(r) + l.Log().Msg("") + })) + h = NewHandler(zerolog.New(out))(h) + h.ServeHTTP(nil, r) + if want, got := `{"proto":"test"}`+"\n", decodeIfBinary(out); want != got { + t.Errorf("Invalid log output, got: %s, want: %s", got, want) + } +} + func TestCombinedHandlers(t *testing.T) { out := &bytes.Buffer{} r := &http.Request{ diff --git a/internal/json/time.go b/internal/json/time.go index 5aff6be..6a8dc91 100644 --- a/internal/json/time.go +++ b/internal/json/time.go @@ -7,9 +7,10 @@ import ( const ( // Import from zerolog/global.go - timeFormatUnix = "" - timeFormatUnixMs = "UNIXMS" + timeFormatUnix = "" + timeFormatUnixMs = "UNIXMS" timeFormatUnixMicro = "UNIXMICRO" + timeFormatUnixNano = "UNIXNANO" ) // AppendTime formats the input time with the given format @@ -22,6 +23,8 @@ func (e Encoder) AppendTime(dst []byte, t time.Time, format string) []byte { return e.AppendInt64(dst, t.UnixNano()/1000000) case timeFormatUnixMicro: return e.AppendInt64(dst, t.UnixNano()/1000) + case timeFormatUnixNano: + return e.AppendInt64(dst, t.UnixNano()) } return append(t.AppendFormat(append(dst, '"'), format), '"') } @@ -33,7 +36,11 @@ func (Encoder) AppendTimes(dst []byte, vals []time.Time, format string) []byte { case timeFormatUnix: return appendUnixTimes(dst, vals) case timeFormatUnixMs: - return appendUnixMsTimes(dst, vals) + return appendUnixNanoTimes(dst, vals, 1000000) + case timeFormatUnixMicro: + return appendUnixNanoTimes(dst, vals, 1000) + case timeFormatUnixNano: + return appendUnixNanoTimes(dst, vals, 1) } if len(vals) == 0 { return append(dst, '[', ']') @@ -64,15 +71,15 @@ func appendUnixTimes(dst []byte, vals []time.Time) []byte { return dst } -func appendUnixMsTimes(dst []byte, vals []time.Time) []byte { +func appendUnixNanoTimes(dst []byte, vals []time.Time, div int64) []byte { if len(vals) == 0 { return append(dst, '[', ']') } dst = append(dst, '[') - dst = strconv.AppendInt(dst, vals[0].UnixNano()/1000000, 10) + dst = strconv.AppendInt(dst, vals[0].UnixNano()/div, 10) if len(vals) > 1 { for _, t := range vals[1:] { - dst = strconv.AppendInt(append(dst, ','), t.UnixNano()/1000000, 10) + dst = strconv.AppendInt(append(dst, ','), t.UnixNano()/div, 10) } } dst = append(dst, ']') diff --git a/journald/journald.go b/journald/journald.go index 8f77d0a..5b53490 100644 --- a/journald/journald.go +++ b/journald/journald.go @@ -102,7 +102,7 @@ func (w journalWriter) Write(p []byte) (n int, err error) { case json.Number: args[jKey] = fmt.Sprint(value) default: - b, err := json.Marshal(value) + b, err := zerolog.InterfaceMarshalFunc(value) if err != nil { args[jKey] = fmt.Sprintf("[error: %v]", err) } else { diff --git a/log.go b/log.go index 3ae1b5b..efd2a33 100644 --- a/log.go +++ b/log.go @@ -99,6 +99,7 @@ package zerolog import ( + "errors" "fmt" "io" "io/ioutil" @@ -189,6 +190,21 @@ func ParseLevel(levelStr string) (Level, error) { return Level(i), nil } +// UnmarshalText implements encoding.TextUnmarshaler to allow for easy reading from toml/yaml/json formats +func (l *Level) UnmarshalText(text []byte) error { + if l == nil { + return errors.New("can't unmarshal a nil *Level") + } + var err error + *l, err = ParseLevel(string(text)) + return err +} + +// MarshalText implements encoding.TextMarshaler to allow for easy writing into toml/yaml/json formats +func (l Level) MarshalText() ([]byte, error) { + return []byte(LevelFieldMarshalFunc(l)), nil +} + // A Logger represents an active logging object that generates lines // of JSON output to an io.Writer. Each logging operation makes a single // call to the Writer's Write method. There is no guarantee on access diff --git a/log_test.go b/log_test.go index 2ffd47e..aa84750 100644 --- a/log_test.go +++ b/log_test.go @@ -782,7 +782,7 @@ func TestCallerMarshalFunc(t *testing.T) { // test default behaviour this is really brittle due to the line numbers // actually mattering for validation - _, file, line, _ := runtime.Caller(0) + pc, file, line, _ := runtime.Caller(0) caller := fmt.Sprintf("%s:%d", file, line+2) log.Log().Caller().Msg("msg") if got, want := decodeIfBinaryToString(out.Bytes()), `{"caller":"`+caller+`","message":"msg"}`+"\n"; got != want { @@ -793,16 +793,16 @@ func TestCallerMarshalFunc(t *testing.T) { // test custom behavior. In this case we'll take just the last directory origCallerMarshalFunc := CallerMarshalFunc defer func() { CallerMarshalFunc = origCallerMarshalFunc }() - CallerMarshalFunc = func(file string, line int) string { + CallerMarshalFunc = func(pc uintptr, file string, line int) string { parts := strings.Split(file, "/") if len(parts) > 1 { return strings.Join(parts[len(parts)-2:], "/") + ":" + strconv.Itoa(line) } - return file + ":" + strconv.Itoa(line) + return runtime.FuncForPC(pc).Name() + ":" + file + ":" + strconv.Itoa(line) } - _, file, line, _ = runtime.Caller(0) - caller = CallerMarshalFunc(file, line+2) + pc, file, line, _ = runtime.Caller(0) + caller = CallerMarshalFunc(pc, file, line+2) log.Log().Caller().Msg("msg") if got, want := decodeIfBinaryToString(out.Bytes()), `{"caller":"`+caller+`","message":"msg"}`+"\n"; got != want { t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) @@ -907,6 +907,33 @@ func TestLevel_String(t *testing.T) { } } +func TestLevel_MarshalText(t *testing.T) { + tests := []struct { + name string + l Level + want string + }{ + {"trace", TraceLevel, "trace"}, + {"debug", DebugLevel, "debug"}, + {"info", InfoLevel, "info"}, + {"warn", WarnLevel, "warn"}, + {"error", ErrorLevel, "error"}, + {"fatal", FatalLevel, "fatal"}, + {"panic", PanicLevel, "panic"}, + {"disabled", Disabled, "disabled"}, + {"nolevel", NoLevel, ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got, err := tt.l.MarshalText(); err != nil { + t.Errorf("MarshalText couldn't marshal: %v", tt.l) + } else if string(got) != tt.want { + t.Errorf("String() = %v, want %v", string(got), tt.want) + } + }) + } +} + func TestParseLevel(t *testing.T) { type args struct { levelStr string @@ -943,3 +970,41 @@ func TestParseLevel(t *testing.T) { }) } } + +func TestUnmarshalTextLevel(t *testing.T) { + type args struct { + levelStr string + } + tests := []struct { + name string + args args + want Level + wantErr bool + }{ + {"trace", args{"trace"}, TraceLevel, false}, + {"debug", args{"debug"}, DebugLevel, false}, + {"info", args{"info"}, InfoLevel, false}, + {"warn", args{"warn"}, WarnLevel, false}, + {"error", args{"error"}, ErrorLevel, false}, + {"fatal", args{"fatal"}, FatalLevel, false}, + {"panic", args{"panic"}, PanicLevel, false}, + {"disabled", args{"disabled"}, Disabled, false}, + {"nolevel", args{""}, NoLevel, false}, + {"-1", args{"-1"}, TraceLevel, false}, + {"-2", args{"-2"}, Level(-2), false}, + {"-3", args{"-3"}, Level(-3), false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var l Level + err := l.UnmarshalText([]byte(tt.args.levelStr)) + if (err != nil) != tt.wantErr { + t.Errorf("UnmarshalText() error = %v, wantErr %v", err, tt.wantErr) + return + } + if l != tt.want { + t.Errorf("UnmarshalText() got = %v, want %v", l, tt.want) + } + }) + } +}