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 }