From 83fe5a20c3c7a4aab583aaac4749e7c27fdc3009 Mon Sep 17 00:00:00 2001 From: Josemar Luedke <230476+josemarluedke@users.noreply.github.com> Date: Sat, 24 May 2025 07:14:29 -0700 Subject: [PATCH] Replace logrus with slog --- go.mod | 1 - go.sum | 2 - logger.go | 258 +++++++++++++++++----------------------- prefixed_logger.go | 92 +++++++------- prefixed_logger_test.go | 9 +- 5 files changed, 158 insertions(+), 204 deletions(-) diff --git a/go.mod b/go.mod index 1787a47..fac7537 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/neighborly/go-errors v0.3.1 github.com/onsi/ginkgo v1.14.2 github.com/onsi/gomega v1.10.1 - github.com/sirupsen/logrus v1.6.0 github.com/vektah/gqlparser/v2 v2.5.14 ) diff --git a/go.sum b/go.sum index a42e3ee..b6b7a18 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= -github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sosodev/duration v1.1.0 h1:kQcaiGbJaIsRqgQy7VGlZrVw1giWO+lDoX3MCPnpVO4= github.com/sosodev/duration v1.1.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/logger.go b/logger.go index 5d4e9fd..a465f8a 100644 --- a/logger.go +++ b/logger.go @@ -2,222 +2,175 @@ package log import ( "context" - "errors" "fmt" "io" + "log/slog" "net/http" + "os" + "strings" "time" "github.com/go-chi/chi/middleware" - "github.com/sirupsen/logrus" ) // Fields Type to pass when we want to call WithFields for structured logging +// it matches the old logrus Fields type for convenience. type Fields map[string]interface{} -// Logger interface -type Logger interface { - Info(args ...interface{}) - Infof(message string, args ...interface{}) - Debug(args ...interface{}) - Debugf(message string, args ...interface{}) - Error(args ...interface{}) - Errorf(message string, args ...interface{}) - Warn(args ...interface{}) - Warnf(message string, args ...interface{}) - Fatal(args ...interface{}) - Fatalf(message string, args ...interface{}) - Panic(args ...interface{}) - Panicf(message string, args ...interface{}) - Writer() *io.PipeWriter -} - var ( - logger *logrus.Logger + logger *slog.Logger ) func init() { New(false, "info") } -// New - Creates a new instance of logrus with customized configuration -func New(isJSONFormatted bool, logLevel string) *logrus.Logger { - var formatter logrus.Formatter - - formatter = &logrus.TextFormatter{ - ForceColors: true, - DisableLevelTruncation: true, +// New creates a new slog.Logger with the given configuration. If a writer is +// provided it will be used as the output destination. +func New(isJSONFormatted bool, logLevel string, out ...io.Writer) *slog.Logger { + w := io.Writer(os.Stdout) + if len(out) > 0 && out[0] != nil { + w = out[0] } + opts := &slog.HandlerOptions{Level: getLevel(logLevel)} + var h slog.Handler if isJSONFormatted { - formatter = &logrus.JSONFormatter{} + h = slog.NewJSONHandler(w, opts) + } else { + h = slog.NewTextHandler(w, opts) } - log := logrus.New() - log.SetFormatter(formatter) - log.SetLevel(getLevel(logLevel)) + log := slog.New(h) logger = log return log } -func GetLogger() Logger { - return logger -} - -// RequestLogger creates a logger with the request ID on it -func RequestLogger(ctx context.Context) Logger { - return logger.WithFields(logrus.Fields{ - "requestID": middleware.GetReqID(ctx), - }) -} +// GetLogger returns the package logger. +func GetLogger() *slog.Logger { return logger } -func Writer() *io.PipeWriter { - return logger.Writer() +// RequestLogger creates a logger with the request ID on it. +func RequestLogger(ctx context.Context) *slog.Logger { + return logger.With(slog.String("requestID", middleware.GetReqID(ctx))) } -func WriterLevel(logLevel string) *io.PipeWriter { - return logger.WriterLevel(getLevel(logLevel)) -} - -func getLevel(logLevel string) logrus.Level { - level := logrus.InfoLevel - switch logLevel { - case "panic": - level = logrus.PanicLevel - case "fatal": - level = logrus.FatalLevel - case "error": - level = logrus.ErrorLevel - case "warn": - level = logrus.WarnLevel - case "info": - level = logrus.InfoLevel +// Helper to convert string log levels into slog.Level values. +func getLevel(lvl string) slog.Level { + switch strings.ToLower(lvl) { case "debug": - level = logrus.DebugLevel + return slog.LevelDebug + case "info": + return slog.LevelInfo + case "warn": + return slog.LevelWarn + case "error", "fatal", "panic": + return slog.LevelError + default: + return slog.LevelInfo } - - return level } -func Info(args ...interface{}) { - logger.Info(args...) -} - -func Infof(message string, args ...interface{}) { - logger.Infof(message, args...) +// helper to convert Fields to slog attributes +func toAttrs(fields Fields) []slog.Attr { + attrs := make([]slog.Attr, 0, len(fields)) + for k, v := range fields { + attrs = append(attrs, slog.Any(k, v)) + } + return attrs } +func Info(args ...interface{}) { logger.Info(fmt.Sprint(args...)) } +func Infof(msg string, args ...interface{}) { logger.Info(fmt.Sprintf(msg, args...)) } func InfoWithFields(fields Fields, args ...interface{}) { - logger.WithFields(logrus.Fields(fields)).Info(args...) + logger.With(toAttrs(fields)...).Info(fmt.Sprint(args...)) } - -func InfoWithFieldsf(fields Fields, message string, args ...interface{}) { - logger.WithFields(logrus.Fields(fields)).Infof(message, args...) -} - -func Debug(args ...interface{}) { - logger.Debug(args...) -} - -func Debugf(message string, args ...interface{}) { - logger.Debugf(message, args...) +func InfoWithFieldsf(fields Fields, msg string, args ...interface{}) { + logger.With(toAttrs(fields)...).Info(fmt.Sprintf(msg, args...)) } +func Debug(args ...interface{}) { logger.Debug(fmt.Sprint(args...)) } +func Debugf(msg string, args ...interface{}) { logger.Debug(fmt.Sprintf(msg, args...)) } func DebugWithFields(fields Fields, args ...interface{}) { - logger.WithFields(logrus.Fields(fields)).Debug(args...) + logger.With(toAttrs(fields)...).Debug(fmt.Sprint(args...)) } - -func DebugWithFieldsf(fields Fields, message string, args ...interface{}) { - logger.WithFields(logrus.Fields(fields)).Debugf(message, args...) -} - -func Error(args ...interface{}) { - logger.Error(args...) -} - -func Errorf(message string, args ...interface{}) { - logger.Errorf(message, args...) +func DebugWithFieldsf(fields Fields, msg string, args ...interface{}) { + logger.With(toAttrs(fields)...).Debug(fmt.Sprintf(msg, args...)) } +func Error(args ...interface{}) { logger.Error(fmt.Sprint(args...)) } +func Errorf(msg string, args ...interface{}) { logger.Error(fmt.Sprintf(msg, args...)) } func ErrorWithFields(fields Fields, args ...interface{}) { - logger.WithFields(logrus.Fields(fields)).Error(args...) + logger.With(toAttrs(fields)...).Error(fmt.Sprint(args...)) } - -func ErrorWithFieldsf(fields Fields, message string, args ...interface{}) { - logger.WithFields(logrus.Fields(fields)).Errorf(message, args...) +func ErrorWithFieldsf(fields Fields, msg string, args ...interface{}) { + logger.With(toAttrs(fields)...).Error(fmt.Sprintf(msg, args...)) } func NewError(args ...interface{}) error { Error(args...) - return errors.New(fmt.Sprint(args...)) + return fmt.Errorf("%s", fmt.Sprint(args...)) } - -func NewErrorf(message string, args ...interface{}) error { - Errorf(message, args...) - return fmt.Errorf(message, args...) +func NewErrorf(msg string, args ...interface{}) error { + Errorf(msg, args...) + return fmt.Errorf(msg, args...) } - func NewErrorWithFields(fields Fields, args ...interface{}) error { ErrorWithFields(fields, args...) - return errors.New(fmt.Sprint(args...)) + return fmt.Errorf("%s", fmt.Sprint(args...)) } - -func NewErrorWithFieldsf(fields Fields, message string, args ...interface{}) error { - ErrorWithFieldsf(fields, message, args...) - return fmt.Errorf(message, args...) -} - -func Warn(args ...interface{}) { - logger.Warn(args...) -} - -func Warnf(message string, args ...interface{}) { - logger.Warnf(message, args...) +func NewErrorWithFieldsf(fields Fields, msg string, args ...interface{}) error { + ErrorWithFieldsf(fields, msg, args...) + return fmt.Errorf(msg, args...) } +func Warn(args ...interface{}) { logger.Warn(fmt.Sprint(args...)) } +func Warnf(msg string, args ...interface{}) { logger.Warn(fmt.Sprintf(msg, args...)) } func WarnWithFields(fields Fields, args ...interface{}) { - logger.WithFields(logrus.Fields(fields)).Warn(args...) + logger.With(toAttrs(fields)...).Warn(fmt.Sprint(args...)) } - -func WarnWithFieldsf(fields Fields, message string, args ...interface{}) { - logger.WithFields(logrus.Fields(fields)).Warnf(message, args...) +func WarnWithFieldsf(fields Fields, msg string, args ...interface{}) { + logger.With(toAttrs(fields)...).Warn(fmt.Sprintf(msg, args...)) } func Fatal(args ...interface{}) { - logger.Fatal(args...) + logger.Error(fmt.Sprint(args...)) + os.Exit(1) } - -func Fatalf(message string, args ...interface{}) { - logger.Fatalf(message, args...) +func Fatalf(msg string, args ...interface{}) { + logger.Error(fmt.Sprintf(msg, args...)) + os.Exit(1) } - func FatalWithFields(fields Fields, args ...interface{}) { - logger.WithFields(logrus.Fields(fields)).Fatal(args...) + logger.With(toAttrs(fields)...).Error(fmt.Sprint(args...)) + os.Exit(1) } - -func FatalWithFieldsf(fields Fields, message string, args ...interface{}) { - logger.WithFields(logrus.Fields(fields)).Fatalf(message, args...) +func FatalWithFieldsf(fields Fields, msg string, args ...interface{}) { + logger.With(toAttrs(fields)...).Error(fmt.Sprintf(msg, args...)) + os.Exit(1) } func Panic(args ...interface{}) { - logger.Panic(args...) + msg := fmt.Sprint(args...) + logger.Error(msg) + panic(msg) } - -func Panicf(message string, args ...interface{}) { - logger.Panicf(message, args...) +func Panicf(msg string, args ...interface{}) { + formatted := fmt.Sprintf(msg, args...) + logger.Error(formatted) + panic(formatted) } - func PanicWithFields(fields Fields, args ...interface{}) { - logger.WithFields(logrus.Fields(fields)).Panic(args...) + msg := fmt.Sprint(args...) + logger.With(toAttrs(fields)...).Error(msg) + panic(msg) } - -func PanicWithFieldsf(fields Fields, message string, args ...interface{}) { - logger.WithFields(logrus.Fields(fields)).Panicf(message, args...) +func PanicWithFieldsf(fields Fields, msg string, args ...interface{}) { + formatted := fmt.Sprintf(msg, args...) + logger.With(toAttrs(fields)...).Error(formatted) + panic(formatted) } -// ServerLogger is a middleware that logs the start and end of each request, along -// with some useful data about what was requested, what the response status was, -// and how long it took to return. +// ServerLogger logs the start and end of each request with useful metadata. func ServerLogger() func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { @@ -225,15 +178,15 @@ func ServerLogger() func(next http.Handler) http.Handler { t1 := time.Now() defer func() { - logger.WithFields(logrus.Fields{ - "proto": r.Proto, - "path": r.URL.Path, - "duration": time.Since(t1), - "status": ww.Status(), - "size": ww.BytesWritten(), - "ip": r.RemoteAddr, - "requestID": middleware.GetReqID(r.Context()), - }).Info("Request Served") + logger.With( + slog.String("proto", r.Proto), + slog.String("path", r.URL.Path), + slog.Duration("duration", time.Since(t1)), + slog.Int("status", ww.Status()), + slog.Int("size", ww.BytesWritten()), + slog.String("ip", r.RemoteAddr), + slog.String("requestID", middleware.GetReqID(r.Context())), + ).Info("Request Served") }() next.ServeHTTP(ww, r) @@ -241,3 +194,8 @@ func ServerLogger() func(next http.Handler) http.Handler { return http.HandlerFunc(fn) } } + +// Writer exposes a writer from the logger's handler when possible. If the +// underlying handler does not support retrieving a writer, nil is returned. +func Writer() *io.PipeWriter { return nil } +func WriterLevel(string) *io.PipeWriter { return nil } diff --git a/prefixed_logger.go b/prefixed_logger.go index a08cb8d..da6ea52 100644 --- a/prefixed_logger.go +++ b/prefixed_logger.go @@ -2,144 +2,144 @@ package log import ( "fmt" - "io" + "log/slog" "github.com/neighborly/go-errors" - "github.com/sirupsen/logrus" ) +// PrefixedLogger wraps a slog.Logger and prefixes all messages with a given +// string. type PrefixedLogger struct { Prefix string - LoggerInstance *logrus.Logger + LoggerInstance *slog.Logger } -func NewPrefixedLogger(prefix string, instance Logger) PrefixedLogger { - var _logger *logrus.Logger +// NewPrefixedLogger returns a PrefixedLogger using the provided slog.Logger. If +// nil is given, a new JSON formatted info-level logger is created. +func NewPrefixedLogger(prefix string, instance *slog.Logger) PrefixedLogger { + var l *slog.Logger if instance == nil { - _logger = New(true, "info") - } else if logrusLogger, ok := instance.(*logrus.Logger); ok { - _logger = logrusLogger - } - - return PrefixedLogger{ - Prefix: prefix, - LoggerInstance: _logger, + l = New(true, "info") + } else { + l = instance } + return PrefixedLogger{Prefix: prefix, LoggerInstance: l} } -func (l *PrefixedLogger) prefixArgs(args []interface{}) []interface{} { - return append([]interface{}{l.Prefix + ": "}, args...) +func (l *PrefixedLogger) prefixArgs(args []interface{}) string { + return fmt.Sprint(append([]interface{}{l.Prefix + ": "}, args...)...) } func (l *PrefixedLogger) prefixMsg(message string) string { return fmt.Sprintf("%s: %s", l.Prefix, message) } -// implement logger interface func (l *PrefixedLogger) Info(args ...interface{}) { - l.LoggerInstance.Info(l.prefixArgs(args)...) + l.LoggerInstance.Info(l.prefixArgs(args)) } func (l *PrefixedLogger) Infof(message string, args ...interface{}) { - l.LoggerInstance.Infof(l.prefixMsg(message), args...) + l.LoggerInstance.Info(l.prefixMsg(fmt.Sprintf(message, args...))) } func (l *PrefixedLogger) Debug(args ...interface{}) { - l.LoggerInstance.Debug(l.prefixArgs(args)...) + l.LoggerInstance.Debug(l.prefixArgs(args)) } func (l *PrefixedLogger) Debugf(message string, args ...interface{}) { - l.LoggerInstance.Debugf(l.prefixMsg(message), args...) + l.LoggerInstance.Debug(l.prefixMsg(fmt.Sprintf(message, args...))) } func (l *PrefixedLogger) Error(args ...interface{}) { - l.LoggerInstance.Error(l.prefixArgs(args)...) + l.LoggerInstance.Error(l.prefixArgs(args)) } func (l *PrefixedLogger) Errorf(message string, args ...interface{}) { - l.LoggerInstance.Errorf(l.prefixMsg(message), args...) + l.LoggerInstance.Error(l.prefixMsg(fmt.Sprintf(message, args...))) } func (l *PrefixedLogger) Warn(args ...interface{}) { - l.LoggerInstance.Warn(l.prefixArgs(args)...) + l.LoggerInstance.Warn(l.prefixArgs(args)) } func (l *PrefixedLogger) Warnf(message string, args ...interface{}) { - l.LoggerInstance.Warnf(l.prefixMsg(message), args...) + l.LoggerInstance.Warn(l.prefixMsg(fmt.Sprintf(message, args...))) } func (l *PrefixedLogger) Fatal(args ...interface{}) { - l.LoggerInstance.Fatal(l.prefixArgs(args)...) + l.LoggerInstance.Error(l.prefixArgs(args)) } func (l *PrefixedLogger) Fatalf(message string, args ...interface{}) { - l.LoggerInstance.Fatalf(l.prefixMsg(message), args...) + l.LoggerInstance.Error(l.prefixMsg(fmt.Sprintf(message, args...))) } func (l *PrefixedLogger) Panic(args ...interface{}) { - l.LoggerInstance.Panic(l.prefixArgs(args)...) + msg := l.prefixArgs(args) + l.LoggerInstance.Error(msg) + panic(msg) } func (l *PrefixedLogger) Panicf(message string, args ...interface{}) { - l.LoggerInstance.Panicf(l.prefixMsg(message), args...) -} - -func (l *PrefixedLogger) Writer() *io.PipeWriter { - return l.LoggerInstance.Writer() + msg := l.prefixMsg(fmt.Sprintf(message, args...)) + l.LoggerInstance.Error(msg) + panic(msg) } // fields - func (l *PrefixedLogger) InfoWithFields(fields Fields, args ...interface{}) { - l.LoggerInstance.WithFields(logrus.Fields(fields)).Info(l.prefixArgs(args)...) + l.LoggerInstance.With(toAttrs(fields)...).Info(l.prefixArgs(args)) } func (l *PrefixedLogger) InfoWithFieldsf(fields Fields, message string, args ...interface{}) { - l.LoggerInstance.WithFields(logrus.Fields(fields)).Infof(l.prefixMsg(message), args...) + l.LoggerInstance.With(toAttrs(fields)...).Info(l.prefixMsg(fmt.Sprintf(message, args...))) } func (l *PrefixedLogger) DebugWithFields(fields Fields, args ...interface{}) { - l.LoggerInstance.WithFields(logrus.Fields(fields)).Debug(l.prefixArgs(args)...) + l.LoggerInstance.With(toAttrs(fields)...).Debug(l.prefixArgs(args)) } func (l *PrefixedLogger) DebugWithFieldsf(fields Fields, message string, args ...interface{}) { - l.LoggerInstance.WithFields(logrus.Fields(fields)).Debugf(l.prefixMsg(message), args...) + l.LoggerInstance.With(toAttrs(fields)...).Debug(l.prefixMsg(fmt.Sprintf(message, args...))) } func (l *PrefixedLogger) ErrorWithFields(fields Fields, args ...interface{}) { - l.LoggerInstance.WithFields(logrus.Fields(fields)).Error(l.prefixArgs(args)...) + l.LoggerInstance.With(toAttrs(fields)...).Error(l.prefixArgs(args)) } func (l *PrefixedLogger) ErrorWithFieldsf(fields Fields, message string, args ...interface{}) { - l.LoggerInstance.WithFields(logrus.Fields(fields)).Errorf(l.prefixMsg(message), args...) + l.LoggerInstance.With(toAttrs(fields)...).Error(l.prefixMsg(fmt.Sprintf(message, args...))) } func (l *PrefixedLogger) WarnWithFields(fields Fields, args ...interface{}) { - l.LoggerInstance.WithFields(logrus.Fields(fields)).Warn(l.prefixArgs(args)...) + l.LoggerInstance.With(toAttrs(fields)...).Warn(l.prefixArgs(args)) } func (l *PrefixedLogger) WarnWithFieldsf(fields Fields, message string, args ...interface{}) { - l.LoggerInstance.WithFields(logrus.Fields(fields)).Warnf(l.prefixMsg(message), args...) + l.LoggerInstance.With(toAttrs(fields)...).Warn(l.prefixMsg(fmt.Sprintf(message, args...))) } func (l *PrefixedLogger) FatalWithFields(fields Fields, args ...interface{}) { - l.LoggerInstance.WithFields(logrus.Fields(fields)).Fatal(l.prefixArgs(args)...) + l.LoggerInstance.With(toAttrs(fields)...).Error(l.prefixArgs(args)) } func (l *PrefixedLogger) FatalWithFieldsf(fields Fields, message string, args ...interface{}) { - l.LoggerInstance.WithFields(logrus.Fields(fields)).Fatalf(l.prefixMsg(message), args...) + l.LoggerInstance.With(toAttrs(fields)...).Error(l.prefixMsg(fmt.Sprintf(message, args...))) } func (l *PrefixedLogger) PanicWithFields(fields Fields, args ...interface{}) { - l.LoggerInstance.WithFields(logrus.Fields(fields)).Panic(l.prefixArgs(args)...) + msg := l.prefixArgs(args) + l.LoggerInstance.With(toAttrs(fields)...).Error(msg) + panic(msg) } func (l *PrefixedLogger) PanicWithFieldsf(fields Fields, message string, args ...interface{}) { - l.LoggerInstance.WithFields(logrus.Fields(fields)).Panicf(l.prefixMsg(message), args...) + msg := l.prefixMsg(fmt.Sprintf(message, args...)) + l.LoggerInstance.With(toAttrs(fields)...).Error(msg) + panic(msg) } // error wrapping - func (l *PrefixedLogger) PrefixError(err error, msg string) error { return errors.Wrapf(err, "%s: %s", l.Prefix, msg) } diff --git a/prefixed_logger_test.go b/prefixed_logger_test.go index d2d6019..ed5e239 100644 --- a/prefixed_logger_test.go +++ b/prefixed_logger_test.go @@ -2,6 +2,7 @@ package log import ( "bytes" + "log/slog" . "github.com/onsi/ginkgo" g "github.com/onsi/gomega" @@ -10,8 +11,7 @@ import ( var _ = Describe("PrefixedLogger", func() { It("should prefix the logger instance", func() { buf := bytes.Buffer{} - logger := New(true, "info") - logger.Out = &buf + logger := New(true, "info", &buf) pl := NewPrefixedLogger("Test", logger) pl2 := NewPrefixedLogger("Foo", logger) @@ -40,13 +40,12 @@ var _ = Describe("PrefixedLogger", func() { It("should create an info log by default if one is not given", func() { buf := bytes.Buffer{} - pl := NewPrefixedLogger("test", nil) - pl.LoggerInstance.Out = &buf + pl := NewPrefixedLogger("test", slog.New(slog.NewJSONHandler(&buf, nil))) pl.Info("goldfish") b := ByteLogs{Log: &buf} b.Parse(nil) - g.Expect(b.Parsed[0]["level"]).To(g.Equal("info")) + g.Expect(b.Parsed[0]["level"]).To(g.Equal("INFO")) g.Expect(b.Parsed[0]["msg"]).To(g.Equal("test: goldfish")) }) })