Mike Schwörer
413178e2d3
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m4s
384 lines
8.5 KiB
Go
384 lines
8.5 KiB
Go
package exerr
|
|
|
|
import (
|
|
"github.com/rs/xid"
|
|
"github.com/rs/zerolog"
|
|
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type ExErr struct {
|
|
UniqueID string `json:"uniqueID"`
|
|
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Category ErrorCategory `json:"category"`
|
|
Severity ErrorSeverity `json:"severity"`
|
|
Type ErrorType `json:"type"`
|
|
|
|
StatusCode *int `json:"statusCode"`
|
|
|
|
Message string `json:"message"`
|
|
WrappedErrType string `json:"wrappedErrType"`
|
|
WrappedErr any `json:"-"`
|
|
Caller string `json:"caller"`
|
|
|
|
OriginalError *ExErr `json:"originalError"`
|
|
|
|
Meta MetaMap `json:"meta"`
|
|
}
|
|
|
|
func (ee *ExErr) Error() string {
|
|
return ee.RecursiveMessage()
|
|
}
|
|
|
|
// Unwrap must be implemented so that some error.XXX methods work
|
|
func (ee *ExErr) Unwrap() error {
|
|
if ee.OriginalError == nil {
|
|
return nil // this is neccessary - otherwise we return a wrapped nil and the `x == nil` comparison fails (= panic in errors.Is and other failures)
|
|
}
|
|
return ee.OriginalError
|
|
}
|
|
|
|
// Is must be implemented so that error.Is(x) works
|
|
func (ee *ExErr) Is(e error) bool {
|
|
return IsFrom(ee, e)
|
|
}
|
|
|
|
// As must be implemented so that error.As(x) works
|
|
//
|
|
//goland:noinspection GoTypeAssertionOnErrors
|
|
func (ee *ExErr) As(target any) bool {
|
|
if dstErr, ok := target.(*ExErr); ok {
|
|
|
|
if dst0, ok := ee.contains(dstErr); ok {
|
|
dstErr = dst0
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
|
|
} else {
|
|
|
|
val := reflect.ValueOf(target)
|
|
|
|
typStr := val.Type().Elem().String()
|
|
|
|
for curr := ee; curr != nil; curr = curr.OriginalError {
|
|
if curr.Category == CatForeign && curr.WrappedErrType == typStr && curr.WrappedErr != nil {
|
|
val.Elem().Set(reflect.ValueOf(curr.WrappedErr))
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (ee *ExErr) Log(evt *zerolog.Event) {
|
|
evt.Msg(ee.FormatLog(LogPrintFull))
|
|
}
|
|
|
|
func (ee *ExErr) FormatLog(lvl LogPrintLevel) string {
|
|
if lvl == LogPrintShort {
|
|
|
|
msg := ee.Message
|
|
if ee.OriginalError != nil && ee.OriginalError.Category == CatForeign {
|
|
msg = msg + " (" + strings.ReplaceAll(ee.OriginalError.Message, "\n", " ") + ")"
|
|
}
|
|
|
|
if ee.Type != TypeWrap {
|
|
return "[" + ee.Type.Key + "] " + msg
|
|
} else {
|
|
return msg
|
|
}
|
|
|
|
} else if lvl == LogPrintOverview {
|
|
|
|
str := "[" + ee.RecursiveType().Key + "] <" + ee.UniqueID + "> " + strings.ReplaceAll(ee.RecursiveMessage(), "\n", " ") + "\n"
|
|
|
|
indent := ""
|
|
for curr := ee; curr != nil; curr = curr.OriginalError {
|
|
indent += " "
|
|
|
|
str += indent
|
|
str += "-> "
|
|
strmsg := strings.Trim(curr.Message, " \r\n\t")
|
|
if lbidx := strings.Index(curr.Message, "\n"); lbidx >= 0 {
|
|
strmsg = strmsg[0:lbidx]
|
|
}
|
|
strmsg = langext.StrLimit(strmsg, 61, "...")
|
|
str += strmsg
|
|
str += "\n"
|
|
|
|
}
|
|
return str
|
|
|
|
} else if lvl == LogPrintFull {
|
|
|
|
str := "[" + ee.RecursiveType().Key + "] <" + ee.UniqueID + "> " + strings.ReplaceAll(ee.RecursiveMessage(), "\n", " ") + "\n"
|
|
|
|
indent := ""
|
|
for curr := ee; curr != nil; curr = curr.OriginalError {
|
|
indent += " "
|
|
|
|
etype := ee.Type.Key
|
|
if ee.Type == TypeWrap {
|
|
etype = "~"
|
|
}
|
|
|
|
str += indent
|
|
str += "-> ["
|
|
str += etype
|
|
if curr.Category == CatForeign {
|
|
str += "|Foreign"
|
|
}
|
|
str += "] "
|
|
str += strings.ReplaceAll(curr.Message, "\n", " ")
|
|
if curr.Caller != "" {
|
|
str += " (@ "
|
|
str += curr.Caller
|
|
str += ")"
|
|
}
|
|
str += "\n"
|
|
|
|
if curr.Meta.Any() {
|
|
meta := indent + " {" + curr.Meta.FormatOneLine(240) + "}"
|
|
if len(meta) < 200 {
|
|
str += meta
|
|
str += "\n"
|
|
} else {
|
|
str += curr.Meta.FormatMultiLine(indent+" ", " ", 1024)
|
|
str += "\n"
|
|
}
|
|
}
|
|
}
|
|
return str
|
|
|
|
} else {
|
|
|
|
return "[?[" + ee.UniqueID + "]?]"
|
|
|
|
}
|
|
}
|
|
|
|
func (ee *ExErr) ShortLog(evt *zerolog.Event) {
|
|
ee.Meta.Apply(evt, langext.Ptr(240)).Msg(ee.FormatLog(LogPrintShort))
|
|
}
|
|
|
|
// RecursiveMessage returns the message to show
|
|
// = first error (top-down) that is not wrapping/foreign/empty
|
|
// = lowest level error (that is not empty)
|
|
// = fallback to self.message
|
|
func (ee *ExErr) RecursiveMessage() string {
|
|
|
|
// ==== [1] ==== first error (top-down) that is not wrapping/foreign/empty
|
|
|
|
for curr := ee; curr != nil; curr = curr.OriginalError {
|
|
if curr.Message != "" && curr.Category != CatWrap && curr.Category != CatForeign {
|
|
return curr.Message
|
|
}
|
|
}
|
|
|
|
// ==== [2] ==== lowest level error (that is not empty)
|
|
|
|
deepestMsg := ""
|
|
for curr := ee; curr != nil; curr = curr.OriginalError {
|
|
if curr.Message != "" {
|
|
deepestMsg = curr.Message
|
|
}
|
|
}
|
|
if deepestMsg != "" {
|
|
return deepestMsg
|
|
}
|
|
|
|
// ==== [3] ==== fallback to self.message
|
|
|
|
return ee.Message
|
|
}
|
|
|
|
// RecursiveType returns the statuscode to use
|
|
// = first error (top-down) that is not wrapping/empty
|
|
func (ee *ExErr) RecursiveType() ErrorType {
|
|
for curr := ee; curr != nil; curr = curr.OriginalError {
|
|
if curr.Type != TypeWrap {
|
|
return curr.Type
|
|
}
|
|
|
|
}
|
|
|
|
// fallback to self
|
|
return ee.Type
|
|
}
|
|
|
|
// RecursiveStatuscode returns the HTTP Statuscode to use
|
|
// = first error (top-down) that has a statuscode set
|
|
func (ee *ExErr) RecursiveStatuscode() *int {
|
|
for curr := ee; curr != nil; curr = curr.OriginalError {
|
|
if curr.StatusCode != nil {
|
|
return langext.Ptr(*curr.StatusCode)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RecursiveCategory returns the ErrorCategory to use
|
|
// = first error (top-down) that has a statuscode set
|
|
func (ee *ExErr) RecursiveCategory() ErrorCategory {
|
|
for curr := ee; curr != nil; curr = curr.OriginalError {
|
|
if curr.Category != CatWrap {
|
|
return curr.Category
|
|
}
|
|
}
|
|
|
|
// fallback to <empty>
|
|
return ee.Category
|
|
}
|
|
|
|
// RecursiveMeta searches (top-down) for teh first error that has a meta value with teh specified key
|
|
// and returns its value (or nil)
|
|
func (ee *ExErr) RecursiveMeta(key string) *MetaValue {
|
|
for curr := ee; curr != nil; curr = curr.OriginalError {
|
|
if metaval, ok := curr.Meta[key]; ok {
|
|
return langext.Ptr(metaval)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Depth returns the depth of recursively contained errors
|
|
func (ee *ExErr) Depth() int {
|
|
if ee.OriginalError == nil {
|
|
return 1
|
|
} else {
|
|
return ee.OriginalError.Depth() + 1
|
|
}
|
|
}
|
|
|
|
// GetMeta returns the meta value with the specified key
|
|
// this method recurses through all wrapped errors and returns the first matching meta value
|
|
func (ee *ExErr) GetMeta(key string) (any, bool) {
|
|
for curr := ee; curr != nil; curr = curr.OriginalError {
|
|
if v, ok := curr.Meta[key]; ok {
|
|
return v.Value, true
|
|
}
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
// GetMetaString functions the same as GetMeta, but returns false if the type does not match
|
|
func (ee *ExErr) GetMetaString(key string) (string, bool) {
|
|
if v1, ok := ee.GetMeta(key); ok {
|
|
if v2, ok := v1.(string); ok {
|
|
return v2, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func (ee *ExErr) GetMetaBool(key string) (bool, bool) {
|
|
if v1, ok := ee.GetMeta(key); ok {
|
|
if v2, ok := v1.(bool); ok {
|
|
return v2, true
|
|
}
|
|
}
|
|
return false, false
|
|
}
|
|
|
|
func (ee *ExErr) GetMetaInt(key string) (int, bool) {
|
|
if v1, ok := ee.GetMeta(key); ok {
|
|
if v2, ok := v1.(int); ok {
|
|
return v2, true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func (ee *ExErr) GetMetaFloat32(key string) (float32, bool) {
|
|
if v1, ok := ee.GetMeta(key); ok {
|
|
if v2, ok := v1.(float32); ok {
|
|
return v2, true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func (ee *ExErr) GetMetaFloat64(key string) (float64, bool) {
|
|
if v1, ok := ee.GetMeta(key); ok {
|
|
if v2, ok := v1.(float64); ok {
|
|
return v2, true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func (ee *ExErr) GetMetaTime(key string) (time.Time, bool) {
|
|
if v1, ok := ee.GetMeta(key); ok {
|
|
if v2, ok := v1.(time.Time); ok {
|
|
return v2, true
|
|
}
|
|
}
|
|
return time.Time{}, false
|
|
}
|
|
|
|
// contains test if the supplied error is contained in this error (anywhere in the chain)
|
|
func (ee *ExErr) contains(original *ExErr) (*ExErr, bool) {
|
|
if original == nil {
|
|
return nil, false
|
|
}
|
|
|
|
if ee == original {
|
|
return ee, true
|
|
}
|
|
|
|
for curr := ee; curr != nil; curr = curr.OriginalError {
|
|
if curr.equalsDirectProperties(curr) {
|
|
return curr, true
|
|
}
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
// equalsDirectProperties tests if ee and other are equals, but only looks at primary properties (not `OriginalError` or `Meta`)
|
|
func (ee *ExErr) equalsDirectProperties(other *ExErr) bool {
|
|
|
|
if ee.UniqueID != other.UniqueID {
|
|
return false
|
|
}
|
|
if ee.Timestamp != other.Timestamp {
|
|
return false
|
|
}
|
|
if ee.Category != other.Category {
|
|
return false
|
|
}
|
|
if ee.Severity != other.Severity {
|
|
return false
|
|
}
|
|
if ee.Type != other.Type {
|
|
return false
|
|
}
|
|
if ee.StatusCode != other.StatusCode {
|
|
return false
|
|
}
|
|
if ee.Message != other.Message {
|
|
return false
|
|
}
|
|
if ee.WrappedErrType != other.WrappedErrType {
|
|
return false
|
|
}
|
|
if ee.Caller != other.Caller {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func newID() string {
|
|
return xid.New().String()
|
|
}
|