Skip to content

Commit df28e24

Browse files
committed
Add a sync.Pool for Entry passed to HandleLog
This commit does several things: * Makes it into one Entry strcut * Creates a object pool for Entry objects passed to HandleLog * This means that if someone wants to use an Event after HandleLog, he/she needs to make a copy via entry.Clone. * This is unfortunate, but the performance benefits are obvious, especially when the common context (fields) are big. ```bash name old time/op new time/op delta Logger_common_context-10 118ns ± 9% 62ns ± 4% -47.23% (p=0.029 n=4+4) Logger_common_context_many_fields-10 2.35µs ± 0% 2.17µs ± 0% -7.79% (p=0.029 n=4+4) name old alloc/op new alloc/op delta Logger_common_context-10 176B ± 0% 0B -100.00% (p=0.029 n=4+4) Logger_common_context_many_fields-10 1.49kB ± 0% 0.00kB -100.00% (p=0.029 n=4+4) name old allocs/op new allocs/op delta Logger_common_context-10 2.00 ± 0% 0.00 -100.00% (p=0.029 n=4+4) Logger_common_context_many_fields-10 2.00 ± 0% 0.00 -100.00% (p=0.029 n=4+4) ``` And the "Zap benchmarks": ```bash name old time/op new time/op delta DisabledWithoutFields/bep/log-10 0.27ns ± 2% 0.29ns ± 3% +7.22% (p=0.029 n=4+4) DisabledAccumulatedContext/bep/log-10 0.25ns ± 2% 0.26ns ± 2% +2.45% (p=0.029 n=4+4) DisabledAddingFields/bep/log-10 0.38ns ± 1% 0.39ns ± 2% ~ (p=0.057 n=4+4) WithoutFields/bep/log-10 336ns ± 1% 302ns ± 1% -10.20% (p=0.029 n=4+4) AccumulatedContext/bep/log-10 4.68µs ± 3% 4.36µs ± 2% -6.73% (p=0.029 n=4+4) AddingFields/bep/log-10 5.11µs ± 1% 5.06µs ± 1% ~ (p=0.057 n=4+4) name old alloc/op new alloc/op delta DisabledWithoutFields/bep/log-10 0.00B 0.00B ~ (all equal) DisabledAccumulatedContext/bep/log-10 0.00B 0.00B ~ (all equal) DisabledAddingFields/bep/log-10 0.00B 0.00B ~ (all equal) WithoutFields/bep/log-10 465B ± 0% 385B ± 0% -17.20% (p=0.029 n=4+4) AccumulatedContext/bep/log-10 5.49kB ± 0% 5.09kB ± 0% -7.32% (p=0.029 n=4+4) AddingFields/bep/log-10 6.36kB ± 0% 5.96kB ± 0% -6.23% (p=0.029 n=4+4) name old allocs/op new allocs/op delta DisabledWithoutFields/bep/log-10 0.00 0.00 ~ (all equal) DisabledAccumulatedContext/bep/log-10 0.00 0.00 ~ (all equal) DisabledAddingFields/bep/log-10 0.00 0.00 ~ (all equal) WithoutFields/bep/log-10 7.00 ± 0% 6.00 ± 0% -14.29% (p=0.029 n=4+4) AccumulatedContext/bep/log-10 56.0 ± 0% 54.0 ± 0% -3.57% (p=0.029 n=4+4) AddingFields/bep/log-10 65.0 ± 0% 63.0 ± 0% -3.08% (p=0.029 n=4+4) ```
1 parent ccfaaa4 commit df28e24

File tree

10 files changed

+135
-90
lines changed

10 files changed

+135
-90
lines changed

‎entry.go‎

