zen/core/zerr/err.go

265 lines
5.8 KiB
Go

package zerr
import (
"context"
"fmt"
"io"
"log/slog"
"strconv"
"strings"
"time"
"connectrpc.com/connect"
"github.com/pborman/indent"
"gitlab.bareksa.com/backend/zen/core/zcaller"
"gitlab.bareksa.com/backend/zen/core/zoptions"
)
var _ Error = (*Err)(nil)
type Err struct {
message string
publicMessage string
code connect.Code
errs []error
caller uintptr
details []any
time time.Time
id string
logger Logger
notifier Notifier
sequence Sequence
}
// Join implements Error.
func (mu *Err) Join(errs ...error) Error {
for _, err := range errs {
if err != nil {
mu.errs = append(mu.errs, err)
}
}
return mu
}
// Resolve implements Error.
func (mu *Err) Resolve() error {
if len(mu.errs) == 0 {
return nil
}
return mu
}
func (mu *Err) Error() string {
if len(mu.errs) == 0 {
return "[nil]"
}
s := &strings.Builder{}
if mu.message != "" {
s.WriteString(mu.message)
}
if len(mu.errs) == 1 {
err := mu.errs[0]
if wb, ok := err.(interface{ WriteBuilder(io.Writer) }); ok {
wb.WriteBuilder(s)
return s.String()
}
next := strings.TrimPrefix(err.Error(), mu.message)
if next == "" {
return s.String()
}
s.WriteString(": ")
s.WriteString(next)
return s.String()
}
if mu.message != "" {
s.WriteString(":\n")
}
w := indent.New(s, " ")
for i, e := range mu.errs {
if i > 0 {
_, _ = io.WriteString(w, "\n")
}
_, _ = io.WriteString(w, strconv.Itoa(i+1))
_, _ = io.WriteString(w, ". ")
if wb, ok := e.(interface{ WriteBuilder(io.Writer) }); ok {
wb.WriteBuilder(w)
} else {
_, _ = io.WriteString(w, e.Error())
}
}
return s.String()
}
// LogValue returns log fields to be consumed by logger.
func (mu *Err) LogValue() slog.Value {
if len(mu.errs) == 0 {
return slog.AnyValue(nil)
}
attrs := make([]slog.Attr, 0, 8)
if len(mu.message) > 0 {
attrs = append(attrs, slog.String("message", mu.message))
}
if len(mu.publicMessage) > 0 {
attrs = append(attrs, slog.String("public", mu.message))
}
if len(mu.id) > 0 {
attrs = append(attrs, slog.String("id", mu.id))
}
if mu.code != connect.CodeUnknown && mu.code != connect.CodeInternal {
attrs = append(attrs, slog.String("code", mu.code.String()))
}
if mu.caller != 0 {
attrs = append(attrs, slog.Attr{Key: "caller", Value: zcaller.Caller(mu.caller).LogValue()})
}
attrs = append(attrs, slog.Attr{Key: "error", Value: mu.RootErrorLog()})
return slog.GroupValue(attrs...)
}
func (mu *Err) LogRecord() slog.Record {
record := slog.NewRecord(mu.GetTime(), slog.LevelError, mu.GetMessage(), mu.GetCaller())
code := mu.GetCode()
if public := mu.GetPublicMessage(); public != "" {
record.AddAttrs(slog.String("public", public))
}
if code != 0 && code != connect.CodeUnknown && code != connect.CodeInternal {
record.AddAttrs(slog.String("code", code.String()))
}
if id := mu.GetID(); id != "" {
record.AddAttrs(slog.String("id", id))
}
if len(mu.GetDetails()) > 0 {
record.AddAttrs(slog.Group("details", mu.GetDetails()...))
}
record.AddAttrs(slog.Attr{Key: "error", Value: mu.RootErrorLog()})
return record
}
func (mu *Err) RootErrorLog() slog.Value {
if len(mu.errs) == 0 {
return slog.AnyValue(nil)
}
if len(mu.errs) == 1 {
return errorValuer{mu.errs[0]}.LogValue()
}
attrs := make([]slog.Attr, 0, len(mu.errs))
i := 0
for _, err := range mu.errs {
if err == nil {
continue
}
i++
attrs = append(attrs, slog.Attr{
Key: strconv.Itoa(i),
Value: errorValuer{err}.LogValue(),
})
}
return slog.GroupValue(attrs...)
}
// Code sets error code. Ignored in Multi.
func (mu *Err) Code(code connect.Code) Error {
mu.code = code
return mu
}
// GetCode Always returns 0 in Multi.
func (mu *Err) GetCode() connect.Code {
return mu.code
}
// Message sets error message. It uses fmt.Sprintf to format the message.
func (mu *Err) Message(msg string, args ...any) Error {
mu.message = fmt.Sprintf(msg, args...)
return mu
}
func (mu *Err) GetMessage() string {
return mu.message
}
func (mu *Err) PublicMessage(msg string, args ...any) Error {
mu.publicMessage = fmt.Sprintf(msg, args...)
return mu
}
func (mu *Err) GetPublicMessage() string {
return mu.publicMessage
}
func (mu *Err) Caller(pc uintptr) Error {
mu.caller = pc
return mu
}
func (mu *Err) GetCaller() uintptr {
return mu.caller
}
// Details sets error details. It's a key-value pair where the odd index is the key and the even index is the value.
//
// Invalid key-value pair number and format will cause the misplaced value to be paired up with "!BADKEY".
//
// Example:
//
// err.Details(
// "key1", 12345,
// "key2", float64(12345.67),
// )
func (mu *Err) Details(fields ...any) Error {
mu.details = fields
return mu
}
func (mu *Err) GetDetails() []any {
return mu.details
}
// Time sets the error time.
//
// Time is already set to current when the error is created or wrapped,
// so this method is only useful when you want to set a different time.
func (mu *Err) Time(t time.Time) Error {
mu.time = t
return mu
}
func (mu *Err) GetTime() time.Time {
return mu.time
}
// ID sets the error ID.
//
// ID is used to identify the error. Used by Zen to consider if an error is the same as another.
func (mu *Err) ID(msg string, args ...any) Error {
mu.id = fmt.Sprintf(msg, args...)
return mu
}
func (mu *Err) GetID() string {
return mu.id
}
func (mu *Err) Log(ctx context.Context) Error {
mu.logger.LogError(ctx, mu)
return mu
}
func (mu *Err) Notify(ctx context.Context, opts ...zoptions.NotifyOption) Error {
mu.notifier.NotifyError(ctx, mu, opts...)
return mu
}
// Sequence returns the state of the error sequence.
func (mu *Err) Sequence() Sequence {
return mu.sequence
}
func (mu *Err) Unwrap() []error {
return mu.errs
}
// IsMulti implements Error.
func (mu *Err) IsMulti() bool {
return len(mu.errs) > 1
}