groupPrefix string
groups []string // all groups started from WithGroup
nOpenGroups int // the number of groups opened in preformattedAttrs
- mu sync.Mutex
+ mu *sync.Mutex
w io.Writer
}
groups: slices.Clip(h.groups),
nOpenGroups: h.nOpenGroups,
w: h.w,
+ mu: h.mu, // mutex shared among all clones of this handler
}
}
"slices"
"strconv"
"strings"
+ "sync"
"testing"
"time"
)
}
}
+func TestConcurrentWrites(t *testing.T) {
+ ctx := context.Background()
+ count := 1000
+ for _, handlerType := range []string{"text", "json"} {
+ t.Run(handlerType, func(t *testing.T) {
+ var buf bytes.Buffer
+ var h Handler
+ switch handlerType {
+ case "text":
+ h = NewTextHandler(&buf, nil)
+ case "json":
+ h = NewJSONHandler(&buf, nil)
+ default:
+ t.Fatalf("unexpected handlerType %q", handlerType)
+ }
+ sub1 := h.WithAttrs([]Attr{Bool("sub1", true)})
+ sub2 := h.WithAttrs([]Attr{Bool("sub2", true)})
+ var wg sync.WaitGroup
+ for i := 0; i < count; i++ {
+ sub1Record := NewRecord(time.Time{}, LevelInfo, "hello from sub1", 0)
+ sub1Record.AddAttrs(Int("i", i))
+ sub2Record := NewRecord(time.Time{}, LevelInfo, "hello from sub2", 0)
+ sub2Record.AddAttrs(Int("i", i))
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ if err := sub1.Handle(ctx, sub1Record); err != nil {
+ t.Error(err)
+ }
+ if err := sub2.Handle(ctx, sub2Record); err != nil {
+ t.Error(err)
+ }
+ }()
+ }
+ wg.Wait()
+ for i := 1; i <= 2; i++ {
+ want := "hello from sub" + strconv.Itoa(i)
+ n := strings.Count(buf.String(), want)
+ if n != count {
+ t.Fatalf("want %d occurrences of %q, got %d", count, want, n)
+ }
+ }
+ })
+ }
+}
+
// Verify the common parts of TextHandler and JSONHandler.
func TestJSONAndTextHandlers(t *testing.T) {
// remove all Attrs