fleet/server/contexts/ctxerr/ctxerr.go

115 lines
3.6 KiB
Go

// Package ctxerr provides functions to wrap errors with annotations and
// stack traces, and to handle those errors such that unique instances of
// those errors will be stored for an amount of time so that it can be
// used to troubleshoot issues.
//
// Typical uses of this package should be to call New or Wrap[f] as close as
// possible from where the error is encountered (or where it needs to be
// created for New), and then to call Handle with the error only once, after it
// bubbled back to the top of the call stack (e.g. in the HTTP handler, or in
// the CLI command, etc.). It is fine to wrap the error with more annotations
// along the way, by calling Wrap[f].
package ctxerr
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/fleetdm/fleet/v4/server/errorstore"
"github.com/rotisserie/eris" //nolint:depguard
)
type key int
const errHandlerKey key = 0
// Cause returns the root error in err's chain.
func Cause(err error) error {
// Until we use only ctxerr.Wrap, we have a mix of pkg/errors, fmt.Errorf
// and eris.Wrap (via ctxerr.Wrap). pkg/errors.Cause looks for a Cause()
// method, while eris.Cause looks for the stdlib-compliant Unwrap(). So
// implement a custom Cause that checks for both until a root is found.
var cerr interface {
Cause() error
}
for {
uerr := errors.Unwrap(err)
if uerr == nil {
if errors.As(err, &cerr) {
uerr = cerr.Cause()
} else {
break
}
}
err = uerr
}
return err
}
// NewContext returns a context derived from ctx that contains the provided
// error handler.
func NewContext(ctx context.Context, eh *errorstore.Handler) context.Context {
return context.WithValue(ctx, errHandlerKey, eh)
}
func fromContext(ctx context.Context) *errorstore.Handler {
v, _ := ctx.Value(errHandlerKey).(*errorstore.Handler)
return v
}
// New creates a new error with the provided error message.
func New(ctx context.Context, errMsg string) error {
return ensureCommonMetadata(ctx, errors.New(errMsg))
}
// Errorf creates a new error with the formatted message.
func Errorf(ctx context.Context, fmsg string, args ...interface{}) error {
return ensureCommonMetadata(ctx, fmt.Errorf(fmsg, args...))
}
// Wrap annotates err with the provided message.
func Wrap(ctx context.Context, err error, msgs ...string) error {
err = ensureCommonMetadata(ctx, err)
if len(msgs) == 0 || err == nil {
return err
}
// do not wrap with eris.Wrap, as we want only the root error closest to the
// actual error condition to capture the stack trace, others just wrap to
// annotate the error.
return fmt.Errorf("%s: %w", strings.Join(msgs, " "), err)
}
// Wrapf annotates err with the provided formatted message.
func Wrapf(ctx context.Context, err error, fmsg string, args ...interface{}) error {
err = ensureCommonMetadata(ctx, err)
if fmsg == "" || err == nil {
return err
}
// do not wrap with eris.Wrap, as we want only the root error closest to the
// actual error condition to capture the stack trace, others just wrap to
// annotate the error.
return fmt.Errorf("%s: %w", fmt.Sprintf(fmsg, args...), err)
}
// Handle handles err by passing it to the registered error handler,
// deduplicating it and storing it for a configured duration.
func Handle(ctx context.Context, err error) {
if eh := fromContext(ctx); eh != nil {
eh.Store(err)
}
}
func ensureCommonMetadata(ctx context.Context, err error) error {
var sf interface{ StackFrames() []uintptr }
if err != nil && !errors.As(err, &sf) {
// no eris error nowhere in the chain, add the common metadata with the stack trace
// TODO: more metadata from ctx: user, host, etc.
err = eris.Wrapf(err, "timestamp: %s", time.Now().Format(time.RFC3339))
}
return err
}