Skip to content

Commit a818939

Browse files
committed
[logger] implement automatic caller capture
Add automatic caller information capture for log entries at or below a specified level threshold. This allows tracking source location of important log messages without manual caller information. - Add WithCallerAtLevel option to enable automatic caller capture - Add caller mapper with DefaultCallerMapper and WithCallerMapper - Implement internal log method for uniform caller frame capture Change-Id: Iede6f8268f54fbcefda911d6921ad31aff0e7526
1 parent e4d4715 commit a818939

3 files changed

Lines changed: 240 additions & 22 deletions

File tree

logger/doc.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,24 @@
4242
//
4343
// lgr := logger.New(adapter, logger.WithLevel(logger.LevelDebug))
4444
//
45+
// # Automatic Caller Capture
46+
//
47+
// The logger can automatically capture and include caller information (file and
48+
// line number) for log entries at or below a specified level threshold. This is
49+
// useful for tracking the source location of important log messages like errors
50+
// and warnings without manually adding caller information.
51+
//
52+
// Enable automatic caller capture using WithCallerAtLevel:
53+
//
54+
// // Capture caller for Error and Warning logs only
55+
// lgr := logger.New(adapter, logger.WithCallerAtLevel(logger.LevelWarning))
56+
// lgr.Error("operation failed", err) // includes caller: "/path/to/file.go:123"
57+
// lgr.Info("processing") // no caller information
58+
//
59+
// By default, automatic caller capture is disabled. When enabled, the caller
60+
// information is formatted and added as a field to log entries using the caller
61+
// mapper (see Customization section below).
62+
//
4563
// Each level has two methods: one without an error parameter (Info, Warning,
4664
// Debug, Trace) and one with an error parameter (InfoE, WarningE, DebugE,
4765
// TraceE). The Error is, obviously, singular and has the error parameter, though
@@ -102,7 +120,7 @@
102120
// For that reason logger abstracts these concepts passing it to the adapters as
103121
// fields, with the help of mappers.
104122
//
105-
// The logger provides three types of mappers:
123+
// The logger provides four types of mappers:
106124
//
107125
// Name Mapper: Converts logger names (from WithName) to fields. By default,
108126
// creates a field with key "logger-name":
@@ -138,6 +156,17 @@
138156
// return fields.F("stack", st.String()) // Use "stack" instead of "stacktrace"
139157
// }))
140158
//
159+
// Caller Mapper: Converts caller frames (from automatic caller capture) to
160+
// fields. By default, creates a field with key "caller" formatted as
161+
// "file:line":
162+
//
163+
// lgr := logger.New(adapter,
164+
// logger.WithCallerAtLevel(logger.LevelError), // Enable automatic capture
165+
// logger.WithCallerMapper(func(frame stacktrace.Frame) fields.Field {
166+
// return fields.F("source", frame.ShortPath()) // Use "source" and short path
167+
// }),
168+
// )
169+
//
141170
// All mappers and formatters can be customized independently during logger
142171
// creation.
143172
//

logger/logger.go

Lines changed: 89 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,29 @@ func WithLevel(level int) Option {
3131
}
3232
}
3333

34+
// WithCallerAtLevel enables automatic caller information capture for log
35+
// entries with level less or equal passed threshold.
36+
//
37+
// When enabled, the logger will automatically capture and include caller
38+
// information (formatted as "/path/to/file:line") as a field in log entries whose
39+
// level is less or equal the threshold. For example, WithCallerAtLevel(LevelWarning)
40+
// will add caller information to Error and Warning logs, but no others.
41+
//
42+
// By default, automatic caller capture is disabled. Use this option when you
43+
// need to track the source location of important log messages like errors and
44+
// warnings.
45+
func WithCallerAtLevel(level int) Option {
46+
return func(l *Logger) {
47+
l.callerMaxLevel = level
48+
}
49+
}
50+
3451
// mappers holds mapping configurations for [Logger].
3552
type mappers struct {
3653
name func(name string) fields.Field
3754
error func(err error) fields.Field
3855
stackTrace func(st *stacktrace.Stack) fields.Field
56+
caller func(frame stacktrace.Frame) fields.Field
3957
}
4058

4159
type mapperOption func(*mappers)
@@ -179,12 +197,37 @@ func WithStackTraceMapper(fn func(st *stacktrace.Stack) fields.Field) Option {
179197
})
180198
}
181199

