Skip to content

Commit bb3745f

Browse files
committed
[logger] rework bufferadapter to be safe for concurrent use
- `bufferadapter.LogEntries` type reworked and now is thread-safe. - `bufferadapter.New` now creates both, adapter and buffer. - New method `bufferadapter.NewWithBuffer` allows usage of preexisting buffer. Change-Id: I193e5f529eaa1591e88cf89bd24dec6042ada981
1 parent 17f1b9c commit bb3745f

File tree

4 files changed

+151
-35
lines changed

4 files changed

+151
-35
lines changed

e/log_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ import (
1616
func TestLog(t *testing.T) {
1717
t.Parallel()
1818

19-
entries := make(bufferadapter.LogEntries, 0)
20-
log := logger.New(bufferadapter.New(&entries), logger.DefaultLogLevel)
19+
adapter, buff := bufferadapter.New()
20+
log := logger.New(adapter, logger.DefaultLogLevel)
2121

2222
e1 := errors.New("e1") //nolint:err113
2323
e2 := fmt.Errorf("e2: %w", e1)
@@ -31,7 +31,7 @@ func TestLog(t *testing.T) {
3131
e.Log(e3, log.DebugE)
3232
e.Log(e4, log.TraceE)
3333

34-
assert.Equal(t, bufferadapter.LogEntries{
34+
assert.Equal(t, []bufferadapter.LogEntry{
3535
{
3636
LoggerName: "",
3737
Level: logger.LevelError,
@@ -67,5 +67,5 @@ func TestLog(t *testing.T) {
6767
Error: nil,
6868
Fields: nil,
6969
},
70-
}, entries)
70+
}, buff.GetAll())
7171
}

logger/bufferadapter/adapter.go

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package bufferadapter
22

33
import (
44
"slices"
5+
"sync"
56

67
"dev.gaijin.team/go/golib/fields"
78
"dev.gaijin.team/go/golib/logger"
@@ -15,26 +16,67 @@ type LogEntry struct {
1516
Fields fields.List `exhaustruct:"optional"`
1617
}
1718

18-
type LogEntries []LogEntry
19+
// LogEntries is a buffer for storing log entries in memory. It is safe for
20+
// concurrent use.
21+
type LogEntries struct {
22+
mu sync.RWMutex `exhaustruct:"optional"`
23+
entries []LogEntry `exhaustruct:"optional"`
24+
}
25+
26+
// Add appends a log entry.
27+
func (le *LogEntries) Add(entry LogEntry) {
28+
le.mu.Lock()
29+
defer le.mu.Unlock()
30+
31+
le.entries = append(le.entries, entry)
32+
}
1933

34+
// Reset clears all log entries, preserving capacity.
2035
func (le *LogEntries) Reset() {
21-
*le = LogEntries{}
36+
le.mu.Lock()
37+
defer le.mu.Unlock()
38+
39+
le.entries = le.entries[:0]
40+
}
41+
42+
// Len returns the number of log entries.
43+
func (le *LogEntries) Len() int {
44+
le.mu.RLock()
45+
defer le.mu.RUnlock()
46+
return len(le.entries)
2247
}
2348

24-
// Adapter is a logger.Adapter that writes logs to the provided [LogEntries]
25-
// slice.
26-
//
27-
// This adapter is designed for testing purposes and is not intended for
28-
// production use.
49+
// Get returns a copy of the log entry at the given index.
50+
// Panics if index is out of range.
51+
func (le *LogEntries) Get(i int) LogEntry {
52+
le.mu.RLock()
53+
defer le.mu.RUnlock()
54+
return le.entries[i]
55+
}
56+
57+
// GetAll returns a copy of log entries buffer.
58+
func (le *LogEntries) GetAll() []LogEntry {
59+
le.mu.RLock()
60+
defer le.mu.RUnlock()
61+
return slices.Clone(le.entries)
62+
}
63+
64+
// Adapter is a logger.Adapter implementation that writes logs to the provided
65+
// [LogEntries] collection.
2966
type Adapter struct {
30-
buff *LogEntries
67+
buff *LogEntries `exhaustruct:"optional"`
3168
name string `exhaustruct:"optional"`
3269
fs fields.List `exhaustruct:"optional"`
3370
}
3471

35-
// New creates a new [Adapter] instance.
36-
func New(buff *LogEntries) *Adapter {
37-
return &Adapter{buff: buff}
72+
// New creates a new [Adapter] instance along with a new [LogEntries] buffer that
73+
// will be used by adapter.
74+
func New() (*Adapter, *LogEntries) {
75+
buff := &LogEntries{
76+
entries: make([]LogEntry, 0),
77+
}
78+
79+
return &Adapter{buff: buff}, buff
3880
}
3981

4082
func (a *Adapter) Log(level int, msg string, err error, fs ...fields.Field) {
@@ -43,11 +85,10 @@ func (a *Adapter) Log(level int, msg string, err error, fs ...fields.Field) {
4385
Level: level,
4486
Msg: msg,
4587
Error: err,
88+
Fields: append(slices.Clone(a.fs), fs...),
4689
}
4790

48-
e.Fields = append(slices.Clone(a.fs), fs...)
49-
50-
*a.buff = append(*a.buff, e)
91+
a.buff.Add(e)
5192
}
5293

5394
func (a *Adapter) WithFields(fs ...fields.Field) logger.Adapter {

logger/bufferadapter/adapter_test.go

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,80 +11,134 @@ import (
1111
"dev.gaijin.team/go/golib/logger/bufferadapter"
1212
)
1313

14+
func Test_New(t *testing.T) {
15+
t.Parallel()
16+
17+
adapter, buff := bufferadapter.New()
18+
require.NotNil(t, adapter)
19+
require.NotNil(t, buff)
20+
21+
adapter.Log(42, "test", nil)
22+
require.Equal(t, 1, buff.Len())
23+
assert.Equal(t, "test", buff.Get(0).Msg)
24+
}
25+
1426
func TestAdapter(t *testing.T) {
1527
t.Parallel()
1628

1729
t.Run(".Log()", func(t *testing.T) {
1830
t.Parallel()
1931

20-
buff := bufferadapter.LogEntries{}
21-
adapter := bufferadapter.New(&buff)
32+
adapter, buff := bufferadapter.New()
2233

2334
adapter.Log(42, "foo", nil)
24-
require.Len(t, buff, 1)
35+
require.Equal(t, 1, buff.Len())
2536
assert.Equal(t, bufferadapter.LogEntry{
2637
Level: 42,
2738
Msg: "foo",
28-
}, buff[0])
39+
}, buff.Get(0))
2940

3041
err := e.New("some error")
3142
adapter.Log(42, "foo", err, fields.F("foo", "bar"))
32-
require.Len(t, buff, 2)
43+
require.Equal(t, 2, buff.Len())
3344
assert.Equal(t, bufferadapter.LogEntry{
3445
Level: 42,
3546
Msg: "foo",
3647
Error: err,
3748
Fields: fields.List{fields.F("foo", "bar")},
38-
}, buff[1])
49+
}, buff.Get(1))
3950

4051
buff.Reset()
41-
assert.Empty(t, buff)
52+
assert.Equal(t, 0, buff.Len())
4253
})
4354

4455
t.Run(".WithFields()", func(t *testing.T) {
4556
t.Parallel()
4657

47-
buff := bufferadapter.LogEntries{}
48-
adapterSrc := bufferadapter.New(&buff)
58+
adapterSrc, buff := bufferadapter.New()
4959
adapter := adapterSrc.WithName("my-logger").WithFields(fields.F("foo", "bar"))
5060

5161
require.NotSame(t, adapterSrc, adapter)
5262

5363
adapter.Log(42, "foo", nil)
54-
require.Len(t, buff, 1)
64+
require.Equal(t, 1, buff.Len())
5565
assert.Equal(t, bufferadapter.LogEntry{
5666
LoggerName: "my-logger",
5767
Level: 42,
5868
Msg: "foo",
5969
Fields: fields.List{fields.F("foo", "bar")},
60-
}, buff[0])
70+
}, buff.Get(0))
6171

6272
adapter.Log(42, "foo", nil, fields.F("baz", "qux"))
63-
require.Len(t, buff, 2)
73+
require.Equal(t, 2, buff.Len())
6474
assert.Equal(t, bufferadapter.LogEntry{
6575
LoggerName: "my-logger",
6676
Level: 42,
6777
Msg: "foo",
6878
Fields: fields.List{fields.F("foo", "bar"), fields.F("baz", "qux")},
69-
}, buff[1])
79+
}, buff.Get(1))
7080
})
7181

