// 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: // // import golog "github.com/ipfs/go-log/v2" // // func init() { // gologshim.SetDefaultHandler(golog.SlogHandler()) // } // // 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. // // Note: SlogHandler() works even when GOLOG_CAPTURE_DEFAULT_SLOG=false, making // it more reliable than using slog.Default().Handler(). // // # 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 ( ) var lvlToLower = map[slog.Level]slog.Value{ slog.LevelDebug: slog.StringValue("debug"), slog.LevelInfo: slog.StringValue("info"), slog.LevelWarn: slog.StringValue("warn"), slog.LevelError: slog.StringValue("error"), } 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 ( slog.Handler) { defaultHandler.Store(&) } // 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 ( *dynamicHandler) () slog.Handler { .once.Do(func() { if := defaultHandler.Load(); != nil { .handler = * } else { .handler = .createFallbackHandler() } := make([]slog.Attr, 0, 1+len(.config.labels)) // Use "logger" attribute for compatibility with go-log's Zap-based format // and existing IPFS/libp2p tooling and dashboards. = append(, slog.String("logger", .system)) = append(, .config.labels...) .handler = .handler.WithAttrs() }) return .handler } func ( *dynamicHandler) () slog.Handler { := &slog.HandlerOptions{ Level: .config.LevelForSystem(.system), AddSource: .config.addSource, ReplaceAttr: func( []string, slog.Attr) slog.Attr { switch .Key { case slog.TimeKey: // ipfs go-log uses "ts" for time .Key = "ts" case slog.LevelKey: if , := .Value.Any().(slog.Level); { // ipfs go-log uses lowercase level names if , := lvlToLower[]; { .Value = } } } return }, } if .config.format == logFormatText { return slog.NewTextHandler(os.Stderr, ) } return slog.NewJSONHandler(os.Stderr, ) } func ( *dynamicHandler) ( context.Context, slog.Level) bool { return .ensureHandler().Enabled(, ) } func ( *dynamicHandler) ( context.Context, slog.Record) error { return .ensureHandler().Handle(, ) } func ( *dynamicHandler) ( []slog.Attr) slog.Handler { return .ensureHandler().WithAttrs() } func ( *dynamicHandler) ( string) slog.Handler { return .ensureHandler().WithGroup() } // 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 ( string) *slog.Logger { := ConfigFromEnv() return slog.New(&dynamicHandler{ system: , config: , }) } type logFormat = int const ( logFormatText logFormat = iota logFormatJSON ) type Config struct { fallbackLvl slog.Level systemToLevel map[string]slog.Level format logFormat addSource bool labels []slog.Attr } func ( *Config) ( string) slog.Level { if , := .systemToLevel[]; { return } return .fallbackLvl } var ConfigFromEnv func() *Config = sync.OnceValue(func() *Config { , , := parseIPFSGoLogEnv(os.Getenv("GOLOG_LOG_LEVEL")) if != nil { fmt.Fprintf(os.Stderr, "Failed to parse GOLOG_LOG_LEVEL: %v", ) = slog.LevelInfo } := &Config{ fallbackLvl: , systemToLevel: , addSource: true, } := os.Getenv("GOLOG_LOG_FORMAT") if == "" { = os.Getenv("GOLOG_LOG_FMT") } if == "json" { .format = logFormatJSON } = os.Getenv("GOLOG_LOG_ADD_SOURCE") if == "0" || == "false" { .addSource = false } := os.Getenv("GOLOG_LOG_LABELS") if != "" { := strings.Split(, ",") if len() > 0 { for , := range { := strings.SplitN(, "=", 2) if len() == 2 { .labels = append(.labels, slog.String([0], [1])) } else { fmt.Fprintf(os.Stderr, "Invalid label format: %s", ) } } } } return }) func parseIPFSGoLogEnv( string) (slog.Level, map[string]slog.Level, error) { := slog.LevelError var map[string]slog.Level if != "" { for , := range strings.Split(, ",") { := strings.SplitN(, "=", 2) var slog.Level := .UnmarshalText([]byte([len()-1])) if != nil { return , nil, } switch len() { case 1: = case 2: if == nil { = make(map[string]slog.Level) } [[0]] = } } } return , , nil }