200+
// DefaultCallerMapper is the default mapper for converting caller frames to
201+
// fields. It creates a field with key "caller" and formats the frame as
202+
// "file:line".
203+
//
204+
// Example output: "/path/to/logger.go:123".
205+
func DefaultCallerMapper(frame stacktrace.Frame) fields.Field {
206+
return fields.F("caller", frame.FullPath())
207+
}
208+
209+
// WithCallerMapper sets a custom caller mapper for the logger.
210+
//
211+
// The caller mapper controls how caller frames (captured via automatic caller
212+
// capture with [WithCallerAtLevel]) are converted to fields. By default,
213+
// [DefaultCallerMapper] is used, which creates fields with the key "caller"
214+
// and formats frames as "file:line".
215+
//
216+
// Custom mappers can change the field key, format caller information
217+
// differently, or include additional frame details like function names.
218+
func WithCallerMapper(fn func(frame stacktrace.Frame) fields.Field) Option {
219+
return withMapperOption(func(cfg *mappers) {
220+
cfg.caller = fn
221+
})
222+
}
223+
182224
// defaultMappers returns a mappers structure initialized with default mappers.
183225
func defaultMappers() *mappers {
184226
return &mappers{
185227
name: DefaultNameMapper,
186228
error: DefaultErrorMapper,
187229
stackTrace: DefaultStackTraceMapper,
230+
caller: DefaultCallerMapper,
188231
}
189232
}
190233

@@ -203,6 +246,11 @@ type Logger struct {
203246

204247
name string
205248
nameFormatter func(prev, next string) string
249+
250+
// callerMaxLevel is the maximum log-level at which caller information is
251+
// automatically captured and added to log entries. Levels at or below this
252+
// threshold will include caller information. Set to -1 to disable.
253+
callerMaxLevel int
206254
}
207255

208256
// New creates new [Logger] with maximum log-level set to LevelInfo and default
@@ -215,11 +263,12 @@ func New(adapter Adapter, opts ...Option) Logger {
215263
}
216264

217265
l := Logger{
218-
maxLevel: LevelInfo,
219-
adapter: adapter,
220-
mappers: defaultMappers(),
221-
name: "",
222-
nameFormatter: NameFormatterHierarchical,
266+
maxLevel: LevelInfo,
267+
adapter: adapter,
268+
mappers: defaultMappers(),
269+
name: "",
270+
nameFormatter: NameFormatterHierarchical,
271+
callerMaxLevel: math.MinInt,
223272
}
224273

