Skip to content

Conversation

@lidel
Copy link
Member

@lidel lidel commented Oct 27, 2025

Problem

go-libp2p migrated from go-log to slog in

Unfortunately, the gologshim from that PR was not enough. go-libp2p. 0.44 breaks dynamic level control via SetLogLevel() which in turn broke things like ipfs log level and the Diagnostic screen from ipfs-webui (ipfs/ipfs-webui#2392) which allows users to dynamically adjust log level per system to investigate bugs.

Solution

This PR adds automatic slog integration that routes slog logs through go-log's zap core when SetupLogging() is called.

This restores runtime level adjustments for go-libp2p subsystems without requiring daemon restarts, making it possible to debug production issues by changing log levels on the fly.

The integration detects subsystem names via logger attributes and applies per-subsystem level control. Libraries can opt-in via duck typing to avoid adding go-log as a dependency (see go-libp2p's gologshim implementation).

Important

Original version of this PR based on turn-key runtime detection was rejected by go-libp2p maintainers, and instead go-libp2p 0.45 requires go-log users to manually set up wiring. See https://github.com/ipfs/go-log/releases/tag/v2.9.0 and libp2p/go-libp2p#3419 (comment)

Context

go-libp2p migrated from go-log to slog, breaking dynamic level control
via SetLogLevel(). This adds automatic slog integration that routes slog
logs through go-log's zap core when SetupLogging() is called.

This restores runtime level adjustments for go-libp2p subsystems without
requiring daemon restarts, making it possible to debug production issues
by changing log levels on the fly.

The integration detects subsystem names via logger attributes and applies
per-subsystem level control. Libraries can opt-in via duck typing to avoid
adding go-log as a dependency (see go-libp2p's gologshim implementation).

Users can disable this behavior with GOLOG_CAPTURE_DEFAULT_SLOG=false.

Prerequisite for addressing ipfs/kubo#11035
@lidel lidel requested a review from gammazero October 27, 2025 23:29
@ipfs ipfs deleted a comment from welcome bot Oct 27, 2025
lidel added a commit to libp2p/go-libp2p that referenced this pull request Oct 27, 2025
The migration to slog in #3364
broke go-log's ability to adjust subsystem log levels at runtime via
SetLogLevel() and control output formatting (colors, json). This was
a key debugging feature that allowed changing log verbosity without
restarting daemons.

This fix detects go-log's slog bridge via duck typing and uses it when
available, restoring dynamic level control and unified formatting.
When go-log isn't present, gologshim falls back to standalone slog
handlers as before.

The lazy handler pattern solves initialization order issues where
package-level loggers are created before go-log's bridge is installed.

Users just need to update to this version of go-libp2p and go-log with
ipfs/go-log#176 - no code changes or init()
hacks required.

Prerequisite for addressing ipfs/kubo#11035
lidel added 2 commits October 28, 2025 00:48
tests were modifying global state without restoring it, causing failures
when run with -shuffle on some platforms
tests were not restoring the global defaultLevel variable, causing race
conditions when getOrCreateAtomicLevel() created new subsystem levels
using the stale defaultLevel from previous tests
@lidel lidel marked this pull request as ready for review October 28, 2025 16:27
Copy link
Contributor

@gammazero gammazero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is brilliant!

@lidel
Copy link
Member Author

lidel commented Nov 3, 2025

Triage notes

@MarcoPolo
Copy link

I've not reviewed this PR. Linking this in case you hadn't seen it: https://pkg.go.dev/go.uber.org/zap/exp/zapslog.

add SlogHandler() function that returns go-log's slog.Handler directly.
this allows applications to wire slog-based libraries to go-log even
when GOLOG_CAPTURE_DEFAULT_SLOG=false.

the bridge is now always created during SetupLogging(), but only
installed as slog.Default() when automatic capture is enabled.

use atomic.Pointer[slog.Handler] for thread-safe access without locks,
following idiomatic Go patterns for single-writer, multiple-reader scenarios.

benefits:
- works when GOLOG_CAPTURE_DEFAULT_SLOG=false
- more explicit API (no indirection through slog.Default())
- simpler application code (no duck-typing verification needed)
- thread-safe without mutex overhead

addresses feedback from libp2p/go-libp2p#3419
@lidel
Copy link
Member Author

lidel commented Nov 5, 2025

@MarcoPolo thx, we've seen it, iirc ended up being not enough, thats why go-log has own opinionated slog bridge.

Update: found my notes from when i was researching our options here:

  • Dynamic core support
    • go-log must preserves LIVE core reference (SAME core → pipe readers work)
    • zapslog approach - breaks LIVE reference
      • When libraries call WithAttrs() (like go-libp2p/gologshim does), zapslog would create a NEW wrapped core that loses connection to the multi-core. Dynamically-added pipe readers won't receive logs from these loggers, breaking things like on-demand ipfs log tail
  • No per-subsystem level control / No "logger" attribute extraction
    • go-log's subsystemAwareHandler provides early atomic filtering for per-subsystem levels (ipfs log level libp2p debug). zapslog has no equivalent - it always delegates to the core's single level
    • go-log specially handles slog.String("logger", "subsystem") to enable dynamic level control. zapslog treats it as a regular attribute.

@lidel
Copy link
Member Author

lidel commented Nov 5, 2025

Since go-libp2p decided to not use automatic bridge described in libp2p/go-libp2p#3419 (review), and go-libp2p 0.45 (libp2p/go-libp2p#3424) will require every end application that uses both go-log and go-libp2p to explicitly wire them together in own code:

  import (
      logging "github.com/ipfs/go-log/v2"
      "github.com/libp2p/go-libp2p/gologshim"
  )

  func init() {
      // use the explicit go-log bridge      
      gologshim.SetDefaultHandler(golog.SlogHandler())
  }

.. I do not think it makes sense to set slog.Default in go-log's init() anymore. Aytomatic magic and touching global slog made sense ONLY if go-libp2p cooperated with go-log and leveraged automatic slog bridge, to fix users apps without asking them to do extra coding.

Now that every go-libp2p >=0.45 user has to have the above snippet in init() anyway, automatic slog bridge feels like diminishing returns. If user needs automatic slog bridging, they can add one line to override global slog:

+ slog.SetDefault(slog.New(golog.SlogHandler()))
gologshim.SetDefaultHandler(golog.SlogHandler())

I will keep GOLOG_CAPTURE_DEFAULT_SLOG but flip it to be disabled by default just to make the go-log release safer (it will not touch global slog.Default() anymore by default, making it safer for everyone to update).

Update: done in b453280

change GOLOG_CAPTURE_DEFAULT_SLOG to default to false, requiring explicit
opt-in for automatic slog.Default() capture. applications wanting slog
integration must now explicitly call slog.SetDefault(slog.New(golog.SlogHandler())).

rationale:
- go-libp2p decided to use explicit wiring (gologshim.SetDefaultHandler)
  rather than relying on automatic slog.Default() capture
- automatic capture was the original motivation for default=true, but is
  no longer needed
- makes go-log a better Go library by not modifying global state by default
- follows Go best practice: libraries should not call slog.SetDefault()
- opt-in is better than opt-out for invasive features

changes:
- setup.go: change condition from != "false" to == "true"
- README.md: updated documentation to show explicit setup pattern,
  reposition GOLOG_CAPTURE_DEFAULT_SLOG as development/opt-in feature
- tests: updated expectations to match new default behavior

references:
- #176 (comment)
- libp2p/go-libp2p#3419 (review)
lidel added a commit to ipfs/kubo that referenced this pull request Nov 5, 2025
update to go-log v2.8.3-0.20251105220843-b453280b0ce2 which changes
GOLOG_CAPTURE_DEFAULT_SLOG to opt-in (default=false). kubo now explicitly
calls slog.SetDefault(slog.New(logging.SlogHandler())) to integrate slog
with go-log's formatting and level control.

changes:
- cmd/ipfs/kubo/start.go: add slog.SetDefault() call for application-wide
  slog integration, maintaining existing gologshim.SetDefaultHandler() for
  go-libp2p subsystem attribution
- docs/environment-variables.md: remove GOLOG_LOG_LABELS and
  GOLOG_CAPTURE_DEFAULT_SLOG sections (no longer relevant to kubo users)
- go.mod: update go-log to b453280b0ce2

while automatic slog.Default() capture was convenient, go-libp2p chose to
require explicit gologshim registration for clarity. for consistency, we
now do the same for slog.Default() - making both integration points
explicit rather than mixing automatic and manual approaches.

references:
- ipfs/go-log#176 (comment)
- ipfs/go-log@b453280
@lidel
Copy link
Member Author

lidel commented Nov 5, 2025

All green in

I'm going to merge this and then make go-log release to ensure it is available before go-libp2p 0.45 (libp2p/go-libp2p#3424) ships

update SlogHandler() godoc to match kubo's usage pattern, showing both:
1. slog.SetDefault() for application-wide integration
2. gologshim.SetDefaultHandler() for go-libp2p subsystem attribution

this provides a complete example of explicit slog integration rather than
just showing the library-specific wiring.
@lidel lidel merged commit f58543b into master Nov 5, 2025
9 checks passed
@lidel lidel mentioned this pull request Nov 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants