Files
signoz/pkg/errors/errors.go
2026-05-25 22:40:33 +05:30

425 lines
12 KiB
Go

package errors
import (
"errors" //nolint:depguard
"fmt"
"log/slog"
"maps"
"time"
"go.opentelemetry.io/otel/attribute"
)
// base is the fundamental struct that implements the error interface.
type base struct {
// t denotes the custom type of the error.
t typ
// c denotes the short code for the error message.
c Code
// m contains error message passed through errors.New.
m string
// e is the actual error being wrapped.
e error
// u denotes the url for the documentation (if present) for the error.
u string
// a denotes any additional error messages (if present).
// NOTE: use attrs['suggestions'] for additional structured suggestions instead of this field.
a []string
// s contains the stacktrace captured at error creation time.
s fmt.Stringer
// r is the retry strategy for the error, if applicable.
r retry
// attrs contains any additional attributes for the error, if present.
// attrs is declared to be map[string]any, however map[string][]string is expected to be used for values.
attrs map[string]any
}
// Stacktrace returns the stacktrace captured at error creation time, formatted as a string.
func (b *base) Stacktrace() string {
if b.s == nil {
return ""
}
return b.s.String()
}
// WithStacktrace replaces the auto-captured stacktrace with a pre-formatted string
// and returns a new base error.
func (b *base) WithStacktrace(s string) *base {
return &base{
t: b.t,
c: b.c,
m: b.m,
e: b.e,
u: b.u,
a: b.a,
s: rawStacktrace(s),
r: b.r,
attrs: b.attrs,
}
}
// base implements Error interface.
func (b *base) Error() string {
if b.e != nil {
return b.e.Error()
}
return b.m
}
// New returns a base error. It requires type, code and message as input.
func New(t typ, code Code, message string) *base {
return &base{
t: t,
c: code,
m: message,
e: nil,
u: "",
a: []string{},
s: newStackTrace(),
}
}
// Newf returns a new base by formatting the error message with the supplied format specifier.
func Newf(t typ, code Code, format string, args ...any) *base {
return &base{
t: t,
c: code,
m: fmt.Sprintf(format, args...),
e: nil,
s: newStackTrace(),
}
}
// Wrapf returns a new error by formatting the error message with the supplied format specifier
// and wrapping another error with base.
func Wrapf(cause error, t typ, code Code, format string, args ...any) *base {
return &base{
t: t,
c: code,
m: fmt.Sprintf(format, args...),
e: cause,
s: newStackTrace(),
}
}
// Wrap returns a new error by wrapping another error with base.
func Wrap(cause error, t typ, code Code, message string) *base {
return &base{
t: t,
c: code,
m: message,
e: cause,
s: newStackTrace(),
}
}
// WithAdditionalf adds an additional error message to the existing error.
func WithAdditionalf(cause error, format string, args ...any) *base {
t, c, m, e, u, a, r, attrs := Unwrapb(cause)
var s fmt.Stringer
if original, ok := cause.(*base); ok {
s = original.s
}
b := &base{
t: t,
c: c,
m: m,
e: e,
u: u,
a: a,
s: s,
r: r,
attrs: attrs,
}
return b.WithAdditional(append(a, fmt.Sprintf(format, args...))...)
}
// WithUrl adds a url to the base error and returns a new base error.
func (b *base) WithUrl(u string) *base {
return &base{
t: b.t,
c: b.c,
m: b.m,
e: b.e,
u: u,
a: b.a,
s: b.s,
r: b.r,
attrs: b.attrs,
}
}
// WithAdditional adds additional messages to the base error and returns a new base error.
func (b *base) WithAdditional(a ...string) *base {
return &base{
t: b.t,
c: b.c,
m: b.m,
e: b.e,
u: b.u,
a: a,
s: b.s,
r: b.r,
attrs: b.attrs,
}
}
// WithRetry adds retry metadata to the base error and returns a new base error.
func (b *base) WithRetry(r retry) *base {
return &base{
t: b.t,
c: b.c,
m: b.m,
e: b.e,
u: b.u,
a: b.a,
s: b.s,
r: r,
attrs: b.attrs,
}
}
// setAttrs replaces the value at key with the given values and returns a new base error.
// Values are always stored as []string under a stable key.
// This helper is not exported; use WithSuggestions (or add a new With* method) instead.
func (b *base) setAttrs(key string, values ...string) *base {
copiedAttrs := make(map[string]any, len(b.attrs)+1)
maps.Copy(copiedAttrs, b.attrs)
replacement := make([]string, len(values))
copy(replacement, values)
copiedAttrs[key] = replacement
return &base{
t: b.t,
c: b.c,
m: b.m,
e: b.e,
u: b.u,
a: b.a,
s: b.s,
r: b.r,
attrs: copiedAttrs,
}
}
// addAttrs appends additional values to the existing list at key and returns a new base error.
// Values are always stored as []string under a stable key.
// This helper is not exported; use AddSuggestions, AddWarnings, or AddInvalidReferences,
// or add a new method that calls addAttrs for any specific attributes.
func (b *base) addAttrs(key string, values ...string) *base {
copiedAttrs := make(map[string]any, len(b.attrs)+1)
maps.Copy(copiedAttrs, b.attrs)
if len(values) > 0 {
existing, _ := copiedAttrs[key].([]string)
merged := make([]string, 0, len(existing)+len(values))
merged = append(merged, existing...)
merged = append(merged, values...)
copiedAttrs[key] = merged
}
return &base{
t: b.t,
c: b.c,
m: b.m,
e: b.e,
u: b.u,
a: b.a,
s: b.s,
r: b.r,
attrs: copiedAttrs,
}
}
// WithSuggestions replaces the list of suggestions on the base error.
func (b *base) WithSuggestions(suggestions ...string) *base {
return b.setAttrs("suggestions", suggestions...)
}
// AddSuggestions appends to the existing list of suggestions on the base error.
func (b *base) AddSuggestions(suggestions ...string) *base {
return b.addAttrs("suggestions", suggestions...)
}
// WithInvalidReference replaces the list of invalid references on the base error with a single entry.
func (b *base) WithInvalidReference(invalidReference string) *base {
return b.setAttrs("invalidReferences", invalidReference)
}
// WithInvalidReferences replaces the list of invalid references on the base error.
func (b *base) WithInvalidReferences(invalidReferences ...string) *base {
return b.setAttrs("invalidReferences", invalidReferences...)
}
// AddInvalidReference appends a single invalid reference to the existing list on the base error.
func (b *base) AddInvalidReference(invalidReference string) *base {
return b.addAttrs("invalidReferences", invalidReference)
}
// AddInvalidReferences appends to the existing list of invalid references on the base error.
func (b *base) AddInvalidReferences(invalidReferences ...string) *base {
return b.addAttrs("invalidReferences", invalidReferences...)
}
// WithWarnings replaces the list of warnings on the base error.
func (b *base) WithWarnings(warnings ...string) *base {
return b.setAttrs("warnings", warnings...)
}
// AddWarnings appends to the existing list of warnings on the base error.
func (b *base) AddWarnings(warnings ...string) *base {
return b.addAttrs("warnings", warnings...)
}
// WithRetryNever sets the retry policy to Never.
func (b *base) WithRetryNever() *base {
return b.WithRetry(retry{policy: RetryNever})
}
// WithRetryImmediate sets the retry policy to Immediate.
func (b *base) WithRetryImmediate() *base {
return b.WithRetry(retry{policy: RetryImmediate})
}
// WithRetryBackoff sets the retry policy to Backoff.
func (b *base) WithRetryBackoff() *base {
return b.WithRetry(retry{policy: RetryBackoff})
}
// WithRetryAfter sets the retry policy to After and requires a delay.
func (b *base) WithRetryAfter(delay time.Duration) *base {
return b.WithRetry(retry{policy: RetryAfter, after: delay})
}
// WithRetryAfterFix sets the retry policy to AfterFix.
func (b *base) WithRetryAfterFix() *base {
return b.WithRetry(retry{policy: RetryAfterFix})
}
// WithRetryAfterAuth sets the retry policy to AfterAuth.
func (b *base) WithRetryAfterAuth() *base {
return b.WithRetry(retry{policy: RetryAfterAuth})
}
// Unwrapb is a combination of built-in errors.As and type casting.
// It finds the first error in cause that matches base,
// and if one is found, returns the individual fields of base.
// Otherwise, it returns TypeInternal, the original error string
// and the error itself.
//
//nolint:staticcheck // ST1008: intentional return order matching struct field order (TCMEUARA)
func Unwrapb(cause error) (typ, Code, string, error, string, []string, retry, map[string]any) {
base, ok := cause.(*base)
if ok {
return base.t, base.c, base.m, base.e, base.u, base.a, base.r, base.attrs
}
return TypeInternal, CodeUnknown, cause.Error(), cause, "", []string{}, retry{policy: RetryNever}, map[string]any{}
}
// Ast checks if the provided error matches the specified custom error type.
func Ast(cause error, typ typ) bool {
t, _, _, _, _, _, _, _ := Unwrapb(cause)
return t == typ
}
// Asc checks if the provided error matches the specified custom error code.
func Asc(cause error, code Code) bool {
_, c, _, _, _, _, _, _ := Unwrapb(cause)
return c.s == code.s
}
// Join is a wrapper around errors.Join.
func Join(errs ...error) error {
return errors.Join(errs...)
}
// As is a wrapper around errors.As.
func As(err error, target any) bool {
return errors.As(err, target)
}
// Is is a wrapper around errors.Is.
func Is(err error, target error) bool {
return errors.Is(err, target)
}
// WrapNotFoundf is a wrapper around Wrapf with TypeNotFound.
func WrapNotFoundf(cause error, code Code, format string, args ...any) *base {
return Wrapf(cause, TypeNotFound, code, format, args...).WithRetry(retry{policy: RetryNever})
}
// NewNotFoundf is a wrapper around Newf with TypeNotFound.
func NewNotFoundf(code Code, format string, args ...any) *base {
return Newf(TypeNotFound, code, format, args...).WithRetry(retry{policy: RetryNever})
}
// WrapInternalf is a wrapper around Wrapf with TypeInternal.
func WrapInternalf(cause error, code Code, format string, args ...any) *base {
return Wrapf(cause, TypeInternal, code, format, args...).WithRetry(retry{policy: RetryNever})
}
// NewInternalf is a wrapper around Newf with TypeInternal.
func NewInternalf(code Code, format string, args ...any) *base {
return Newf(TypeInternal, code, format, args...).WithRetry(retry{policy: RetryNever})
}
// WrapInvalidInputf is a wrapper around Wrapf with TypeInvalidInput.
func WrapInvalidInputf(cause error, code Code, format string, args ...any) *base {
return Wrapf(cause, TypeInvalidInput, code, format, args...).WithRetry(retry{policy: RetryAfterFix})
}
// NewInvalidInputf is a wrapper around Newf with TypeInvalidInput.
func NewInvalidInputf(code Code, format string, args ...any) *base {
return Newf(TypeInvalidInput, code, format, args...).WithRetry(retry{policy: RetryAfterFix})
}
// NewMethodNotAllowedf is a wrapper around Newf with TypeMethodNotAllowed.
func NewMethodNotAllowedf(code Code, format string, args ...any) *base {
return Newf(TypeMethodNotAllowed, code, format, args...).WithRetry(retry{policy: RetryNever})
}
// WrapTimeoutf is a wrapper around Wrapf with TypeTimeout.
func WrapTimeoutf(cause error, code Code, format string, args ...any) *base {
return Wrapf(cause, TypeTimeout, code, format, args...).WithRetry(retry{policy: RetryBackoff})
}
// NewTimeoutf is a wrapper around Newf with TypeTimeout.
func NewTimeoutf(code Code, format string, args ...any) *base {
return Newf(TypeTimeout, code, format, args...).WithRetry(retry{policy: RetryBackoff})
}
// NewUnauthenticatedf is a wrapper around Newf with TypeUnauthenticated.
func NewUnauthenticatedf(code Code, format string, args ...any) *base {
return Newf(TypeUnauthenticated, code, format, args...).WithRetry(retry{policy: RetryAfterAuth})
}
// NewForbiddenf is a wrapper around Newf with TypeForbidden.
func NewForbiddenf(code Code, format string, args ...any) *base {
return Newf(TypeForbidden, code, format, args...).WithRetry(retry{policy: RetryNever})
}
// Attr returns an slog.Attr with a standardized "exception" key for the given error.
func Attr(err error) slog.Attr {
return slog.Any("exception", err)
}
// TypeAttr returns an OTel attribute.KeyValue with the "error.type" semconv key
// set to the error's type string.
func TypeAttr(err error) attribute.KeyValue {
t, _, _, _, _, _, _, _ := Unwrapb(err)
return attribute.String("error.type", t.String())
}
func RetryAfterOf(err error) (delay time.Duration, ok bool) {
_, _, _, _, _, _, r, _ := Unwrapb(err)
delay = r.after
return delay, r.policy == RetryAfter
}