diff --git a/go/pkg/log/log.go b/go/pkg/log/log.go index 4d955a6..68ec3dc 100644 --- a/go/pkg/log/log.go +++ b/go/pkg/log/log.go @@ -1,7 +1,7 @@ package log import ( - "errors" + "io" "log/slog" "net/http" "os" @@ -11,6 +11,7 @@ import ( "github.com/mattn/go-isatty" slogmulti "github.com/samber/slog-multi" "github.com/tigorlazuardi/bluemage/go/config" + "github.com/tigorlazuardi/bluemage/go/pkg/errs" "github.com/tigorlazuardi/bluemage/go/pkg/telemetry" "gopkg.in/natefinch/lumberjack.v2" ) @@ -20,8 +21,10 @@ func NewHandler(cfg *config.Config) (slog.Handler, func() error) { cleanup := func() error { return nil } if cfg.Bool("log.enable") { - log, sync := createStandardLogger(cfg) - cleanup = sync + log := createStandardLogger(cfg) + if closer, ok := log.(io.Closer); ok { + cleanup = closer.Close + } handlers = append(handlers, log) } @@ -29,7 +32,7 @@ func NewHandler(cfg *config.Config) (slog.Handler, func() error) { log, clean := createFileLogger(cfg) cl := cleanup cleanup = func() error { - return errors.Join(cl(), clean()) + return errs.Join(cl(), clean()) } handlers = append(handlers, log) } @@ -66,19 +69,16 @@ func createFileLogger(cfg *config.Config) (slog.Handler, func() error) { AddSource: cfg.Bool("log.source"), Level: lvl, } - return slog.NewJSONHandler(Lock(AddSync(output)), opts), output.Close + return slog.NewJSONHandler(Lock(output), opts), output.Close } -func createStandardLogger(cfg *config.Config) (slog.Handler, func() error) { - var output WriteSyncer - var cleanup func() error +func createStandardLogger(cfg *config.Config) slog.Handler { + var output WriteLocker if strings.ToLower(cfg.String("log.output")) == "stdout" { - output = Lock(AddSync(colorable.NewColorableStdout())) - cleanup = output.Sync + output = Lock(colorable.NewColorableStdout()) } else { - output = Lock(AddSync(colorable.NewColorableStderr())) - cleanup = output.Sync + output = Lock(colorable.NewColorableStderr()) } var lvl slog.Level @@ -89,9 +89,9 @@ func createStandardLogger(cfg *config.Config) (slog.Handler, func() error) { } if isatty.IsTerminal(os.Stdout.Fd()) { - return NewPrettyHandler(output, opts), cleanup + return NewPrettyHandler(output, opts) } else { - return slog.NewJSONHandler(output, opts), cleanup + return slog.NewJSONHandler(output, opts) } } diff --git a/go/pkg/log/pretty.go b/go/pkg/log/pretty.go index 6b19a4f..6ad4391 100644 --- a/go/pkg/log/pretty.go +++ b/go/pkg/log/pretty.go @@ -17,7 +17,7 @@ import ( type PrettyHandler struct { opts *slog.HandlerOptions - output WriteSyncer + output WriteLocker replaceAttr func(groups []string, attr slog.Attr) slog.Attr withAttrs []slog.Attr withGroup []string @@ -27,7 +27,7 @@ type PrettyHandler struct { } // NewPrettyHandler creates a human friendly readable logs. -func NewPrettyHandler(writer WriteSyncer, opts *slog.HandlerOptions) *PrettyHandler { +func NewPrettyHandler(writer WriteLocker, opts *slog.HandlerOptions) *PrettyHandler { if opts == nil { opts = &slog.HandlerOptions{Level: slog.LevelDebug} } @@ -56,24 +56,24 @@ func NewPrettyHandler(writer WriteSyncer, opts *slog.HandlerOptions) *PrettyHand // and then flush the buffer. // // Flush blocks other log writes until it's done. -func (pr *PrettyHandler) Flush() error { +func (pr *PrettyHandler) Flush() { pr.isFlushing.Store(true) defer func() { pr.isFlushing.Store(false) }() pr.wg.Wait() - err := pr.output.Sync() for { select { // signal to handlers that flushing is done. case pr.flushingChan <- struct{}{}: default: - return err + return } } } // Sync attemps to flush the buffer of the underlying writer. -func (pr *PrettyHandler) Sync() error { - return pr.output.Sync() +func (pr *PrettyHandler) Close() error { + pr.Flush() + return nil } // Enabled implements slog.Handler interface. diff --git a/go/pkg/log/writer.go b/go/pkg/log/writer.go index f21bd60..6bc7e3c 100644 --- a/go/pkg/log/writer.go +++ b/go/pkg/log/writer.go @@ -2,68 +2,27 @@ package log import ( "io" - "os" "sync" ) -type WriteSyncer interface { +type WriteLocker interface { io.Writer - Sync() error + sync.Locker } -// WrapOsFile wraps an *os.File in a WriteSyncer. -// -// To support multithreaded logging and to support -// flushing the buffer before the program exits. -func WrapOsFile(f *os.File) WriteSyncer { - return Lock(f) -} - -// AddSync converts an io.Writer to a WriteSyncer. It attempts to be -// intelligent: if the concrete type of the io.Writer implements WriteSyncer, -// we'll use the existing Sync method. If it doesn't, we'll add a no-op Sync. -func AddSync(w io.Writer) WriteSyncer { - switch w := w.(type) { - case WriteSyncer: - return w - default: - return writerWrapper{w} - } -} - -type lockedWriteSyncer struct { +type writeLock struct { sync.Mutex - ws WriteSyncer + io.Writer } // Lock wraps a WriteSyncer in a mutex to make it safe for concurrent use. In // particular, *os.Files must be locked before use. -func Lock(ws WriteSyncer) WriteSyncer { - if _, ok := ws.(*lockedWriteSyncer); ok { +// +// If w is already a WriteLocker, it will be returned as is. +func Lock(w io.Writer) WriteLocker { + if wl, ok := w.(WriteLocker); ok { // no need to layer on another lock - return ws + return wl } - return &lockedWriteSyncer{ws: ws} -} - -func (s *lockedWriteSyncer) Write(bs []byte) (int, error) { - s.Lock() - n, err := s.ws.Write(bs) - s.Unlock() - return n, err -} - -func (s *lockedWriteSyncer) Sync() error { - s.Lock() - err := s.ws.Sync() - s.Unlock() - return err -} - -type writerWrapper struct { - io.Writer -} - -func (w writerWrapper) Sync() error { - return nil + return &writeLock{Writer: w} }