Skip to content

Commit f58543b

Browse files
authored
fix: go-libp2p and slog interop (#176)
go-libp2p migrated from go-log to slog, breaking dynamic level control via SetLogLevel() and ability to dynamically plug outputs (e.g ipfs log tail). This adds necessary slog integration that routes slog logs through go-log's zap core, allowing Kubo and other applications to restore log control like it was before go-libp2p 0.44 made breaking changes.
1 parent 77c15ef commit f58543b

File tree

6 files changed

+1191
-16
lines changed

6 files changed

+1191
-16
lines changed

README.md

Lines changed: 239 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
# go-log
22

3-
[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](https://protocol.ai)
43
[![](https://img.shields.io/badge/project-IPFS-blue.svg?style=flat-square)](https://ipfs.io/)
54
[![GoDoc](https://pkg.go.dev/badge/github.com/ipfs/go-log/v2.svg)](https://pkg.go.dev/github.com/ipfs/go-log/v2)
65

7-
> The logging library used by go-ipfs
6+
> The logging library used by IPFS Kubo
87
9-
go-log wraps [zap](https://github.com/uber-go/zap) to provide a logging facade. go-log manages logging
10-
instances and allows for their levels to be controlled individually.
8+
go-log wraps [zap](https://github.com/uber-go/zap) to provide per-subsystem level control and optional log/slog integration for unified logging across IPFS/libp2p components.
9+
10+
## Table of Contents
11+
12+
- [Install](#install)
13+
- [Usage](#usage)
14+
- [Environment Variables](#environment-variables)
15+
- [Slog Integration](#slog-integration)
16+
- [Application Setup (Required)](#application-setup-required)
17+
- [For library authors](#for-library-authors)
18+
- [Approach 1: Duck-typing detection (automatic)](#approach-1-duck-typing-detection-automatic)
19+
- [Approach 2: Explicit handler passing (manual)](#approach-2-explicit-handler-passing-manual)
20+
- [Contribute](#contribute)
21+
- [License](#license)
1122

1223
## Install
1324

@@ -53,7 +64,7 @@ if err != nil {
5364
}
5465
```
5566

56-
### Environment Variables
67+
## Environment Variables
5768

5869
This package can be configured through various environment variables.
5970

@@ -122,6 +133,229 @@ pairs. For example, the following add `{"app": "example_app", "dc": "sjc-1"}` to
122133
export GOLOG_LOG_LABELS="app=example_app,dc=sjc-1"
123134
```
124135

136+
#### `GOLOG_CAPTURE_DEFAULT_SLOG`
137+
138+
By default, go-log does NOT automatically install its slog handler as `slog.Default()`. Applications should explicitly call `slog.SetDefault(slog.New(golog.SlogHandler()))` for slog integration (see Slog Integration section below).
139+
140+
Alternatively, you can enable automatic installation by setting:
141+
142+
```bash
143+
export GOLOG_CAPTURE_DEFAULT_SLOG="true"
144+
```
145+
146+
When enabled, go-log automatically installs its handler as `slog.Default()` during `SetupLogging()`, which allows libraries using `slog` to automatically use go-log's formatting and dynamic level control.
147+
148+
## Slog Integration
149+
150+
go-log provides integration with Go's `log/slog` package for unified log management. This provides:
151+
152+
1. **Unified formatting**: slog logs use the same format as go-log (color/nocolor/json)
153+
2. **Dynamic level control**: slog loggers respect `SetLogLevel()` and environment variables
154+
3. **Subsystem-aware filtering**: slog loggers with subsystem attributes get per-subsystem level control
155+
156+
**Note**: This slog bridge exists as an intermediate solution while go-log uses zap internally. In the future, go-log may migrate from zap to native slog, which would simplify this integration.
157+
158+
### Application Setup (Required)
159+
160+
For slog-based logging to use go-log's formatting and level control, applications must explicitly set go-log's handler as the slog default:
161+
162+
```go
163+
import (
164+
"log/slog"
165+
golog "github.com/ipfs/go-log/v2"
166+
"github.com/libp2p/go-libp2p/gologshim"
167+
)
168+
169+
func init() {
170+
// Set go-log's handler as the application-wide slog default.
171+
// This ensures all slog-based logging uses go-log's formatting.
172+
slog.SetDefault(slog.New(golog.SlogHandler()))
173+
174+
// Wire libraries that use explicit handler passing (like go-libp2p).
175+
// This ensures proper subsystem attribution for per-logger level control.
176+
gologshim.SetDefaultHandler(golog.SlogHandler())
177+
}
178+
```
179+
180+
This two-layer approach ensures:
181+
- **Application-level**: All slog usage (application code + libraries) flows through go-log
182+
- **Library-level**: Libraries with explicit wiring (like go-libp2p) include proper subsystem attributes
183+
184+
### How it works
185+
186+
When configured as shown above, slog-based libraries gain unified formatting and dynamic level control.
187+
188+
**Attributes added by libraries:**
189+
- `logger`: Subsystem name (e.g., "foo", "bar", "baz")
190+
- Any additional labels from `GOLOG_LOG_LABELS`
191+
192+
Example:
193+
```go
194+
var log = logging.Logger("foo") // gologshim
195+
log.Debug("operation failed", "err", err)
196+
```
197+
198+
When integrated with go-log, output is formatted by go-log (JSON format shown here, also supports color/nocolor):
199+
```json
200+
{
201+
"level": "debug",
202+
"ts": "2025-10-27T12:34:56.789+0100",
203+
"logger": "foo",
204+
"caller": "foo/foo.go:72",
205+
"msg": "operation failed",
206+
"err": "connection refused"
207+
}
208+
```
209+
210+
### Controlling slog logger levels
211+
212+
These loggers respect go-log's level configuration:
213+
214+
```bash
215+
# Via environment variable (before daemon starts)
216+
export GOLOG_LOG_LEVEL="error,foo=debug"
217+
218+
# Via API (while daemon is running)
219+
logging.SetLogLevel("foo", "debug")
220+
```
221+
222+
This works even if the logger is created lazily or hasn't been created yet. Level settings are preserved and applied when the logger is first used.
223+
224+
### Direct slog usage without subsystem
225+
226+
When using slog.Default() directly without adding a "logger" attribute, logs still work but have limitations:
227+
228+
**What works:**
229+
- Logs appear in output with go-log's formatting (JSON/color/nocolor)
230+
- Uses global log level from `GOLOG_LOG_LEVEL` fallback or `SetAllLoggers()`
231+
232+
**Limitations:**
233+
- No subsystem-specific level control via `SetLogLevel("subsystem", "level")`
234+
- Empty logger name in output
235+
- Less efficient (no early atomic level filtering)
236+
237+
**Example:**
238+
```go
239+
// Direct slog usage - uses global level only
240+
slog.Info("message") // LoggerName = "", uses global level
241+
242+
// Library with subsystem - subsystem-aware
243+
log := mylib.Logger("foo")
244+
log.Info("message") // LoggerName = "foo", uses subsystem level
245+
```
246+
247+
For libraries, use the "logger" attribute pattern to enable per-subsystem control.
248+
249+
### Why "logger" attribute?
250+
251+
go-log uses `"logger"` as the attribute key for subsystem names to maintain backward compatibility with its existing Zap-based output format:
252+
253+
- Maintains compatibility with existing go-log output format
254+
- Existing tooling, dashboards, and log processors already parse the "logger" field
255+
- Simplifies migration path from Zap to slog bridge
256+
257+
Libraries integrating with go-log should use this same attribute key to ensure proper subsystem-aware level control.
258+
259+
### For library authors
260+
261+
Libraries using slog can integrate with go-log without adding go-log as a dependency. There are two approaches:
262+
263+
#### Approach 1: Duck-typing detection (automatic)
264+
265+
Detect go-log's slog bridge via an interface marker to avoid requiring go-log in library's go.mod:
266+
267+
```go
268+
// In your library's logging package
269+
func Logger(subsystem string) *slog.Logger {
270+
// Check if slog.Default() is go-log's bridge.
271+
// This works when applications call slog.SetDefault(slog.New(golog.SlogHandler())).
272+
handler := slog.Default().Handler()
273+
274+
type goLogBridge interface {
275+
GoLogBridge()
276+
}
277+
if _, ok := handler.(goLogBridge); ok {
278+
// go-log's bridge is active - use it with subsystem attribute
279+
h := handler.WithAttrs([]slog.Attr{
280+
slog.String("logger", subsystem),
281+
})
282+
return slog.New(h)
283+
}
284+
285+
// Standalone handler when go-log is not present
286+
return slog.New(createStandaloneHandler(subsystem))
287+
}
288+
```
289+
290+
Usage in your library:
291+
```go
292+
var log = mylib.Logger("foo")
293+
log.Debug("operation completed", "key", value)
294+
```
295+
296+
This pattern allows libraries to automatically integrate when the application has set up go-log's handler, without requiring go-log as a dependency.
297+
298+
#### Approach 2: Explicit handler passing (manual)
299+
300+
Alternatively, expose a way for applications to provide a handler explicitly:
301+
302+
```go
303+
// In your library's logging package
304+
var defaultHandler atomic.Pointer[slog.Handler]
305+
306+
func SetDefaultHandler(handler slog.Handler) {
307+
defaultHandler.Store(&handler)
308+
}
309+
310+
func Logger(subsystem string) *slog.Logger {
311+
if h := defaultHandler.Load(); h != nil {
312+
// Use provided handler with subsystem attribute
313+
return slog.New((*h).WithAttrs([]slog.Attr{
314+
slog.String("logger", subsystem),
315+
}))
316+
}
317+
// Standalone handler when go-log is not present
318+
return slog.New(createStandaloneHandler(subsystem))
319+
}
320+
```
321+
322+
Usage in your library:
323+
```go
324+
var log = mylib.Logger("bar")
325+
log.Info("started service", "addr", addr)
326+
```
327+
328+
**Application side** must explicitly wire it, for example, go-libp2p requires:
329+
330+
```go
331+
import (
332+
golog "github.com/ipfs/go-log/v2"
333+
"github.com/libp2p/go-libp2p/gologshim"
334+
)
335+
336+
func init() {
337+
// Use go-log's SlogHandler() to get the bridge directly.
338+
// This works regardless of GOLOG_CAPTURE_DEFAULT_SLOG setting.
339+
gologshim.SetDefaultHandler(golog.SlogHandler())
340+
}
341+
```
342+
343+
**Tradeoff**: Approach 2 requires manual coordination in every application, while Approach 1 works automatically when applications set up `slog.Default()`.
344+
345+
For a complete example, see [go-libp2p's gologshim](https://github.com/libp2p/go-libp2p/blob/master/gologshim/gologshim.go).
346+
347+
### Enabling automatic slog capture (opt-in)
348+
349+
**Note**: This is mostly used during development, when a library author decides between Approach 1 or 2 for proper (subsystem-aware) integration with go-log.
350+
351+
You can enable automatic installation of go-log's handler during `SetupLogging()`:
352+
353+
```bash
354+
export GOLOG_CAPTURE_DEFAULT_SLOG="true"
355+
```
356+
357+
When enabled, go-log automatically installs its handler as `slog.Default()`, which allows slog-based libraries to automatically use go-log's formatting without explicit application setup.
358+
125359
## Contribute
126360

127361
Feel free to join in. All welcome. Open an [issue](https://github.com/ipfs/go-log/issues)!

core.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ func (l *lockedMultiCore) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *za
5151
func (l *lockedMultiCore) Write(ent zapcore.Entry, fields []zapcore.Field) error {
5252
l.mu.RLock()
5353
defer l.mu.RUnlock()
54+
5455
var errs []error
5556
for i := range l.cores {
5657
err := l.cores[i].Write(ent, fields)

0 commit comments

Comments
 (0)