Lines changed: 45 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -9,34 +9,22 @@ import (
99

1010
// assert interface compliance.
1111
var (
12-
_ LevelLogger = (*EntryFields)(nil)
1312
_ LevelLogger = (*Entry)(nil)
1413
)
1514

16-
// EntryFields represents a single log entry at a given log level.
17-
type EntryFields struct {
18-
Logger *logger `json:"-"`
19-
Fields Fields `json:"-"`
20-
Level Level `json:"level"`
21-
}
22-
23-
// Entry holds a Entry with a Message and a Timestamp.
24-
// This is what is actually logged.
15+
// Entry represents a single log entry at a given log level.
2516
type Entry struct {
26-
*EntryFields
17+
logger *logger
18+
2719
Timestamp time.Time `json:"timestamp"`
20+
Level Level `json:"level"`
21+
Fields Fields `json:"-"`
2822
Message string `json:"message"`
2923
}
3024

31-
// FieldsDistinct returns a list of fields with duplicate names removed,
32-
// keeping the last.
33-
func (e *EntryFields) FieldsDistinct() Fields {
34-
return e.distinctFieldsLastByName()
35-
}
36-
3725
func (e Entry) MarshalJSON() ([]byte, error) {
3826
fields := make(map[string]any)
39-
for _, f := range e.FieldsDistinct() {
27+
for _, f := range e.Fields {
4028
fields[f.Name] = f.Value
4129
}
4230

@@ -51,18 +39,18 @@ func (e Entry) MarshalJSON() ([]byte, error) {
5139
}
5240

5341
// NewEntry returns a new entry for `log`.
54-
func NewEntry(log *logger) *EntryFields {
55-
return &EntryFields{
56-
Logger: log,
42+
func NewEntry(log *logger) *Entry {
43+
return &Entry{
44+
logger: log,
5745
}
5846
}
5947

60-
func (e EntryFields) WithLevel(level Level) *EntryFields {
48+
func (e Entry) WithLevel(level Level) *Entry {
6149
e.Level = level
6250
return &e
6351
}
6452

65-
func (e *EntryFields) WithFields(fielder Fielder) *EntryFields {
53+
func (e *Entry) WithFields(fielder Fielder) *Entry {
6654
if e.isLevelDisabled() {
6755
return e
6856
}
@@ -71,14 +59,14 @@ func (e *EntryFields) WithFields(fielder Fielder) *EntryFields {
7159
return &x
7260
}
7361

74-
func (e *EntryFields) WithField(key string, value any) *EntryFields {
62+
func (e *Entry) WithField(key string, value any) *Entry {
7563
if e.isLevelDisabled() {
7664
return e
7765
}
7866
return e.WithFields(Fields{{key, value}})
7967
}
8068

81-
func (e *EntryFields) WithDuration(d time.Duration) *EntryFields {
69+
func (e *Entry) WithDuration(d time.Duration) *Entry {
8270
if e.isLevelDisabled() {
8371
return e
8472
}
@@ -89,7 +77,7 @@ func (e *EntryFields) WithDuration(d time.Duration) *EntryFields {
8977
//
9078
// The given error may implement .Fielder, if it does the method
9179
// will add all its `.Fields()` into the returned entry.
92-
func (e *EntryFields) WithError(err error) *EntryFields {
80+
func (e *Entry) WithError(err error) *Entry {
9381
if err == nil || e.isLevelDisabled() {
9482
return e
9583
}
@@ -118,48 +106,52 @@ func (e *EntryFields) WithError(err error) *EntryFields {
118106
return ctx
119107
}
120108

121-
func (e *EntryFields) isLevelDisabled() bool {
122-
return e.Level < e.Logger.Level
109+
func (e *Entry) isLevelDisabled() bool {
110+
return e.Level < e.logger.Level
123111
}
124112

125113
// Log a message at the given level.
126-
func (e *EntryFields) Log(s fmt.Stringer) {
127-
e.Logger.log(e, s)
114+
func (e *Entry) Log(s fmt.Stringer) {
115+
e.logger.log(e, s)
116+
}
117+
118+
// Clone returns a new Entry with the same fields.
119+
func (e *Entry) Clone() *Entry {
120+
x := *e
121+
x.Fields = make(Fields, len(e.Fields))
122+
copy(x.Fields, e.Fields)
123+
return &x
128124
}
129125

130-
// distinctFieldsLastByName returns the fields with duplicate names removed,
131-
// keeping the rightmost field (last) with a given name.
132-
func (e *EntryFields) distinctFieldsLastByName() Fields {
133-
fields := make(Fields, 0, len(e.Fields))
126+
func (e *Entry) reset() {
127+
e.logger = nil
128+
e.Level = 0
129+
e.Fields = e.Fields[:0]
130+
e.Message = ""
131+
e.Timestamp = time.Time{}
132+
}
133+
134+
// finalize populates dst with Level and Fields merged from e and Message and Timestamp set.
135+
func (e *Entry) finalize(dst *Entry, msg string) {
136+
dst.Message = msg
137+
dst.Timestamp = e.logger.Clock.Now()
138+
dst.Level = e.Level
139+
140+
// There mau be fields logged with the same name, keep the latest.
134141
for i := len(e.Fields) - 1; i >= 0; i-- {
135142
f := e.Fields[i]
136143
var seen bool
137-
for _, f2 := range fields {
144+
for _, f2 := range dst.Fields {
138145
if f.Name == f2.Name {
139146
seen = true
140147
break
141148
}
142149
}
143150
if !seen {
144151
// Insert first.
145-
fields = append(fields, Field{})
146-
copy(fields[1:], fields[:])
147-
fields[0] = f
152+
dst.Fields = append(dst.Fields, Field{})
153+
copy(dst.Fields[1:], dst.Fields[:])
154+
dst.Fields[0] = f
148155
}
149156
}
150-
151-
if len(fields) == 0 {
152-
return nil
153-
}
154-
155-
return fields
156-
}
157-
158-
// finalize returns a copy of the Entry with Fields merged.
159-
func (e *EntryFields) finalize(msg string) *Entry {
160-
return &Entry{
161-
EntryFields: e,
162-
Message: msg,
163-
Timestamp: e.Logger.Clock.Now(),
164-
}
165157
}

‎entry_test.go‎

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,38 +15,38 @@ func TestEntry_WithFields(t *testing.T) {
1515

1616
c := a.WithFields(Fields{{"foo", "hello"}, {"bar", "world"}})
1717
d := c.WithFields(Fields{{"baz", "jazz"}})
18-
qt.Assert(t, b.distinctFieldsLastByName(), qt.DeepEquals, Fields{{"foo", "bar"}})
19-
qt.Assert(t, c.distinctFieldsLastByName(), qt.DeepEquals, Fields{{"foo", "hello"}, {"bar", "world"}})
20-
qt.Assert(t, d.distinctFieldsLastByName(), qt.DeepEquals, Fields{{"foo", "hello"}, {"bar", "world"}, {"baz", "jazz"}})
18+
qt.Assert(t, b.Fields, qt.DeepEquals, Fields{{"foo", "bar"}})
19+
qt.Assert(t, c.Fields, qt.DeepEquals, Fields{{"foo", "hello"}, {"bar", "world"}})
20+
qt.Assert(t, d.Fields, qt.DeepEquals, Fields{{"foo", "hello"}, {"bar", "world"}, {"baz", "jazz"}})
2121

22-
e := c.finalize("upload")
22+
/*e := c.finalize("upload")
2323
qt.Assert(t, "upload", qt.Equals, e.Message)
2424
qt.Assert(t, Fields{{"foo", "hello"}, {"bar", "world"}}, qt.DeepEquals, e.Fields)
2525
qt.Assert(t, InfoLevel, qt.Equals, e.Level)
26-
qt.Assert(t, time.Now().IsZero(), qt.IsFalse)
26+
qt.Assert(t, time.Now().IsZero(), qt.IsFalse)*/
2727
}
2828

2929
func TestEntry_WithField(t *testing.T) {
3030
a := NewLogger(LoggerConfig{Handler: NoopHandler, Level: InfoLevel}).WithLevel(InfoLevel)
3131
b := a.WithField("foo", "baz").WithField("foo", "bar")
32-
qt.Assert(t, a.distinctFieldsLastByName(), qt.IsNil)
33-
qt.Assert(t, b.distinctFieldsLastByName(), qt.DeepEquals, Fields{{"foo", "bar"}})
32+
qt.Assert(t, a.Fields, qt.IsNil)
33+
qt.Assert(t, b.Fields, qt.DeepEquals, Fields{{"foo", "bar"}})
3434
}
3535

3636
func TestEntry_WithError(t *testing.T) {
3737
a := NewLogger(LoggerConfig{Handler: NoopHandler, Level: InfoLevel}).WithLevel(InfoLevel)
3838
b := a.WithError(fmt.Errorf("boom"))
39-
qt.Assert(t, a.distinctFieldsLastByName(), qt.IsNil)
40-
qt.Assert(t, b.distinctFieldsLastByName(), qt.DeepEquals, Fields{{"error", "boom"}})
39+
qt.Assert(t, a.Fields, qt.IsNil)
40+
qt.Assert(t, b.Fields, qt.DeepEquals, Fields{{"error", "boom"}})
4141
}
4242

4343
func TestEntry_WithError_fields(t *testing.T) {
4444
a := NewLogger(LoggerConfig{Handler: NoopHandler, Level: InfoLevel}).WithLevel(InfoLevel)
4545
b := a.WithError(errFields("boom"))
46-
qt.Assert(t, a.distinctFieldsLastByName(), qt.IsNil)
46+
qt.Assert(t, a.Fields, qt.IsNil)
4747
qt.Assert(t,
4848

49-
b.distinctFieldsLastByName(), qt.DeepEquals, Fields{
49+
b.Fields, qt.DeepEquals, Fields{
5050
{"error", "boom"},
5151
{"reason", "timeout"},
5252
})
@@ -56,14 +56,14 @@ func TestEntry_WithError_fields(t *testing.T) {
5656
func TestEntry_WithError_nil(t *testing.T) {
5757
a := NewLogger(LoggerConfig{Handler: NoopHandler, Level: InfoLevel}).WithLevel(InfoLevel)
5858
b := a.WithError(nil)
59-
qt.Assert(t, a.distinctFieldsLastByName(), qt.IsNil)
60-
qt.Assert(t, b.distinctFieldsLastByName(), qt.IsNil)
59+
qt.Assert(t, a.Fields, qt.IsNil)
60+
qt.Assert(t, b.Fields, qt.IsNil)
6161
}
6262

6363
func TestEntry_WithDuration(t *testing.T) {
6464
a := NewLogger(LoggerConfig{Handler: NoopHandler, Level: InfoLevel}).WithLevel(InfoLevel)
6565
b := a.WithDuration(time.Second * 2)
66-
qt.Assert(t, b.distinctFieldsLastByName(), qt.DeepEquals, Fields{{"duration", int64(2000)}})
66+
qt.Assert(t, b.Fields, qt.DeepEquals, Fields{{"duration", int64(2000)}})
6767
}
6868

6969
type errFields string

‎handlers/cli/cli.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func (h *Handler) HandleLog(e *log.Entry) error {
6767

6868
color.Fprintf(h.Writer, "%s %-25s", bold.Sprintf("%*s", h.Padding+1, level), e.Message)
6969

70-
for _, field := range e.FieldsDistinct() {
70+
for _, field := range e.Fields {
7171
if field.Name == "source" {
7272
continue
7373
}

‎handlers/memory/memory.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@ func New() *Handler {
2323
func (h *Handler) HandleLog(e *log.Entry) error {
2424
h.mu.Lock()
2525
defer h.mu.Unlock()
26-
h.Entries = append(h.Entries, e)
26+
h.Entries = append(h.Entries, e.Clone())
2727
return nil
2828
}

‎handlers/text/text.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func (h *Handler) HandleLog(e *log.Entry) error {
6969
ts := time.Since(start) / time.Second
7070
fmt.Fprintf(h.Writer, "\033[%dm%6s\033[0m[%04d] %-25s", color, level, ts, e.Message)
7171

72-
for _, f := range e.FieldsDistinct() {
72+
for _, f := range e.Fields {
7373
fmt.Fprintf(h.Writer, " \033[%dm%s\033[0m=%v", color, f.Name, f.Value)
7474
}
7575

‎interface.go‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
// Logger is the main interface for the logger.
99
type Logger interface {
1010
// WithLevel returns a new entry with `level` set.
11-
WithLevel(Level) *EntryFields
11+
WithLevel(Level) *Entry
1212
}
1313

1414
// LevelLogger
@@ -18,22 +18,22 @@ type LevelLogger interface {
1818
Log(s fmt.Stringer)
1919

2020
// WithLevel returns a new entry with `level` set.
21-
WithLevel(Level) *EntryFields
21+
WithLevel(Level) *Entry
2222

2323
// WithFields returns a new entry with the`fields` in fields set.
2424
// This is a noop if LevelLogger's level is less than Logger's.
25-
WithFields(fields Fielder) *EntryFields
25+
WithFields(fields Fielder) *Entry
2626

2727
// WithLevel returns a new entry with the field f set with value v
2828
// This is a noop if LevelLogger's level is less than Logger's.
29-
WithField(f string, v any) *EntryFields
29+
WithField(f string, v any) *Entry
3030

3131
// WithDuration returns a new entry with the "duration" field set
3232
// to the given duration in milliseconds.
3333
// This is a noop if LevelLogger's level is less than Logger's.
34-
WithDuration(time.Duration) *EntryFields
34+
WithDuration(time.Duration) *Entry
3535

3636
// WithError returns a new entry with the "error" set to `err`.
3737
// This is a noop if err is nil or LevelLogger's level is less than Logger's.
38-
WithError(error) *EntryFields
38+
WithError(error) *Entry
3939
}

‎levels_test.go‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,7 @@ func TestParseLevel(t *testing.T) {
3939
func TestLevel_MarshalJSON(t *testing.T) {
4040
e := Entry{
4141
Message: "hello",
42-
EntryFields: &EntryFields{
43-
Level: InfoLevel,
44-
},
42+
Level: InfoLevel,
4543
}
4644

4745
expect := `{"fields":{},"level":"info","timestamp":"0001-01-01T00:00:00Z","message":"hello"}`

‎logger.go‎

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,11 @@ func (f HandlerFunc) HandleLog(e *Entry) error {
7979
//
8080
// It is left up to Handlers to implement thread-safety.
8181
type Handler interface {
82-
HandleLog(*Entry) error
82+
// HandleLog is invoked for each log event.
83+
// Note that i e is going to be used after the call to HandleLog returns,
84+
// it must be cloned with e.Clone().
85+
// TODO(bep) interface?
86+
HandleLog(e *Entry) error
8387
}
8488

8589
// NoopHandler is a no-op handler that discards all log messages.
@@ -139,43 +143,45 @@ type Clock interface {
139143
}
140144

141145
// WithLevel returns a new entry with `level` set.
142-
func (l *logger) WithLevel(level Level) *EntryFields {
146+
func (l *logger) WithLevel(level Level) *Entry {
143147
return NewEntry(l).WithLevel(level)
144148
}
145149

146150
// WithFields returns a new entry with `fields` set.
147-
func (l *logger) WithFields(fields Fielder) *EntryFields {
151+
func (l *logger) WithFields(fields Fielder) *Entry {
148152
return NewEntry(l).WithFields(fields.Fields())
149153
}
150154

151155
// WithField returns a new entry with the `key` and `value` set.
152156
//
153157
// Note that the `key` should not have spaces in it - use camel
154158
// case or underscores
155-
func (l *logger) WithField(key string, value any) *EntryFields {
159+
func (l *logger) WithField(key string, value any) *Entry {
156160
return NewEntry(l).WithField(key, value)
157161
}
158162

159163
// WithDuration returns a new entry with the "duration" field set
160164
// to the given duration in milliseconds.
161-
func (l *logger) WithDuration(d time.Duration) *EntryFields {
165+
func (l *logger) WithDuration(d time.Duration) *Entry {
162166
return NewEntry(l).WithDuration(d)
163167
}
164168

165169
// WithError returns a new entry with the "error" set to `err`.
166-
func (l *logger) WithError(err error) *EntryFields {
170+
func (l *logger) WithError(err error) *Entry {
167171
return NewEntry(l).WithError(err)
168172
}
169173

170-
// log the message, invoking the handler. We clone the entry here
171-
// to bypass the overhead in Entry methods when the level is not
172-
// met.
173-
func (l *logger) log(e *EntryFields, s fmt.Stringer) {
174+
// log the message, invoking the handler.
175+
func (l *logger) log(e *Entry, s fmt.Stringer) {
174176
if e.Level < l.Level {
175177
return
176178
}
177179

178-
if err := l.Handler.HandleLog(e.finalize(s.String())); err != nil {
180+
finalized := objectPools.GetEntry()
181+
defer objectPools.PutEntry(finalized)
182+
e.finalize(finalized, s.String())
183+
184+
if err := l.Handler.HandleLog(finalized); err != nil {
179185
stdlog.Printf("error logging: %s", err)
180186
}
181187

0 commit comments

Comments
 (0)