mirror of
https://github.com/fleetdm/fleet
synced 2026-05-10 18:51:03 +00:00
115 lines
3.6 KiB
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
|
|
}
|