log: fix syncer causing errors

This commit is contained in:
Tigor Hutasuhut 2024-08-27 09:29:42 +07:00
parent 97341ec698
commit 2486702215
3 changed files with 31 additions and 72 deletions

View file

@ -1,7 +1,7 @@
package log package log
import ( import (
"errors" "io"
"log/slog" "log/slog"
"net/http" "net/http"
"os" "os"
@ -11,6 +11,7 @@ import (
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
slogmulti "github.com/samber/slog-multi" slogmulti "github.com/samber/slog-multi"
"github.com/tigorlazuardi/bluemage/go/config" "github.com/tigorlazuardi/bluemage/go/config"
"github.com/tigorlazuardi/bluemage/go/pkg/errs"
"github.com/tigorlazuardi/bluemage/go/pkg/telemetry" "github.com/tigorlazuardi/bluemage/go/pkg/telemetry"
"gopkg.in/natefinch/lumberjack.v2" "gopkg.in/natefinch/lumberjack.v2"
) )
@ -20,8 +21,10 @@ func NewHandler(cfg *config.Config) (slog.Handler, func() error) {
cleanup := func() error { return nil } cleanup := func() error { return nil }
if cfg.Bool("log.enable") { if cfg.Bool("log.enable") {
log, sync := createStandardLogger(cfg) log := createStandardLogger(cfg)
cleanup = sync if closer, ok := log.(io.Closer); ok {
cleanup = closer.Close
}
handlers = append(handlers, log) handlers = append(handlers, log)
} }
@ -29,7 +32,7 @@ func NewHandler(cfg *config.Config) (slog.Handler, func() error) {
log, clean := createFileLogger(cfg) log, clean := createFileLogger(cfg)
cl := cleanup cl := cleanup
cleanup = func() error { cleanup = func() error {
return errors.Join(cl(), clean()) return errs.Join(cl(), clean())
} }
handlers = append(handlers, log) handlers = append(handlers, log)
} }
@ -66,19 +69,16 @@ func createFileLogger(cfg *config.Config) (slog.Handler, func() error) {
AddSource: cfg.Bool("log.source"), AddSource: cfg.Bool("log.source"),
Level: lvl, 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) { func createStandardLogger(cfg *config.Config) slog.Handler {
var output WriteSyncer var output WriteLocker
var cleanup func() error
if strings.ToLower(cfg.String("log.output")) == "stdout" { if strings.ToLower(cfg.String("log.output")) == "stdout" {
output = Lock(AddSync(colorable.NewColorableStdout())) output = Lock(colorable.NewColorableStdout())
cleanup = output.Sync
} else { } else {
output = Lock(AddSync(colorable.NewColorableStderr())) output = Lock(colorable.NewColorableStderr())
cleanup = output.Sync
} }
var lvl slog.Level var lvl slog.Level
@ -89,9 +89,9 @@ func createStandardLogger(cfg *config.Config) (slog.Handler, func() error) {
} }
if isatty.IsTerminal(os.Stdout.Fd()) { if isatty.IsTerminal(os.Stdout.Fd()) {
return NewPrettyHandler(output, opts), cleanup return NewPrettyHandler(output, opts)
} else { } else {
return slog.NewJSONHandler(output, opts), cleanup return slog.NewJSONHandler(output, opts)
} }
} }

View file

@ -17,7 +17,7 @@ import (
type PrettyHandler struct { type PrettyHandler struct {
opts *slog.HandlerOptions opts *slog.HandlerOptions
output WriteSyncer output WriteLocker
replaceAttr func(groups []string, attr slog.Attr) slog.Attr replaceAttr func(groups []string, attr slog.Attr) slog.Attr
withAttrs []slog.Attr withAttrs []slog.Attr
withGroup []string withGroup []string
@ -27,7 +27,7 @@ type PrettyHandler struct {
} }
// NewPrettyHandler creates a human friendly readable logs. // 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 { if opts == nil {
opts = &slog.HandlerOptions{Level: slog.LevelDebug} opts = &slog.HandlerOptions{Level: slog.LevelDebug}
} }
@ -56,24 +56,24 @@ func NewPrettyHandler(writer WriteSyncer, opts *slog.HandlerOptions) *PrettyHand
// and then flush the buffer. // and then flush the buffer.
// //
// Flush blocks other log writes until it's done. // Flush blocks other log writes until it's done.
func (pr *PrettyHandler) Flush() error { func (pr *PrettyHandler) Flush() {
pr.isFlushing.Store(true) pr.isFlushing.Store(true)
defer func() { pr.isFlushing.Store(false) }() defer func() { pr.isFlushing.Store(false) }()
pr.wg.Wait() pr.wg.Wait()
err := pr.output.Sync()
for { for {
select { select {
// signal to handlers that flushing is done. // signal to handlers that flushing is done.
case pr.flushingChan <- struct{}{}: case pr.flushingChan <- struct{}{}:
default: default:
return err return
} }
} }
} }
// Sync attemps to flush the buffer of the underlying writer. // Sync attemps to flush the buffer of the underlying writer.
func (pr *PrettyHandler) Sync() error { func (pr *PrettyHandler) Close() error {
return pr.output.Sync() pr.Flush()
return nil
} }
// Enabled implements slog.Handler interface. // Enabled implements slog.Handler interface.

View file

@ -2,68 +2,27 @@ package log
import ( import (
"io" "io"
"os"
"sync" "sync"
) )
type WriteSyncer interface { type WriteLocker interface {
io.Writer io.Writer
Sync() error sync.Locker
} }
// WrapOsFile wraps an *os.File in a WriteSyncer. type writeLock struct {
//
// 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 {
sync.Mutex sync.Mutex
ws WriteSyncer io.Writer
} }
// Lock wraps a WriteSyncer in a mutex to make it safe for concurrent use. In // Lock wraps a WriteSyncer in a mutex to make it safe for concurrent use. In
// particular, *os.Files must be locked before use. // 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 // no need to layer on another lock
return ws return wl
} }
return &lockedWriteSyncer{ws: ws} return &writeLock{Writer: w}
}
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
} }