Mike Schwörer
88642770c5
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m4s
481 lines
12 KiB
Go
481 lines
12 KiB
Go
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
|
|
noLog 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) NoLog() *Builder {
|
|
b.noLog = true
|
|
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 langext.IsNil(val) {
|
|
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
|
|
// Can be gloablly configured with ZeroLogErrTraces and ZeroLogAllTraces
|
|
// Can be locally suppressed with Builder.NoLog()
|
|
func (b *Builder) Build() error {
|
|
warnOnPkgConfigNotInitialized()
|
|
|
|
if pkgconfig.ZeroLogErrTraces && !b.noLog && (b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal) {
|
|
b.errorData.ShortLog(stackSkipLogger.Error())
|
|
} else if pkgconfig.ZeroLogAllTraces && !b.noLog {
|
|
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
|
|
}
|