diff --git a/gologshim/gologshim.go b/gologshim/gologshim.go index 14d5a0f82b..19c98e6064 100644 --- a/gologshim/gologshim.go +++ b/gologshim/gologshim.go @@ -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) @@ -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 } @@ -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) } @@ -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, }) diff --git a/gologshim/gologshim_test.go b/gologshim/gologshim_test.go index 8bf5274b8f..dec64bfe64 100644 --- a/gologshim/gologshim_test.go +++ b/gologshim/gologshim_test.go @@ -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 @@ -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") @@ -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) }