diff --git a/cmd/proxy/actions/basicauth_test.go b/cmd/proxy/actions/basicauth_test.go index 1f02f84fc..aa5742517 100644 --- a/cmd/proxy/actions/basicauth_test.go +++ b/cmd/proxy/actions/basicauth_test.go @@ -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", slog.LevelDebug, "") 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/main.go b/cmd/proxy/main.go index e56547c4d..e3cf95c5d 100644 --- a/cmd/proxy/main.go +++ b/cmd/proxy/main.go @@ -43,16 +43,16 @@ func main() { 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 slog Errors. - logrusErrorWriter := logger.WriterLevel(slog.LevelError) + slogErrorWriter := logger.WriterLevel(slog.LevelError) defer func() { - if err := logrusErrorWriter.Close(); err != nil { + if err := slogErrorWriter.Close(); err != nil { logger.WithError(err).Warn("Could not close logrus 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/pkg/log/entry.go b/pkg/log/entry.go index 2c9bd9190..09347a3f7 100644 --- a/pkg/log/entry.go +++ b/pkg/log/entry.go @@ -17,28 +17,64 @@ import ( // an Entry copy which ensures no // Fields are being overwritten. type Entry interface { - // Keep the existing interface methods unchanged + // Debugf logs a debug message with formatting Debugf(string, ...interface{}) + + // Infof logs an info message with formatting Infof(string, ...interface{}) + + // Warnf logs a warning message with formatting Warnf(string, ...interface{}) + + // Errorf logs an error message with formatting Errorf(string, ...interface{}) + + // Fatalf logs a fatal message with formatting and terminates the program Fatalf(string, ...interface{}) + + // Panicf logs a panic message with formatting and panics Panicf(string, ...interface{}) + + // Printf logs a message with formatting at default level Printf(string, ...interface{}) + // Debug logs a debug message Debug(...interface{}) + + // Info logs an info message Info(...interface{}) + + // Warn logs a warning message Warn(...interface{}) + + // Error logs an error message Error(...interface{}) + + // Fatal logs a fatal message and terminates the program Fatal(...interface{}) + + // Panic logs a panic message and panics Panic(...interface{}) + + // Print logs a message at default level Print(...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 + + // 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 } diff --git a/pkg/log/format.go b/pkg/log/format.go index 56f1a226b..b94f1292a 100644 --- a/pkg/log/format.go +++ b/pkg/log/format.go @@ -1,6 +1,7 @@ package log import ( + "io" "log/slog" "os" "sort" @@ -9,8 +10,8 @@ import ( "github.com/fatih/color" ) -func getGCPFormatter(level slog.Level) *slog.Logger { - return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ +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 { @@ -78,9 +79,9 @@ func sortFields(data map[string]any) []string { return keys } -func parseFormat(format string, level slog.Level) *slog.Logger { +func parseFormat(format string, level slog.Level, w io.Writer) *slog.Logger { if format == "json" { - return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level})) + return slog.New(slog.NewJSONHandler(w, &slog.HandlerOptions{Level: level})) } return getDevFormatter(level) diff --git a/pkg/log/log.go b/pkg/log/log.go index de35ea089..a39384efc 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "context" + "fmt" "io" "log/slog" "os" @@ -21,13 +22,16 @@ type Logger struct { // 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 slog.Level, format string) *Logger { - l := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: level})) +func New(cloudProvider string, level slog.Level, format string, w io.Writer) *Logger { + var l *slog.Logger switch cloudProvider { case "GCP": - l = getGCPFormatter(level) + l = getGCPFormatter(level, w) default: - l = parseFormat(format, level) + l = parseFormat(format, level, w) + } + if l == nil { + l = slog.New(slog.NewTextHandler(w, &slog.HandlerOptions{Level: level})) } slog.SetDefault(l) return &Logger{Logger: l} @@ -69,7 +73,6 @@ func (l *Logger) WithContext(ctx context.Context) Entry { return l.WithFields(keys) } -// Define WriterLevel func (l *Logger) WriterLevel(level slog.Level) *io.PipeWriter { pipeReader, pipeWriter := io.Pipe() go func() { @@ -82,6 +85,11 @@ func (l *Logger) WriterLevel(level slog.Level) *io.PipeWriter { return pipeWriter } +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{ diff --git a/pkg/log/log_test.go b/pkg/log/log_test.go index 3b36c2ac6..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" - "log/slog" - + "github.com/stretchr/testify/require" ) @@ -87,7 +87,7 @@ var testCases = []input{ format: "plain", cloudProvider: "none", level: slog.LevelDebug, - fields: map[string]any{"xyz": "abc", "abc": "xyz"}, + fields: map[string]any{"xyz": "abc", "abc": "xyz"}, logFunc: func(e Entry) time.Time { t := time.Now() e.Warnf("warn message") @@ -112,7 +112,7 @@ var testCases = []input{ format: "json", cloudProvider: "none", level: slog.LevelDebug, - fields: map[string]any{"xyz": "abc", "abc": "xyz"}, + 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,7 +143,6 @@ func TestCloudLogger(t *testing.T) { }) } } - func TestNoOpLogger(t *testing.T) { l := NoOpLogger() require.NotPanics(t, func() { l.Info("test") }) diff --git a/pkg/middleware/log_entry_test.go b/pkg/middleware/log_entry_test.go index 1b7b4e9a2..3b8af3414 100644 --- a/pkg/middleware/log_entry_test.go +++ b/pkg/middleware/log_entry_test.go @@ -24,7 +24,7 @@ func TestLogContext(t *testing.T) { r.HandleFunc("/test", h) buf := &bytes.Buffer{} - lggr := log.New("", slog.LevelDebug, "") + lggr := log.New("", slog.LevelDebug, "", buf) opts := slog.HandlerOptions{Level: slog.LevelDebug} handler := slog.NewJSONHandler(buf, &opts) lggr.Logger = slog.New(handler)