package exerr import ( "fmt" "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"` Extra map[string]any `json:"extra"` 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 { if ee.WrappedErr != nil { if werr, ok := ee.WrappedErr.(error); ok { return werr } } 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 { // [LogPrintShort] // // - Only print message and type // - Used e.g. for logging to the console when Build is called // - also used in Print() if level == Warn/Info // // [LogPrintOverview] // // - print message, extra and errortrace // // [LogPrintFull] // // - print full error, with meta and extra, and trace, etc // - Used in Output() and Print() // if lvl == LogPrintShort { msg := ee.Message if msg == "" { msg = ee.RecursiveMessage() } 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" for exk, exv := range ee.Extra { str += fmt.Sprintf(" # [[[ %s ==> %v ]]]\n", exk, exv) } 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" for exk, exv := range ee.Extra { str += fmt.Sprintf(" # [[[ %s ==> %v ]]]\n", exk, exv) } indent := "" for curr := ee; curr != nil; curr = curr.OriginalError { indent += " " etype := curr.Type.Key if curr.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 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 } func (ee *ExErr) GetExtra(key string) (any, bool) { if v, ok := ee.Extra[key]; ok { return v, true } return nil, false } func (ee *ExErr) UniqueIDs() []string { ids := []string{ee.UniqueID} for curr := ee; curr != nil; curr = curr.OriginalError { ids = append(ids, curr.UniqueID) } return ids } // 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() }