|
1 | 1 | # go-log |
2 | 2 |
|
3 | | -[](https://protocol.ai) |
4 | 3 | [](https://ipfs.io/) |
5 | 4 | [](https://pkg.go.dev/github.com/ipfs/go-log/v2) |
6 | 5 |
|
7 | | -> The logging library used by go-ipfs |
| 6 | +> The logging library used by IPFS Kubo |
8 | 7 |
|
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) |
11 | 22 |
|
12 | 23 | ## Install |
13 | 24 |
|
@@ -53,7 +64,7 @@ if err != nil { |
53 | 64 | } |
54 | 65 | ``` |
55 | 66 |
|
56 | | -### Environment Variables |
| 67 | +## Environment Variables |
57 | 68 |
|
58 | 69 | This package can be configured through various environment variables. |
59 | 70 |
|
@@ -122,6 +133,229 @@ pairs. For example, the following add `{"app": "example_app", "dc": "sjc-1"}` to |
122 | 133 | export GOLOG_LOG_LABELS="app=example_app,dc=sjc-1" |
123 | 134 | ``` |
124 | 135 |
|
| 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 | + |
125 | 359 | ## Contribute |
126 | 360 |
|
127 | 361 | Feel free to join in. All welcome. Open an [issue](https://github.com/ipfs/go-log/issues)! |
|
0 commit comments