1+ // Package gologshim provides slog-based logging for go-libp2p that works
2+ // standalone or integrates with go-log for unified log management across
3+ // IPFS/libp2p applications, without adding go-log as a dependency.
4+ //
5+ // # Usage
6+ //
7+ // Create loggers using the Logger function:
8+ //
9+ // var log = gologshim.Logger("subsystem")
10+ // log.Debug("message", "key", "value")
11+ //
12+ // # Integration with go-log
13+ //
14+ // Applications can optionally connect go-libp2p to go-log by calling SetDefaultHandler:
15+ //
16+ // func init() {
17+ // gologshim.SetDefaultHandler(slog.Default().Handler())
18+ // }
19+ //
20+ // When integrated, go-libp2p logs use go-log's formatting and can be controlled
21+ // programmatically via go-log's SetLogLevel("subsystem", "level") API to adjust
22+ // log verbosity per subsystem at runtime without restarting.
23+ //
24+ // # Standalone Usage
25+ //
26+ // Without calling SetDefaultHandler, gologshim creates standalone slog handlers
27+ // writing to stderr. This mode is useful when go-log is not present or when you
28+ // want independent log configuration via backward-compatible (go-log) environment variables:
29+ //
30+ // - GOLOG_LOG_LEVEL: Set log levels per subsystem (e.g., "error,ping=debug")
31+ // - GOLOG_LOG_FORMAT/GOLOG_LOG_FMT: Output format ("json" or text)
32+ // - GOLOG_LOG_ADD_SOURCE: Include source location (default: true)
33+ // - GOLOG_LOG_LABELS: Add key=value labels to all logs
34+ //
35+ // For integration details, see: https://github.com/ipfs/go-log/blob/master/README.md#slog-integration
36+ //
37+ // Note: This package exists as an intermediate solution while go-log uses zap
38+ // internally. If go-log migrates from zap to native slog, this bridge layer
39+ // could be simplified or removed entirely.
140package gologshim
241
342import (
43+ "context"
444 "fmt"
545 "log/slog"
646 "os"
747 "strings"
848 "sync"
49+ "sync/atomic"
950)
1051
1152var lvlToLower = map [slog.Level ]slog.Value {
@@ -15,26 +56,52 @@ var lvlToLower = map[slog.Level]slog.Value{
1556 slog .LevelError : slog .StringValue ("error" ),
1657}
1758
18- // Logger returns a *slog.Logger with a logging level defined by the
19- // GOLOG_LOG_LEVEL env var. Supports different levels for different systems. e.g.
20- // GOLOG_LOG_LEVEL=foo=info,bar=debug,warn
21- // sets the foo system at level info, the bar system at level debug and the
22- // fallback level to warn.
23- //
24- // Prefer a parameterized logger over a global logger.
25- func Logger (system string ) * slog.Logger {
26- var h slog.Handler
27- c := ConfigFromEnv ()
28- handlerOpts := & slog.HandlerOptions {
29- Level : c .LevelForSystem (system ),
30- AddSource : c .addSource ,
59+ var defaultHandler atomic.Pointer [slog.Handler ]
60+
61+ // SetDefaultHandler allows an application to change the underlying handler used
62+ // by gologshim as long as it's changed *before* the first log by the logger.
63+ func SetDefaultHandler (handler slog.Handler ) {
64+ defaultHandler .Store (& handler )
65+ }
66+
67+ // dynamicHandler delays bridge detection until first log call to handle init order issues
68+ type dynamicHandler struct {
69+ system string
70+ config * Config
71+ once sync.Once
72+ handler slog.Handler
73+ }
74+
75+ func (h * dynamicHandler ) ensureHandler () slog.Handler {
76+ h .once .Do (func () {
77+ if hPtr := defaultHandler .Load (); hPtr != nil {
78+ h .handler = * hPtr
79+ } else {
80+ h .handler = h .createFallbackHandler ()
81+ }
82+ attrs := make ([]slog.Attr , 0 , 1 + len (h .config .labels ))
83+ // Use "logger" attribute for compatibility with go-log's Zap-based format
84+ // and existing IPFS/libp2p tooling and dashboards.
85+ attrs = append (attrs , slog .String ("logger" , h .system ))
86+ attrs = append (attrs , h .config .labels ... )
87+ h .handler = h .handler .WithAttrs (attrs )
88+ })
89+
90+ return h .handler
91+ }
92+
93+ func (h * dynamicHandler ) createFallbackHandler () slog.Handler {
94+ opts := & slog.HandlerOptions {
95+ Level : h .config .LevelForSystem (h .system ),
96+ AddSource : h .config .addSource ,
3197 ReplaceAttr : func (_ []string , a slog.Attr ) slog.Attr {
32- if a .Key == slog .TimeKey {
98+ switch a .Key {
99+ case slog .TimeKey :
33100 // ipfs go-log uses "ts" for time
34101 a .Key = "ts"
35- } else if a .Key == slog .LevelKey {
36- // ipfs go-log uses lowercase level names
102+ case slog .LevelKey :
37103 if lvl , ok := a .Value .Any ().(slog.Level ); ok {
104+ // ipfs go-log uses lowercase level names
38105 if s , ok := lvlToLower [lvl ]; ok {
39106 a .Value = s
40107 }
@@ -43,16 +110,40 @@ func Logger(system string) *slog.Logger {
43110 return a
44111 },
45112 }
46- if c .format == logFormatText {
47- h = slog .NewTextHandler (os .Stderr , handlerOpts )
48- } else {
49- h = slog .NewJSONHandler (os .Stderr , handlerOpts )
113+ if h .config .format == logFormatText {
114+ return slog .NewTextHandler (os .Stderr , opts )
50115 }
51- attrs := make ([]slog.Attr , 1 + len (c .labels ))
52- attrs = append (attrs , slog .String ("logger" , system ))
53- attrs = append (attrs , c .labels ... )
54- h = h .WithAttrs (attrs )
55- return slog .New (h )
116+
117+ return slog .NewJSONHandler (os .Stderr , opts )
118+ }
119+
120+ func (h * dynamicHandler ) Enabled (ctx context.Context , lvl slog.Level ) bool {
121+ return h .ensureHandler ().Enabled (ctx , lvl )
122+ }
123+
124+ func (h * dynamicHandler ) Handle (ctx context.Context , r slog.Record ) error {
125+ return h .ensureHandler ().Handle (ctx , r )
126+ }
127+
128+ func (h * dynamicHandler ) WithAttrs (attrs []slog.Attr ) slog.Handler {
129+ return h .ensureHandler ().WithAttrs (attrs )
130+ }
131+
132+ func (h * dynamicHandler ) WithGroup (name string ) slog.Handler {
133+ return h .ensureHandler ().WithGroup (name )
134+ }
135+
136+ // Logger returns a *slog.Logger with a logging level defined by the
137+ // GOLOG_LOG_LEVEL env var. Supports different levels for different systems. e.g.
138+ // GOLOG_LOG_LEVEL=foo=info,bar=debug,warn
139+ // sets the foo system at level info, the bar system at level debug and the
140+ // fallback level to warn.
141+ func Logger (system string ) * slog.Logger {
142+ c := ConfigFromEnv ()
143+ return slog .New (& dynamicHandler {
144+ system : system ,
145+ config : c ,
146+ })
56147}
57148
58149type logFormat = int
0 commit comments