diff --git a/.golangci.yml b/.golangci.yml index 1acc7bc2e7..25f5261b51 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -21,7 +21,6 @@ linters-settings: list-type: blacklist include-go-root: false packages-with-error-message: - - github.com/rotisserie/eris: "use ctxerr.New or ctxerr.Wrap[f] instead" - github.com/pkg/errors: "use ctxerr if a context.Context is available or stdlib errors.New / fmt.Errorf with the %w verb" gosec: diff --git a/go.mod b/go.mod index f958a8ebb7..c786d5400f 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( cloud.google.com/go/pubsub v1.16.0 github.com/AbGuthrie/goquery/v2 v2.0.1 github.com/DATA-DOG/go-sqlmock v1.5.0 - github.com/Masterminds/semver v1.5.0 // indirect + github.com/Masterminds/semver v1.5.0 github.com/OneOfOne/xxhash v1.2.8 // indirect github.com/PuerkitoBio/goquery v1.8.0 // indirect github.com/VividCortex/gohistogram v1.0.0 // indirect @@ -81,7 +81,6 @@ require ( github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/procfs v0.7.3 // indirect github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect - github.com/rotisserie/eris v0.5.1 github.com/rs/zerolog v1.20.0 github.com/russellhaering/goxmldsig v1.1.0 github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect diff --git a/go.sum b/go.sum index 1456080b67..fc1fc8a666 100644 --- a/go.sum +++ b/go.sum @@ -412,7 +412,6 @@ github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fleetdm/goose v0.0.0-20220214194029-91b5e5eb8e77 h1:oaRSVdXLGFxX0aQa5UI8GDr6+lRiscSM40B6zl8oUKI= github.com/fleetdm/goose v0.0.0-20220214194029-91b5e5eb8e77/go.mod h1:d7Q+0eCENnKQUhkfAUVLfGnD4QcgJMF/uB9WRTN9TDI= -github.com/flynn/go-docopt v0.0.0-20140912013429-f6dd2ebbb31e h1:Ss/B3/5wWRh8+emnK0++g5zQzwDTi30W10pKxKc4JXI= github.com/flynn/go-docopt v0.0.0-20140912013429-f6dd2ebbb31e/go.mod h1:HyVoz1Mz5Co8TFO8EupIdlcpwShBmY98dkT2xeHkvEI= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= @@ -568,9 +567,7 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -981,8 +978,6 @@ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S github.com/ngrok/sqlmw v0.0.0-20211220175533-9d16fdc47b31 h1:FFHgfAIoAXCCL4xBoAugZVpekfGmZ/fBBueneUKBv7I= github.com/ngrok/sqlmw v0.0.0-20211220175533-9d16fdc47b31/go.mod h1:E26fwEtRNigBfFfHDWsklmo0T7Ixbg0XXgck+Hq4O9k= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nukosuke/go-zendesk v0.10.3 h1:JUB7IHUmbVTBoMbqLuS/qw7RLSzH8ijUgNVGFvRRRng= -github.com/nukosuke/go-zendesk v0.10.3/go.mod h1:FMC25ZV0/fo2kP9c0jleOmSoKsKmTdnycPkyQCkiykI= github.com/nukosuke/go-zendesk v0.12.0 h1:28uiIHZwS5UkZnBOvJXXOeD6MXu3skF/RQjw7FlNWsg= github.com/nukosuke/go-zendesk v0.12.0/go.mod h1:FMC25ZV0/fo2kP9c0jleOmSoKsKmTdnycPkyQCkiykI= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -1092,8 +1087,6 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rotisserie/eris v0.5.1 h1:SbzZloAUjoKX0eiQW187wop45Q5740Pz212NlIz5mLs= -github.com/rotisserie/eris v0.5.1/go.mod h1:JmkIDhvuvDk1kDFGe5RZ3LXIrkEGEN0E6HskH5BCehE= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= @@ -1119,8 +1112,6 @@ github.com/scjalliance/comshim v0.0.0-20190308082608-cf06d2532c4e/go.mod h1:9Tc1 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sebdah/goldie v1.0.0 h1:9GNhIat69MSlz/ndaBg48vl9dF5fI+NBB6kfOxgfkMc= github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4= -github.com/secure-systems-lab/go-securesystemslib v0.3.0 h1:PH0mUKuUSXVEVDbrKMgGPcrqrnKA8gJii614+EKKi7g= -github.com/secure-systems-lab/go-securesystemslib v0.3.0/go.mod h1:o8hhjkbNl2gOamKUA/eNW3xUrntHT9L4W89W1nfj43U= github.com/secure-systems-lab/go-securesystemslib v0.3.1 h1:LJuyMziazadwmQRRu1M7GMUo5S1oH1+YxU9FjuSFU8k= github.com/secure-systems-lab/go-securesystemslib v0.3.1/go.mod h1:o8hhjkbNl2gOamKUA/eNW3xUrntHT9L4W89W1nfj43U= github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs= @@ -1200,14 +1191,11 @@ github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMT github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg= github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo= -github.com/theupdateframework/go-tuf v0.0.0-20220121203041-e3557e322879 h1:UeDpdrX16scCvbdgdMsrztZsQLDofld/Zo+WGDe/PBE= -github.com/theupdateframework/go-tuf v0.0.0-20220121203041-e3557e322879/go.mod h1:I0Gs4Tev4hYQ5wiNqN8VJ7qS0gw7KOZNQuckC624RmE= github.com/theupdateframework/go-tuf v0.2.0 h1:lQajPG9M03zT7CXfytRzPKC7AVaS9ndPdxu7ROJTR2A= github.com/theupdateframework/go-tuf v0.2.0/go.mod h1:E5XP0wXitrFUHe4b8cUcAAdxBW4LbfnqF4WXXGLgWNo= github.com/theupdateframework/notary v0.6.1/go.mod h1:MOfgIfmox8s7/7fduvB2xyPPMJCrjRLRizA8OFwpnKY= diff --git a/server/contexts/ctxerr/ctxerr.go b/server/contexts/ctxerr/ctxerr.go index 6e382767ff..e4733ec843 100644 --- a/server/contexts/ctxerr/ctxerr.go +++ b/server/contexts/ctxerr/ctxerr.go @@ -13,103 +13,236 @@ package ctxerr import ( "context" + "encoding/json" "errors" "fmt" "strings" "time" - - "github.com/fleetdm/fleet/v4/server/errorstore" - "github.com/rotisserie/eris" //nolint:depguard ) type key int const errHandlerKey key = 0 +// Defining here for testing purposes +var nowFn = time.Now + +// FleetError is the error implementation used by this package. +type FleetError struct { + msg string // error message to be prepended to cause + stack stackTracer // stack trace where this error was created + cause error // original error that caused this error if non-nil + data json.RawMessage // additional metadata about the error (timestamps, etc) +} + +type fleetErrorJSON struct { + Message string `json:"message,omitempty"` + Data json.RawMessage `json:"data,omitempty"` + Stack []string `json:"stack,omitempty"` +} + +// Error implements the error interface. +func (e *FleetError) Error() string { + if e.cause == nil { + return e.msg + } + return fmt.Sprintf("%s: %s", e.msg, e.cause.Error()) +} + +// Unwrap implements the error Unwrap interface introduced in go1.13. +func (e *FleetError) Unwrap() error { + return e.cause +} + +// MarshalJSON implements the marshaller interface, giving us control on how +// errors are json-encoded +func (e *FleetError) MarshalJSON() ([]byte, error) { + return json.Marshal(fleetErrorJSON{ + Message: e.msg, + Data: e.data, + Stack: e.stack.List(), + }) +} + +// Stack returns a call stack for the error +func (e *FleetError) Stack() []string { + return e.stack.List() +} + +// setMetadata adds common metadata attributes to the `data` map provided. +// NOTE: this will mutate the data provided and override other values with the same keys. +func setMetadata(ctx context.Context, data map[string]interface{}) map[string]interface{} { + if data == nil { + data = map[string]interface{}{} + } + + // TODO: add more metadata from ctx + data["timestamp"] = nowFn().Format(time.RFC3339) + return data +} + +func encodeData(ctx context.Context, data map[string]interface{}, augment bool) json.RawMessage { + if augment { + data = setMetadata(ctx, data) + } + + encoded, err := json.Marshal(data) + if err != nil { + msg := fmt.Sprintf(`{"error": "there was an error encoding additional data: %s"}`, err.Error()) + encoded = json.RawMessage(msg) + } + return encoded +} + +func newError(ctx context.Context, msg string, cause error, data map[string]interface{}) error { + stack := newStack(2) + edata := encodeData(ctx, data, true) + return &FleetError{msg, stack, cause, edata} +} + +func wrapError(ctx context.Context, msg string, cause error, data map[string]interface{}) error { + if msg == "" || cause == nil { + return cause + } + + stack := newStack(2) + var ferr *FleetError + isFleetError := errors.As(cause, &ferr) + + // If the error is a FleetError, don't add the full stack trace as it should + // already be present. + if isFleetError { + stack = stack[:1] + } + + edata := encodeData(ctx, data, !isFleetError) + return &FleetError{msg, stack, cause, edata} +} + +// New creates a new error with the given message. +func New(ctx context.Context, msg string) error { + return newError(ctx, msg, nil, nil) +} + +// NewWithData creates a new error and attaches additional metadata to it +func NewWithData(ctx context.Context, msg string, data map[string]interface{}) error { + return newError(ctx, msg, nil, data) +} + +// Errorf creates a new error with the given message. +func Errorf(ctx context.Context, format string, args ...interface{}) error { + msg := fmt.Sprintf(format, args...) + return newError(ctx, msg, nil, nil) +} + +// Wrap creates a new error with the given message, wrapping another error. +func Wrap(ctx context.Context, cause error, msgs ...string) error { + msg := strings.Join(msgs, " ") + return wrapError(ctx, msg, cause, nil) +} + +// WrapWithData creates a new error with the given message, wrapping another +// error and attaching the data provided to it. +func WrapWithData(ctx context.Context, cause error, msg string, data map[string]interface{}) error { + return wrapError(ctx, msg, cause, data) +} + +// Wrapf creates a new error with the given message, wrapping another error. +func Wrapf(ctx context.Context, cause error, format string, args ...interface{}) error { + msg := fmt.Sprintf(format, args...) + return wrapError(ctx, msg, cause, nil) +} + // 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) + uerr := Unwrap(err) if uerr == nil { - if errors.As(err, &cerr) { - uerr = cerr.Cause() - } else { - break - } + return err } err = uerr } +} - return err +// FleetCause is similar to Cause, but returns the root-most +// FleetError in the chain +func FleetCause(err error) *FleetError { + var ferr, aux *FleetError = nil, nil + var ok bool + + for err != nil { + if aux, ok = err.(*FleetError); ok { + ferr = aux + } + err = Unwrap(err) + } + + return ferr +} + +// Unwrap is a wrapper of built-in errors.Unwrap. It returns the result of +// calling the Unwrap method on err, if err's type contains an Unwrap method +// returning error. Otherwise, Unwrap returns nil. +func Unwrap(err error) error { + return errors.Unwrap(err) +} + +// MarshalJSON provides a JSON representation of a whole error chain. +func MarshalJSON(err error) ([]byte, error) { + chain := make([]interface{}, 0) + + for err != nil { + switch v := err.(type) { + case json.Marshaler: + chain = append(chain, v) + default: + chain = append(chain, map[string]interface{}{"message": err.Error()}) + } + + err = Unwrap(err) + } + + // reverse the chain to present errors in chronological order. + for i := len(chain)/2 - 1; i >= 0; i-- { + opp := len(chain) - 1 - i + chain[i], chain[opp] = chain[opp], chain[i] + } + + return json.MarshalIndent(struct { + Cause interface{} `json:"cause"` + Wraps []interface{} `json:"wraps,omitempty"` + }{ + Cause: chain[0], + Wraps: chain[1:], + }, "", " ") +} + +type handler interface { + Store(error) } // NewContext returns a context derived from ctx that contains the provided // error handler. -func NewContext(ctx context.Context, eh *errorstore.Handler) context.Context { +func NewContext(ctx context.Context, eh handler) context.Context { return context.WithValue(ctx, errHandlerKey, eh) } -func fromContext(ctx context.Context) *errorstore.Handler { - v, _ := ctx.Value(errHandlerKey).(*errorstore.Handler) +func fromContext(ctx context.Context) handler { + v, _ := ctx.Value(errHandlerKey).(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) { + // as a last resource, wrap the error if there isn't + // a FleetError in the chain + var ferr *FleetError + if !errors.As(err, &ferr) { + err = Wrap(ctx, err, "missing FleetError in chain") + } + 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 -} diff --git a/server/contexts/ctxerr/ctxerr_test.go b/server/contexts/ctxerr/ctxerr_test.go index 627312d632..066273639e 100644 --- a/server/contexts/ctxerr/ctxerr_test.go +++ b/server/contexts/ctxerr/ctxerr_test.go @@ -2,31 +2,49 @@ package ctxerr import ( "context" + "encoding/json" "errors" "fmt" "io" + "regexp" "testing" "time" - "github.com/fleetdm/fleet/v4/server/datastore/redis/redistest" - "github.com/fleetdm/fleet/v4/server/errorstore" - kitlog "github.com/go-kit/kit/log" pkgerrors "github.com/pkg/errors" //nolint:depguard + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestCause(t *testing.T) { +type mockHandler struct { + StoreImpl func(err error) +} + +func (h mockHandler) Store(err error) { + h.StoreImpl(err) +} + +func setup() (context.Context, func()) { ctx := context.Background() - eh := errorstore.NewHandler(ctx, redistest.NopRedis(), kitlog.NewNopLogger(), time.Minute) + eh := mockHandler{} ctx = NewContext(ctx, eh) + nowFn = func() time.Time { + now, _ := time.Parse(time.RFC3339, "1969-06-19T21:44:05Z") + return now + } + + return ctx, func() { nowFn = time.Now } +} + +func TestCause(t *testing.T) { + ctx, cleanup := setup() + defer cleanup() errNew := errors.New("new") fmtWrap := fmt.Errorf("fmt: %w", errNew) pkgWrap := pkgerrors.Wrap(errNew, "pkg") pkgFmtWrap := pkgerrors.Wrap(fmtWrap, "pkg") fmtPkgWrap := fmt.Errorf("fmt: %w", pkgWrap) - ctxNew := New(ctx, "ctxerr") // this returns an eris error that wraps a standard error - ctxNewRoot := errors.Unwrap(ctxNew) // this gets the standard error wrapped in ctxNew + ctxNew := New(ctx, "ctxerr") ctxWrap := Wrap(ctx, ctxNew, "wrap") ctxDoubleWrap := Wrap(ctx, ctxWrap, "re-wrap") pkgFmtCtxWrap := pkgerrors.Wrap(fmt.Errorf("fmt: %w", ctxWrap), "pkg") @@ -42,11 +60,11 @@ func TestCause(t *testing.T) { {pkgWrap, errNew}, {pkgFmtWrap, errNew}, {fmtPkgWrap, errNew}, - {ctxNew, ctxNewRoot}, - {ctxWrap, ctxNewRoot}, - {ctxDoubleWrap, ctxNewRoot}, - {pkgFmtCtxWrap, ctxNewRoot}, - {fmtPkgCtxWrap, ctxNewRoot}, + {ctxNew, ctxNew}, + {ctxWrap, ctxNew}, + {ctxDoubleWrap, ctxNew}, + {pkgFmtCtxWrap, ctxNew}, + {fmtPkgCtxWrap, ctxNew}, } for _, c := range cases { t.Run(fmt.Sprintf("%T: %[1]v", c.in), func(t *testing.T) { @@ -55,3 +73,271 @@ func TestCause(t *testing.T) { }) } } + +func TestNew(t *testing.T) { + ctx, cleanup := setup() + defer cleanup() + err := New(ctx, "new").(*FleetError) + + require.Equal(t, err.msg, "new") + require.NotEmpty(t, err.stack.List()) + require.Nil(t, err.cause) +} + +func TestNewWithData(t *testing.T) { + t.Run("with valid data", func(t *testing.T) { + ctx, cleanup := setup() + defer cleanup() + data := map[string]interface{}{"foo": "bar"} + err := NewWithData(ctx, "new", data).(*FleetError) + + require.Equal(t, err.msg, "new") + require.NotEmpty(t, err.stack.List()) + require.Nil(t, err.cause) + require.Equal(t, err.data, json.RawMessage(`{"foo":"bar","timestamp":"1969-06-19T21:44:05Z"}`)) + }) + + t.Run("with invalid data", func(t *testing.T) { + ctx, cleanup := setup() + defer cleanup() + data := map[string]interface{}{"foo": make(chan int)} + err := NewWithData(ctx, "new", data).(*FleetError) + require.Equal(t, err.msg, "new") + require.NotEmpty(t, err.stack.List()) + require.Nil(t, err.cause) + assert.Regexp(t, regexp.MustCompile(`{"error": ".+"}`), string(err.data)) + }) +} + +func TestErrorf(t *testing.T) { + ctx, cleanup := setup() + defer cleanup() + err := Errorf(ctx, "%s %d", "new", 1).(*FleetError) + + require.Equal(t, err.msg, "new 1") + require.NotEmpty(t, err.stack.List()) + require.Nil(t, err.cause) +} + +func TestWrap(t *testing.T) { + t.Run("with message provided", func(t *testing.T) { + ctx, cleanup := setup() + defer cleanup() + cause := errors.New("cause") + err := Wrap(ctx, cause, "new").(*FleetError) + + require.Equal(t, err.msg, "new") + require.NotEmpty(t, err.stack.List()) + require.NotNil(t, err.cause) + }) + + t.Run("without message provided", func(t *testing.T) { + ctx, cleanup := setup() + defer cleanup() + cause := errors.New("cause") + err := Wrap(ctx, cause) + require.Equal(t, err, cause) + }) + + t.Run("with nil error provided", func(t *testing.T) { + ctx, cleanup := setup() + defer cleanup() + err := Wrap(ctx, nil) + require.Equal(t, err, nil) + }) +} + +func TestWrapNewWithData(t *testing.T) { + t.Run("with valid data", func(t *testing.T) { + ctx, cleanup := setup() + defer cleanup() + cause := errors.New("cause") + data := map[string]interface{}{"foo": "bar"} + err := WrapWithData(ctx, cause, "new", data).(*FleetError) + + require.Equal(t, err.msg, "new") + require.NotEmpty(t, err.stack.List()) + require.NotNil(t, err.cause) + require.Equal(t, err.data, json.RawMessage(`{"foo":"bar","timestamp":"1969-06-19T21:44:05Z"}`)) + }) + + t.Run("with invalid data", func(t *testing.T) { + ctx, cleanup := setup() + defer cleanup() + cause := errors.New("cause") + data := map[string]interface{}{"foo": make(chan int)} + err := WrapWithData(ctx, cause, "new", data).(*FleetError) + require.Equal(t, err.msg, "new") + require.NotEmpty(t, err.stack.List()) + require.NotNil(t, err.cause) + assert.Regexp(t, regexp.MustCompile(`{"error": ".+"}`), string(err.data)) + }) + + t.Run("without message provided", func(t *testing.T) { + ctx, cleanup := setup() + defer cleanup() + cause := errors.New("cause") + err := WrapWithData(ctx, cause, "", map[string]interface{}{"foo": "bar"}) + require.Equal(t, err, cause) + }) + + t.Run("with nil error provided", func(t *testing.T) { + ctx, cleanup := setup() + defer cleanup() + err := WrapWithData(ctx, nil, "msg", map[string]interface{}{"foo": "bar"}) + require.Equal(t, err, nil) + }) +} + +func TestWrapf(t *testing.T) { + ctx, cleanup := setup() + defer cleanup() + cause := errors.New("cause") + err := Wrapf(ctx, cause, "%s %d", "new", 1).(*FleetError) + + require.Equal(t, err.msg, "new 1") + require.NotEmpty(t, err.stack.List()) + require.NotNil(t, err.cause) +} + +func TestUnwrap(t *testing.T) { + ctx, cleanup := setup() + defer cleanup() + cause := errors.New("cause") + err := Wrap(ctx, cause, "new") + + require.Equal(t, Unwrap(err), cause) +} + +func TestFleetErrorMarshalling(t *testing.T) { + cases := []struct { + msg string + in FleetError + out string + }{ + {"only error", FleetError{"a", mockStack{}, nil, nil}, `{"message": "a"}`}, + {"errors and stack", FleetError{"a", mockStack{[]string{"test"}}, errors.New("err"), nil}, `{"message": "a", "stack": ["test"]}`}, + { + "errors, stack and data", + FleetError{"a", mockStack{[]string{"test"}}, errors.New("err"), json.RawMessage(`{"foo":"bar"}`)}, + `{"message": "a", "stack": ["test"], "data": {"foo": "bar"}}`, + }, + } + + for _, c := range cases { + t.Run(c.msg, func(t *testing.T) { + json, err := c.in.MarshalJSON() + + require.NoError(t, err) + require.JSONEq(t, c.out, string(json)) + }) + } +} + +func TestMarshalJSON(t *testing.T) { + ctx, cleanup := setup() + defer cleanup() + + errNew := errors.New("a") + + errWrap := Wrap(ctx, errNew, "b").(*FleetError) + errWrap.stack = mockStack{[]string{"sb"}} + + errNewWithData := NewWithData(ctx, "c", map[string]interface{}{"f": "c"}).(*FleetError) + errNewWithData.stack = mockStack{[]string{"sc"}} + + cases := []struct { + msg string + in error + out string + }{ + { + "non-wrapped errors", + errNew, + `{"cause": {"message": "a"}}`, + }, + { + "wrapped error", + errWrap, + `{"cause": {"message": "a"}, "wraps": [{"message": "b", "data": {"timestamp": "1969-06-19T21:44:05Z"}, "stack": ["sb"]}]}`, + }, + { + "wrapped error with data", + errNewWithData, + `{"cause": {"message": "c", "stack": ["sc"], "data": {"f": "c", "timestamp": "1969-06-19T21:44:05Z"}}}`, + }, + } + + for _, c := range cases { + t.Run(c.msg, func(t *testing.T) { + json, err := MarshalJSON(c.in) + require.NoError(t, err) + require.JSONEq(t, c.out, string(json)) + }) + } +} + +func TestStackMethod(t *testing.T) { + ctx, cleanup := setup() + defer cleanup() + + errNew := errors.New("a") + errWrap := Wrap(ctx, errNew, "b").(*FleetError) + errWrap.stack = mockStack{[]string{"sb"}} + + require.Equal(t, []string{"sb"}, errWrap.Stack()) +} + +func TestFleetCause(t *testing.T) { + ctx, cleanup := setup() + defer cleanup() + + var nilErr *FleetError = nil + errNew := errors.New("a") + errWrapRoot := Wrap(ctx, errNew, "wrapRoot") + errWrap1 := Wrap(ctx, errWrapRoot, "wrap1") + errWrap2 := Wrap(ctx, errWrap1, "wrap2") + + cases := []struct { + msg string + in error + out error + }{ + {"non-fleet, unwrapped errors returns nil", errNew, nilErr}, + {"fleet unwrapped errors returns the error itself", errWrapRoot, errWrapRoot}, + {"deeply nested errors return the root fleet error", errWrap1, errWrapRoot}, + {"deeply nested errors return the root fleet error", errWrap2, errWrapRoot}, + } + + for _, c := range cases { + t.Run(c.msg, func(t *testing.T) { + actual := FleetCause(c.in) + require.Equal(t, c.out, actual) + }) + } +} + +func TestHandle(t *testing.T) { + t.Run("stores the error when invoked", func(t *testing.T) { + ctx := context.Background() + eh := mockHandler{} + err := New(ctx, "new") + eh.StoreImpl = func(serr error) { + require.Equal(t, serr, err) + } + ctx = NewContext(ctx, eh) + Handle(ctx, err) + }) + + t.Run("wraps when there's no FleetError in the chain", func(t *testing.T) { + ctx := context.Background() + eh := mockHandler{} + err := errors.New("new") + eh.StoreImpl = func(serr error) { + var ferr *FleetError + require.ErrorAs(t, serr, &ferr) + } + ctx = NewContext(ctx, eh) + Handle(ctx, err) + }) +} diff --git a/server/contexts/ctxerr/stack.go b/server/contexts/ctxerr/stack.go new file mode 100644 index 0000000000..d3692f5c30 --- /dev/null +++ b/server/contexts/ctxerr/stack.go @@ -0,0 +1,44 @@ +package ctxerr + +import ( + "fmt" + "path/filepath" + "runtime" +) + +const ( + maxDepth = 10 // maximum number of stack frames to record +) + +type stackTracer interface { + List() []string +} + +// stack holds a snapshot of program counters. +type stack []uintptr + +// newStack captures a stack trace. skip specifies the number of frames to skip from +// a stack trace. skip=0 records stack.New call as the innermost frame. +func newStack(skip int) stack { + pc := make([]uintptr, maxDepth+1) + pc = pc[:runtime.Callers(skip+2, pc)] + return stack(pc) +} + +// List collects stack traces formatted as strings. +func (s stack) List() []string { + var lines []string + + cf := runtime.CallersFrames(s) + for { + f, more := cf.Next() + line := fmt.Sprintf("%s (%s:%d)", f.Function, filepath.Base(f.File), f.Line) + lines = append(lines, line) + + if !more { + break + } + } + + return lines +} diff --git a/server/contexts/ctxerr/stack_test.go b/server/contexts/ctxerr/stack_test.go new file mode 100644 index 0000000000..9447f7abb1 --- /dev/null +++ b/server/contexts/ctxerr/stack_test.go @@ -0,0 +1,35 @@ +package ctxerr + +import ( + "regexp" + "testing" + + "github.com/stretchr/testify/require" +) + +type mockStack struct { + trace []string +} + +func (s mockStack) List() []string { + return s.trace +} + +func buildStack(depth int) stack { + if depth == 0 { + return newStack(0) + } + return buildStack(depth - 1) +} + +func TestStack(t *testing.T) { + trace := buildStack(maxDepth) + lines := trace.List() + + require.Equal(t, len(lines), len(trace)) + + re := regexp.MustCompile(`server/contexts/ctxerr\.buildStack \(stack_test.go:\d+\)$`) + for i, line := range lines { + require.Regexpf(t, re, line, "expected line %d to match %q, got %q", i, re, line) + } +} diff --git a/server/errorstore/errors.go b/server/errorstore/errors.go index 5524c3d4eb..6e6dfeb947 100644 --- a/server/errorstore/errors.go +++ b/server/errorstore/errors.go @@ -12,7 +12,6 @@ import ( "crypto/sha256" "encoding/base64" "encoding/json" - "errors" "fmt" "net/http" "strconv" @@ -20,12 +19,12 @@ import ( "sync/atomic" "time" + "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/datastore/redis" "github.com/fleetdm/fleet/v4/server/fleet" kitlog "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" redigo "github.com/gomodule/redigo/redis" - "github.com/rotisserie/eris" //nolint:depguard ) // Handler defines an error handler. Call Handler.Store to handle an error, and @@ -134,74 +133,23 @@ func sha256b64(s string) string { } func hashError(err error) string { - // Ok so the hashing process is as follows: - // - // a) we want to hash the type and error message of the *root* error (the - // last unwrapped error) so that if by mistake the same error is sent to - // Handler.Handle in multiple places in the code, after being wrapped - // differently any number of times, it still hashes to the same value - // (because the root is the same). The type is not sufficient because some - // errors have the same type but variable parts (e.g. a struct value that - // implements the error interface and the message contains a file name that - // caused the error and that is stored in a struct field). - // - // b) in addition that a), we also want to hash all locations in the stack - // trace, so that the same error type and message (say, sql.ErrNoRows or - // io.UnexpectedEOF) caused at two different places in the code are not - // considered the same error. To get that location, the error must be wrapped - // at some point by eris.Wrap (or must be a user-created error via eris.New). - // We cannot hash only the leaf frame in the stack trace as that would all be - // ctxerr.New or ctxerr.Wrap (i.e. whatever common helper function used to - // create the eris error). - // - // c) if we call eris.Unpack on an error that is not *directly* an "eris" - // error (i.e. an error value returned from eris.Wrap or eris.New), then - // eris.Unpack will not return any location information. So if for example - // the error was wrapped with the pkg/errors.Wrap or the stdlib's fmt.Errorf - // calls at some point, eris.Unpack will not give us any location info. To - // get around this, we look for an eris-created error in the wrapped chain, - // and only give up hashing the location if we can't find any. - // - // d) there is no easy way to identify an "eris" error (i.e. we cannot simply - // use errors.As(err, )) as eris does not export its - // error type, and it actually uses 2 different internal error types. To get - // around this, we look for an error that has the `StackFrames() []uintptr` - // method, as both of eris internal errors implement that (see - // https://github.com/rotisserie/eris/blob/v0.5.1/eris.go#L182). - - var sf interface{ StackFrames() []uintptr } - if errors.As(err, &sf) { - err = sf.(error) - } - - unpackedErr := eris.Unpack(err) - - if unpackedErr.ErrExternal == nil && - len(unpackedErr.ErrRoot.Stack) == 0 && - len(unpackedErr.ErrChain) == 0 { - return sha256b64(unpackedErr.ErrRoot.Msg) - } + cause := ctxerr.Cause(err) + ferr := ctxerr.FleetCause(err) var sb strings.Builder - if unpackedErr.ErrExternal != nil { - root := eris.Cause(unpackedErr.ErrExternal) - fmt.Fprintf(&sb, "%T\n%s\n", root, root.Error()) + // hash the cause type and message (it might not be a FleetError) + fmt.Fprintf(&sb, "%T\n%s\n", cause, cause.Error()) + + // hash the stack trace of the root FleetError in the chain + if ferr != nil { + fmt.Fprintf(&sb, strings.Join(ferr.Stack(), "\n")) } - if len(unpackedErr.ErrRoot.Stack) > 0 { - for _, frame := range unpackedErr.ErrRoot.Stack { - fmt.Fprintf(&sb, "%s:%d\n", frame.File, frame.Line) - } - } else if len(unpackedErr.ErrChain) > 0 { - lastFrame := unpackedErr.ErrChain[0].Frame - fmt.Fprintf(&sb, "%s:%d", lastFrame.File, lastFrame.Line) - } return sha256b64(sb.String()) } func hashAndMarshalError(externalErr error) (errHash string, errAsJson string, err error) { - m := eris.ToJSON(externalErr, true) - bytes, err := json.MarshalIndent(m, "", " ") + bytes, err := ctxerr.MarshalJSON(externalErr) if err != nil { return "", "", err } diff --git a/server/errorstore/errors_test.go b/server/errorstore/errors_test.go index eb08a64fb6..42de7dc220 100644 --- a/server/errorstore/errors_test.go +++ b/server/errorstore/errors_test.go @@ -14,34 +14,42 @@ import ( "testing" "time" + "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/datastore/redis/redistest" "github.com/fleetdm/fleet/v4/server/fleet" kitlog "github.com/go-kit/kit/log" pkgErrors "github.com/pkg/errors" //nolint:depguard - "github.com/rotisserie/eris" //nolint:depguard "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +type mockHandler struct{} + +func (h mockHandler) Store(err error) {} + +var eh = mockHandler{} +var ctxb = context.Background() +var ctx = ctxerr.NewContext(ctxb, eh) + func alwaysErrors() error { return pkgErrors.New("always errors") } func alwaysCallsAlwaysErrors() error { return alwaysErrors() } -func alwaysErisErrors() error { return eris.New("always eris errors") } +func alwaysFleetErrors() error { return ctxerr.New(ctx, "always fleet errors") } func alwaysNewError(eh *Handler) error { - err := eris.New("always new errors") + err := ctxerr.New(ctx, "always new errors") eh.Store(err) return err } func alwaysNewErrorTwo(eh *Handler) error { - err := eris.New("always new errors two") + err := ctxerr.New(ctx, "always new errors two") eh.Store(err) return err } -func alwaysWrappedErr() error { return eris.Wrap(io.EOF, "always EOF") } +func alwaysWrappedErr() error { return ctxerr.Wrap(ctx, io.EOF, "always EOF") } func TestHashErr(t *testing.T) { t.Run("without stack trace, same error is same hash", func(t *testing.T) { @@ -51,15 +59,15 @@ func TestHashErr(t *testing.T) { }) t.Run("different location, same error is different hash", func(t *testing.T) { - err1 := alwaysErisErrors() - err2 := alwaysErisErrors() + err1 := alwaysFleetErrors() + err2 := alwaysFleetErrors() assert.NotEqual(t, hashError(err1), hashError(err2)) }) t.Run("same error, wrapped, same hash", func(t *testing.T) { - eris1 := alwaysErisErrors() + ferror1 := alwaysFleetErrors() - w1, w2 := fmt.Errorf("wrap: %w", eris1), pkgErrors.Wrap(eris1, "wrap") + w1, w2 := fmt.Errorf("wrap: %w", ferror1), pkgErrors.Wrap(ferror1, "wrap") h1, h2 := hashError(w1), hashError(w2) assert.Equal(t, h1, h2) }) @@ -71,42 +79,29 @@ func TestHashErr(t *testing.T) { res, jsonBytes, err := hashAndMarshalError(generatedErr) require.NoError(t, err) assert.Equal(t, "mWoqz7iS1IPOZXGhpzHLl_DVQOyemWxCmvkpLz8uEZk=", res) - assert.True(t, strings.HasPrefix(jsonBytes, `{ - "external": "some err`)) require.NoError(t, json.Unmarshal([]byte(jsonBytes), &m)) generatedErr2 := pkgErrors.New("some other err") res, jsonBytes, err = hashAndMarshalError(generatedErr2) require.NoError(t, err) assert.Equal(t, "8AXruOzQmQLF4H3SrzLxXSwFQgZ8DcbkoF1owo0RhTs=", res) - assert.True(t, strings.HasPrefix(jsonBytes, `{ - "external": "some other err`)) require.NoError(t, json.Unmarshal([]byte(jsonBytes), &m)) }) } -func TestHashErrEris(t *testing.T) { +func TestHashErrFleetError(t *testing.T) { t.Run("Marshal", func(t *testing.T) { - wd, err := os.Getwd() - require.NoError(t, err) + var m map[string]interface{} - generatedErr := eris.New("some err") + generatedErr := ctxerr.New(ctx, "some err") res, jsonBytes, err := hashAndMarshalError(generatedErr) require.NoError(t, err) assert.NotEmpty(t, res) - - assert.Regexp(t, regexp.MustCompile(fmt.Sprintf(`\{ - "root": \{ - "message": "some err", - "stack": \[ - "errorstore.TestHashErrEris\.func\d+:%s/errors_test\.go:\d+" - \] - \} -\}`, regexp.QuoteMeta(wd))), jsonBytes) + require.NoError(t, json.Unmarshal([]byte(jsonBytes), &m)) }) t.Run("HashWrapped", func(t *testing.T) { - // hashing an eris error that wraps a root error hashes to the same + // hashing a fleet error that wraps a root error hashes to the same // value if it is from the same location, even if wrapped differently // afterwards. err := alwaysWrappedErr() @@ -118,8 +113,8 @@ func TestHashErrEris(t *testing.T) { }) t.Run("HashNew", func(t *testing.T) { - err := alwaysErisErrors() - werr := eris.Wrap(err, "wrap eris") + err := alwaysFleetErrors() + werr := ctxerr.Wrap(ctx, err, "wrap ctxterr") werr1, werr2 := pkgErrors.Wrap(err, "wrap pkg"), fmt.Errorf("wrap fmt: %w", err) wantHash := hashError(err) h0, h1, h2 := hashError(werr), hashError(werr1), hashError(werr2) @@ -130,8 +125,8 @@ func TestHashErrEris(t *testing.T) { t.Run("HashSameRootDifferentLocation", func(t *testing.T) { err1 := alwaysWrappedErr() - err2 := func() error { return eris.Wrap(io.EOF, "always EOF") }() - err3 := func() error { return eris.Wrap(io.EOF, "always EOF") }() + err2 := func() error { return ctxerr.Wrap(ctx, io.EOF, "always EOF") }() + err3 := func() error { return ctxerr.Wrap(ctx, io.EOF, "always EOF") }() h1, h2, h3 := hashError(err1), hashError(err2), hashError(err3) assert.NotEqual(t, h1, h2) assert.NotEqual(t, h1, h3) @@ -143,12 +138,12 @@ func TestUnwrapAll(t *testing.T) { root := sql.ErrNoRows werr := pkgErrors.Wrap(root, "pkg wrap") gerr := fmt.Errorf("fmt wrap: %w", werr) - eerr := eris.Wrap(gerr, "eris wrap") - eerr2 := eris.Wrap(eerr, "eris wrap 2") + eerr := ctxerr.Wrap(ctx, gerr, "fleet wrap") + eerr2 := ctxerr.Wrap(ctx, eerr, "fleet wrap 2") - uw := eris.Cause(eerr2) + uw := ctxerr.Cause(eerr2) assert.Equal(t, uw, root) - assert.Nil(t, eris.Cause(nil)) + assert.Nil(t, ctxerr.Cause(nil)) } func TestErrorHandler(t *testing.T) { @@ -238,15 +233,20 @@ func testErrorHandlerCollectsErrors(t *testing.T, pool fleet.RedisPool, wd strin require.NoError(t, err) require.Len(t, errors, 1) - assert.Regexp(t, regexp.MustCompile(fmt.Sprintf(`\{ - "root": \{ + assert.Regexp(t, regexp.MustCompile(`\{ + "cause": \{ "message": "always new errors", + "data": \{ + "timestamp": ".+" + \}, "stack": \[ - "errorstore\.TestErrorHandler\.func\d\.\d+:%s/errors_test\.go:\d+", - "errorstore\.testErrorHandlerCollectsErrors:%[1]s/errors_test\.go:\d+", - "errorstore\.alwaysNewError:%s/errors_test\.go:\d+" + "github\.com\/fleetdm\/fleet\/v4\/server\/errorstore\.alwaysNewError \(errors_test\.go\:\d+\)", + "github\.com\/fleetdm\/fleet\/v4\/server\/errorstore\.testErrorHandlerCollectsErrors \(errors_test\.go\:\d+\)", + "github\.com\/fleetdm\/fleet\/v4\/server\/errorstore\.TestErrorHandler\.func\d\.\d \(errors_test\.go\:\d+\)", + ".+", + ".+" \] - \}`, wd, wd)), errors[0]) + \}`), errors[0]) errors, err = eh.Retrieve(flush) require.NoError(t, err) @@ -304,25 +304,35 @@ func testErrorHandlerCollectsDifferentErrors(t *testing.T, pool fleet.RedisPool, // order is not guaranteed by scan keys for _, jsonErr := range errors { if strings.Contains(jsonErr, "new errors two") { - assert.Regexp(t, regexp.MustCompile(fmt.Sprintf(`\{ - "root": \{ + assert.Regexp(t, regexp.MustCompile(`\{ + "cause": \{ "message": "always new errors two", + "data": \{ + "timestamp": ".+" + \}, "stack": \[ - "errorstore\.TestErrorHandler\.func\d\.\d+:%s/errors_test\.go:\d+", - "errorstore\.testErrorHandlerCollectsDifferentErrors:%[1]s/errors_test\.go:\d+", - "errorstore\.alwaysNewErrorTwo:%[1]s/errors_test\.go:\d+" + "github\.com\/fleetdm\/fleet\/v4\/server\/errorstore\.alwaysNewErrorTwo \(errors_test\.go\:\d+\)", + "github\.com\/fleetdm\/fleet\/v4\/server\/errorstore\.testErrorHandlerCollectsDifferentErrors \(errors_test\.go\:\d+\)", + "github\.com\/fleetdm\/fleet\/v4\/server\/errorstore\.TestErrorHandler\.func\d\.\d \(errors_test\.go\:\d+\)", + ".+", + ".+" \] - \}`, wd)), jsonErr) + \}`), jsonErr) } else { - assert.Regexp(t, regexp.MustCompile(fmt.Sprintf(`\{ - "root": \{ + assert.Regexp(t, regexp.MustCompile(`\{ + "cause": \{ "message": "always new errors", + "data": \{ + "timestamp": ".+" + \}, "stack": \[ - "errorstore\.TestErrorHandler\.func\d\.\d+:%s/errors_test\.go:\d+", - "errorstore\.testErrorHandlerCollectsDifferentErrors:%[1]s/errors_test\.go:\d+", - "errorstore\.alwaysNewError:%[1]s/errors_test\.go:\d+" + "github\.com\/fleetdm\/fleet\/v4\/server\/errorstore\.alwaysNewError \(errors_test\.go\:\d+\)", + "github\.com\/fleetdm\/fleet\/v4\/server\/errorstore\.testErrorHandlerCollectsDifferentErrors \(errors_test\.go\:\d+\)", + "github\.com\/fleetdm\/fleet\/v4\/server\/errorstore\.TestErrorHandler\.func\d.\d \(errors_test\.go\:\d+\)", + ".+", + ".+" \] - \}`, wd)), jsonErr) + \}`), jsonErr) } } @@ -369,7 +379,7 @@ func TestHttpHandler(t *testing.T) { require.Equal(t, res.Code, 200) var errs []struct { - Root struct { + Cause struct { Message string } Wrap []struct { @@ -378,8 +388,8 @@ func TestHttpHandler(t *testing.T) { } require.NoError(t, json.Unmarshal(res.Body.Bytes(), &errs)) require.Len(t, errs, 2) - require.NotEmpty(t, errs[0].Root.Message) - require.NotEmpty(t, errs[1].Root.Message) + require.NotEmpty(t, errs[0].Cause.Message) + require.NotEmpty(t, errs[1].Cause.Message) }) t.Run("flushes errors after retrieving if the flush flag is true", func(t *testing.T) { @@ -390,7 +400,7 @@ func TestHttpHandler(t *testing.T) { require.Equal(t, res.Code, 200) var errs []struct { - Root struct { + Cause struct { Message string } Wrap []struct { @@ -399,8 +409,8 @@ func TestHttpHandler(t *testing.T) { } require.NoError(t, json.Unmarshal(res.Body.Bytes(), &errs)) require.Len(t, errs, 2) - require.NotEmpty(t, errs[0].Root.Message) - require.NotEmpty(t, errs[1].Root.Message) + require.NotEmpty(t, errs[0].Cause.Message) + require.NotEmpty(t, errs[1].Cause.Message) req = httptest.NewRequest("GET", "/?flush=true", nil) res = httptest.NewRecorder()