225274
lp := &l
@@ -234,11 +283,12 @@ func New(adapter Adapter, opts ...Option) Logger {
234283
// will also create no-op loggers.
235284
func NewNop() Logger {
236285
return Logger{
237-
maxLevel: math.MaxInt,
238-
adapter: nil,
239-
mappers: nil,
240-
name: "",
241-
nameFormatter: nil,
286+
maxLevel: math.MaxInt,
287+
adapter: nil,
288+
mappers: nil,
289+
name: "",
290+
nameFormatter: nil,
291+
callerMaxLevel: math.MinInt,
242292
}
243293
}
244294

@@ -258,7 +308,7 @@ func (l Logger) IsNop() bool {
258308
// where the application cannot continue. It's OK to pass nil as the error.
259309
// To attach a stack trace, use [Logger.WithStackTrace].
260310
func (l Logger) Error(msg string, err error, fs ...fields.Field) {
261-
l.Log(LevelError, msg, err, fs...)
311+
l.log(LevelError, msg, err, fs...)
262312
}
263313

264314
// Warning logs a message with the [LevelWarning] log-level.
@@ -267,7 +317,7 @@ func (l Logger) Error(msg string, err error, fs ...fields.Field) {
267317
// prevent the application from continuing, such as a deprecated API usage or
268318
// a retry-able failure. For warnings with an error, use [Logger.WarningE].
269319
func (l Logger) Warning(msg string, fs ...fields.Field) {
270-
l.Log(LevelWarning, msg, nil, fs...)
320+
l.log(LevelWarning, msg, nil, fs...)
271321
}
272322

273323
// WarningE logs a message with the [LevelWarning] log-level and the provided
@@ -276,55 +326,55 @@ func (l Logger) Warning(msg string, fs ...fields.Field) {
276326
// Use WarningE to log any recoverable error, such as an error during a remote
277327
// API call where the service did not respond and the application will retry.
278328
func (l Logger) WarningE(msg string, err error, fs ...fields.Field) {
279-
l.Log(LevelWarning, msg, err, fs...)
329+
l.log(LevelWarning, msg, err, fs...)
280330
}
281331

282332
// Info logs a message with the [LevelInfo] log-level.
283333
//
284334
// Use Info to log informational messages that highlight the progress of the
285335
// application.
286336
func (l Logger) Info(msg string, fs ...fields.Field) {
287-
l.Log(LevelInfo, msg, nil, fs...)
337+
l.log(LevelInfo, msg, nil, fs...)
288338
}
289339

290340
// InfoE logs a message with the [LevelInfo] log-level and the provided error.
291341
//
292342
// Use InfoE to log informational messages that highlight the progress of the
293343
// application along with an error.
294344
func (l Logger) InfoE(msg string, err error, fs ...fields.Field) {
295-
l.Log(LevelInfo, msg, err, fs...)
345+
l.log(LevelInfo, msg, err, fs...)
296346
}
297347

298348
// Debug logs a message with the [LevelDebug] log-level.
299349
//
300350
// Use Debug to log detailed information that is useful during development and
301351
// debugging.
302352
func (l Logger) Debug(msg string, fs ...fields.Field) {
303-
l.Log(LevelDebug, msg, nil, fs...)
353+
l.log(LevelDebug, msg, nil, fs...)
304354
}
305355

306356
// DebugE logs a message with the [LevelDebug] log-level and the provided error.
307357
//
308358
// Use DebugE to log detailed information that is useful during development and
309359
// debugging along with an error.
310360
func (l Logger) DebugE(msg string, err error, fs ...fields.Field) {
311-
l.Log(LevelDebug, msg, err, fs...)
361+
l.log(LevelDebug, msg, err, fs...)
312362
}
313363

314364
// Trace logs a message with the [LevelTrace] log-level.
315365
//
316366
// Use Trace to log very detailed information, typically of interest only when
317367
// diagnosing problems.
318368
func (l Logger) Trace(msg string, fs ...fields.Field) {
319-
l.Log(LevelTrace, msg, nil, fs...)
369+
l.log(LevelTrace, msg, nil, fs...)
320370
}
321371

322372
// TraceE logs a message with the [LevelTrace] log-level and the provided error.
323373
//
324374
// Use TraceE to log very detailed information, typically of interest only when
325375
// diagnosing problems along with an error.
326376
func (l Logger) TraceE(msg string, err error, fs ...fields.Field) {
327-
l.Log(LevelTrace, msg, err, fs...)
377+
l.log(LevelTrace, msg, err, fs...)
328378
}
329379

330380
// Log logs a message with the given log-level, optional error, and fields.
@@ -340,10 +390,26 @@ func (l Logger) TraceE(msg string, err error, fs ...fields.Field) {
340390
//
341391
// For no-op loggers, this method returns immediately without any operation.
342392
func (l Logger) Log(level int, msg string, err error, fs ...fields.Field) {
393+
l.log(level, msg, err, fs...)
394+
}
395+
396+
// log is the internal logging method, its sole reason to exist is to uniformly
397+
// catch caller frame in case it is required.
398+
//
399+
//revive:disable-next-line:confusing-naming
400+
func (l Logger) log(level int, msg string, err error, fs ...fields.Field) {
343401
if level > l.maxLevel || l.IsNop() {
344402
return
345403
}
346404

405+
if level <= l.callerMaxLevel {
406+
const callerSkip = 2 // skip log and the calling method (Error, Info, etc.)
407+
408+
frame := stacktrace.CaptureCaller(callerSkip)
409+
410+
fs = append(fs, l.mappers.caller(frame))
411+
}
412+
347413
if l.name != "" {
348414
fs = append(fs, l.mappers.name(l.name))
349415
}
@@ -435,7 +501,9 @@ func (l Logger) Flush() error {
435501
// created via [New] or [NewNop]. Zero-value loggers should not be used, but in
436502
// rare cases it is required to check if value is not initialized.
437503
func (l Logger) IsZero() bool {
438-
return l.adapter == nil && l.maxLevel == 0 && l.mappers == nil
504+
// properly created loggers have adapters set and callerMaxLevel initialized,
505+
// therefore check for those fields should suffice.
506+
return l.adapter == nil && l.callerMaxLevel == 0
439507
}
440508

441509
// formatterIsEqual checks if f1 is equal to f2.
@@ -470,6 +538,6 @@ func IsEqual(l1, l2 Logger) bool {
470538
// expect an error logging function rather than a full logger.
471539
func NewErrorLogger(lgr Logger, level int) e.ErrorLogger {
472540
return func(msg string, err error, fs ...fields.Field) {
473-
lgr.Log(level, msg, err, fs...)
541+
lgr.log(level, msg, err, fs...)
474542
}
475543
}

0 commit comments

Comments
 (0)