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
141 changes: 116 additions & 25 deletions gologshim/gologshim.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,52 @@
// 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
//
// Create loggers using the Logger function:
//
// var log = gologshim.Logger("subsystem")
// log.Debug("message", "key", "value")
//
// # 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:
//
// - 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)
// - GOLOG_LOG_LABELS: Add key=value labels to all logs
//
// For integration details, see: https://github.com/ipfs/go-log/blob/master/README.md#slog-integration
//
// Note: This package exists as an intermediate solution while go-log uses zap
// internally. If go-log migrates from zap to native slog, this bridge layer
// could be simplified or removed entirely.
package gologshim

import (
"context"
"fmt"
"log/slog"
"os"
"strings"
"sync"
"sync/atomic"
)

var lvlToLower = map[slog.Level]slog.Value{
Expand All @@ -15,26 +56,52 @@ var lvlToLower = map[slog.Level]slog.Value{
slog.LevelError: slog.StringValue("error"),
}

// Logger returns a *slog.Logger with a logging level defined by the
// GOLOG_LOG_LEVEL env var. Supports different levels for different systems. e.g.
// GOLOG_LOG_LEVEL=foo=info,bar=debug,warn
// sets the foo system at level info, the bar system at level debug and the
// fallback level to warn.
//
// Prefer a parameterized logger over a global logger.
func Logger(system string) *slog.Logger {
var h slog.Handler
c := ConfigFromEnv()
handlerOpts := &slog.HandlerOptions{
Level: c.LevelForSystem(system),
AddSource: c.addSource,
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)
}

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

func (h *dynamicHandler) ensureHandler() slog.Handler {
h.once.Do(func() {
if hPtr := defaultHandler.Load(); hPtr != nil {
h.handler = *hPtr
} else {
h.handler = h.createFallbackHandler()
}
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
}

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 {
// ipfs go-log uses lowercase level names
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 @@ -43,16 +110,40 @@ func Logger(system string) *slog.Logger {
return a
},
}
if c.format == logFormatText {
h = slog.NewTextHandler(os.Stderr, handlerOpts)
} else {
h = slog.NewJSONHandler(os.Stderr, handlerOpts)
if h.config.format == logFormatText {
return slog.NewTextHandler(os.Stderr, opts)
}
attrs := make([]slog.Attr, 1+len(c.labels))
attrs = append(attrs, slog.String("logger", system))
attrs = append(attrs, c.labels...)
h = h.WithAttrs(attrs)
return slog.New(h)

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

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

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

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

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

// Logger returns a *slog.Logger with a logging level defined by the
// GOLOG_LOG_LEVEL env var. Supports different levels for different systems. e.g.
// GOLOG_LOG_LEVEL=foo=info,bar=debug,warn
// sets the foo system at level info, the bar system at level debug and the
// fallback level to warn.
func Logger(system string) *slog.Logger {
c := ConfigFromEnv()
return slog.New(&dynamicHandler{
system: system,
config: c,
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, cannot rely on init() order across multiple packages.
👍

}

type logFormat = int
Expand Down
Loading
Loading