diff --git a/README.md b/README.md index 6aefae1..5dc4403 100644 --- a/README.md +++ b/README.md @@ -7,18 +7,14 @@ with simple APIs and configurable behavior. ## Why another logger? -Golog is designed to address mainly two issues: +Golog is designed to address mainly an issues: -#### Reduce the amount of PII (personally identifiable information) data in logs - -Golog exposes APIs which does not allow to simply introduce a struct or a map as part of the log fields. - -This design pushes the consumers of this library to care about PII data and -pushes to reduce as much as possible the amount of data which can be logged. +#### Add tracing and other extra data into the logging behavior +Golog expects to have a context passed down to the logging API. -It is possible to extend the logger behavior -for handling complex data type -by writing custom field factory functions as shown in the customization section. +The `context.Context` in Go is usually the holder for tracing information and +embedding one of the decorators available to the logger plugs this behavior for free +in all the places where the logger is used. #### Add tracing and other extra data into the logging behavior diff --git a/benchmarks/logger/logger_test.go b/benchmarks/logger/logger_test.go index 0e2d46a..995f533 100644 --- a/benchmarks/logger/logger_test.go +++ b/benchmarks/logger/logger_test.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "testing" + "time" "github.com/sirupsen/logrus" "go.uber.org/zap" @@ -19,46 +20,22 @@ goos: darwin goarch: amd64 pkg: github.com/damianopetrungaro/golog/benchmarks/logger cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz -BenchmarkLogger/golog-12 1266547 929.9 ns/op 2826 B/op 26 allocs/op -BenchmarkLogger/zap-12 1000000 1066 ns/op 2836 B/op 20 allocs/op -BenchmarkLogger/logrus-12 344604 3395 ns/op 6168 B/op 69 allocs/op -BenchmarkLogger/golog.Check-12 56982846 20.10 ns/op 64 B/op 1 allocs/op -BenchmarkLogger/zap.Check-12 1000000000 0.9662 ns/op 0 B/op 0 allocs/op +BenchmarkLogger/golog-12 73681 16878 ns/op 17361 B/op 128 allocs/op +BenchmarkLogger/zap-12 58617 20173 ns/op 28346 B/op 125 allocs/op +BenchmarkLogger/logrus-12 66474 18344 ns/op 13882 B/op 172 allocs/op +BenchmarkLogger/golog.Check-12 53974438 22.63 ns/op 64 B/op 1 allocs/op +BenchmarkLogger/zap.Check-12 1000000000 0.8838 ns/op 0 B/op 0 allocs/op PASS -ok github.com/damianopetrungaro/golog/benchmarks/logger 6.781s +ok github.com/damianopetrungaro/golog/benchmarks/logger 6.494s */ func BenchmarkLogger(b *testing.B) { - b.Run("logrus", func(b *testing.B) { - logrus.SetFormatter(&logrus.JSONFormatter{}) - logrus.SetOutput(io.Discard) - logrus.SetLevel(logrus.DebugLevel) - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - logrus.WithFields(logrus.Fields{ - "int": 10, - "ints": []int{1, 2, 3, 4, 5, 6, 7}, - "string": "a string", - "strings": []string{"one", "one", "one", "one", "one", "one"}, - "int_2": 10, - "ints_2": []int{1, 2, 3, 4, 5, 6, 7}, - "string_2": "a string", - "strings_2": []string{"one", "one", "one", "one", "one", "one"}, - "int_3": 10, - "ints_3": []int{1, 2, 3, 4, 5, 6, 7}, - "string_3": "a string", - "strings_3": []string{"one", "one", "one", "one", "one", "one"}, - "error": fmt.Errorf("an error occurred"), - }).Debug("This is a message") - } - }) - }) + users := helperUsers() b.Run("golog", func(b *testing.B) { ctx := context.Background() writer := golog.NewBufWriter( - golog.NewTextEncoder(golog.DefaultTextConfig()), + golog.NewJsonEncoder(golog.DefaultJsonConfig()), bufio.NewWriter(io.Discard), golog.DefaultErrorHandler(), golog.DEBUG, @@ -84,6 +61,7 @@ func BenchmarkLogger(b *testing.B) { golog.String("string_3", "a string"), golog.Strings("strings_3", []string{"one", "one", "one", "one", "one", "one"}), golog.Err(fmt.Errorf("an error occurred")), + golog.Any("users", users), ).Debug(ctx, "This is a message") } }) @@ -118,11 +96,39 @@ func BenchmarkLogger(b *testing.B) { zap.String("string_3", "a string"), zap.Strings("strings_3", []string{"one", "one", "one", "one", "one", "one"}), zap.Error(fmt.Errorf("an error occurred")), + zap.Any("users", users), ).Debug("This is a message") } }) }) + b.Run("logrus", func(b *testing.B) { + logrus.SetFormatter(&logrus.JSONFormatter{}) + logrus.SetOutput(io.Discard) + logrus.SetLevel(logrus.DebugLevel) + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logrus.WithFields(logrus.Fields{ + "int": 10, + "ints": []int{1, 2, 3, 4, 5, 6, 7}, + "string": "a string", + "strings": []string{"one", "one", "one", "one", "one", "one"}, + "int_2": 10, + "ints_2": []int{1, 2, 3, 4, 5, 6, 7}, + "string_2": "a string", + "strings_2": []string{"one", "one", "one", "one", "one", "one"}, + "int_3": 10, + "ints_3": []int{1, 2, 3, 4, 5, 6, 7}, + "string_3": "a string", + "strings_3": []string{"one", "one", "one", "one", "one", "one"}, + "error": fmt.Errorf("an error occurred"), + "users": users, + }).Debug("This is a message") + } + }) + }) + b.Run("golog.Check", func(b *testing.B) { ctx := context.Background() writer := golog.NewBufWriter( @@ -152,6 +158,7 @@ func BenchmarkLogger(b *testing.B) { golog.String("string_3", "a string"), golog.Strings("strings_3", []string{"one", "one", "one", "one", "one", "one"}), golog.Err(fmt.Errorf("an error occurred")), + golog.Any("users", users), ) } } @@ -188,9 +195,32 @@ func BenchmarkLogger(b *testing.B) { zap.String("string_3", "a string"), zap.Strings("strings_3", []string{"one", "one", "one", "one", "one", "one"}), zap.Error(fmt.Errorf("an error occurred")), + zap.Any("users", users), ) } } }) }) } + +func helperUser() any { + return struct { + ID string + Name string + Age int + BirthDate time.Time + }{ + ID: "123", + Name: "John", + Age: 123, + BirthDate: time.Now(), + } +} + +func helperUsers() any { + us := make([]any, 100) + for i := 0; i < 100; i++ { + us[i] = helperUser() + } + return us +} diff --git a/encoder.go b/encoder.go index dbb23dd..455bda9 100644 --- a/encoder.go +++ b/encoder.go @@ -2,6 +2,7 @@ package golog import ( "bytes" + "encoding/json" "io" "strconv" "time" @@ -404,6 +405,20 @@ func (j JsonEncoder) encodeField(f Field, w *bytes.Buffer) { j.addArrayElemQuoted(w, v.Format(j.Config.TimeLayout), i != len(val)-1) } }) + default: + j.addElemEncoder(w, f.Key(), val) + } +} + +func (j JsonEncoder) addElemEncoder(w *bytes.Buffer, k string, val any) { + w.WriteString(`"`) + w.WriteString(k) + w.WriteString(`":`) + if err := json.NewEncoder(w).Encode(val); err != nil { + w.WriteString(`"`) + w.WriteString(err.Error()) + w.WriteString(`"`) + return } } diff --git a/example_test.go b/example_test.go index 512e206..c96885b 100644 --- a/example_test.go +++ b/example_test.go @@ -11,6 +11,26 @@ import ( func ExampleLogger() { + user := struct { + ID string + Name string + }{ + ID: "123", + Name: "John", + } + + invalidJsonMap := map[any]any{ + 1: 1, + "2": 2, + "metadata": map[string]string{"count": "12"}, + } + + validJsonMap := map[string]any{ + "1": 1, + "2": 2, + "metadata": map[string]string{"count": "12"}, + } + w := golog.NewBufWriter( golog.NewJsonEncoder(golog.DefaultJsonConfig()), bufio.NewWriter(os.Stdout), @@ -35,10 +55,19 @@ func ExampleLogger() { logger.Info(ctx, "an info message") loggerWithErr.Warn(ctx, "a warning message") + loggerWithErrAndUser := loggerWithErr.With(golog.Any("user", user)) + loggerWithErrAndUser.With(golog.Any("unsupported map", invalidJsonMap)).Warn(ctx, "a warning message with an unsupported map") + loggerWithErrAndUser.With(golog.Any("supported map", validJsonMap)).Warn(ctx, "a warning message with a supported map") + // Output: // {"level":"ERROR","message":"an error message","hello":"world"} // {"level":"ERROR","message":"another error message","hello":"world"} // {"level":"INFO","message":"an info message","hello":"world"} // {"level":"WARN","message":"a warning message","hello":"world","error":"error: ops!"} + // {"level":"WARN","message":"a warning message with an unsupported map","hello":"world","error":"error: ops!","user":{"ID":"123","Name":"John"} + //,"unsupported map":"json: unsupported type: map[interface {}]interface {}"} + // {"level":"WARN","message":"a warning message with a supported map","hello":"world","error":"error: ops!","user":{"ID":"123","Name":"John"} + //,"supported map":{"1":1,"2":2,"metadata":{"count":"12"}} + //} // } diff --git a/field.go b/field.go index 8c20595..08ba81c 100644 --- a/field.go +++ b/field.go @@ -183,6 +183,11 @@ func Times(k string, v []time.Time) Field { return Field{k: k, v: v} } +//Any ... +func Any(k string, v any) Field { + return Field{k: k, v: v} +} + //Err creates a field containing a value of type ") Fi" func Err(err error) Field { const k = "error"