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
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)
}
}

View file

@ -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.

View file

@ -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}
}