log: fix race when adding a log output while logging

Handle read the list of extra outputs without holding the mutex, so
calling AddOutput while logging caused a data race.

This reads the extra outputs once under the mutex.
This commit is contained in:
Nick Craig-Wood committed 2026-09-20 11:14:24 +01:00
1 parent 10cede9f7f
commit 67ec40a809
2 files changed
+16 -5

No files matched your search

+7 -5
View File
@@ -360,17 +360,19 @@ func (h *OutputHandler) Handle(ctx context.Context, r slog.Record) (err error) {
buf *bytes.Buffer
)
// Read the format under the mutex once so it is consistent
// throughout this call. The mutex is released before calling
// jsonLog/textLog (which may re-acquire it) to avoid deadlock.
// Read the format and the extra outputs under the mutex once
// so they are consistent throughout this call. The mutex is
// released before calling jsonLog/textLog (which may re-acquire
// it) to avoid deadlock.
h.mu.Lock()
format := h.format
outputExtra := h.outputExtra
h.mu.Unlock()
// Check whether we need to build Text or JSON logs or both
needJSON := format&logFormatJSON != 0
needText := !needJSON
for _, out := range h.outputExtra {
for _, out := range outputExtra {
if out.json {
needJSON = true
} else {
@@ -413,7 +415,7 @@ func (h *OutputHandler) Handle(ctx context.Context, r slog.Record) (err error) {
}
// Log to any additional destinations required
for _, out := range h.outputExtra {
for _, out := range outputExtra {
if out.json {
out.output(r.Level, bufJSON.String())
} else {
+9
View File
@@ -346,6 +346,15 @@ func TestOutputHandlerConcurrency(t *testing.T) {
}
})
// Goroutine calling AddOutput alternating JSON and text
wg.Go(func() {
noop := func(_ slog.Level, _ string) {}
for j := range 20 {
h.AddOutput(j%2 == 0, noop)
time.Sleep(time.Millisecond)
}
})
// Use a channel with a timeout to detect deadlocks
done := make(chan struct{})
go func() {