112 lines
2.2 KiB
Go
112 lines
2.2 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
func main() {
|
|
handler := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
|
|
ReplaceAttr: replaceAttr,
|
|
AddSource: true,
|
|
})
|
|
|
|
slog.SetDefault(slog.New(handler))
|
|
|
|
if err := f(); err != nil {
|
|
slog.Info("something went wrong", slog.Any("err", err))
|
|
}
|
|
}
|
|
|
|
func f() error {
|
|
return g()
|
|
}
|
|
|
|
func g() error {
|
|
return errors.New("test error")
|
|
}
|
|
|
|
func replaceAttr(groups []string, a slog.Attr) slog.Attr {
|
|
switch a.Value.Kind() {
|
|
// other cases
|
|
|
|
case slog.KindAny:
|
|
switch v := a.Value.Any().(type) {
|
|
case error:
|
|
a.Value = fmtErr(v)
|
|
}
|
|
}
|
|
|
|
return a
|
|
}
|
|
|
|
// fmtErr returns a slog.GroupValue with keys "msg" and "trace". If the error
|
|
// does not implement interface { StackTrace() errors.StackTrace }, the "trace"
|
|
// key is omitted.
|
|
func fmtErr(err error) slog.Value {
|
|
var groupValues []slog.Attr
|
|
|
|
groupValues = append(groupValues, slog.String("msg", err.Error()))
|
|
|
|
type StackTracer interface {
|
|
StackTrace() errors.StackTrace
|
|
}
|
|
|
|
// Find the trace to the location of the first errors.New,
|
|
// errors.Wrap, or errors.WithStack call.
|
|
var st StackTracer
|
|
for err := err; err != nil; err = errors.Unwrap(err) {
|
|
if x, ok := err.(StackTracer); ok {
|
|
st = x
|
|
}
|
|
}
|
|
|
|
if st != nil {
|
|
groupValues = append(groupValues,
|
|
slog.Any("trace", traceLines(st.StackTrace())),
|
|
)
|
|
}
|
|
|
|
return slog.GroupValue(groupValues...)
|
|
}
|
|
|
|
func traceLines(frames errors.StackTrace) []string {
|
|
traceLines := make([]string, len(frames))
|
|
|
|
// Iterate in reverse to skip uninteresting, consecutive runtime frames at
|
|
// the bottom of the trace.
|
|
var skipped int
|
|
skipping := true
|
|
for i := len(frames) - 1; i >= 0; i-- {
|
|
// Adapted from errors.Frame.MarshalText(), but avoiding repeated
|
|
// calls to FuncForPC and FileLine.
|
|
pc := uintptr(frames[i]) - 1
|
|
fn := runtime.FuncForPC(pc)
|
|
if fn == nil {
|
|
traceLines[i] = "unknown"
|
|
skipping = false
|
|
continue
|
|
}
|
|
|
|
name := fn.Name()
|
|
|
|
if skipping && strings.HasPrefix(name, "runtime.") {
|
|
skipped++
|
|
continue
|
|
} else {
|
|
skipping = false
|
|
}
|
|
|
|
filename, lineNr := fn.FileLine(pc)
|
|
|
|
traceLines[i] = fmt.Sprintf("%s %s:%d", name, filename, lineNr)
|
|
}
|
|
|
|
return traceLines[:len(traceLines)-skipped]
|
|
}
|