Files
holos/internal/errors/errors.go
Jeff McCune 2ad0c2a93e render: refactor tm to typeMeta
Gary and I noted tm wasn't clear when I was showing him code.
2024-10-16 17:11:09 -07:00

132 lines
2.9 KiB
Go

// Package errors provides error wrapping with location information
package errors
import (
"context"
"errors"
"fmt"
"log/slog"
"path/filepath"
"runtime"
)
// ErrUnsupported is errors.ErrUnsupported
var ErrUnsupported = errors.ErrUnsupported
func NotImplemented() error {
return wrap(New("not implemented"), 2)
}
// Format calls fmt.Errorf(format, a...) then wraps the error with the source
// location of the caller.
func Format(format string, a ...any) error {
return wrap(fmt.Errorf(format, a...), 2)
}
// As calls errors.As
func As(err error, target any) bool {
return errors.As(err, target)
}
// Is calls errors.Is
func Is(err error, target error) bool {
return errors.Is(err, target)
}
// Join calls errors.Join
func Join(errs ...error) error {
return errors.Join(errs...)
}
// New calls errors.New
func New(text string) error {
return errors.New(text)
}
// Unwrap calls errors.Unwrap
func Unwrap(err error) error {
return errors.Unwrap(err)
}
// Source represents the Source file and line where an error was encountered
type Source struct {
File string `json:"file"`
Line int `json:"line"`
}
func (s *Source) Loc() string {
gp := filepath.Base(filepath.Dir(filepath.Dir(s.File)))
p := filepath.Base(filepath.Dir(s.File))
return fmt.Sprintf("%s/%s/%s:%d", gp, p, filepath.Base(s.File), s.Line)
}
// ErrorAt wraps an error with the Source location the error was encountered at
// for tracing from a top level error handler.
type ErrorAt struct {
Err error
Source Source
}
// Unwrap implements error wrapping.
func (e *ErrorAt) Unwrap() error {
return e.Err
}
// Error returns the error string with Source location.
func (e *ErrorAt) Error() string {
return e.Source.Loc() + ": " + e.Err.Error()
}
func wrap(err error, skip int) error {
// Nothing to do
if err == nil {
return nil
}
// Already a holos error no need to do anything.
var errAt *ErrorAt
if errors.As(err, &errAt) {
return err
}
// Try to wrap err with caller info
if _, file, line, ok := runtime.Caller(skip); ok {
return &ErrorAt{
Err: err,
Source: Source{
File: file,
Line: line,
},
}
}
return err
}
// Wrap wraps err in a ErrorAt or returns err if err is nil, already a
// ErrorAt, or caller info is not available.
//
// XXX: Refactor to Err(error, ...slog.Attr). Often want to add attributes for
// the top level logger.
func Wrap(err error) error {
return wrap(err, 2)
}
// Log logs err with Source location if Err is a ErrorAt
func Log(log *slog.Logger, ctx context.Context, level slog.Level, err error, msg string, args ...any) {
var errAt *ErrorAt
if ok := errors.As(err, &errAt); ok {
args = append(args,
slog.String("err", errAt.Unwrap().Error()),
slog.String("loc", errAt.Source.Loc()),
)
} else {
if _, file, line, ok := runtime.Caller(1); ok {
source := Source{file, line}
args = append(args, slog.String("loc", source.Loc()))
}
args = append(args, slog.String("err", err.Error()))
}
log.Log(ctx, level, msg, args...)
}