package exerr import ( "bytes" "context" "encoding/json" "fmt" "github.com/gin-gonic/gin" "github.com/rs/zerolog" "go.mongodb.org/mongo-driver/bson/primitive" "gogs.mikescher.com/BlackForestBytes/goext/dataext" "gogs.mikescher.com/BlackForestBytes/goext/enums" "gogs.mikescher.com/BlackForestBytes/goext/langext" "net/http" "os" "runtime/debug" "strings" "time" ) // // ==== USAGE ===== // // If some method returns an error _always wrap it into an exerror: // value, err := do_something(..) // if err != nil { // return nil, exerror.Wrap(err, "do something failed").Build() // } // // If possible add metadata to the error (eg the id that was not found, ...), the methods are the same as in zerolog // return nil, exerror.Wrap(err, "do something failed").Str("someid", id).Int("count", in.Count).Build() // // You can change the errortype with `.User()` and `.System()` (User-errors are 400 and System-errors 500) // You can also manually set the statuscode with `.WithStatuscode(http.NotFound)` // You can set the type with `WithType(..)` // // New Errors (that don't wrap an existing err object) are created with New // return nil, exerror.New(exerror.TypeInternal, "womethign wen horrible wrong").Build() // You can eitehr use an existing ErrorType, the "catch-all" ErrInternal, or add you own ErrType in consts.go // // All errors should be handled one of the following four ways: // - return the error to the caller and let him handle it: // (also auto-prints the error to the log) // => Wrap/New + Build // - Print the error // (also auto-sends it to the error-service) // This is useful for errors that happen asynchron or are non-fatal for the current request // => Wrap/New + Print // - Return the error to the Rest-API caller // (also auto-prints the error to the log) // (also auto-sends it to the error-service) // => Wrap/New + Output // - Print and stop the service // (also auto-sends it to the error-service) // => Wrap/New + Fatal // var stackSkipLogger zerolog.Logger func init() { cw := zerolog.ConsoleWriter{ Out: os.Stdout, TimeFormat: "2006-01-02 15:04:05 Z07:00", } multi := zerolog.MultiLevelWriter(cw) stackSkipLogger = zerolog.New(multi).With().Timestamp().CallerWithSkipFrameCount(4).Logger() } type Builder struct { errorData *ExErr containsGinData bool } func Get(err error) *Builder { return &Builder{errorData: FromError(err)} } func New(t ErrorType, msg string) *Builder { return &Builder{errorData: newExErr(CatSystem, t, msg)} } func Wrap(err error, msg string) *Builder { if err == nil { return &Builder{errorData: newExErr(CatSystem, TypeInternal, msg)} // prevent NPE if we call Wrap with err==nil } if !pkgconfig.RecursiveErrors { v := FromError(err) v.Message = msg return &Builder{errorData: v} } return &Builder{errorData: wrapExErr(FromError(err), msg, CatWrap, 1)} } // ---------------------------------------------------------------------------- func (b *Builder) WithType(t ErrorType) *Builder { b.errorData.Type = t return b } func (b *Builder) WithStatuscode(status int) *Builder { b.errorData.StatusCode = &status return b } func (b *Builder) WithMessage(msg string) *Builder { b.errorData.Message = msg return b } // ---------------------------------------------------------------------------- // Err changes the Severity to ERROR (default) // The error will be: // // - On Build(): // // - Short-Logged as Err // // - On Print(): // // - Logged as Err // // - Send to the error-service // // - On Output(): // // - Logged as Err // // - Send to the error-service func (b *Builder) Err() *Builder { b.errorData.Severity = SevErr return b } // Warn changes the Severity to WARN // The error will be: // // - On Build(): // // - -(nothing)- // // - On Print(): // // - Short-Logged as Warn // // - On Output(): // // - Logged as Warn func (b *Builder) Warn() *Builder { b.errorData.Severity = SevWarn return b } // Info changes the Severity to INFO // The error will be: // // - On Build(): // // - -(nothing)- // // - On Print(): // // - -(nothing)- // // - On Output(): // // - -(nothing)- func (b *Builder) Info() *Builder { b.errorData.Severity = SevInfo return b } // ---------------------------------------------------------------------------- // User sets the Category to CatUser // // Errors with category func (b *Builder) User() *Builder { b.errorData.Category = CatUser return b } func (b *Builder) System() *Builder { b.errorData.Category = CatSystem return b } // ---------------------------------------------------------------------------- func (b *Builder) Id(key string, val fmt.Stringer) *Builder { return b.addMeta(key, MDTID, newIDWrap(val)) } func (b *Builder) StrPtr(key string, val *string) *Builder { return b.addMeta(key, MDTStringPtr, val) } func (b *Builder) Str(key string, val string) *Builder { return b.addMeta(key, MDTString, val) } func (b *Builder) Int(key string, val int) *Builder { return b.addMeta(key, MDTInt, val) } func (b *Builder) Int8(key string, val int8) *Builder { return b.addMeta(key, MDTInt8, val) } func (b *Builder) Int16(key string, val int16) *Builder { return b.addMeta(key, MDTInt16, val) } func (b *Builder) Int32(key string, val int32) *Builder { return b.addMeta(key, MDTInt32, val) } func (b *Builder) Int64(key string, val int64) *Builder { return b.addMeta(key, MDTInt64, val) } func (b *Builder) Float32(key string, val float32) *Builder { return b.addMeta(key, MDTFloat32, val) } func (b *Builder) Float64(key string, val float64) *Builder { return b.addMeta(key, MDTFloat64, val) } func (b *Builder) Bool(key string, val bool) *Builder { return b.addMeta(key, MDTBool, val) } func (b *Builder) Bytes(key string, val []byte) *Builder { return b.addMeta(key, MDTBytes, val) } func (b *Builder) ObjectID(key string, val primitive.ObjectID) *Builder { return b.addMeta(key, MDTObjectID, val) } func (b *Builder) Time(key string, val time.Time) *Builder { return b.addMeta(key, MDTTime, val) } func (b *Builder) Dur(key string, val time.Duration) *Builder { return b.addMeta(key, MDTDuration, val) } func (b *Builder) Strs(key string, val []string) *Builder { return b.addMeta(key, MDTStringArray, val) } func (b *Builder) Ints(key string, val []int) *Builder { return b.addMeta(key, MDTIntArray, val) } func (b *Builder) Ints32(key string, val []int32) *Builder { return b.addMeta(key, MDTInt32Array, val) } func (b *Builder) Type(key string, cls interface{}) *Builder { return b.addMeta(key, MDTString, fmt.Sprintf("%T", cls)) } func (b *Builder) Interface(key string, val interface{}) *Builder { return b.addMeta(key, MDTAny, newAnyWrap(val)) } func (b *Builder) Any(key string, val any) *Builder { return b.addMeta(key, MDTAny, newAnyWrap(val)) } func (b *Builder) Stringer(key string, val fmt.Stringer) *Builder { if val == nil { return b.addMeta(key, MDTString, "(!nil)") } else { return b.addMeta(key, MDTString, val.String()) } } func (b *Builder) Enum(key string, val enums.Enum) *Builder { return b.addMeta(key, MDTEnum, newEnumWrap(val)) } func (b *Builder) Stack() *Builder { return b.addMeta("@Stack", MDTString, string(debug.Stack())) } func (b *Builder) Errs(key string, val []error) *Builder { for i, valerr := range val { b.addMeta(fmt.Sprintf("%v[%v]", key, i), MDTString, Get(valerr).errorData.FormatLog(LogPrintFull)) } return b } func (b *Builder) GinReq(ctx context.Context, g *gin.Context, req *http.Request) *Builder { if v := ctx.Value("start_timestamp"); v != nil { if t, ok := v.(time.Time); ok { b.Time("ctx.startTimestamp", t) b.Time("ctx.endTimestamp", time.Now()) } } b.Str("gin.method", req.Method) b.Str("gin.path", g.FullPath()) b.Strs("gin.header", extractHeader(g.Request.Header)) if req.URL != nil { b.Str("gin.url", req.URL.String()) } if ctxVal := g.GetString("apiversion"); ctxVal != "" { b.Str("gin.context.apiversion", ctxVal) } if ctxVal := g.GetString("uid"); ctxVal != "" { b.Str("gin.context.uid", ctxVal) } if ctxVal := g.GetString("fcmId"); ctxVal != "" { b.Str("gin.context.fcmid", ctxVal) } if ctxVal := g.GetString("reqid"); ctxVal != "" { b.Str("gin.context.reqid", ctxVal) } if req.Method != "GET" && req.Body != nil { if req.Header.Get("Content-Type") == "application/json" { if brc, ok := req.Body.(dataext.BufferedReadCloser); ok { if bin, err := brc.BufferedAll(); err == nil { if len(bin) < 16*1024 { var prettyJSON bytes.Buffer err = json.Indent(&prettyJSON, bin, "", " ") if err == nil { b.Str("gin.body", string(prettyJSON.Bytes())) } else { b.Bytes("gin.body", bin) } } else { b.Str("gin.body", fmt.Sprintf("[[%v bytes | %s]]", len(bin), req.Header.Get("Content-Type"))) } } } } if req.Header.Get("Content-Type") == "multipart/form-data" || req.Header.Get("Content-Type") == "x-www-form-urlencoded" { if brc, ok := req.Body.(dataext.BufferedReadCloser); ok { if bin, err := brc.BufferedAll(); err == nil { if len(bin) < 16*1024 { b.Bytes("gin.body", bin) } else { b.Str("gin.body", fmt.Sprintf("[[%v bytes | %s]]", len(bin), req.Header.Get("Content-Type"))) } } } } } b.containsGinData = true return b } func formatHeader(header map[string][]string) string { ml := 1 for k, _ := range header { if len(k) > ml { ml = len(k) } } r := "" for k, v := range header { if r != "" { r += "\n" } for _, hval := range v { value := hval value = strings.ReplaceAll(value, "\n", "\\n") value = strings.ReplaceAll(value, "\r", "\\r") value = strings.ReplaceAll(value, "\t", "\\t") r += langext.StrPadRight(k, " ", ml) + " := " + value } } return r } func extractHeader(header map[string][]string) []string { r := make([]string, 0, len(header)) for k, v := range header { for _, hval := range v { value := hval value = strings.ReplaceAll(value, "\n", "\\n") value = strings.ReplaceAll(value, "\r", "\\r") value = strings.ReplaceAll(value, "\t", "\\t") r = append(r, k+": "+value) } } return r } // ---------------------------------------------------------------------------- // Build creates a new error, ready to pass up the stack // If the errors is not SevWarn or SevInfo it gets also logged (in short form, without stacktrace) onto stdout func (b *Builder) Build() error { warnOnPkgConfigNotInitialized() if pkgconfig.ZeroLogErrTraces && (b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal) { b.errorData.ShortLog(stackSkipLogger.Error()) } else if pkgconfig.ZeroLogAllTraces { b.errorData.ShortLog(stackSkipLogger.Error()) } b.CallListener(MethodBuild) return b.errorData } // Output prints the error onto the gin stdout. // The error also gets printed to stdout/stderr // If the error is SevErr|SevFatal we also send it to the error-service func (b *Builder) Output(ctx context.Context, g *gin.Context) { if !b.containsGinData && g.Request != nil { // Auto-Add gin metadata if the caller hasn't already done it b.GinReq(ctx, g, g.Request) } b.errorData.Output(g) if b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal { b.errorData.Log(stackSkipLogger.Error()) } else if b.errorData.Severity == SevWarn { b.errorData.Log(stackSkipLogger.Warn()) } b.CallListener(MethodOutput) } // Print prints the error // If the error is SevErr we also send it to the error-service func (b *Builder) Print() { if b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal { b.errorData.Log(stackSkipLogger.Error()) } else if b.errorData.Severity == SevWarn { b.errorData.ShortLog(stackSkipLogger.Warn()) } b.CallListener(MethodPrint) } func (b *Builder) Format(level LogPrintLevel) string { return b.errorData.FormatLog(level) } // Fatal prints the error and terminates the program // If the error is SevErr we also send it to the error-service func (b *Builder) Fatal() { b.errorData.Severity = SevFatal b.errorData.Log(stackSkipLogger.WithLevel(zerolog.FatalLevel)) b.CallListener(MethodFatal) os.Exit(1) } // ---------------------------------------------------------------------------- func (b *Builder) addMeta(key string, mdtype metaDataType, val interface{}) *Builder { b.errorData.Meta.add(key, mdtype, val) return b }