go / caller
I use slog.Handler wrappers to make logging helpers
transparent in stack traces.
The problem
When you wrap slog calls, the source location points
to the wrapper, not the actual caller:
func LogError(err error) {
slog.Error("operation failed", "error", err)
// source shows: logger.go:15 ← the wrapper
// you want: handler.go:42 ← the actual caller
}
The pattern
Wrap the handler and adjust the program counter:
package logger
import (
"context"
"log/slog"
"runtime"
)
// CallerHandler wraps a handler to adjust source locations,
// skipping wrapper functions.
type CallerHandler struct {
slog.Handler
skip int
}
// NewCallerHandler wraps h, skipping n additional frames
// when recording source locations.
func NewCallerHandler(h slog.Handler, skip int) *CallerHandler {
return &CallerHandler{Handler: h, skip: skip}
}
func (h *CallerHandler) Handle(ctx context.Context, r slog.Record) error {
// Adjust the PC to skip wrapper frames
var pcs [1]uintptr
runtime.Callers(h.skip+3, pcs[:]) // +3 for Callers, Handle, slog internals
r.PC = pcs[0]
return h.Handler.Handle(ctx, r)
}
func (h *CallerHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return &CallerHandler{Handler: h.Handler.WithAttrs(attrs), skip: h.skip}
}
func (h *CallerHandler) WithGroup(name string) slog.Handler {
return &CallerHandler{Handler: h.Handler.WithGroup(name), skip: h.skip}
}
Usage
Create a logger with adjusted caller depth:
func init() {
h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
AddSource: true,
})
// Skip 1 additional frame for our wrapper functions
slog.SetDefault(slog.New(NewCallerHandler(h, 1)))
}
// Now wrapper functions are transparent
func LogError(msg string, err error) {
slog.Error(msg, "error", err)
}
func main() {
LogError("query failed", sql.ErrNoRows)
// source shows main.go:XX, not the LogError wrapper
}
Alternative: pass skip explicitly
For simple cases, use slog.LogAttrs with explicit depth:
func LogError(msg string, err error) {
// Skip 1 frame (this function)
slog.Default().Handler().Handle(
context.Background(),
slog.NewRecord(time.Now(), slog.LevelError, msg, callerPC(1)),
)
}
func callerPC(skip int) uintptr {
var pcs [1]uintptr
runtime.Callers(skip+2, pcs[:])
return pcs[0]
}
When to use
- Custom logging wrappers (
LogError,LogRequest, etc.) - Middleware that logs requests
- Error handling helpers that include logging