Skip to content

Commit 3f09fa4

Browse files
lidelMarcoPolo
andauthored
feat(gologshim): Add SetDefaultHandler (#3418)
Co-authored-by: Marco Munizaga <[email protected]>
1 parent 4d16b4c commit 3f09fa4

File tree

2 files changed

+363
-25
lines changed

2 files changed

+363
-25
lines changed

gologshim/gologshim.go

Lines changed: 116 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,52 @@
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.
140
package gologshim
241

342
import (
43+
"context"
444
"fmt"
545
"log/slog"
646
"os"
747
"strings"
848
"sync"
49+
"sync/atomic"
950
)
1051

1152
var 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

58149
type logFormat = int

0 commit comments

Comments
 (0)