7282
t.Run(".WithName()", func(t *testing.T) {
7383
t.Parallel()
7484

75-
buff := bufferadapter.LogEntries{}
76-
adapterSrc := bufferadapter.New(&buff)
85+
adapterSrc, buff := bufferadapter.New()
7786
adapter := adapterSrc.WithFields(fields.F("foo", "bar")).WithName("my-logger")
7887

7988
require.NotSame(t, adapterSrc, adapter)
8089

8190
adapter.Log(42, "foo", nil)
82-
require.Len(t, buff, 1)
91+
require.Equal(t, 1, buff.Len())
8392
assert.Equal(t, bufferadapter.LogEntry{
8493
LoggerName: "my-logger",
8594
Level: 42,
8695
Msg: "foo",
8796
Fields: fields.List{fields.F("foo", "bar")},
88-
}, buff[0])
97+
}, buff.Get(0))
98+
})
99+
}
100+
101+
func TestLogEntries(t *testing.T) {
102+
t.Parallel()
103+
104+
t.Run("GetAll()", func(t *testing.T) {
105+
t.Parallel()
106+
107+
adapter, buff := bufferadapter.New()
108+
109+
adapter.Log(10, "first", nil)
110+
adapter.Log(20, "second", nil)
111+
adapter.Log(30, "third", nil)
112+
113+
entries := buff.GetAll()
114+
require.Len(t, entries, 3)
115+
assert.Equal(t, "first", entries[0].Msg)
116+
assert.Equal(t, "second", entries[1].Msg)
117+
assert.Equal(t, "third", entries[2].Msg)
118+
})
119+
120+
t.Run("Get() panics on out of range", func(t *testing.T) {
121+
t.Parallel()
122+
123+
_, buff := bufferadapter.New()
124+
125+
assert.Panics(t, func() {
126+
buff.Get(0)
127+
})
128+
})
129+
130+
t.Run("Reset() empties the buffer", func(t *testing.T) {
131+
t.Parallel()
132+
133+
adapter, buff := bufferadapter.New()
134+
135+
for i := range 10 {
136+
adapter.Log(i, "test", nil)
137+
}
138+
139+
require.Equal(t, 10, buff.Len())
140+
141+
buff.Reset()
142+
assert.Equal(t, 0, buff.Len())
89143
})
90144
}

logger/bufferadapter/doc.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Package bufferadapter provides an in-memory logger adapter for testing.
2+
//
3+
// This package implements the logger.Adapter interface by storing log entries in
4+
// memory, allowing tests to inspect what was logged. The adapter is safe for
5+
// concurrent use and designed specifically for testing purposes.
6+
//
7+
// # Usage
8+
//
9+
// Create a buffer adapter and use it with a logger:
10+
//
11+
// adapter, buff := bufferadapter.New()
12+
// log := logger.New(adapter, logger.LevelInfo)
13+
//
14+
// log.Info("test message", fields.F("key", "value"))
15+
//
16+
// // Assert in tests
17+
// entries := buff.GetAll()
18+
// require.Len(t, entries, 1)
19+
// assert.Equal(t, "test message", entries[0].Msg)
20+
// assert.Equal(t, logger.LevelInfo, entries[0].Level)
21+
package bufferadapter

0 commit comments

Comments
 (0)