Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 32 additions & 47 deletions gologshim/gologshim.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,49 +40,50 @@ var lvlToLower = map[slog.Level]slog.Value{
slog.LevelError: slog.StringValue("error"),
}

// goLogBridge detects go-log's slog bridge via duck typing, without import dependency
type goLogBridge interface {
GoLogBridge()
var defaultHandler atomic.Pointer[slog.Handler]

// SetDefaultHandler allows an application to change the underlying handler used
// by gologshim as long as it's changed *before* the first log by the logger.
func SetDefaultHandler(handler slog.Handler) {
defaultHandler.Store(&handler)
}

// lazyBridgeHandler delays bridge detection until first log call to handle init order issues
type lazyBridgeHandler struct {
// dynamicHandler delays bridge detection until first log call to handle init order issues
type dynamicHandler struct {
system string
config *Config
once sync.Once
handler atomic.Pointer[slog.Handler]
handler slog.Handler
}

func (h *lazyBridgeHandler) ensureHandler() slog.Handler {
if handler := h.handler.Load(); handler != nil {
return *handler
}

func (h *dynamicHandler) ensureHandler() slog.Handler {
h.once.Do(func() {
var handler slog.Handler
if bridge, ok := slog.Default().Handler().(goLogBridge); ok {
attrs := make([]slog.Attr, 0, 1+len(h.config.labels))
attrs = append(attrs, slog.String("logger", h.system))
attrs = append(attrs, h.config.labels...)
handler = bridge.(slog.Handler).WithAttrs(attrs)
if hPtr := defaultHandler.Load(); hPtr != nil {
h.handler = *hPtr
} else {
handler = h.createFallbackHandler()
h.handler = h.createFallbackHandler()
}
h.handler.Store(&handler)
attrs := make([]slog.Attr, 0, 1+len(h.config.labels))
attrs = append(attrs, slog.String("logger", h.system))
attrs = append(attrs, h.config.labels...)
h.handler = h.handler.WithAttrs(attrs)
})

return *h.handler.Load()
return h.handler
}

func (h *lazyBridgeHandler) createFallbackHandler() slog.Handler {
func (h *dynamicHandler) createFallbackHandler() slog.Handler {
opts := &slog.HandlerOptions{
Level: h.config.LevelForSystem(h.system),
AddSource: h.config.addSource,
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
switch a.Key {
case slog.TimeKey:
// ipfs go-log uses "ts" for time
a.Key = "ts"
} else if a.Key == slog.LevelKey {
case slog.LevelKey:
if lvl, ok := a.Value.Any().(slog.Level); ok {
// ipfs go-log uses lowercase level names
if s, ok := lvlToLower[lvl]; ok {
a.Value = s
}
Expand All @@ -91,31 +92,26 @@ func (h *lazyBridgeHandler) createFallbackHandler() slog.Handler {
return a
},
}
var handler slog.Handler
if h.config.format == logFormatText {
handler = slog.NewTextHandler(os.Stderr, opts)
} else {
handler = slog.NewJSONHandler(os.Stderr, opts)
return slog.NewTextHandler(os.Stderr, opts)
}
attrs := make([]slog.Attr, 1+len(h.config.labels))
attrs[0] = slog.String("logger", h.system)
copy(attrs[1:], h.config.labels)
return handler.WithAttrs(attrs)

return slog.NewJSONHandler(os.Stderr, opts)
}

func (h *lazyBridgeHandler) Enabled(ctx context.Context, lvl slog.Level) bool {
func (h *dynamicHandler) Enabled(ctx context.Context, lvl slog.Level) bool {
return h.ensureHandler().Enabled(ctx, lvl)
}

func (h *lazyBridgeHandler) Handle(ctx context.Context, r slog.Record) error {
func (h *dynamicHandler) Handle(ctx context.Context, r slog.Record) error {
return h.ensureHandler().Handle(ctx, r)
}

func (h *lazyBridgeHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
func (h *dynamicHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return h.ensureHandler().WithAttrs(attrs)
}

func (h *lazyBridgeHandler) WithGroup(name string) slog.Handler {
func (h *dynamicHandler) WithGroup(name string) slog.Handler {
return h.ensureHandler().WithGroup(name)
}

Expand All @@ -126,18 +122,7 @@ func (h *lazyBridgeHandler) WithGroup(name string) slog.Handler {
// fallback level to warn.
func Logger(system string) *slog.Logger {
c := ConfigFromEnv()

// If go-log bridge available, use it immediately
if _, ok := slog.Default().Handler().(goLogBridge); ok {
attrs := make([]slog.Attr, 0, 1+len(c.labels))
attrs = append(attrs, slog.String("logger", system))
attrs = append(attrs, c.labels...)
h := slog.Default().Handler().WithAttrs(attrs)
return slog.New(h)
}

// Use lazy handler for init order issues
return slog.New(&lazyBridgeHandler{
return slog.New(&dynamicHandler{
system: system,
config: c,
})
Expand Down
14 changes: 10 additions & 4 deletions gologshim/gologshim_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@ import (
"log/slog"
"os"
"strings"
"sync"
"testing"
)

// mockBridge simulates go-log's slog bridge for testing duck typing detection
type mockBridge struct {
sync.Mutex
slog.Handler
logs *bytes.Buffer
}

func (m *mockBridge) GoLogBridge() {}

func (m *mockBridge) Handle(_ context.Context, r slog.Record) error {
m.Lock()
defer m.Unlock()
m.logs.WriteString(r.Message)
m.logs.WriteString("\n")
return nil
Expand All @@ -44,7 +46,7 @@ func TestGoLogBridgeDetection(t *testing.T) {
Handler: slog.NewTextHandler(&buf, nil),
logs: &buf,
}
slog.SetDefault(slog.New(bridge))
SetDefaultHandler(bridge)

// Create logger - should detect bridge
log := Logger("test-subsystem")
Expand Down Expand Up @@ -92,12 +94,16 @@ func TestLazyBridgeInitialization(t *testing.T) {
Handler: slog.NewTextHandler(&bridgeBuf, nil),
logs: &bridgeBuf,
}
slog.SetDefault(slog.New(bridge))
SetDefaultHandler(bridge)

// Log in goroutine to detect races
go log.Info("lazy init message")
// First log call should detect the bridge via lazy initialization
log.Info("lazy init message")

bridge.Lock()
output := bridgeBuf.String()
bridge.Unlock()
if !strings.Contains(output, "lazy init message") {
t.Errorf("Lazy handler should have detected bridge, got: %s", output)
}
Expand Down
Loading