log icon indicating copy to clipboard operation
log copied to clipboard

Output `io.Writer` concurrency gotcha compared to stdlib

Open xStrom opened this issue 5 months ago • 0 comments

When using log/slog methods WithAttrs or WithGroup you get a cloned handler that has some predefined inputs.

Now charmbracelet/log has similar functionality provided by the With method that provides you a cloned Logger.

There is a curcial difference in making the cloned logger safe for concurrent use. While log/slog does provide that safety, charmbracelet/log does not. This can be surprising.

Specifically this is about concurrent usege of the configured output io.Writer interface.

log/slog shares a mutex between all clones for protecting concurrent writes to the output.

func (h *commonHandler) clone() *commonHandler {
	// We can't use assignment because we can't copy the mutex.
	return &commonHandler{
		// ... parts omitted for brevity ...
		mu:                h.mu, // mutex shared among all clones of this handler
	}
}

charmbracelet/log doesn't have an output-specific mutex at all. It uses a more catch-all mutex for this purpose and does not share it among clones.

// With returns a new logger with the given keyvals added.
func (l *Logger) With(keyvals ...interface{}) *Logger {
        // ... parts omitted for brevity ...
	l.mu.Lock()
	sl := *l
	l.mu.Unlock()
	sl.mu = &sync.RWMutex{}
	return &sl
}

I don't know whether you consider this a bug or a feature, but at the very least some documentation to highlight this behavior would be useful, especially as it differs from stdlib.

As it stands now, the configured output io.Writer interface needs to be safe for concurrent use. This is kind of tricky in the os.Stderr case, because it will depend on the specific kernel implementation whether concurrent writes are interleaved, intermixed, overlapped or what.

xStrom avatar Jul 19 '25 11:07 xStrom