Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
113 changes: 58 additions & 55 deletions gologshim/gologshim.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
// Package gologshim provides slog-based logging that integrates with go-log
// when available, without requiring go-log as a dependency.
// Package gologshim provides slog-based logging for go-libp2p that works
// standalone or integrates with go-log for unified log management across
// IPFS/libp2p applications, without adding go-log as a dependency.
//
// Usage:
// # Usage
//
// var log = logging.Logger("subsystem")
// Create loggers using the Logger function:
//
// var log = gologshim.Logger("subsystem")
// log.Debug("message", "key", "value")
//
// When go-log's slog bridge is detected (via duck typing), gologshim
// automatically integrates for unified formatting and dynamic level control.
// Otherwise, it creates standalone slog handlers writing to stderr.
// # Integration with go-log
//
// Applications can optionally connect go-libp2p to go-log by calling SetDefaultHandler:
//
// func init() {
// gologshim.SetDefaultHandler(slog.Default().Handler())
// }
//
// When integrated, go-libp2p logs use go-log's formatting and can be controlled
// programmatically via go-log's SetLogLevel("subsystem", "level") API to adjust
// log verbosity per subsystem at runtime without restarting.
//
// # Standalone Usage
//
// Without calling SetDefaultHandler, gologshim creates standalone slog handlers
// writing to stderr. This mode is useful when go-log is not present or when you
// want independent log configuration via backward-compatible (go-log) environment variables:
//
// Environment variables (see go-log README for full details):
// - GOLOG_LOG_LEVEL: Set log levels per subsystem (e.g., "error,ping=debug")
// - GOLOG_LOG_FORMAT/GOLOG_LOG_FMT: Output format ("json" or text)
// - GOLOG_LOG_ADD_SOURCE: Include source location (default: true)
Expand Down Expand Up @@ -40,49 +56,52 @@ 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))
// Use "logger" attribute for compatibility with go-log's Zap-based format
// and existing IPFS/libp2p tooling and dashboards.
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 +110,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 +140,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