Skip to content

Commit 2afeab7

Browse files
committed
fix logging to correctly manage attributes
1 parent 7edc23a commit 2afeab7

File tree

2 files changed

+239
-135
lines changed

2 files changed

+239
-135
lines changed

logger.go

Lines changed: 123 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"runtime"
99
"runtime/debug"
1010
"strings"
11+
"sync"
1112
"time"
1213

1314
"github.com/lmittmann/tint"
@@ -41,15 +42,20 @@ func SLog(ctx context.Context) *slog.Logger {
4142
return Log(ctx).SLog()
4243
}
4344

44-
type iLogger struct {
45-
ctx context.Context
46-
// Function to exit the application, defaults to `os.Exit()`
47-
ExitFunc exitFunc
48-
45+
// LogEntry handles logging functionality with immutable chained calls
46+
type LogEntry struct {
47+
ctx context.Context
4948
log *slog.Logger
5049
stackTraces bool
5150
}
5251

52+
// logEntryPool maintains a pool of LogEntry objects to reduce GC pressure
53+
var logEntryPool = sync.Pool{
54+
New: func() interface{} {
55+
return &LogEntry{}
56+
},
57+
}
58+
5359
type LogOptions struct {
5460
*slog.HandlerOptions
5561
PrintFormat string
@@ -90,6 +96,7 @@ func ParseLevel(levelStr string) (slog.Level, error) {
9096
}
9197
}
9298

99+
// NewLogger creates a new instance of LogEntry with the provided context and options
93100
func NewLogger(ctx context.Context, opts *LogOptions) *LogEntry {
94101
logLevel := opts.Level.Level()
95102
outputWriter := os.Stdout
@@ -105,189 +112,175 @@ func NewLogger(ctx context.Context, opts *LogOptions) *LogEntry {
105112
}
106113

107114
tintHandler := tint.NewHandler(outputWriter, handlerOptions)
108-
109115
log := slog.New(tintHandler)
110-
111116
slog.SetDefault(log)
112117

113-
il := &iLogger{ctx: ctx, log: log, stackTraces: opts.ShowStackTrace}
114-
return newLogEntry(il)
115-
}
116-
117-
func (l *iLogger) clone(ctx context.Context) *iLogger {
118-
sl := *l.log
119-
return &iLogger{ctx: ctx, log: &sl, stackTraces: l.stackTraces}
120-
}
121-
122-
func (l *iLogger) WithError(err error) {
123-
l.log = l.log.With(tint.Err(err))
118+
// Get a LogEntry from the pool
119+
entry := logEntryPool.Get().(*LogEntry)
120+
entry.ctx = ctx
121+
entry.log = log
122+
entry.stackTraces = opts.ShowStackTrace
123+
124+
return entry
124125
}
125126

126-
func (l *iLogger) WithAttr(attr ...any) {
127-
l.log = l.log.With(attr...)
128-
}
129-
130-
func (l *iLogger) WithField(key string, value any) {
131-
l.log = l.log.With(key, value)
132-
}
133-
134-
func (l *iLogger) With(args ...any) {
135-
l.log = l.log.With(args...)
136-
}
137-
138-
func (l *iLogger) _ctx() context.Context {
139-
if l.ctx == nil {
140-
return context.Background()
127+
// Release returns the LogEntry to the pool for reuse
128+
// Call this when you're done with a LogEntry and won't use it again
129+
func (e *LogEntry) Release() {
130+
if e == nil {
131+
return
141132
}
142-
return l.ctx
143-
}
144133

145-
func (l *iLogger) Log(ctx context.Context, level slog.Level, msg string, fields ...any) {
146-
l.log.Log(ctx, level, msg, fields...)
147-
}
134+
// Reset fields before returning to pool
135+
e.ctx = nil
136+
e.log = nil
137+
e.stackTraces = false
148138

149-
func (l *iLogger) Logf(ctx context.Context, level slog.Level, format string, args ...interface{}) {
150-
if l.Enabled(ctx, level) {
151-
l.log.Log(ctx, level, fmt.Sprintf(format, args...))
152-
}
153-
}
154-
155-
func (l *iLogger) Trace(msg string, args ...any) {
156-
l.Debug(msg, args...)
157-
}
158-
159-
func (l *iLogger) Debug(msg string, args ...any) {
160-
log := l.withFileLineNum()
161-
log.DebugContext(l._ctx(), msg, args...)
162-
}
163-
164-
func (l *iLogger) Info(msg string, args ...any) {
165-
l.log.InfoContext(l._ctx(), msg, args...)
166-
}
167-
168-
func (l *iLogger) Warn(msg string, args ...any) {
169-
l.log.WarnContext(l._ctx(), msg, args...)
139+
logEntryPool.Put(e)
170140
}
171141

172-
func (l *iLogger) Error(msg string, args ...any) {
173-
log := l.withFileLineNum()
174-
175-
if l.stackTraces {
176-
log.ErrorContext(l._ctx(), fmt.Sprintf(" %s\n%s\n", msg, debug.Stack()), args...)
142+
// clone creates a new LogEntry with the same properties as the original
143+
func (e *LogEntry) clone() *LogEntry {
144+
if e == nil {
145+
return NewLogger(context.Background(), DefaultLogOptions())
177146
}
178147

179-
log.ErrorContext(l._ctx(), msg, args...)
180-
}
181-
182-
func (l *iLogger) Fatal(msg string, args ...any) {
183-
log := l.withFileLineNum()
148+
// Get a new entry from the pool
149+
clone := logEntryPool.Get().(*LogEntry)
150+
clone.ctx = e.ctx
151+
clone.log = e.log
152+
clone.stackTraces = e.stackTraces
184153

185-
if l.stackTraces {
186-
log.ErrorContext(l._ctx(), fmt.Sprintf(" %s\n%s\n", msg, debug.Stack()), args...)
187-
}
188-
l.log.ErrorContext(l._ctx(), msg, args...)
189-
l.Exit(1)
154+
return clone
190155
}
191156

192-
func (l *iLogger) Panic(msg string, _ ...any) {
193-
panic(fmt.Sprintf(" %s\n%s\n", msg, debug.Stack()))
157+
// WithContext returns a new LogEntry with the given context
158+
func (e *LogEntry) WithContext(ctx context.Context) *LogEntry {
159+
clone := e.clone()
160+
clone.ctx = ctx
161+
return clone
194162
}
195163

196-
func (l *iLogger) Exit(code int) {
197-
if l.ExitFunc == nil {
198-
l.ExitFunc = os.Exit
199-
}
200-
l.ExitFunc(code)
164+
// WithError returns a new LogEntry with the error added
165+
func (e *LogEntry) WithError(err error) *LogEntry {
166+
return e.With(tint.Err(err))
201167
}
202168

203-
func (l *iLogger) Enabled(ctx context.Context, level slog.Level) bool {
204-
return l.log.Enabled(ctx, level)
169+
// WithField returns a new LogEntry with the field added
170+
func (e *LogEntry) WithField(key string, value any) *LogEntry {
171+
return e.With(key, value)
205172
}
206173

207-
func (l *iLogger) withFileLineNum() *slog.Logger {
208-
_, file, line, ok := runtime.Caller(CallerDepth)
209-
if ok {
210-
return l.log.With(tint.Attr(FileLineAttr, slog.Any("file", fmt.Sprintf("%s:%d", file, line))))
174+
// With returns a new LogEntry with the provided attributes added
175+
func (e *LogEntry) With(args ...any) *LogEntry {
176+
// No args, return the same logger
177+
if len(args) == 0 {
178+
return e
211179
}
212-
return l.log
213-
}
214-
215-
// LogEntry Need a type to handle the chained calls.
216-
type LogEntry struct {
217-
l *iLogger
218-
}
219180

220-
func newLogEntry(l *iLogger) *LogEntry {
221-
return &LogEntry{l: l}
181+
clone := e.clone()
182+
clone.log = clone.log.With(args...)
183+
return clone
222184
}
223185

224-
type exitFunc func(int)
225-
226-
func (e *LogEntry) LevelEnabled(ctx context.Context, level slog.Level) bool {
227-
return e.l.Enabled(ctx, level)
228-
}
229-
230-
func (e *LogEntry) SLog() *slog.Logger {
231-
return e.l.log
232-
}
233-
234-
func (e *LogEntry) WithContext(ctx context.Context) *LogEntry {
235-
return newLogEntry(e.l.clone(ctx))
186+
// _ctx returns the context or background if nil
187+
func (e *LogEntry) _ctx() context.Context {
188+
if e.ctx == nil {
189+
return context.Background()
190+
}
191+
return e.ctx
236192
}
237193

194+
// Log logs a message at the given level
238195
func (e *LogEntry) Log(ctx context.Context, level slog.Level, msg string, fields ...any) {
239-
e.l.Log(ctx, level, msg, fields...)
196+
e.log.Log(ctx, level, msg, fields...)
240197
}
198+
199+
// Logf logs a formatted message at the given level
241200
func (e *LogEntry) Logf(ctx context.Context, level slog.Level, format string, args ...interface{}) {
242-
e.l.Logf(ctx, level, format, args...)
201+
if e.Enabled(ctx, level) {
202+
e.log.Log(ctx, level, fmt.Sprintf(format, args...))
203+
}
243204
}
244205

206+
// Trace logs a message at debug level (alias for backward compatibility)
245207
func (e *LogEntry) Trace(msg string, args ...any) {
246-
e.l.Debug(msg, args...)
208+
e.Debug(msg, args...)
247209
}
248210

211+
// Debug logs a message at debug level
249212
func (e *LogEntry) Debug(msg string, args ...any) {
250-
e.l.Debug(msg, args...)
213+
log := e.withFileLineNum()
214+
log.DebugContext(e._ctx(), msg, args...)
251215
}
252216

217+
// Info logs a message at info level
253218
func (e *LogEntry) Info(msg string, args ...any) {
254-
e.l.Info(msg, args...)
219+
e.log.InfoContext(e._ctx(), msg, args...)
255220
}
256221

222+
// Printf logs a formatted message at info level
257223
func (e *LogEntry) Printf(format string, args ...any) {
258-
e.l.Logf(e.l._ctx(), slog.LevelInfo, format, args...)
224+
e.Logf(e._ctx(), slog.LevelInfo, format, args...)
259225
}
260226

227+
// Warn logs a message at warn level
261228
func (e *LogEntry) Warn(msg string, args ...any) {
262-
e.l.Warn(msg, args...)
229+
e.log.WarnContext(e._ctx(), msg, args...)
263230
}
264231

232+
// Error logs a message at error level
265233
func (e *LogEntry) Error(msg string, args ...any) {
266-
e.l.Error(msg, args...)
234+
log := e.withFileLineNum()
235+
236+
if e.stackTraces {
237+
log.ErrorContext(e._ctx(), fmt.Sprintf(" %s\n%s\n", msg, debug.Stack()), args...)
238+
}
239+
240+
log.ErrorContext(e._ctx(), msg, args...)
267241
}
268242

243+
// Fatal logs a message at error level and exits with code 1
269244
func (e *LogEntry) Fatal(msg string, args ...any) {
270-
e.l.Fatal(msg, args...)
245+
log := e.withFileLineNum()
246+
247+
if e.stackTraces {
248+
log.ErrorContext(e._ctx(), fmt.Sprintf(" %s\n%s\n", msg, debug.Stack()), args...)
249+
}
250+
e.log.ErrorContext(e._ctx(), msg, args...)
251+
e.Exit(1)
271252
}
272253

254+
// Panic logs a message and panics
273255
func (e *LogEntry) Panic(msg string, _ ...any) {
274-
e.l.Panic(msg)
256+
panic(fmt.Sprintf(" %s\n%s\n", msg, debug.Stack()))
275257
}
276258

277-
func (e *LogEntry) WithAttr(attr ...any) *LogEntry {
278-
e.l.WithAttr(attr...)
279-
return e
259+
// Exit terminates the application with the given code
260+
func (e *LogEntry) Exit(code int) {
261+
os.Exit(code)
280262
}
281263

282-
func (e *LogEntry) WithError(err error) *LogEntry {
283-
e.l.WithError(err)
284-
return e
264+
// Enabled returns whether the logger will log at the given level
265+
func (e *LogEntry) Enabled(ctx context.Context, level slog.Level) bool {
266+
return e.log.Enabled(ctx, level)
285267
}
286-
func (e *LogEntry) WithField(key string, value any) *LogEntry {
287-
e.l.WithField(key, value)
288-
return e
268+
269+
// LevelEnabled is an alias for Enabled for backward compatibility
270+
func (e *LogEntry) LevelEnabled(ctx context.Context, level slog.Level) bool {
271+
return e.Enabled(ctx, level)
289272
}
290-
func (e *LogEntry) With(args ...any) *LogEntry {
291-
e.l.With(args...)
292-
return e
273+
274+
// SLog returns the underlying slog.Logger
275+
func (e *LogEntry) SLog() *slog.Logger {
276+
return e.log
277+
}
278+
279+
// withFileLineNum adds file and line information to the log entry
280+
func (e *LogEntry) withFileLineNum() *slog.Logger {
281+
_, file, line, ok := runtime.Caller(CallerDepth)
282+
if ok {
283+
return e.log.With(tint.Attr(FileLineAttr, slog.Any("file", fmt.Sprintf("%s:%d", file, line))))
284+
}
285+
return e.log
293286
}

0 commit comments

Comments
 (0)