goext/exerr/builder.go
Mike Schwörer 9321938dad
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m0s
v0.0.433 fix exerr missing gindata when using ginext.Error and add config for Output logging to stderr
2024-04-15 10:25:30 +02:00

486 lines
13 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 {
wrappedErr error
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{wrappedErr: err, errorData: v}
}
return &Builder{wrappedErr: err, 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.DisableErrorWrapping && b.wrappedErr != nil {
return b.wrappedErr
}
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.errorData.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) && (pkgconfig.ZeroLogErrGinOutput || pkgconfig.ZeroLogAllGinOutput) {
b.errorData.Log(stackSkipLogger.Error())
} else if (b.errorData.Severity == SevWarn) && (pkgconfig.ZeroLogAllGinOutput) {
b.errorData.Log(stackSkipLogger.Warn())
}
b.errorData.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.errorData.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.errorData.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
}