commit 0fbc5dd1d9a0f0f3fa4d1f0c43ea0be089d448fa Author: Elara Musayelyan Date: Thu Jun 30 03:07:29 2022 -0700 Initial Commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..123a8b2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Arsen Musayelyan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1da4bdf --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# Logger + +Logger is a very simple and fast logger that can write both machine-readable and human-readable logs. + +### Why? + +I made logger because I really liked zerolog, and especially its `ConsoleLogger`, as it looks really nice in the terminal. I use it in all my command-line applications, but there's an issue. Zerolog can only output in JSON or CBOR, depending on buid tags. It cannot produce human-readable output. Therefore, zerolog has to use reflection to unmarshal JSON generated by the logger and then make human-readable output out of it. This is, in my opinion, incredibly and unnecessarily wasteful, so I made logger. Logger can use any logging implementation that implements the `logger.Logger` interface. I currently have four implementations: `JSONLogger`, `PrettyLogger`, `MultiLogger`, and `NopLogger`. The names should be self-explanatory. + +### Who is logger for? + +If you need a fast and simple logger, but don't need more advanced features such as context and hooks, logger is for you. It cuts out unnecessary features, providing a very simple codebase that is easy to test and has much less potential for bugs, and it's faster than zerolog while also allowing more flexibility and a similar API. + +### Who is logger not for? + +If you need more advanced features, such as context and hooks, use either zerolog or the even faster zap library. Logger keeps it as simple as possible, only providing logging and nothing else. + +### Benchmarks + +Logger is very fast. Here are its benchmarks, done on my laptop: + +```text +goos: linux +goarch: amd64 +pkg: logger +cpu: 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz +BenchmarkJSON/one-field-8 4216245 294.0 ns/op 160 B/op 3 allocs/op +BenchmarkJSON/two-field-8 1939634 594.3 ns/op 188 B/op 5 allocs/op +BenchmarkJSON/all-8 310526 3955 ns/op 752 B/op 21 allocs/op +BenchmarkPretty/one-field-8 1603789 658.5 ns/op 168 B/op 4 allocs/op +BenchmarkPretty/two-field-8 1388920 864.5 ns/op 200 B/op 6 allocs/op +BenchmarkPretty/all-8 285554 3726 ns/op 760 B/op 22 allocs/op +``` + +To run the benchmarks yourself, simply clone this repo and run `go test -bench=.`. Keep in mind that they will be different, depending on what your computer's specs are. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8819346 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module go.arsenm.dev/logger + +go 1.18 + +require ( + github.com/gookit/color v1.5.1 + github.com/mattn/go-isatty v0.0.14 +) + +require ( + github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4e84964 --- /dev/null +++ b/go.sum @@ -0,0 +1,19 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gookit/color v1.5.1 h1:Vjg2VEcdHpwq+oY63s/ksHrgJYCTo0bwWvmmYWdE9fQ= +github.com/gookit/color v1.5.1/go.mod h1:wZFzea4X8qN6vHOSP2apMb4/+w/orMznEzYsIHPaqKM= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/json.go b/json.go new file mode 100644 index 0000000..b5a4658 --- /dev/null +++ b/json.go @@ -0,0 +1,280 @@ +package logger + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "os" + "strconv" + "time" +) + +var _ Logger = (*JSONLogger)(nil) + +// JSONLogger implements the Logger interface +// using JSON for log messages. +type JSONLogger struct { + Out io.Writer + Level LogLevel + + noPanic bool + noExit bool +} + +// NewJSON creates and returns a new JSONLogger +// If the input writer is io.Discard, NopLogger +// will be returned. +func NewJSON(out io.Writer) *JSONLogger { + return &JSONLogger{Out: out, Level: LogLevelInfo} +} + +// NoPanic prevents the logger from panicking on panic events +func (jl *JSONLogger) NoPanic() { + jl.noPanic = true +} + +// NoExit prevents the logger from exiting on fatal events +func (jl *JSONLogger) NoExit() { + jl.noExit = true +} + +// Debug creates a new debug event with the given message +func (jl *JSONLogger) Debug(msg string) LogBuilder { + return newJSONLogBuilder(jl, msg, LogLevelDebug) +} + +// Debugf creates a new debug event with the formatted message +func (jl *JSONLogger) Debugf(format string, v ...any) LogBuilder { + return newJSONLogBuilder(jl, fmt.Sprintf(format, v...), LogLevelDebug) +} + +// Info creates a new info event with the given message +func (jl *JSONLogger) Info(msg string) LogBuilder { + return newJSONLogBuilder(jl, msg, LogLevelInfo) +} + +// Infof creates a new info event with the formatted message +func (jl *JSONLogger) Infof(format string, v ...any) LogBuilder { + return newJSONLogBuilder(jl, fmt.Sprintf(format, v...), LogLevelInfo) +} + +// Warn creates a new warn event with the given message +func (jl *JSONLogger) Warn(msg string) LogBuilder { + return newJSONLogBuilder(jl, msg, LogLevelWarn) +} + +// Warnf creates a new warn event with the formatted message +func (jl *JSONLogger) Warnf(format string, v ...any) LogBuilder { + return newJSONLogBuilder(jl, fmt.Sprintf(format, v...), LogLevelWarn) +} + +// Error creates a new error event with the given message +func (jl *JSONLogger) Error(msg string) LogBuilder { + return newJSONLogBuilder(jl, msg, LogLevelError) +} + +// Errorf creates a new error event with the formatted message +func (jl *JSONLogger) Errorf(format string, v ...any) LogBuilder { + return newJSONLogBuilder(jl, fmt.Sprintf(format, v...), LogLevelError) +} + +// Fatal creates a new fatal event with the given message +// +// When sent, fatal events will cause a call to os.Exit(1) +func (jl *JSONLogger) Fatal(msg string) LogBuilder { + return newJSONLogBuilder(jl, msg, LogLevelFatal) +} + +// Fatalf creates a new fatal event with the formatted message +// +// When sent, fatal events will cause a call to os.Exit(1) +func (jl *JSONLogger) Fatalf(format string, v ...any) LogBuilder { + return newJSONLogBuilder(jl, fmt.Sprintf(format, v...), LogLevelFatal) +} + +// Panic creates a new panic event with the given message +// +// When sent, panic events will cause a panic +func (jl *JSONLogger) Panic(msg string) LogBuilder { + return newJSONLogBuilder(jl, msg, LogLevelPanic) +} + +// Panicf creates a new panic event with the formatted message +// +// When sent, panic events will cause a panic +func (jl *JSONLogger) Panicf(format string, v ...any) LogBuilder { + return newJSONLogBuilder(jl, fmt.Sprintf(format, v...), LogLevelPanic) +} + +// JSONLogBuilder implements the LogBuilder interface +// using JSON for log messages +type JSONLogBuilder struct { + l *JSONLogger + lvl LogLevel + out writer +} + +func newJSONLogBuilder(jl *JSONLogger, msg string, lvl LogLevel) LogBuilder { + if jl.Out == io.Discard || lvl < jl.Level { + return NopLogBuilder{} + } + lb := &JSONLogBuilder{ + out: writer{&bytes.Buffer{}, jl.Out}, + lvl: lvl, + l: jl, + } + lb.out.WriteString(`{"msg":"`) + lb.out.WriteString(msg) + lb.out.WriteString(`","level":"`) + lb.out.WriteString(logLevelNames[lvl]) + lb.out.WriteByte('"') + return lb +} + +// writeKey writes a JSON key to the buffer +func (jlb *JSONLogBuilder) writeKey(k string) { + jlb.out.WriteString(`,"`) + jlb.out.WriteString(k) + jlb.out.WriteString(`":`) +} + +// Int adds an int field to the output +func (jlb *JSONLogBuilder) Int(key string, val int) LogBuilder { + return jlb.Int64(key, int64(val)) +} + +// Int64 adds an int64 field to the output +func (jlb *JSONLogBuilder) Int64(key string, val int64) LogBuilder { + jlb.writeKey(key) + jlb.out.WriteString(strconv.FormatInt(val, 10)) + return jlb +} + +// Int32 adds an int32 field to the output +func (jlb *JSONLogBuilder) Int32(key string, val int32) LogBuilder { + return jlb.Int64(key, int64(val)) +} + +// Int16 adds an int16 field to the output +func (jlb *JSONLogBuilder) Int16(key string, val int16) LogBuilder { + return jlb.Int64(key, int64(val)) +} + +// Int8 adds an int8 field to the output +func (jlb *JSONLogBuilder) Int8(key string, val int8) LogBuilder { + return jlb.Int64(key, int64(val)) +} + +// Uint adds a uint field to the output +func (jlb *JSONLogBuilder) Uint(key string, val uint) LogBuilder { + return jlb.Uint64(key, uint64(val)) +} + +// Uint64 adds a uint64 field to the output +func (jlb *JSONLogBuilder) Uint64(key string, val uint64) LogBuilder { + jlb.writeKey(key) + jlb.out.WriteString(strconv.FormatUint(val, 10)) + return jlb +} + +// Uint32 adds a uint32 field to the output +func (jlb *JSONLogBuilder) Uint32(key string, val uint32) LogBuilder { + return jlb.Uint64(key, uint64(val)) +} + +// Uint16 adds a uint16 field to the output +func (jlb *JSONLogBuilder) Uint16(key string, val uint16) LogBuilder { + return jlb.Uint64(key, uint64(val)) +} + +// Uint8 adds a uint8 field to the output +func (jlb *JSONLogBuilder) Uint8(key string, val uint8) LogBuilder { + return jlb.Uint64(key, uint64(val)) +} + +// float adds a float of specified bitsize to the output +func (jlb *JSONLogBuilder) float(key string, val float64, bitsize int) LogBuilder { + jlb.writeKey(key) + jlb.out.WriteString(strconv.FormatFloat(val, 'f', -1, bitsize)) + return jlb +} + +// Float64 adds a float64 field to the output +func (jlb *JSONLogBuilder) Float64(key string, val float64) LogBuilder { + return jlb.float(key, val, 64) +} + +// Float32 adds a float32 field to the output +func (jlb *JSONLogBuilder) Float32(key string, val float32) LogBuilder { + return jlb.float(key, float64(val), 32) +} + +// Stringer calls the String method of an fmt.Stringer +// and adds the resulting string as a field to the output +func (jlb *JSONLogBuilder) Stringer(key string, s fmt.Stringer) LogBuilder { + return jlb.Str(key, s.String()) +} + +// Bytes writes base64-encoded bytes as a field to the output +func (jlb *JSONLogBuilder) Bytes(key string, b []byte) LogBuilder { + return jlb.Str(key, base64.StdEncoding.EncodeToString(b)) +} + +// Timestamp adds the time formatted as RFC3339Nano +// as a field to the output using the key "timestamp" +func (jlb *JSONLogBuilder) Timestamp() LogBuilder { + return jlb.Str("timestamp", time.Now().Format(time.RFC3339Nano)) +} + +// Bool adds a bool as a field to the output +func (jlb *JSONLogBuilder) Bool(key string, val bool) LogBuilder { + jlb.writeKey(key) + if val { + jlb.out.WriteString("true") + } else { + jlb.out.WriteString("false") + } + return jlb +} + +// Str adds a string as a field to the output +func (jlb *JSONLogBuilder) Str(key, val string) LogBuilder { + jlb.writeKey(key) + jlb.out.WriteByte('"') + jlb.out.WriteString(val) + jlb.out.WriteByte('"') + return jlb +} + +// Any uses reflection to marshal any type and writes +// the result as a field to the output. This is much slower +// than the type-specific functions. +func (jlb *JSONLogBuilder) Any(key string, val any) LogBuilder { + jlb.writeKey(key) + data, err := json.Marshal(val) + if err != nil { + panic(err) + } + jlb.out.Write(data) + return jlb +} + +// Err adds an error as a field to the output +func (jlb *JSONLogBuilder) Err(err error) LogBuilder { + return jlb.Str("error", err.Error()) +} + +// Send sends the event to the output. +// +// After calling send, do not use the event again. +func (jlb *JSONLogBuilder) Send() { + jlb.out.WriteByte('}') + jlb.out.Flush() + if jlb.lvl == LogLevelFatal && !jlb.l.noExit { + os.Exit(1) + } else if jlb.lvl == LogLevelPanic && !jlb.l.noPanic { + panic("") + } +} diff --git a/log/log.go b/log/log.go new file mode 100644 index 0000000..a7c89d8 --- /dev/null +++ b/log/log.go @@ -0,0 +1,58 @@ +package log + +import ( + "logger" + "os" +) + +var Logger logger.Logger = logger.NewJSON(os.Stderr) + +// NoPanic prevents the logger from panicking on panic events +func NoPanic() { + Logger.NoPanic() +} + +// NoExit prevents the logger from exiting on fatal events +func NoExit() { + Logger.NoExit() +} + +// Debug creates a new debug event with the given message +func Debug(msg string) logger.LogBuilder { + return Logger.Debug(msg) +} + +// Debugf creates a new debug event with the formatted message +func Debugf(format string, v ...any) logger.LogBuilder { + return Logger.Debugf(format, v...) +} + +// Info creates a new info event with the given message +func Info(msg string) logger.LogBuilder { + return Logger.Info(msg) +} + +// Infof creates a new info event with the formatted message +func Infof(format string, v ...any) logger.LogBuilder { + return Logger.Infof(format, v...) +} + +// Warn creates a new warn event with the given message +func Warn(msg string) logger.LogBuilder { + return Logger.Warn(msg) +} + +// Warnf creates a new warn event with the formatted message +func Warnf(format string, v ...any) logger.LogBuilder { + return Logger.Warnf(format, v...) +} + +// Error creates a new error event with the given message +func Error(msg string) logger.LogBuilder { + return Logger.Error(msg) +} + +// Errorf creates a new error event with the formatted message +func Errorf(format string, v ...any) logger.LogBuilder { + return Logger.Errorf(format, v...) +} diff --git a/logger_test.go b/logger_test.go new file mode 100644 index 0000000..bc28d38 --- /dev/null +++ b/logger_test.go @@ -0,0 +1,293 @@ +package logger_test + +import ( + "bytes" + "encoding/json" + "errors" + "logger" + "os" + "runtime" + "testing" + "time" +) + +var buf = &bytes.Buffer{} +var jsonlog = logger.NewJSON(buf) +var prettylog = logger.NewPretty(buf) + +func TestJSON(t *testing.T) { + t.Run("empty", func(t *testing.T) { + jsonlog.Info("").Send() + + if got, want := getStr(), `{"msg":"","level":"info"}`; got != want { + t.Errorf("got: %s, want: %s", got, want) + } + }) + + t.Run("one-field", func(t *testing.T) { + jsonlog.Info("Test").Int("n", 1234).Send() + + if got, want := getStr(), `{"msg":"Test","level":"info","n":1234}`; got != want { + t.Errorf("got: %s, want: %s", got, want) + } + }) + + t.Run("two-field", func(t *testing.T) { + jsonlog. + Info("Test"). + Int("n", 1234). + Float32("pi", 3.14). + Send() + + if got, want := getStr(), `{"msg":"Test","level":"info","n":1234,"pi":3.14}`; got != want { + t.Errorf("got: %s, want: %s", got, want) + } + }) + + t.Run("any", func(t *testing.T) { + jsonlog.Info("Test").Any("any", nil).Send() + + if got, want := getStr(), `{"msg":"Test","level":"info","any":null}`; got != want { + t.Errorf("got: %s, want: %s", got, want) + } + }) + + t.Run("all", func(t *testing.T) { + jsonlog. + Info("All"). + Int("int", -1). + Int8("int8", -1). + Int16("int16", -1). + Int32("int32", -1). + Int64("int64", -1). + Uint("uint", 1). + Uint8("uint8", 1). + Uint16("uint16", 1). + Uint32("uint32", 1). + Uint64("uint64", 1). + Float32("float32", 3.14). + Float64("float64", 6.28). + Bool("bool", true). + Str("string", ""). + Bytes("[]byte", []byte{0x12, 0x34, 0x56}). + Stringer("stringer", time.Second). + Any("any", nil). + Err(errors.New("err")). + Send() + + if got, want := getStr(), `{"msg":"All","level":"info","int":-1,"int8":-1,"int16":-1,"int32":-1,"int64":-1,"uint":1,"uint8":1,"uint16":1,"uint32":1,"uint64":1,"float32":3.14,"float64":6.28,"bool":true,"string":"","[]byte":"EjRW","stringer":"1s","any":null,"error":"err"}`; got != want { + t.Errorf("got: %s, want: %s", got, want) + } + }) + + t.Run("decode", func(t *testing.T) { + var log struct { + Message string `json:"msg"` + Level string `json:"level"` + N int `json:"n"` + } + jsonlog.Info("Test").Int("n", 1234).Send() + + err := json.Unmarshal(buf.Bytes(), &log) + if err != nil { + t.Error(err) + } + buf.Reset() + + if log.Message != "Test" { + t.Errorf("expected message Test, got %s", log.Message) + } + if log.Level != "info" { + t.Errorf("expected level info, got %s", log.Level) + } + if log.N != 1234 { + t.Errorf("expected n 1234, got %d", log.N) + } + }) +} + +func TestPretty(t *testing.T) { + t.Run("empty", func(t *testing.T) { + prettylog.Info("").Send() + + ctime := time.Now().Format(time.Kitchen) + if got, want := getStr(), ctime+" INF \n"; got != want { + t.Errorf("got: %s, want: %s", got, want) + } + }) + + t.Run("one-field", func(t *testing.T) { + prettylog.Info("Test").Int("n", 1234).Send() + + ctime := time.Now().Format(time.Kitchen) + if got, want := getStr(), ctime+" INF Test n=1234\n"; got != want { + t.Errorf("got: %s, want: %s", got, want) + } + }) + + t.Run("two-field", func(t *testing.T) { + prettylog. + Info("Test"). + Int("n", 1234). + Float32("pi", 3.14). + Send() + + ctime := time.Now().Format(time.Kitchen) + if got, want := getStr(), ctime+" INF Test n=1234 pi=3.14\n"; got != want { + t.Errorf("got: %s, want: %s", got, want) + } + }) + + t.Run("any", func(t *testing.T) { + prettylog.Info("Test").Any("any", nil).Send() + + ctime := time.Now().Format(time.Kitchen) + if got, want := getStr(), ctime+" INF Test any=null\n"; got != want { + t.Errorf("got: %s, want: %s", got, want) + } + }) + + t.Run("all", func(t *testing.T) { + prettylog. + Info("All"). + Int("int", -1). + Int8("int8", -1). + Int16("int16", -1). + Int32("int32", -1). + Int64("int64", -1). + Uint("uint", 1). + Uint8("uint8", 1). + Uint16("uint16", 1). + Uint32("uint32", 1). + Uint64("uint64", 1). + Float32("float32", 3.14). + Float64("float64", 6.28). + Bool("bool", true). + Str("string", ""). + Bytes("[]byte", []byte{0x12, 0x34, 0x56}). + Stringer("stringer", time.Second). + Any("any", nil). + Err(errors.New("err")). + Send() + + ctime := time.Now().Format(time.Kitchen) + if got, want := getStr(), ctime+` INF All int=-1 int8=-1 int16=-1 int32=-1 int64=-1 uint=1 uint8=1 uint16=1 uint32=1 uint64=1 float32=3.14 float64=6.28 bool=true string="" []byte="123456" stringer="1s" any=null error="err"`+"\n"; got != want { + t.Errorf("got: %#v, want: %#v", got, want) + } + }) +} + +func BenchmarkJSON(b *testing.B) { + devnull, err := os.Open(getNullPath()) + if err != nil { + b.Fatal(err) + } + + jsonlog.Out = devnull + jsonlog.Level = logger.LogLevelDebug + + b.Run("one-field", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + jsonlog.Debug("Benchmark").Int("int", 1) + } + }) + + b.Run("two-field", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + jsonlog.Debug("Benchmark").Int("int", 1).Float32("pi", 3.14) + } + }) + + b.Run("all", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + jsonlog. + Debug("All"). + Int("int", -1). + Int8("int8", -1). + Int16("int16", -1). + Int32("int32", -1). + Int64("int64", -1). + Uint("uint", 1). + Uint8("uint8", 1). + Uint16("uint16", 1). + Uint32("uint32", 1). + Uint64("uint64", 1). + Float32("float32", 3.14). + Float64("float64", 6.28). + Bool("bool", true). + Str("string", ""). + Bytes("[]byte", []byte{0x12, 0x34, 0x56}). + Stringer("stringer", time.Second). + Any("any", nil). + Err(errors.New("err")). + Send() + } + }) +} + +func BenchmarkPretty(b *testing.B) { + devnull, err := os.Open(getNullPath()) + if err != nil { + b.Fatal(err) + } + + prettylog.Out = devnull + prettylog.Level = logger.LogLevelDebug + + b.Run("one-field", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + prettylog.Debug("Benchmark").Int("int", 1) + } + }) + + b.Run("two-field", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + prettylog.Debug("Benchmark").Int("int", 1).Float32("pi", 3.14) + } + }) + + b.Run("all", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + prettylog. + Debug("All"). + Int("int", -1). + Int8("int8", -1). + Int16("int16", -1). + Int32("int32", -1). + Int64("int64", -1). + Uint("uint", 1). + Uint8("uint8", 1). + Uint16("uint16", 1). + Uint32("uint32", 1). + Uint64("uint64", 1). + Float32("float32", 3.14). + Float64("float64", 6.28). + Bool("bool", true). + Str("string", ""). + Bytes("[]byte", []byte{0x12, 0x34, 0x56}). + Stringer("stringer", time.Second). + Any("any", nil). + Err(errors.New("err")). + Send() + } + }) +} + +func getStr() string { + defer buf.Reset() + return buf.String() +} + +func getNullPath() string { + if runtime.GOOS == "windows" { + return "nul" + } else { + return "/dev/null" + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..fbc18a9 --- /dev/null +++ b/main.go @@ -0,0 +1,130 @@ +package logger + +import ( + "fmt" +) + +// LogLevel represents a log level +type LogLevel uint8 + +// Log levels +const ( + LogLevelDebug LogLevel = iota + LogLevelInfo + LogLevelWarn + LogLevelError + LogLevelFatal + LogLevelPanic +) + +var logLevelNames = [...]string{ + LogLevelDebug: "debug", + LogLevelInfo: "info", + LogLevelWarn: "warn", + LogLevelError: "error", + LogLevelFatal: "fatal", + LogLevelPanic: "panic", +} + +// Logger represents a logger +type Logger interface { + // NoPanic prevents the logger from panicking on panic events + NoPanic() + + // NoExit prevents the logger from exiting on fatal events + NoExit() + + // Debug creates a new debug event with the given message + Debug(string) LogBuilder + + // Debugf creates a new debug event with the formatted message + Debugf(string, ...any) LogBuilder + + // Info creates a new info event with the given message + Info(string) LogBuilder + + // Infof creates a new info event with the formatted message + Infof(string, ...any) LogBuilder + + // Warn creates a new warn event with the given message + Warn(string) LogBuilder + + // Warnf creates a new warn event with the formatted message + Warnf(string, ...any) LogBuilder + + // Error creates a new error event with the given message + Error(string) LogBuilder + + // Errorf creates a new error event with the formatted message + Errorf(string, ...any) LogBuilder + + // Fatal creates a new fatal event with the given message + // + // When sent, fatal events will cause a call to os.Exit(1) + Fatal(string) LogBuilder + + // Fatalf creates a new fatal event with the formatted message + // + // When sent, fatal events will cause a call to os.Exit(1) + Fatalf(string, ...any) LogBuilder + + // Panic creates a new panic event with the given message + // + // When sent, panic events will cause a panic + Panic(string) LogBuilder + + // Panicf creates a new panic event with the formatted message + // + // When sent, panic events will cause a panic + Panicf(string, ...any) LogBuilder +} + +// LogBuilder represents a log event builder +type LogBuilder interface { + // Int adds an int field to the output + Int(string, int) LogBuilder + // Int8 adds an int8 field to the output + Int8(string, int8) LogBuilder + // Int16 adds an int16 field to the output + Int16(string, int16) LogBuilder + // Int32 adds an int32 field to the output + Int32(string, int32) LogBuilder + // Int64 adds an int64 field to the output + Int64(string, int64) LogBuilder + // Uint adds a uint field to the output + Uint(string, uint) LogBuilder + // Uint8 adds a uint8 field to the output + Uint8(string, uint8) LogBuilder + // Uint16 adds a uint16 field to the output + Uint16(string, uint16) LogBuilder + // Uint32 adds a uint32 field to the output + Uint32(string, uint32) LogBuilder + // Uint64 adds a uint64 field to the output + Uint64(string, uint64) LogBuilder + // Float32 adds a float32 field to the output + Float32(string, float32) LogBuilder + // Float64 adds a float64 field to the output + Float64(string, float64) LogBuilder + // Stringer calls the String method of an fmt.Stringer + // and adds the resulting string as a field to the output + Stringer(string, fmt.Stringer) LogBuilder + // Bytes adds []byte as a field to the output + Bytes(string, []byte) LogBuilder + // Timestamp adds the time formatted as RFC3339Nano + // as a field to the output using the key "timestamp" + Timestamp() LogBuilder + // Bool adds a bool as a field to the output + Bool(string, bool) LogBuilder + // Str adds a string as a field to the output + Str(string, string) LogBuilder + // Any uses reflection to marshal any type and writes + // the result as a field to the output. This is much slower + // than the type-specific functions. + Any(string, any) LogBuilder + // Err adds an error as a field to the output + Err(error) LogBuilder + // Send sends the event to the output. + // + // After calling send, do not use the event again. + Send() +} diff --git a/multi.go b/multi.go new file mode 100644 index 0000000..8f46d54 --- /dev/null +++ b/multi.go @@ -0,0 +1,321 @@ +package logger + +import ( + "fmt" + "os" +) + +var _ Logger = (*MultiLogger)(nil) + +// MultiLogger implements the Logger interface by +// writing to multiple underlying loggers sequentially. +type MultiLogger struct { + Loggers []Logger + noPanic bool + noExit bool +} + +// NewMulti creates and returns a new MultiLogger +func NewMulti(l ...Logger) *MultiLogger { + for _, logger := range l { + logger.NoPanic() + logger.NoExit() + } + return &MultiLogger{Loggers: l} +} + +// NoExit prevents the logger from exiting on fatal events +func (ml *MultiLogger) NoExit() { + ml.noExit = true +} + +// NoPanic prevents the logger from panicking on panic events +func (ml *MultiLogger) NoPanic() { + ml.noPanic = true +} + +// Debug creates a new debug event with the given message +func (ml *MultiLogger) Debug(msg string) LogBuilder { + lbs := make([]LogBuilder, len(ml.Loggers)) + for index, logger := range ml.Loggers { + lbs[index] = logger.Debug(msg) + } + return &MultiLogBuilder{ml, lbs, LogLevelDebug} +} + +// Debugf creates a new debug event with the formatted message +func (ml *MultiLogger) Debugf(format string, v ...any) LogBuilder { + lbs := make([]LogBuilder, len(ml.Loggers)) + for index, logger := range ml.Loggers { + lbs[index] = logger.Debugf(format, v...) + } + return &MultiLogBuilder{ml, lbs, LogLevelDebug} +} + +// Info creates a new info event with the given message +func (ml *MultiLogger) Info(msg string) LogBuilder { + lbs := make([]LogBuilder, len(ml.Loggers)) + for index, logger := range ml.Loggers { + lbs[index] = logger.Info(msg) + } + return &MultiLogBuilder{ml, lbs, LogLevelInfo} +} + +// Infof creates a new info event with the formatted message +func (ml *MultiLogger) Infof(format string, v ...any) LogBuilder { + lbs := make([]LogBuilder, len(ml.Loggers)) + for index, logger := range ml.Loggers { + lbs[index] = logger.Infof(format, v...) + } + return &MultiLogBuilder{ml, lbs, LogLevelInfo} +} + +// Warn creates a new warn event with the given message +func (ml *MultiLogger) Warn(msg string) LogBuilder { + lbs := make([]LogBuilder, len(ml.Loggers)) + for index, logger := range ml.Loggers { + lbs[index] = logger.Warn(msg) + } + return &MultiLogBuilder{ml, lbs, LogLevelWarn} +} + +// Warnf creates a new warn event with the formatted message +func (ml *MultiLogger) Warnf(format string, v ...any) LogBuilder { + lbs := make([]LogBuilder, len(ml.Loggers)) + for index, logger := range ml.Loggers { + lbs[index] = logger.Warnf(format, v...) + } + return &MultiLogBuilder{ml, lbs, LogLevelWarn} +} + +// Error creates a new error event with the given message +func (ml *MultiLogger) Error(msg string) LogBuilder { + lbs := make([]LogBuilder, len(ml.Loggers)) + for index, logger := range ml.Loggers { + lbs[index] = logger.Error(msg) + } + return &MultiLogBuilder{ml, lbs, LogLevelError} +} + +// Errorf creates a new error event with the formatted message +func (ml *MultiLogger) Errorf(format string, v ...any) LogBuilder { + lbs := make([]LogBuilder, len(ml.Loggers)) + for index, logger := range ml.Loggers { + lbs[index] = logger.Errorf(format, v...) + } + return &MultiLogBuilder{ml, lbs, LogLevelError} +} + +// Error creates a new error event with the given message +func (ml *MultiLogger) Fatal(msg string) LogBuilder { + lbs := make([]LogBuilder, len(ml.Loggers)) + for index, logger := range ml.Loggers { + lbs[index] = logger.Fatal(msg) + } + return &MultiLogBuilder{ml, lbs, LogLevelFatal} +} + +// Errorf creates a new error event with the formatted message +func (ml *MultiLogger) Fatalf(format string, v ...any) LogBuilder { + lbs := make([]LogBuilder, len(ml.Loggers)) + for index, logger := range ml.Loggers { + lbs[index] = logger.Fatalf(format, v...) + } + return &MultiLogBuilder{ml, lbs, LogLevelFatal} +} + +// Error creates a new error event with the given message +func (ml *MultiLogger) Panic(msg string) LogBuilder { + lbs := make([]LogBuilder, len(ml.Loggers)) + for index, logger := range ml.Loggers { + lbs[index] = logger.Panic(msg) + } + return &MultiLogBuilder{ml, lbs, LogLevelPanic} +} + +// Errorf creates a new error event with the formatted message +func (ml *MultiLogger) Panicf(format string, v ...any) LogBuilder { + lbs := make([]LogBuilder, len(ml.Loggers)) + for index, logger := range ml.Loggers { + lbs[index] = logger.Panicf(format, v...) + } + return &MultiLogBuilder{ml, lbs, LogLevelPanic} +} + +// MultiLogBuilder implements the LogBuilder interface +// by writing to multiple underlying LogBuilders sequentially. +type MultiLogBuilder struct { + l *MultiLogger + lbs []LogBuilder + lvl LogLevel +} + +// Int adds an int field to the output +func (mlb *MultiLogBuilder) Int(key string, val int) LogBuilder { + for _, lb := range mlb.lbs { + lb.Int(key, val) + } + return mlb +} + +// Int64 adds an int64 field to the output +func (mlb *MultiLogBuilder) Int64(key string, val int64) LogBuilder { + for _, lb := range mlb.lbs { + lb.Int64(key, val) + } + return mlb +} + +// Int32 adds an int32 field to the output +func (mlb *MultiLogBuilder) Int32(key string, val int32) LogBuilder { + for _, lb := range mlb.lbs { + lb.Int32(key, val) + } + return mlb +} + +// Int16 adds an int16 field to the output +func (mlb *MultiLogBuilder) Int16(key string, val int16) LogBuilder { + for _, lb := range mlb.lbs { + lb.Int16(key, val) + } + return mlb +} + +// Int8 adds an int8 field to the output +func (mlb *MultiLogBuilder) Int8(key string, val int8) LogBuilder { + for _, lb := range mlb.lbs { + lb.Int8(key, val) + } + return mlb +} + +// Uint adds a uint field to the output +func (mlb *MultiLogBuilder) Uint(key string, val uint) LogBuilder { + for _, lb := range mlb.lbs { + lb.Uint(key, val) + } + return mlb +} + +// Uint64 adds a uint64 field to the output +func (mlb *MultiLogBuilder) Uint64(key string, val uint64) LogBuilder { + for _, lb := range mlb.lbs { + lb.Uint64(key, val) + } + return mlb +} + +// Uint32 adds a uint32 field to the output +func (mlb *MultiLogBuilder) Uint32(key string, val uint32) LogBuilder { + for _, lb := range mlb.lbs { + lb.Uint32(key, val) + } + return mlb +} + +// Uint16 adds a uint16 field to the output +func (mlb *MultiLogBuilder) Uint16(key string, val uint16) LogBuilder { + for _, lb := range mlb.lbs { + lb.Uint16(key, val) + } + return mlb +} + +// Uint8 adds a uint8 field to the output +func (mlb *MultiLogBuilder) Uint8(key string, val uint8) LogBuilder { + for _, lb := range mlb.lbs { + lb.Uint8(key, val) + } + return mlb +} + +// Float64 adds a float64 field to the output +func (mlb *MultiLogBuilder) Float64(key string, val float64) LogBuilder { + for _, lb := range mlb.lbs { + lb.Float64(key, val) + } + return mlb +} + +// Float32 adds a float32 field to the output +func (mlb *MultiLogBuilder) Float32(key string, val float32) LogBuilder { + for _, lb := range mlb.lbs { + lb.Float32(key, val) + } + return mlb +} + +// Stringer calls the String method of an fmt.Stringer +// and adds the resulting string as a field to the output +func (mlb *MultiLogBuilder) Stringer(key string, s fmt.Stringer) LogBuilder { + for _, lb := range mlb.lbs { + lb.Stringer(key, s) + } + return mlb +} + +// Bytes writes base64-encoded bytes as a field to the output +func (mlb *MultiLogBuilder) Bytes(key string, b []byte) LogBuilder { + for _, lb := range mlb.lbs { + lb.Bytes(key, b) + } + return mlb +} + +// Timestamp adds the time formatted as RFC3339Nano +// as a field to the output using the key "timestamp" +func (mlb *MultiLogBuilder) Timestamp() LogBuilder { + for _, lb := range mlb.lbs { + lb.Timestamp() + } + return mlb +} + +// Bool adds a bool as a field to the output +func (mlb *MultiLogBuilder) Bool(key string, val bool) LogBuilder { + for _, lb := range mlb.lbs { + lb.Bool(key, val) + } + return mlb +} + +// Str adds a string as a field to the output +func (mlb *MultiLogBuilder) Str(key, val string) LogBuilder { + for _, lb := range mlb.lbs { + lb.Str(key, val) + } + return mlb +} + +// Any uses reflection to marshal any type and writes +// the result as a field to the output. This is much slower +// than the type-specific functions. +func (mlb *MultiLogBuilder) Any(key string, val any) LogBuilder { + for _, lb := range mlb.lbs { + lb.Any(key, val) + } + return mlb +} + +// Err adds an error as a field to the output +func (mlb *MultiLogBuilder) Err(err error) LogBuilder { + for _, lb := range mlb.lbs { + lb.Err(err) + } + return mlb +} + +// Send sends the event to the output. +// +// After calling send, do not use the event again. +func (mlb *MultiLogBuilder) Send() { + for _, lb := range mlb.lbs { + lb.Send() + } + if mlb.lvl == LogLevelFatal && !mlb.l.noExit { + os.Exit(1) + } else if mlb.lvl == LogLevelPanic && !mlb.l.noPanic { + panic("") + } +} diff --git a/nop.go b/nop.go new file mode 100644 index 0000000..7d14cf3 --- /dev/null +++ b/nop.go @@ -0,0 +1,152 @@ +package logger + +import ( + "fmt" +) + +var _ Logger = (*NopLogger)(nil) + +// NopLogger implements the Logger interface +// using human-readable output for log messages. +type NopLogger struct{} + +// NewNop creates and returns a new NopLogger +func NewNop() NopLogger { + return NopLogger{} +} + +// NoPanic prevents the logger from panicking on panic events +func (nl NopLogger) NoPanic() {} + +// NoExit prevents the logger from exiting on fatal events +func (nl NopLogger) NoExit() {} + +// Debug creates a new debug event with the given message +func (nl NopLogger) Debug(msg string) LogBuilder { + return NopLogBuilder{} +} + +// Debugf creates a new debug event with the formatted message +func (nl NopLogger) Debugf(format string, v ...any) LogBuilder { + return NopLogBuilder{} +} + +// Info creates a new info event with the given message +func (nl NopLogger) Info(msg string) LogBuilder { + return NopLogBuilder{} +} + +// Infof creates a new info event with the formatted message +func (nl NopLogger) Infof(format string, v ...any) LogBuilder { + return NopLogBuilder{} +} + +// Warn creates a new warn event with the given message +func (nl NopLogger) Warn(msg string) LogBuilder { + return NopLogBuilder{} +} + +// Warnf creates a new warn event with the formatted message +func (nl NopLogger) Warnf(format string, v ...any) LogBuilder { + return NopLogBuilder{} +} + +// Error creates a new error event with the given message +func (nl NopLogger) Error(msg string) LogBuilder { + return NopLogBuilder{} +} + +// Errorf creates a new error event with the formatted message +func (nl NopLogger) Errorf(format string, v ...any) LogBuilder { + return NopLogBuilder{} +} + +// Fatal creates a new fatal event with the given message +func (nl NopLogger) Fatal(msg string) LogBuilder { + return NopLogBuilder{} +} + +// Fatalf creates a new fatal event with the formatted message +func (nl NopLogger) Fatalf(format string, v ...any) LogBuilder { + return NopLogBuilder{} +} + +// Panic creates a new panic event with the given message +func (nl NopLogger) Panic(msg string) LogBuilder { + return NopLogBuilder{} +} + +// Panicf creates a new panic event with the formatted message +func (nl NopLogger) Panicf(format string, v ...any) LogBuilder { + return NopLogBuilder{} +} + +// NopLogBuilder implements the LogBuilder interface +// using human-readable output for log messages +type NopLogBuilder struct{} + +// Int adds an int field to the output +func (nlb NopLogBuilder) Int(key string, val int) LogBuilder { return nlb } + +// Int64 adds an int64 field to the output +func (nlb NopLogBuilder) Int64(key string, val int64) LogBuilder { return nlb } + +// Int32 adds an int32 field to the output +func (nlb NopLogBuilder) Int32(key string, val int32) LogBuilder { return nlb } + +// Int16 adds an int16 field to the output +func (nlb NopLogBuilder) Int16(key string, val int16) LogBuilder { return nlb } + +// Int8 adds an int8 field to the output +func (nlb NopLogBuilder) Int8(key string, val int8) LogBuilder { return nlb } + +// Uint adds a uint field to the output +func (nlb NopLogBuilder) Uint(key string, val uint) LogBuilder { return nlb } + +// Uint64 adds a uint64 field to the output +func (nlb NopLogBuilder) Uint64(key string, val uint64) LogBuilder { return nlb } + +// Uint32 adds a uint32 field to the output +func (nlb NopLogBuilder) Uint32(key string, val uint32) LogBuilder { return nlb } + +// Uint16 adds a uint16 field to the output +func (nlb NopLogBuilder) Uint16(key string, val uint16) LogBuilder { return nlb } + +// Uint8 adds a uint8 field to the output +func (nlb NopLogBuilder) Uint8(key string, val uint8) LogBuilder { return nlb } + +// Float64 adds a float64 field to the output +func (nlb NopLogBuilder) Float64(key string, val float64) LogBuilder { return nlb } + +// Float32 adds a float32 field to the output +func (nlb NopLogBuilder) Float32(key string, val float32) LogBuilder { return nlb } + +// Stringer calls the String method of an fmt.Stringer +// and adds the resulting string as a field to the output +func (nlb NopLogBuilder) Stringer(key string, s fmt.Stringer) LogBuilder { return nlb } + +// Bytes writes hex-encoded bytes as a field to the output +func (nlb NopLogBuilder) Bytes(key string, b []byte) LogBuilder { return nlb } + +// Timestamp adds the time formatted as RFC3339Nano +// as a field to the output +func (nlb NopLogBuilder) Timestamp() LogBuilder { return nlb } + +// Bool adds a bool as a field to the output +func (nlb NopLogBuilder) Bool(key string, val bool) LogBuilder { return nlb } + +// Str adds a string as a field to the output +func (nlb NopLogBuilder) Str(key, val string) LogBuilder { return nlb } + +// Any uses reflection to marshal any type and writes +// the result as a field to the output. This is much slower +// than the type-specific functions. +func (nlb NopLogBuilder) Any(key string, val any) LogBuilder { return nlb } + +// Err adds an error as a field to the output +func (nlb NopLogBuilder) Err(err error) LogBuilder { return nlb } + +// Send sends the event to the output. +// +// After calling send, do not use the event again. +func (nlb NopLogBuilder) Send() {} diff --git a/pretty.go b/pretty.go new file mode 100644 index 0000000..2468471 --- /dev/null +++ b/pretty.go @@ -0,0 +1,348 @@ +package logger + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "os" + "strconv" + "time" + + "github.com/gookit/color" + "github.com/mattn/go-isatty" +) + +var _ Logger = (*PrettyLogger)(nil) + +// PrettyLogger implements the Logger interface +// using human-readable output for log messages. +type PrettyLogger struct { + Out io.Writer + Level LogLevel + + TimeFormat string + + UseColor bool + + TimeColor color.Color + MsgColor color.Color + KeyColor color.Color + + DebugColor color.Color + InfoColor color.Color + WarnColor color.Color + ErrColor color.Color + FatalColor color.Color + PanicColor color.Color + + noPanic bool + noExit bool +} + +// NewPretty creates and returns a new PrettyLogger. +// If the input writer is io.Discard, NopLogger +// will be returned. +func NewPretty(out io.Writer) *PrettyLogger { + useColor := false + if f, ok := out.(*os.File); ok { + useColor = isatty.IsTerminal(f.Fd()) + } + + return &PrettyLogger{ + Out: out, + Level: LogLevelInfo, + + TimeFormat: time.Kitchen, + + UseColor: useColor, + + TimeColor: color.FgGray, + MsgColor: color.Normal, + KeyColor: color.FgCyan, + + DebugColor: color.FgYellow, + InfoColor: color.FgGreen, + WarnColor: color.FgRed, + ErrColor: color.FgLightRed, + FatalColor: color.FgRed.Darken(), + PanicColor: color.FgRed.Darken(), + } +} + +// NoPanic prevents the logger from panicking on panic events +func (pl *PrettyLogger) NoPanic() { + pl.noPanic = true +} + +// NoExit prevents the logger from exiting on fatal events +func (pl *PrettyLogger) NoExit() { + pl.noExit = true +} + +// Debug creates a new debug event with the given message +func (pl *PrettyLogger) Debug(msg string) LogBuilder { + return newPrettyLogBuilder(pl, msg, LogLevelDebug) +} + +// Debugf creates a new debug event with the formatted message +func (pl *PrettyLogger) Debugf(format string, v ...any) LogBuilder { + return newPrettyLogBuilder(pl, fmt.Sprintf(format, v...), LogLevelDebug) +} + +// Info creates a new info event with the given message +func (pl *PrettyLogger) Info(msg string) LogBuilder { + return newPrettyLogBuilder(pl, msg, LogLevelInfo) +} + +// Infof creates a new info event with the formatted message +func (pl *PrettyLogger) Infof(format string, v ...any) LogBuilder { + return newPrettyLogBuilder(pl, fmt.Sprintf(format, v...), LogLevelInfo) +} + +// Warn creates a new warn event with the given message +func (pl *PrettyLogger) Warn(msg string) LogBuilder { + return newPrettyLogBuilder(pl, msg, LogLevelWarn) +} + +// Warnf creates a new warn event with the formatted message +func (pl *PrettyLogger) Warnf(format string, v ...any) LogBuilder { + return newPrettyLogBuilder(pl, fmt.Sprintf(format, v...), LogLevelWarn) +} + +// Error creates a new error event with the given message +func (pl *PrettyLogger) Error(msg string) LogBuilder { + return newPrettyLogBuilder(pl, msg, LogLevelError) +} + +// Errorf creates a new error event with the formatted message +func (pl *PrettyLogger) Errorf(format string, v ...any) LogBuilder { + return newPrettyLogBuilder(pl, fmt.Sprintf(format, v...), LogLevelError) +} + +// Fatal creates a new fatal event with the given message +// +// When sent, fatal events will cause a call to os.Exit(1) +func (pl *PrettyLogger) Fatal(msg string) LogBuilder { + return newPrettyLogBuilder(pl, msg, LogLevelFatal) +} + +// Fatalf creates a new fatal event with the formatted message +// +// When sent, fatal events will cause a call to os.Exit(1) +func (pl *PrettyLogger) Fatalf(format string, v ...any) LogBuilder { + return newPrettyLogBuilder(pl, fmt.Sprintf(format, v...), LogLevelFatal) +} + +// Panic creates a new panic event with the given message +// +// When sent, panic events will cause a panic +func (pl *PrettyLogger) Panic(msg string) LogBuilder { + return newPrettyLogBuilder(pl, msg, LogLevelPanic) +} + +// Panicf creates a new panic event with the formatted message +// +// When sent, panic events will cause a panic +func (pl *PrettyLogger) Panicf(format string, v ...any) LogBuilder { + return newPrettyLogBuilder(pl, fmt.Sprintf(format, v...), LogLevelPanic) +} + +// PrettyLogBuilder implements the LogBuilder interface +// using human-readable output for log messages +type PrettyLogBuilder struct { + l *PrettyLogger + lvl LogLevel + out writer +} + +func newPrettyLogBuilder(pl *PrettyLogger, msg string, lvl LogLevel) LogBuilder { + if pl.Out == io.Discard || lvl < pl.Level { + return NopLogBuilder{} + } + lb := &PrettyLogBuilder{ + l: pl, + out: writer{&bytes.Buffer{}, pl.Out}, + lvl: lvl, + } + lb.writeColor(lb.l.TimeColor, time.Now().Format(lb.l.TimeFormat)) + lb.out.WriteByte(' ') + + switch lvl { + case LogLevelDebug: + lb.writeColor(lb.l.DebugColor, "DBG") + case LogLevelInfo: + lb.writeColor(lb.l.InfoColor, "INF") + case LogLevelWarn: + lb.writeColor(lb.l.WarnColor, "WRN") + case LogLevelError: + lb.writeColor(lb.l.ErrColor, "ERR") + case LogLevelFatal: + lb.writeColor(lb.l.FatalColor, "FTL") + case LogLevelPanic: + lb.writeColor(lb.l.PanicColor, "PNC") + } + lb.out.WriteByte(' ') + + lb.writeColor(lb.l.MsgColor, msg) + return lb +} + +// writeKey writes a JSON key to the buffer +func (plb *PrettyLogBuilder) writeKey(k string) { + plb.out.WriteByte(' ') + plb.writeColor(plb.l.KeyColor, k) + plb.out.WriteByte('=') +} + +// Int adds an int field to the output +func (plb *PrettyLogBuilder) Int(key string, val int) LogBuilder { + return plb.Int64(key, int64(val)) +} + +// Int64 adds an int64 field to the output +func (plb *PrettyLogBuilder) Int64(key string, val int64) LogBuilder { + plb.writeKey(key) + plb.out.WriteString(strconv.FormatInt(val, 10)) + return plb +} + +// Int32 adds an int32 field to the output +func (plb *PrettyLogBuilder) Int32(key string, val int32) LogBuilder { + return plb.Int64(key, int64(val)) +} + +// Int16 adds an int16 field to the output +func (plb *PrettyLogBuilder) Int16(key string, val int16) LogBuilder { + return plb.Int64(key, int64(val)) +} + +// Int8 adds an int8 field to the output +func (plb *PrettyLogBuilder) Int8(key string, val int8) LogBuilder { + return plb.Int64(key, int64(val)) +} + +// Uint adds a uint field to the output +func (plb *PrettyLogBuilder) Uint(key string, val uint) LogBuilder { + return plb.Uint64(key, uint64(val)) +} + +// Uint64 adds a uint64 field to the output +func (plb *PrettyLogBuilder) Uint64(key string, val uint64) LogBuilder { + plb.writeKey(key) + plb.out.WriteString(strconv.FormatUint(val, 10)) + return plb +} + +// Uint32 adds a uint32 field to the output +func (plb *PrettyLogBuilder) Uint32(key string, val uint32) LogBuilder { + return plb.Uint64(key, uint64(val)) +} + +// Uint16 adds a uint16 field to the output +func (plb *PrettyLogBuilder) Uint16(key string, val uint16) LogBuilder { + return plb.Uint64(key, uint64(val)) +} + +// Uint8 adds a uint8 field to the output +func (plb *PrettyLogBuilder) Uint8(key string, val uint8) LogBuilder { + return plb.Uint64(key, uint64(val)) +} + +// float adds a float of specified bitsize to the output +func (plb *PrettyLogBuilder) float(key string, val float64, bitsize int) LogBuilder { + plb.writeKey(key) + plb.out.WriteString(strconv.FormatFloat(val, 'f', -1, bitsize)) + return plb +} + +// Float64 adds a float64 field to the output +func (plb *PrettyLogBuilder) Float64(key string, val float64) LogBuilder { + return plb.float(key, val, 64) +} + +// Float32 adds a float32 field to the output +func (plb *PrettyLogBuilder) Float32(key string, val float32) LogBuilder { + return plb.float(key, float64(val), 32) +} + +// Stringer calls the String method of an fmt.Stringer +// and adds the resulting string as a field to the output +func (plb *PrettyLogBuilder) Stringer(key string, s fmt.Stringer) LogBuilder { + return plb.Str(key, s.String()) +} + +// Bytes writes hex-encoded bytes as a field to the output +func (plb *PrettyLogBuilder) Bytes(key string, b []byte) LogBuilder { + return plb.Str(key, hex.EncodeToString(b)) +} + +// Timestamp adds the time formatted as RFC3339Nano +// as a field to the output +func (plb *PrettyLogBuilder) Timestamp() LogBuilder { + return plb.Str("timestamp", time.Now().Format(time.RFC3339Nano)) +} + +// Bool adds a bool as a field to the output +func (plb *PrettyLogBuilder) Bool(key string, val bool) LogBuilder { + plb.writeKey(key) + if val { + plb.out.WriteString("true") + } else { + plb.out.WriteString("false") + } + return plb +} + +// Str adds a string as a field to the output +func (plb *PrettyLogBuilder) Str(key, val string) LogBuilder { + plb.writeKey(key) + plb.out.WriteByte('"') + plb.out.WriteString(val) + plb.out.WriteByte('"') + return plb +} + +// Any uses reflection to marshal any type and writes +// the result as a field to the output. This is much slower +// than the type-specific functions. +func (plb *PrettyLogBuilder) Any(key string, val any) LogBuilder { + plb.writeKey(key) + data, err := json.Marshal(val) + if err != nil { + panic(err) + } + plb.out.Write(data) + return plb +} + +// Err adds an error as a field to the output +func (plb *PrettyLogBuilder) Err(err error) LogBuilder { + plb.out.WriteByte(' ') + plb.writeColor(plb.l.ErrColor, "error=") + plb.writeColor(plb.l.ErrColor, `"`+err.Error()+`"`) + return plb +} + +// Send sends the event to the output. +// +// After calling send, do not use the event again. +func (plb *PrettyLogBuilder) Send() { + plb.out.WriteByte('\n') + plb.out.Flush() + if plb.lvl == LogLevelFatal && !plb.l.noExit { + os.Exit(1) + } else if plb.lvl == LogLevelPanic && !plb.l.noPanic { + panic("") + } +} + +// writeColor writes a string to the buffer using the given color +func (plb *PrettyLogBuilder) writeColor(c color.Color, s string) { + if plb.l.UseColor { + plb.out.WriteString(c.Text(s)) + } else { + plb.out.WriteString(s) + } +} diff --git a/writer.go b/writer.go new file mode 100644 index 0000000..5e5973e --- /dev/null +++ b/writer.go @@ -0,0 +1,22 @@ +package logger + +import ( + "bytes" + "io" +) + +// writer combines a buffer and a writer, +// adding a flush function to flush the buffer +// to the underlying writer. This is used +// to avoid file I/O, making the logger faster. +type writer struct { + *bytes.Buffer + w io.Writer +} + +// Flush writes the buffer contents to the +// underlying writer +func (w writer) Flush() error { + _, err := io.Copy(w.w, w.Buffer) + return err +}