charmbracelet / charmbracelet/log

Output `io.Writer` concurrency gotcha compared to stdlib

Open
#177 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
3.4k
Forks
103
PR merge metrics
No merged PRs in 30d

Description

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](https://cs.opensource.google/go/go/+/refs/tags/go1.24.5:src/log/slog/handler.go;l=211) for protecting concurrent writes to the output.

```go
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](https://github.com/charmbracelet/log/blob/4dcdb75977075373f3eca87b2b7b75ed18af3c7d/logger.go#L337).

```go
// 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.

Contributor guide

Open the contributing guide

Research direction

Review logger.go's With method and the existing documentation around cloned loggers and the configured io.Writer. Document that cloned loggers do not share an output mutex and that the configured writer must support concurrent use, noting the contrast with log/slog; done when users can find and understand this caveat.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
observability-sre
Issue type
Documentation
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.