diff --git a/cmd/proxy/actions/app.go b/cmd/proxy/actions/app.go index acc686710..d2ee7b03b 100644 --- a/cmd/proxy/actions/app.go +++ b/cmd/proxy/actions/app.go @@ -77,7 +77,7 @@ func App(logger *log.Logger, conf *config.Config) (http.Handler, error) { conf.GoEnv, ) if err != nil { - logger.Info(err) + logger.Info(err.Error()) } else { defer flushTraces() } @@ -87,7 +87,7 @@ func App(logger *log.Logger, conf *config.Config) (http.Handler, error) { // was specified by the user. flushStats, err := observ.RegisterStatsExporter(r, conf.StatsExporter, Service) if err != nil { - logger.Info(err) + logger.Info(err.Error()) } else { defer flushStats() } diff --git a/cmd/proxy/actions/basicauth_test.go b/cmd/proxy/actions/basicauth_test.go index 54e72e026..aa5742517 100644 --- a/cmd/proxy/actions/basicauth_test.go +++ b/cmd/proxy/actions/basicauth_test.go @@ -3,13 +3,13 @@ package actions import ( "bytes" "context" + "log/slog" "net/http" "net/http/httptest" "strings" "testing" "github.com/gomods/athens/pkg/log" - "github.com/sirupsen/logrus" ) var basicAuthTests = [...]struct { @@ -70,10 +70,10 @@ func TestBasicAuth(t *testing.T) { w := httptest.NewRecorder() r := httptest.NewRequest(http.MethodGet, tc.path, nil) r.SetBasicAuth(tc.user, tc.pass) - lggr := log.New("none", logrus.DebugLevel, "") buf := &bytes.Buffer{} - lggr.Out = buf - ctx := log.SetEntryInContext(context.Background(), lggr) + lggr := log.New("none", slog.LevelDebug, "", buf) + entry := lggr.WithFields(map[string]any{}) + ctx := log.SetEntryInContext(context.Background(), entry) r = r.WithContext(ctx) handler.ServeHTTP(w, r) resp := w.Result() diff --git a/cmd/proxy/actions/index.go b/cmd/proxy/actions/index.go index 1e4744e8d..5b0af31a8 100644 --- a/cmd/proxy/actions/index.go +++ b/cmd/proxy/actions/index.go @@ -3,6 +3,7 @@ package actions import ( "encoding/json" "fmt" + "log/slog" "net/http" "strconv" "time" @@ -10,7 +11,6 @@ import ( "github.com/gomods/athens/pkg/errors" "github.com/gomods/athens/pkg/index" "github.com/gomods/athens/pkg/log" - "github.com/sirupsen/logrus" ) // indexHandler implements GET baseURL/index. @@ -46,13 +46,13 @@ func getIndexLines(r *http.Request, index index.Indexer) ([]*index.Line, error) if limitStr := r.FormValue("limit"); limitStr != "" { limit, err = strconv.Atoi(limitStr) if err != nil || limit <= 0 { - return nil, errors.E(op, err, errors.KindBadRequest, logrus.InfoLevel) + return nil, errors.E(op, err, errors.KindBadRequest, slog.LevelInfo) } } if sinceStr := r.FormValue("since"); sinceStr != "" { since, err = time.Parse(time.RFC3339, sinceStr) if err != nil { - return nil, errors.E(op, err, errors.KindBadRequest, logrus.InfoLevel) + return nil, errors.E(op, err, errors.KindBadRequest, slog.LevelInfo) } } list, err := index.Lines(r.Context(), since, limit) diff --git a/cmd/proxy/main.go b/cmd/proxy/main.go index d139d715e..1bffae82f 100644 --- a/cmd/proxy/main.go +++ b/cmd/proxy/main.go @@ -6,6 +6,7 @@ import ( "flag" "fmt" stdlog "log" + "log/slog" "net" "net/http" _ "net/http/pprof" @@ -18,7 +19,6 @@ import ( "github.com/gomods/athens/pkg/build" "github.com/gomods/athens/pkg/config" athenslog "github.com/gomods/athens/pkg/log" - "github.com/sirupsen/logrus" ) var ( @@ -37,21 +37,22 @@ func main() { stdlog.Fatalf("Could not load config file: %v", err) } - logLvl, err := logrus.ParseLevel(conf.LogLevel) + var logLvl slog.Level + err = logLvl.UnmarshalText([]byte(conf.LogLevel)) if err != nil { stdlog.Fatalf("Could not parse log level %q: %v", conf.LogLevel, err) } - logger := athenslog.New(conf.CloudRuntime, logLvl, conf.LogFormat) + logger := athenslog.New(conf.CloudRuntime, logLvl, conf.LogFormat, os.Stdout) - // Turn standard logger output into logrus Errors. - logrusErrorWriter := logger.WriterLevel(logrus.ErrorLevel) + // Turn standard logger output into slog Errors. + slogErrorWriter := logger.WriterLevel(slog.LevelError) defer func() { - if err := logrusErrorWriter.Close(); err != nil { - logger.WithError(err).Warn("Could not close logrus writer pipe") + if err := slogErrorWriter.Close(); err != nil { + logger.WithError(err).Warn("Could not close slog writer pipe") } }() - stdlog.SetOutput(logrusErrorWriter) + stdlog.SetOutput(slogErrorWriter) stdlog.SetFlags(stdlog.Flags() &^ (stdlog.Ldate | stdlog.Ltime)) handler, err := actions.App(logger, conf) diff --git a/go.mod b/go.mod index d297be167..e280aea5f 100644 --- a/go.mod +++ b/go.mod @@ -35,7 +35,6 @@ require ( github.com/lib/pq v1.10.9 github.com/minio/minio-go/v6 v6.0.57 github.com/mitchellh/go-homedir v1.1.0 - github.com/sirupsen/logrus v1.9.3 github.com/spf13/afero v1.11.0 github.com/stretchr/testify v1.9.0 github.com/technosophos/moniker v0.0.0-20180509230615-a5dbd03a2245 diff --git a/go.sum b/go.sum index d88d1feda..6f2f07701 100644 --- a/go.sum +++ b/go.sum @@ -1188,18 +1188,6 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= -github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= -github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= diff --git a/pkg/download/protocol_test.go b/pkg/download/protocol_test.go index d7070e013..1a74996a9 100644 --- a/pkg/download/protocol_test.go +++ b/pkg/download/protocol_test.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "io" + "log/slog" "os" "path/filepath" "regexp" @@ -501,14 +502,25 @@ type testEntry struct { var _ log.Entry = &testEntry{} -func (e *testEntry) Debugf(format string, args ...any) { - e.msg = format -} -func (*testEntry) Infof(format string, args ...any) {} -func (*testEntry) Warnf(format string, args ...any) {} -func (*testEntry) Errorf(format string, args ...any) {} -func (*testEntry) WithFields(fields map[string]any) log.Entry { return nil } -func (*testEntry) SystemErr(err error) {} +func (e *testEntry) Debugf(format string, args ...any) { e.msg = format } +func (e *testEntry) Infof(format string, args ...any) { e.msg = format } +func (e *testEntry) Warnf(format string, args ...any) { e.msg = format } +func (e *testEntry) Errorf(format string, args ...any) { e.msg = format } +func (e *testEntry) Fatalf(format string, args ...any) { e.msg = format } +func (e *testEntry) Printf(format string, args ...any) { e.msg = format } + +func (*testEntry) Debug(args ...any) {} +func (*testEntry) Info(args ...any) {} +func (*testEntry) Warn(args ...any) {} +func (*testEntry) Error(args ...any) {} +func (*testEntry) Fatal(args ...any) {} + +func (*testEntry) WithFields(fields map[string]any) log.Entry { return nil } +func (*testEntry) SystemErr(err error) {} +func (*testEntry) WithField(key string, value any) log.Entry { return nil } +func (*testEntry) WithError(err error) log.Entry { return nil } +func (*testEntry) WithContext(ctx context.Context) log.Entry { return nil } +func (*testEntry) WriterLevel(level slog.Level) *io.PipeWriter { return nil } func Test_copyContextWithCustomTimeout(t *testing.T) { testEntry := &testEntry{} diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go index b2378b19d..7894bd46a 100644 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -3,10 +3,9 @@ package errors import ( "errors" "fmt" + "log/slog" "net/http" "runtime" - - "github.com/sirupsen/logrus" ) // Kind enums. @@ -37,7 +36,7 @@ type Error struct { Module M Version V Err error - Severity logrus.Level + Severity slog.Level } // Error returns the underlying error's @@ -111,7 +110,7 @@ func E(op Op, args ...any) error { e.Module = a case V: e.Version = a - case logrus.Level: + case slog.Level: e.Severity = a case int: e.Kind = a @@ -126,17 +125,15 @@ func E(op Op, args ...any) error { // Severity returns the log level of an error // if none exists, then the level is Error because // it is an unexpected. -func Severity(err error) logrus.Level { +func Severity(err error) slog.Level { var e Error if !errors.As(err, &e) { - return logrus.ErrorLevel + return slog.LevelError } - // if there's no severity (0 is Panic level in logrus - // which we should not use since cloud providers only have - // debug, info, warn, and error) then look for the + // if there's no severity then look for the // child's severity. - if e.Severity < logrus.ErrorLevel { + if e.Severity < slog.LevelError { return Severity(e.Err) } @@ -146,13 +143,13 @@ func Severity(err error) logrus.Level { // Expect is a helper that returns an Info level // if the error has the expected kind, otherwise // it returns an Error level. -func Expect(err error, kinds ...int) logrus.Level { +func Expect(err error, kinds ...int) slog.Level { for _, kind := range kinds { if Kind(err) == kind { - return logrus.InfoLevel + return slog.LevelInfo } } - return logrus.ErrorLevel + return slog.LevelError } // Kind recursively searches for the diff --git a/pkg/errors/errors_test.go b/pkg/errors/errors_test.go index b3c43b441..e1a0dee7c 100644 --- a/pkg/errors/errors_test.go +++ b/pkg/errors/errors_test.go @@ -2,10 +2,10 @@ package errors import ( "errors" + "log/slog" "net/http" "testing" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) @@ -35,16 +35,16 @@ func TestSeverity(t *testing.T) { msg := "test error" err := E(op, msg) - require.Equal(t, logrus.ErrorLevel, Severity(err)) + require.Equal(t, slog.LevelError, Severity(err)) - err = E(op, msg, logrus.WarnLevel) - require.Equal(t, logrus.WarnLevel, Severity(err)) + err = E(op, msg, slog.LevelWarn) + require.Equal(t, slog.LevelWarn, Severity(err)) err = E(op, err) - require.Equal(t, logrus.WarnLevel, Severity(err)) + require.Equal(t, slog.LevelWarn, Severity(err)) - err = E(op, err, logrus.InfoLevel) - require.Equal(t, logrus.InfoLevel, Severity(err)) + err = E(op, err, slog.LevelInfo) + require.Equal(t, slog.LevelInfo, Severity(err)) } func TestKind(t *testing.T) { @@ -85,14 +85,14 @@ func TestExpect(t *testing.T) { err := E("TestExpect", "error message", KindBadRequest) severity := Expect(err, KindBadRequest) - require.Equalf(t, severity, logrus.InfoLevel, "expected an info level log but got %v", severity) + require.Equalf(t, severity, slog.LevelInfo, "expected an info level log but got %v", severity) severity = Expect(err, KindAlreadyExists) - require.Equalf(t, severity, logrus.ErrorLevel, "expected an error level but got %v", severity) + require.Equalf(t, severity, slog.LevelError, "expected an error level but got %v", severity) severity = Expect(err, KindAlreadyExists, KindBadRequest) - require.Equalf(t, severity, logrus.InfoLevel, "expected an info level log but got %v", severity) + require.Equalf(t, severity, slog.LevelInfo, "expected an info level log but got %v", severity) severity = Expect(err, KindAlreadyExists, KindNotImplemented) - require.Equalf(t, severity, logrus.ErrorLevel, "expected an error level but got %v", severity) + require.Equalf(t, severity, slog.LevelError, "expected an error level but got %v", severity) } diff --git a/pkg/log/entry.go b/pkg/log/entry.go index da0869803..144b79fef 100644 --- a/pkg/log/entry.go +++ b/pkg/log/entry.go @@ -1,66 +1,234 @@ package log import ( + "bufio" + "context" + "fmt" + "io" + "log/slog" + "os" + "github.com/gomods/athens/pkg/errors" - "github.com/sirupsen/logrus" ) +// LogOps handles basic logging operations. +type LogOps interface { + // Debug logs a debug message. + Debug(args ...interface{}) + + // Info logs an info message. + Info(args ...interface{}) + + // Warn logs a warning message. + Warn(args ...interface{}) + + // Error logs an error message. + Error(args ...interface{}) + + // Fatal logs a fatal message and terminates the program. + Fatal(args ...interface{}) +} + +// FormattedLogOps is an extension of LogOps that supports formatted logging. +type FormattedLogOps interface { + // Debugf logs a debug message with formatting. + Debugf(format string, args ...interface{}) + + // Infof logs an info message with formatting. + Infof(format string, args ...interface{}) + + // Warnf logs a warning message with formatting. + Warnf(format string, args ...interface{}) + + // Errorf logs an error message with formatting. + Errorf(format string, args ...interface{}) + + // Fatalf logs a fatal message with formatting and terminates the program. + Fatalf(format string, args ...interface{}) + + // Printf logs a message with formatting. + Printf(format string, args ...interface{}) +} + +// ContextualLogOps is a contextual logger that can be used to log messages with additional fields. +type ContextualLogOps interface { + // WithFields returns a new Entry with the provided fields added. + WithFields(fields map[string]any) Entry + + // WithField returns a new Entry with a single field added. + WithField(key string, value any) Entry + + // WithError returns a new Entry with the error added to the fields. + WithError(err error) Entry + + // WithContext returns a new Entry with the context added to the fields. + WithContext(ctx context.Context) Entry +} + +// SystemLogger is an interface to handle system errors. +type SystemLogger interface { + // SystemErr handles system errors with appropriate logging levels. + SystemErr(err error) + + // WriterLevel returns an io.PipeWriter for the specified logging level. + WriterLevel(level slog.Level) *io.PipeWriter +} + // Entry is an abstraction to the -// Logger and the logrus.Entry +// Logger and the slog.Entry // so that *Logger always creates // an Entry copy which ensures no // Fields are being overwritten. type Entry interface { - // Basic Logging Operation - Debugf(format string, args ...any) - Infof(format string, args ...any) - Warnf(format string, args ...any) - Errorf(format string, args ...any) - - // Attach contextual information to the logging entry - WithFields(fields map[string]any) Entry - - // SystemErr is a method that disects the error - // and logs the appropriate level and fields for it. - SystemErr(err error) + LogOps + FormattedLogOps + ContextualLogOps + SystemLogger } type entry struct { - *logrus.Entry + logger *slog.Logger } func (e *entry) WithFields(fields map[string]any) Entry { - ent := e.Entry.WithFields(fields) - return &entry{ent} + attrs := make([]any, 0, len(fields)*2) + for k, v := range fields { + attrs = append(attrs, slog.Any(k, v)) + } + return &entry{logger: e.logger.With(attrs...)} +} + +func (e *entry) WithField(key string, value any) Entry { + return &entry{logger: e.logger.With(key, value)} +} + +func (e *entry) WithError(err error) Entry { + return &entry{logger: e.logger.With("error", err)} +} + +func (e *entry) WithContext(ctx context.Context) Entry { + return &entry{logger: e.logger.With("context", ctx)} } func (e *entry) SystemErr(err error) { var athensErr errors.Error if !errors.AsErr(err, &athensErr) { - e.Error(err) + e.Error(err.Error()) return } ent := e.WithFields(errFields(athensErr)) switch errors.Severity(err) { - case logrus.WarnLevel: + case slog.LevelWarn: ent.Warnf("%v", err) - case logrus.InfoLevel: + case slog.LevelInfo: ent.Infof("%v", err) - case logrus.DebugLevel: + case slog.LevelDebug: ent.Debugf("%v", err) default: ent.Errorf("%v", err) } } -func errFields(err errors.Error) logrus.Fields { - f := logrus.Fields{} - f["operation"] = err.Op - f["kind"] = errors.KindText(err) - f["module"] = err.Module - f["version"] = err.Version - f["ops"] = errors.Ops(err) +func (e *entry) Debug(args ...interface{}) { + e.logger.Debug(fmt.Sprint(args...)) +} +func (e *entry) Info(args ...interface{}) { + e.logger.Info(fmt.Sprint(args...)) +} + +func (e *entry) Warn(args ...interface{}) { + e.logger.Warn(fmt.Sprint(args...)) +} + +func (e *entry) Error(args ...interface{}) { + e.logger.Error(fmt.Sprint(args...)) +} + +func (e *entry) Fatal(args ...interface{}) { + e.logger.Error(fmt.Sprint(args...)) + os.Exit(1) +} + +func (e *entry) Panic(args ...interface{}) { + e.logger.Error(fmt.Sprint(args...)) +} + +func (e *entry) Print(args ...interface{}) { + e.logger.Info(fmt.Sprint(args...)) +} + +func (e *entry) Debugf(format string, args ...interface{}) { + e.logger.Debug(fmt.Sprintf(format, args...)) +} + +func (e *entry) Infof(format string, args ...interface{}) { + e.logger.Info(fmt.Sprintf(format, args...)) +} + +func (e *entry) Warnf(format string, args ...interface{}) { + e.logger.Warn(fmt.Sprintf(format, args...)) +} + +func (e *entry) Errorf(format string, args ...interface{}) { + e.logger.Error(fmt.Sprintf(format, args...)) +} + +func (e *entry) Fatalf(format string, args ...interface{}) { + e.logger.Error(fmt.Sprintf(format, args...)) + os.Exit(1) +} + +func (e *entry) Panicf(format string, args ...interface{}) { + e.logger.Error(fmt.Sprintf(format, args...)) // slog doesn't have Panic +} + +func (e *entry) Printf(format string, args ...interface{}) { + e.logger.Info(fmt.Sprintf(format, args...)) +} + +func (e *entry) WriterLevel(level slog.Level) *io.PipeWriter { + reader, writer := io.Pipe() + + var logFunc func(args ...interface{}) + + // Determine which log function to use based on the specified log level + switch level { + case slog.LevelDebug: + logFunc = e.Debug + case slog.LevelInfo: + logFunc = e.Print + case slog.LevelWarn: + logFunc = e.Warn + case slog.LevelError: + logFunc = e.Error + default: + logFunc = e.Print + } + + // Start a new goroutine to scan and write to logger + go func(r *io.PipeReader, logFn func(...interface{})) { + scanner := bufio.NewScanner(r) + scanner.Buffer(make([]byte, 65536), 65536) + for scanner.Scan() { + logFn(scanner.Text()) + } + err := r.Close() + if err != nil { + e.Error(err) + } + }(reader, logFunc) + + return writer +} + +func errFields(err errors.Error) map[string]any { + f := map[string]any{ + "kind": errors.KindText(err), + "module": err.Module, + "version": err.Version, + "ops": errors.Ops(err), + } return f } diff --git a/pkg/log/format.go b/pkg/log/format.go index 4db34ae19..33ff21589 100644 --- a/pkg/log/format.go +++ b/pkg/log/format.go @@ -1,73 +1,72 @@ package log import ( - "bytes" - "fmt" - "sort" - "strings" + "io" + "log/slog" + "os" "time" "github.com/fatih/color" - "github.com/sirupsen/logrus" ) -func getGCPFormatter() logrus.Formatter { - return &logrus.JSONFormatter{ - FieldMap: logrus.FieldMap{ - logrus.FieldKeyLevel: "severity", - logrus.FieldKeyMsg: "message", - logrus.FieldKeyTime: "timestamp", +func getGCPFormatter(level slog.Level, w io.Writer) *slog.Logger { + return slog.New(slog.NewJSONHandler(w, &slog.HandlerOptions{ + Level: level, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + switch a.Key { + case slog.LevelKey: + return slog.String("severity", a.Value.String()) + case slog.MessageKey: + return slog.String("message", a.Value.String()) + case slog.TimeKey: + return slog.String("timestamp", a.Value.Time().Format(time.RFC3339)) + default: + return a + } }, - } -} - -func getDevFormatter() logrus.Formatter { - return devFormatter{} + })) } -type devFormatter struct{} - const lightGrey = 0xffccc -func (devFormatter) Format(e *logrus.Entry) ([]byte, error) { - var buf bytes.Buffer - var sprintf func(format string, a ...any) string - switch e.Level { - case logrus.DebugLevel: - sprintf = color.New(lightGrey).Sprintf - case logrus.WarnLevel: - sprintf = color.YellowString - case logrus.ErrorLevel: - sprintf = color.RedString - default: - sprintf = color.CyanString - } - lvl := strings.ToUpper(e.Level.String()) - buf.WriteString(sprintf(lvl)) - buf.WriteString("[" + e.Time.Format(time.Kitchen) + "]") - buf.WriteString(": ") - buf.WriteString(e.Message) - buf.WriteByte('\t') - for _, k := range sortFields(e.Data) { - fmt.Fprintf(&buf, "%s=%s ", color.MagentaString(k), e.Data[k]) - } - buf.WriteByte('\n') - return buf.Bytes(), nil -} - -func sortFields(data logrus.Fields) []string { - keys := []string{} - for k := range data { - keys = append(keys, k) - } - sort.Strings(keys) - return keys +func getDevFormatter(level slog.Level) *slog.Logger { + return slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: level, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey { + t := a.Value.Time() + return slog.String(slog.TimeKey, t.Format(time.Kitchen)) + } + if a.Key == slog.LevelKey { + level := a.Value.Any().(slog.Level) + var colored string + switch level { + case slog.LevelDebug: + colored = color.New(lightGrey).Sprint(level) + case slog.LevelWarn: + colored = color.YellowString(level.String()) + case slog.LevelError: + colored = color.RedString(level.String()) + default: + colored = color.CyanString(level.String()) + } + return slog.String(slog.LevelKey, colored) + } + if len(groups) == 0 { + return slog.Attr{ + Key: color.MagentaString(a.Key), + Value: a.Value, + } + } + return a + }, + })) } -func parseFormat(format string) logrus.Formatter { +func parseFormat(format string, level slog.Level, w io.Writer) *slog.Logger { if format == "json" { - return &logrus.JSONFormatter{} + return slog.New(slog.NewJSONHandler(w, &slog.HandlerOptions{Level: level})) } - return getDevFormatter() + return getDevFormatter(level) } diff --git a/pkg/log/log.go b/pkg/log/log.go index 8013d4b74..3cb6f2317 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -1,47 +1,95 @@ package log import ( - "github.com/sirupsen/logrus" + "bufio" + "context" + "fmt" + "io" + "log/slog" + "os" ) // Logger is the main struct that any athens // internal service should use to communicate things. type Logger struct { - *logrus.Logger + *slog.Logger } // New constructs a new logger based on the // environment and the cloud platform it is // running on. TODO: take cloud arg and env // to construct the correct JSON formatter. -func New(cloudProvider string, level logrus.Level, format string) *Logger { - l := logrus.New() +func New(cloudProvider string, level slog.Level, format string, w io.Writer) *Logger { + var l *slog.Logger switch cloudProvider { case "GCP": - l.Formatter = getGCPFormatter() + l = getGCPFormatter(level, w) default: - l.Formatter = parseFormat(format) + l = parseFormat(format, level, w) } - l.Level = level + if l == nil { + l = slog.New(slog.NewTextHandler(w, &slog.HandlerOptions{Level: level})) + } + slog.SetDefault(l) return &Logger{Logger: l} } // SystemErr Entry implementation. func (l *Logger) SystemErr(err error) { - e := &entry{Entry: logrus.NewEntry(l.Logger)} + e := &entry{l.Logger} e.SystemErr(err) } // WithFields Entry implementation. func (l *Logger) WithFields(fields map[string]any) Entry { - e := l.Logger.WithFields(fields) + attrs := make([]any, 0, len(fields)) + for k, v := range fields { + attrs = append(attrs, slog.Any(k, v)) + } + return &entry{logger: l.Logger.With(attrs...)} +} + +func (l *Logger) WithField(key string, value any) Entry { + keys := map[string]any{ + key: value, + } + return l.WithFields(keys) +} + +func (l *Logger) WithError(err error) Entry { + keys := map[string]any{ + "error": err, + } + return l.WithFields(keys) +} + +func (l *Logger) WithContext(ctx context.Context) Entry { + keys := map[string]any{ + "context": ctx, + } + return l.WithFields(keys) +} + +func (l *Logger) WriterLevel(level slog.Level) *io.PipeWriter { + pipeReader, pipeWriter := io.Pipe() + go func() { + scanner := bufio.NewScanner(pipeReader) + for scanner. + Scan() { + l.Info(scanner.Text()) + } + }() + return pipeWriter +} - return &entry{e} +func (l *Logger) Fatal(args ...any) { + l.Logger.Error(fmt.Sprint(args...)) + os.Exit(1) } // NoOpLogger provides a Logger that does nothing. func NoOpLogger() *Logger { return &Logger{ - Logger: &logrus.Logger{}, + Logger: slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})), } } diff --git a/pkg/log/log_context.go b/pkg/log/log_context.go index fd36069de..54a65ddf3 100644 --- a/pkg/log/log_context.go +++ b/pkg/log/log_context.go @@ -19,7 +19,7 @@ func SetEntryInContext(ctx context.Context, e Entry) context.Context { func EntryFromContext(ctx context.Context) Entry { e, ok := ctx.Value(logEntryKey).(Entry) if !ok || e == nil { - return NoOpLogger() + return &entry{NoOpLogger().Logger} } return e } diff --git a/pkg/log/log_test.go b/pkg/log/log_test.go index 2639c7a12..7172c127c 100644 --- a/pkg/log/log_test.go +++ b/pkg/log/log_test.go @@ -3,11 +3,11 @@ package log import ( "bytes" "fmt" + "log/slog" "strings" "testing" "time" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" ) @@ -15,8 +15,8 @@ type input struct { name string cloudProvider string format string - level logrus.Level - fields logrus.Fields + level slog.Level + fields map[string]any logFunc func(e Entry) time.Time output string } @@ -25,8 +25,8 @@ var testCases = []input{ { name: "gcp_debug", cloudProvider: "GCP", - level: logrus.DebugLevel, - fields: logrus.Fields{}, + level: slog.LevelDebug, + fields: map[string]any{}, logFunc: func(e Entry) time.Time { t := time.Now() e.Infof("info message") @@ -37,8 +37,8 @@ var testCases = []input{ { name: "gcp_error", cloudProvider: "GCP", - level: logrus.DebugLevel, - fields: logrus.Fields{}, + level: slog.LevelDebug, + fields: map[string]any{}, logFunc: func(e Entry) time.Time { t := time.Now() e.Errorf("err message") @@ -49,8 +49,8 @@ var testCases = []input{ { name: "gcp_empty", cloudProvider: "GCP", - level: logrus.ErrorLevel, - fields: logrus.Fields{}, + level: slog.LevelError, + fields: map[string]any{}, logFunc: func(e Entry) time.Time { t := time.Now() e.Infof("info message") @@ -61,8 +61,8 @@ var testCases = []input{ { name: "gcp_fields", cloudProvider: "GCP", - level: logrus.DebugLevel, - fields: logrus.Fields{"field1": "value1", "field2": 2}, + level: slog.LevelDebug, + fields: map[string]any{"field1": "value1", "field2": 2}, logFunc: func(e Entry) time.Time { t := time.Now() e.Debugf("debug message") @@ -73,8 +73,8 @@ var testCases = []input{ { name: "gcp_logs", cloudProvider: "GCP", - level: logrus.DebugLevel, - fields: logrus.Fields{}, + level: slog.LevelDebug, + fields: map[string]any{}, logFunc: func(e Entry) time.Time { t := time.Now() e.Warnf("warn message") @@ -86,8 +86,8 @@ var testCases = []input{ name: "default plain", format: "plain", cloudProvider: "none", - level: logrus.DebugLevel, - fields: logrus.Fields{"xyz": "abc", "abc": "xyz"}, + level: slog.LevelDebug, + fields: map[string]any{"xyz": "abc", "abc": "xyz"}, logFunc: func(e Entry) time.Time { t := time.Now() e.Warnf("warn message") @@ -98,8 +98,8 @@ var testCases = []input{ { name: "default", cloudProvider: "none", - level: logrus.DebugLevel, - fields: logrus.Fields{"xyz": "abc", "abc": "xyz"}, + level: slog.LevelDebug, + fields: map[string]any{"xyz": "abc", "abc": "xyz"}, logFunc: func(e Entry) time.Time { t := time.Now() e.Warnf("warn message") @@ -111,8 +111,8 @@ var testCases = []input{ name: "default json", format: "json", cloudProvider: "none", - level: logrus.DebugLevel, - fields: logrus.Fields{"xyz": "abc", "abc": "xyz"}, + level: slog.LevelDebug, + fields: map[string]any{"xyz": "abc", "abc": "xyz"}, logFunc: func(e Entry) time.Time { t := time.Now() e.Warnf("warn message") @@ -125,9 +125,8 @@ var testCases = []input{ func TestCloudLogger(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - lggr := New(tc.cloudProvider, tc.level, tc.format) - var buf bytes.Buffer - lggr.Out = &buf + buf := &bytes.Buffer{} + lggr := New(tc.cloudProvider, tc.level, tc.format, buf) e := lggr.WithFields(tc.fields) entryTime := tc.logFunc(e) out := buf.String() @@ -144,8 +143,7 @@ func TestCloudLogger(t *testing.T) { }) } } - func TestNoOpLogger(t *testing.T) { l := NoOpLogger() - require.NotPanics(t, func() { l.Infof("test") }) + require.NotPanics(t, func() { l.Info("test") }) } diff --git a/pkg/middleware/log_entry.go b/pkg/middleware/log_entry.go index 8d6016cf7..980637cae 100644 --- a/pkg/middleware/log_entry.go +++ b/pkg/middleware/log_entry.go @@ -6,7 +6,6 @@ import ( "github.com/gomods/athens/pkg/log" "github.com/gomods/athens/pkg/requestid" "github.com/gorilla/mux" - "github.com/sirupsen/logrus" ) // LogEntryMiddleware builds a log.Entry, setting the request fields @@ -15,7 +14,7 @@ func LogEntryMiddleware(lggr *log.Logger) mux.MiddlewareFunc { return func(h http.Handler) http.Handler { f := func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - ent := lggr.WithFields(logrus.Fields{ + ent := lggr.WithFields(map[string]interface{}{ "http-method": r.Method, "http-path": r.URL.Path, "request-id": requestid.FromContext(ctx), diff --git a/pkg/middleware/log_entry_test.go b/pkg/middleware/log_entry_test.go index 2135748fd..3b8af3414 100644 --- a/pkg/middleware/log_entry_test.go +++ b/pkg/middleware/log_entry_test.go @@ -3,6 +3,7 @@ package middleware import ( "bytes" "fmt" + "log/slog" "net/http" "net/http/httptest" "strings" @@ -10,7 +11,6 @@ import ( "github.com/gomods/athens/pkg/log" "github.com/gorilla/mux" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" ) @@ -23,10 +23,11 @@ func TestLogContext(t *testing.T) { r := mux.NewRouter() r.HandleFunc("/test", h) - var buf bytes.Buffer - lggr := log.New("", logrus.DebugLevel, "") - lggr.Formatter = &logrus.JSONFormatter{DisableTimestamp: true} - lggr.Out = &buf + buf := &bytes.Buffer{} + lggr := log.New("", slog.LevelDebug, "", buf) + opts := slog.HandlerOptions{Level: slog.LevelDebug} + handler := slog.NewJSONHandler(buf, &opts) + lggr.Logger = slog.New(handler) r.Use(LogEntryMiddleware(lggr)) diff --git a/pkg/middleware/request.go b/pkg/middleware/request.go index 1bff12e6d..fb1df4548 100644 --- a/pkg/middleware/request.go +++ b/pkg/middleware/request.go @@ -6,7 +6,6 @@ import ( "github.com/fatih/color" "github.com/gomods/athens/pkg/log" - logrus "github.com/sirupsen/logrus" ) type responseWriter struct { @@ -25,7 +24,7 @@ func RequestLogger(h http.Handler) http.Handler { f := func(w http.ResponseWriter, r *http.Request) { rw := &responseWriter{w, 0} h.ServeHTTP(rw, r) - log.EntryFromContext(r.Context()).WithFields(logrus.Fields{ + log.EntryFromContext(r.Context()).WithFields(map[string]any{ "http-status": fmtResponseCode(rw.statusCode), }).Infof("incoming request") }