mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
implement a thin wrapper around stdlib errors (#5733)
This solves #5679 , and also implements #5515, #5509 and lays the ground for #5516 With the introduction of Wrap, Is and As in the standard library, we've now got built-in support for wrapping. On top of that, a common pattern in the community is to define errors tailored to the context of each project while still conforming to the error and Unwrap interfaces (see Upspin, Chromium) The output now includes stack traces and additional info
This commit is contained in:
parent
ade929bc90
commit
894fa22c71
9 changed files with 655 additions and 213 deletions
|
|
@ -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:
|
||||
|
|
|
|||
3
go.mod
3
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
|
||||
|
|
|
|||
12
go.sum
12
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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
44
server/contexts/ctxerr/stack.go
Normal file
44
server/contexts/ctxerr/stack.go
Normal file
|
|
@ -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
|
||||
}
|
||||
35
server/contexts/ctxerr/stack_test.go
Normal file
35
server/contexts/ctxerr/stack_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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, <some Eris error type>)) 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue