mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-11 15:40:18 -05:00
Bumps [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) from 0.0.5 to 1.0.6. - [Commits](https://github.com/olekukonko/tablewriter/compare/v0.0.5...v1.0.6) --- updated-dependencies: - dependency-name: github.com/olekukonko/tablewriter dependency-version: 1.0.6 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com>
958 lines
25 KiB
Go
958 lines
25 KiB
Go
package errors
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"runtime"
|
||
"strings"
|
||
"sync"
|
||
"sync/atomic"
|
||
)
|
||
|
||
const (
|
||
ctxTimeout = "[error] timeout" // Context key for marking timeout errors
|
||
ctxRetry = "[error] retry" // Context key for marking retryable errors
|
||
|
||
contextSize = 4 // Default size of fixed-size context array
|
||
bufferSize = 256 // Initial buffer size for JSON marshaling
|
||
warmUpSize = 100 // Number of errors to pre-warm the pool
|
||
stackDepth = 32 // Default maximum stack trace depth
|
||
)
|
||
|
||
type ErrorCategory string
|
||
|
||
// ErrorOpts provides options for customizing error creation.
|
||
type ErrorOpts struct {
|
||
SkipStack int // Number of stack frames to skip when capturing the stack trace
|
||
}
|
||
|
||
// Config defines the configuration for the errors package.
|
||
type Config struct {
|
||
StackDepth int // Maximum depth of the stack trace; 0 uses default
|
||
ContextSize int // Initial size of the context map; 0 uses default
|
||
DisablePooling bool // Disables object pooling for errors if true
|
||
FilterInternal bool // Filters internal package frames from stack traces if true
|
||
AutoFree bool // Automatically frees errors to pool if true
|
||
}
|
||
|
||
// cachedConfig holds the current configuration, updated only on Configure().
|
||
type cachedConfig struct {
|
||
stackDepth int
|
||
contextSize int
|
||
disablePooling bool
|
||
filterInternal bool
|
||
autoFree bool
|
||
}
|
||
|
||
var (
|
||
currentConfig cachedConfig
|
||
configMu sync.RWMutex
|
||
errorPool = NewErrorPool() // Custom pool for Error instances
|
||
stackPool = sync.Pool{ // Pool for stack trace slices
|
||
New: func() interface{} {
|
||
return make([]uintptr, currentConfig.stackDepth)
|
||
},
|
||
}
|
||
emptyError = &Error{
|
||
smallContext: [contextSize]contextItem{},
|
||
msg: "",
|
||
name: "",
|
||
template: "",
|
||
cause: nil,
|
||
}
|
||
)
|
||
|
||
//var bufferPool = sync.Pool{
|
||
// New: func() interface{} {
|
||
// return bytes.NewBuffer(make([]byte, 0, bufferSize))
|
||
// },
|
||
//}
|
||
|
||
// contextItem represents a single key-value pair in the smallContext array.
|
||
type contextItem struct {
|
||
key string
|
||
value interface{}
|
||
}
|
||
|
||
// Error represents a custom error with enhanced features like context, stack traces, and wrapping.
|
||
type Error struct {
|
||
// Primary error information (most frequently accessed)
|
||
msg string // Error message
|
||
name string // Error name/type
|
||
stack []uintptr // Stack trace
|
||
|
||
// Secondary error metadata
|
||
template string // Message template used if msg is empty
|
||
category string // Error category (e.g., "network", "validation")
|
||
count uint64 // Occurrence count for tracking frequency
|
||
code int32 // HTTP-like error code
|
||
smallCount int32 // Number of items in smallContext
|
||
|
||
// Context and chaining
|
||
context map[string]interface{} // Additional context as key-value pairs
|
||
cause error // Wrapped underlying error
|
||
callback func() // Optional callback executed on Error()
|
||
smallContext [contextSize]contextItem // Fixed-size context storage for efficiency
|
||
|
||
// Synchronization
|
||
mu sync.RWMutex // Protects concurrent access to mutable fields
|
||
}
|
||
|
||
// init initializes the package with default configuration and pre-warms the error pool.
|
||
func init() {
|
||
currentConfig = cachedConfig{
|
||
stackDepth: stackDepth,
|
||
contextSize: contextSize,
|
||
disablePooling: false,
|
||
filterInternal: true,
|
||
autoFree: true,
|
||
}
|
||
WarmPool(warmUpSize) // Pre-warm pool with initial errors
|
||
}
|
||
|
||
// Configure updates the global configuration for the errors package.
|
||
// Thread-safe; should be called before heavy usage for optimal performance.
|
||
// Changes apply immediately to all subsequent error operations.
|
||
func Configure(cfg Config) {
|
||
configMu.Lock()
|
||
defer configMu.Unlock()
|
||
|
||
if cfg.StackDepth != 0 {
|
||
currentConfig.stackDepth = cfg.StackDepth
|
||
}
|
||
if cfg.ContextSize != 0 {
|
||
currentConfig.contextSize = cfg.ContextSize
|
||
}
|
||
currentConfig.disablePooling = cfg.DisablePooling
|
||
currentConfig.filterInternal = cfg.FilterInternal
|
||
currentConfig.autoFree = cfg.AutoFree
|
||
}
|
||
|
||
// newError creates a new Error instance, using the pool if enabled.
|
||
// Initializes smallContext and stack appropriately.
|
||
func newError() *Error {
|
||
if currentConfig.disablePooling {
|
||
return &Error{
|
||
smallContext: [contextSize]contextItem{},
|
||
stack: nil,
|
||
}
|
||
}
|
||
return errorPool.Get()
|
||
}
|
||
|
||
// Empty creates a new empty error with no stack trace.
|
||
// Useful as a base for building errors incrementally.
|
||
func Empty() *Error {
|
||
return emptyError
|
||
}
|
||
|
||
// Named creates a new error with a specific name and stack trace.
|
||
// The name is used as the error message if no other message is set.
|
||
func Named(name string) *Error {
|
||
e := newError()
|
||
e.name = name
|
||
return e.WithStack()
|
||
}
|
||
|
||
// New creates a fast, lightweight error without stack tracing.
|
||
// Use instead of Trace() when stack traces aren't needed for better performance.
|
||
func New(text string) *Error {
|
||
if text == "" {
|
||
return emptyError.Copy() // Global pre-allocated empty error
|
||
}
|
||
err := newError()
|
||
err.msg = text
|
||
return err
|
||
}
|
||
|
||
// Newf is an alias to Errorf for fmt.Errorf compatibility.
|
||
// Creates a formatted error without stack traces.
|
||
func Newf(format string, args ...interface{}) *Error {
|
||
err := newError()
|
||
err.msg = fmt.Sprintf(format, args...)
|
||
return err
|
||
}
|
||
|
||
// Std creates a standard error using errors.New, provided for backward compatibility.
|
||
// This function serves as a lightweight wrapper around the standard library's error creation,
|
||
// allowing users to opt into basic error handling without adopting the full features of this package.
|
||
func Std(text string) error {
|
||
return errors.New(text)
|
||
}
|
||
|
||
// Stdf creates a formatted standard error using fmt.Errorf, provided for backward compatibility.
|
||
// This function wraps the standard library's formatted error creation, offering a simple alternative
|
||
// to the package's enhanced error handling while maintaining compatibility with existing codebases.
|
||
func Stdf(format string, a ...interface{}) error {
|
||
return fmt.Errorf(format, a...)
|
||
}
|
||
|
||
// Trace creates an error with stack trace capture enabled.
|
||
// Use when call stacks are needed for debugging; has performance overhead.
|
||
func Trace(text string) *Error {
|
||
e := New(text)
|
||
return e.WithStack()
|
||
}
|
||
|
||
// Tracef creates a formatted error with stack trace.
|
||
// Combines Errorf and WithStack for convenience.
|
||
func Tracef(format string, args ...interface{}) *Error {
|
||
e := Newf(format, args...)
|
||
return e.WithStack()
|
||
}
|
||
|
||
// As attempts to assign the error or one in its chain to the target interface.
|
||
// It only assigns when the target is a **Error and the current error node has a non-empty name.
|
||
// If the current node has an empty name, it delegates to its wrapped cause.
|
||
func (e *Error) As(target interface{}) bool {
|
||
if e == nil {
|
||
return false
|
||
}
|
||
// Handle *Error target (for stderrors.As compatibility)
|
||
if targetPtr, ok := target.(*Error); ok {
|
||
current := e
|
||
for current != nil {
|
||
if current.name != "" {
|
||
*targetPtr = *current
|
||
return true
|
||
}
|
||
if next, ok := current.cause.(*Error); ok {
|
||
current = next
|
||
} else if current.cause != nil {
|
||
return errors.As(current.cause, target)
|
||
} else {
|
||
return false
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
// Handle *error target - unwrap to innermost error
|
||
if targetErr, ok := target.(*error); ok {
|
||
innermost := error(e)
|
||
current := error(e)
|
||
for current != nil {
|
||
if err, ok := current.(*Error); ok && err.cause != nil {
|
||
current = err.cause
|
||
innermost = current
|
||
} else {
|
||
break
|
||
}
|
||
}
|
||
*targetErr = innermost
|
||
return true
|
||
}
|
||
|
||
// Delegate to cause for other types
|
||
if e.cause != nil {
|
||
return errors.As(e.cause, target)
|
||
}
|
||
return false
|
||
}
|
||
|
||
// Callback sets a function to be called when Error() is invoked.
|
||
// Useful for logging or side effects; returns the error for chaining.
|
||
func (e *Error) Callback(fn func()) *Error {
|
||
e.callback = fn
|
||
return e
|
||
}
|
||
|
||
// Category returns the error's category, if set.
|
||
// Returns an empty string if no category is defined.
|
||
func (e *Error) Category() string {
|
||
return e.category
|
||
}
|
||
|
||
// Code returns the error's status code, if set.
|
||
// Returns 0 if no code is defined.
|
||
func (e *Error) Code() int {
|
||
return int(e.code)
|
||
}
|
||
|
||
// Context returns the error's context as a map.
|
||
// Converts smallContext to a map if needed; returns nil if empty.
|
||
func (e *Error) Context() map[string]interface{} {
|
||
e.mu.RLock()
|
||
defer e.mu.RUnlock()
|
||
|
||
if e.smallCount > 0 && e.context == nil {
|
||
e.context = make(map[string]interface{}, e.smallCount)
|
||
for i := int32(0); i < e.smallCount; i++ {
|
||
e.context[e.smallContext[i].key] = e.smallContext[i].value
|
||
}
|
||
}
|
||
return e.context
|
||
}
|
||
|
||
// Copy creates a deep copy of the error, preserving all fields except stack.
|
||
// The new error does not capture a new stack trace unless explicitly added.
|
||
func (e *Error) Copy() *Error {
|
||
if e == emptyError {
|
||
return &Error{
|
||
smallContext: [contextSize]contextItem{},
|
||
}
|
||
}
|
||
|
||
newErr := newError()
|
||
|
||
newErr.msg = e.msg
|
||
newErr.name = e.name
|
||
newErr.template = e.template
|
||
newErr.cause = e.cause
|
||
newErr.code = e.code
|
||
newErr.category = e.category
|
||
newErr.count = e.count
|
||
|
||
if e.smallCount > 0 {
|
||
newErr.smallCount = e.smallCount
|
||
for i := int32(0); i < e.smallCount; i++ {
|
||
newErr.smallContext[i] = e.smallContext[i]
|
||
}
|
||
} else if e.context != nil {
|
||
newErr.context = make(map[string]interface{}, len(e.context))
|
||
for k, v := range e.context {
|
||
newErr.context[k] = v
|
||
}
|
||
}
|
||
|
||
if e.stack != nil && len(e.stack) > 0 {
|
||
if newErr.stack == nil {
|
||
newErr.stack = stackPool.Get().([]uintptr)
|
||
}
|
||
newErr.stack = append(newErr.stack[:0], e.stack...)
|
||
}
|
||
|
||
return newErr
|
||
}
|
||
|
||
// Count returns the number of times the error has been incremented.
|
||
// Useful for tracking occurrence frequency.
|
||
func (e *Error) Count() uint64 {
|
||
return e.count
|
||
}
|
||
|
||
// Err returns the error as an error interface.
|
||
// Provided for compatibility; simply returns the error itself.
|
||
func (e *Error) Err() error {
|
||
return e
|
||
}
|
||
|
||
// Error returns the string representation of the error.
|
||
// Prioritizes msg, then template, then name, falling back to "unknown error".
|
||
// Executes callback if set before returning the message.
|
||
func (e *Error) Error() string {
|
||
if e.callback != nil {
|
||
e.callback()
|
||
}
|
||
var msg string
|
||
switch {
|
||
case e.msg != "":
|
||
msg = e.msg
|
||
case e.template != "":
|
||
msg = e.template
|
||
case e.name != "":
|
||
msg = e.name
|
||
default:
|
||
msg = "unknown error"
|
||
}
|
||
if e.cause != nil {
|
||
causeMsg := e.cause.Error()
|
||
if msg != "" && causeMsg != "" {
|
||
msg = msg + ": " + causeMsg
|
||
} else if causeMsg != "" {
|
||
msg = causeMsg
|
||
}
|
||
}
|
||
return msg
|
||
}
|
||
|
||
// Errorf creates a formatted error without stack traces.
|
||
// Compatible with fmt.Errorf; does not capture stack trace for performance.
|
||
func Errorf(format string, args ...interface{}) *Error {
|
||
err := newError()
|
||
err.msg = fmt.Sprintf(format, args...)
|
||
return err
|
||
}
|
||
|
||
// FastStack returns a lightweight stack trace without function names.
|
||
// Filters internal frames if FilterInternal is enabled; returns nil if no stack.
|
||
func (e *Error) FastStack() []string {
|
||
if e.stack == nil {
|
||
return nil
|
||
}
|
||
configMu.RLock()
|
||
filter := currentConfig.filterInternal
|
||
configMu.RUnlock()
|
||
|
||
pcs := e.stack
|
||
frames := make([]string, 0, len(pcs))
|
||
for _, pc := range pcs {
|
||
fn := runtime.FuncForPC(pc)
|
||
if fn == nil {
|
||
frames = append(frames, "unknown")
|
||
continue
|
||
}
|
||
file, line := fn.FileLine(pc)
|
||
if filter && isInternalFrame(runtime.Frame{File: file, Function: fn.Name()}) {
|
||
continue
|
||
}
|
||
frames = append(frames, fmt.Sprintf("%s:%d", file, line))
|
||
}
|
||
return frames
|
||
}
|
||
|
||
// Find searches the error chain for the first error matching pred.
|
||
// Starts with the current error and follows Unwrap() and Cause() chains.
|
||
func (e *Error) Find(pred func(error) bool) error {
|
||
if e == nil || pred == nil {
|
||
return nil
|
||
}
|
||
return Find(e, pred)
|
||
}
|
||
|
||
// Format returns a formatted string representation of the error.
|
||
// Includes message, code, context, and stack trace if present.
|
||
func (e *Error) Format() string {
|
||
var sb strings.Builder
|
||
|
||
// Error message
|
||
sb.WriteString("Error: " + e.Error() + "\n")
|
||
|
||
// Metadata
|
||
if e.code != 0 {
|
||
sb.WriteString(fmt.Sprintf("Code: %d\n", e.code))
|
||
}
|
||
|
||
// Context (only show context added at this level)
|
||
if ctx := e.contextAtThisLevel(); len(ctx) > 0 {
|
||
sb.WriteString("Context:\n")
|
||
for k, v := range ctx {
|
||
sb.WriteString(fmt.Sprintf(" %s: %v\n", k, v))
|
||
}
|
||
}
|
||
|
||
// Stack trace
|
||
if e.stack != nil {
|
||
sb.WriteString("Stack:\n")
|
||
for i, frame := range e.Stack() {
|
||
sb.WriteString(fmt.Sprintf(" %d. %s\n", i+1, frame))
|
||
}
|
||
}
|
||
|
||
return sb.String()
|
||
}
|
||
|
||
// contextAtThisLevel returns context specific to this error level, excluding inherited context.
|
||
// Combines smallContext and context map into a single map; returns nil if empty.
|
||
func (e *Error) contextAtThisLevel() map[string]interface{} {
|
||
if e.context == nil && e.smallCount == 0 {
|
||
return nil
|
||
}
|
||
|
||
ctx := make(map[string]interface{})
|
||
// Add smallContext items
|
||
for i := 0; i < int(e.smallCount); i++ {
|
||
ctx[e.smallContext[i].key] = e.smallContext[i].value
|
||
}
|
||
// Add map context items
|
||
if e.context != nil {
|
||
for k, v := range e.context {
|
||
ctx[k] = v
|
||
}
|
||
}
|
||
return ctx
|
||
}
|
||
|
||
// Free resets the error and returns it to the pool if pooling is enabled.
|
||
// Does nothing beyond reset if pooling is disabled.
|
||
func (e *Error) Free() {
|
||
if currentConfig.disablePooling {
|
||
return
|
||
}
|
||
|
||
e.Reset()
|
||
|
||
if e.stack != nil {
|
||
stackPool.Put(e.stack[:cap(e.stack)])
|
||
e.stack = nil
|
||
}
|
||
errorPool.Put(e)
|
||
}
|
||
|
||
// Has checks if the error contains meaningful content.
|
||
// Returns true if msg, template, name, or cause is non-empty/nil.
|
||
func (e *Error) Has() bool {
|
||
return e != nil && (e.msg != "" || e.template != "" || e.name != "" || e.cause != nil)
|
||
}
|
||
|
||
// HasContextKey checks if the specified key exists in the error's context.
|
||
// Searches both smallContext and context map; thread-safe.
|
||
func (e *Error) HasContextKey(key string) bool {
|
||
e.mu.RLock()
|
||
defer e.mu.RUnlock()
|
||
|
||
if e.smallCount > 0 {
|
||
for i := int32(0); i < e.smallCount; i++ {
|
||
if e.smallContext[i].key == key {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
if e.context != nil {
|
||
_, exists := e.context[key]
|
||
return exists
|
||
}
|
||
return false
|
||
}
|
||
|
||
// Increment increases the error's count by 1 and returns the error.
|
||
// Uses atomic operation for thread safety.
|
||
func (e *Error) Increment() *Error {
|
||
atomic.AddUint64(&e.count, 1)
|
||
return e
|
||
}
|
||
|
||
// Is checks if the error matches a target error by pointer equality, name, or wrapped cause.
|
||
// Ensures compatibility with stderrors.Is by prioritizing chain traversal.
|
||
func (e *Error) Is(target error) bool {
|
||
if e == nil || target == nil {
|
||
return e == target
|
||
}
|
||
if e == target {
|
||
return true
|
||
}
|
||
if e.name != "" {
|
||
if te, ok := target.(*Error); ok && te.name != "" && e.name == te.name {
|
||
return true
|
||
}
|
||
}
|
||
// Add string comparison for standard errors
|
||
if stdErr, ok := target.(error); ok && e.Error() == stdErr.Error() {
|
||
return true
|
||
}
|
||
if e.cause != nil {
|
||
return errors.Is(e.cause, target)
|
||
}
|
||
return false
|
||
}
|
||
|
||
// IsEmpty checks if the error has no meaningful content (empty message, no name/template/cause).
|
||
// Returns true for nil errors or errors with no data.
|
||
func (e *Error) IsEmpty() bool {
|
||
if e == nil {
|
||
return true
|
||
}
|
||
return e.msg == "" && e.template == "" && e.name == "" && e.cause == nil
|
||
}
|
||
|
||
// IsNull checks if an error is nil or represents a SQL NULL value.
|
||
// Considers both the error itself and any context values; returns true if all context is null.
|
||
func (e *Error) IsNull() bool {
|
||
if e == nil || e == emptyError {
|
||
return true
|
||
}
|
||
// If no context or cause, and no content, it's not null
|
||
if e.smallCount == 0 && e.context == nil && e.cause == nil {
|
||
return false
|
||
}
|
||
|
||
// Check cause first - if it’s null, the whole error is null
|
||
if e.cause != nil {
|
||
var isNull bool
|
||
if ce, ok := e.cause.(*Error); ok {
|
||
isNull = ce.IsNull()
|
||
} else {
|
||
isNull = sqlNull(e.cause)
|
||
}
|
||
if isNull {
|
||
return true
|
||
}
|
||
// If cause isn’t null, continue checking this error’s context
|
||
}
|
||
|
||
// Check small context
|
||
if e.smallCount > 0 {
|
||
allNull := true
|
||
for i := 0; i < int(e.smallCount); i++ {
|
||
isNull := sqlNull(e.smallContext[i].value)
|
||
if !isNull {
|
||
allNull = false
|
||
break
|
||
}
|
||
}
|
||
if !allNull {
|
||
return false
|
||
}
|
||
}
|
||
|
||
// Check regular context
|
||
if e.context != nil {
|
||
allNull := true
|
||
for _, v := range e.context {
|
||
isNull := sqlNull(v)
|
||
if !isNull {
|
||
allNull = false
|
||
break
|
||
}
|
||
}
|
||
if !allNull {
|
||
return false
|
||
}
|
||
}
|
||
|
||
// Null if we have context and it’s all null
|
||
return e.smallCount > 0 || e.context != nil
|
||
}
|
||
|
||
var (
|
||
jsonBufferPool = sync.Pool{
|
||
New: func() interface{} {
|
||
return bytes.NewBuffer(make([]byte, 0, bufferSize))
|
||
},
|
||
}
|
||
)
|
||
|
||
// MarshalJSON serializes the error to JSON, including name, message, context, cause, and stack.
|
||
// Handles nested *Error causes and custom marshalers efficiently.
|
||
func (e *Error) MarshalJSON() ([]byte, error) {
|
||
// Get buffer from pool
|
||
buf := jsonBufferPool.Get().(*bytes.Buffer)
|
||
defer jsonBufferPool.Put(buf)
|
||
buf.Reset()
|
||
|
||
// Create new encoder each time (no Reset available)
|
||
enc := json.NewEncoder(buf)
|
||
enc.SetEscapeHTML(false)
|
||
|
||
// Prepare error data
|
||
je := struct {
|
||
Name string `json:"name,omitempty"`
|
||
Message string `json:"message,omitempty"`
|
||
Context map[string]interface{} `json:"context,omitempty"`
|
||
Cause interface{} `json:"cause,omitempty"`
|
||
Stack []string `json:"stack,omitempty"`
|
||
Code int `json:"code,omitempty"`
|
||
}{
|
||
Name: e.name,
|
||
Message: e.msg,
|
||
Code: e.Code(),
|
||
}
|
||
|
||
// Handle context
|
||
if ctx := e.Context(); len(ctx) > 0 {
|
||
je.Context = ctx
|
||
}
|
||
|
||
// Handle stack
|
||
if e.stack != nil {
|
||
je.Stack = e.Stack()
|
||
}
|
||
|
||
// Handle cause
|
||
if e.cause != nil {
|
||
switch c := e.cause.(type) {
|
||
case *Error:
|
||
je.Cause = c
|
||
case json.Marshaler:
|
||
je.Cause = c
|
||
default:
|
||
je.Cause = c.Error()
|
||
}
|
||
}
|
||
|
||
// Encode
|
||
if err := enc.Encode(je); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Return bytes without trailing newline
|
||
result := buf.Bytes()
|
||
if len(result) > 0 && result[len(result)-1] == '\n' {
|
||
result = result[:len(result)-1]
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// Msgf sets the error message using a formatted string.
|
||
// Overwrites any existing message; returns the error for chaining.
|
||
func (e *Error) Msgf(format string, args ...interface{}) *Error {
|
||
e.msg = fmt.Sprintf(format, args...)
|
||
return e
|
||
}
|
||
|
||
// Name returns the error's name, if set.
|
||
// Returns an empty string if no name is defined.
|
||
func (e *Error) Name() string {
|
||
return e.name
|
||
}
|
||
|
||
// Reset clears all fields of the error, preparing it for reuse.
|
||
// Does not free the stack; use Free() to return to pool.
|
||
func (e *Error) Reset() {
|
||
e.msg = ""
|
||
e.name = ""
|
||
e.template = ""
|
||
e.category = ""
|
||
e.code = 0
|
||
e.count = 0
|
||
e.cause = nil
|
||
e.callback = nil
|
||
|
||
if e.context != nil {
|
||
for k := range e.context {
|
||
delete(e.context, k)
|
||
}
|
||
}
|
||
e.smallCount = 0
|
||
|
||
if e.stack != nil {
|
||
e.stack = e.stack[:0]
|
||
}
|
||
}
|
||
|
||
// Stack returns a detailed stack trace as a slice of strings.
|
||
// Filters internal frames if FilterInternal is enabled; returns nil if no stack.
|
||
func (e *Error) Stack() []string {
|
||
if e.stack == nil {
|
||
return nil
|
||
}
|
||
|
||
frames := runtime.CallersFrames(e.stack)
|
||
var trace []string
|
||
for {
|
||
frame, more := frames.Next()
|
||
if frame == (runtime.Frame{}) {
|
||
break
|
||
}
|
||
|
||
if currentConfig.filterInternal && isInternalFrame(frame) {
|
||
continue
|
||
}
|
||
|
||
trace = append(trace, fmt.Sprintf("%s %s:%d",
|
||
frame.Function,
|
||
frame.File,
|
||
frame.Line))
|
||
|
||
if !more {
|
||
break
|
||
}
|
||
}
|
||
return trace
|
||
}
|
||
|
||
// Trace ensures the error has a stack trace, capturing it if missing.
|
||
// Skips capture if stack already exists; returns the error for chaining.
|
||
func (e *Error) Trace() *Error {
|
||
if e.stack == nil {
|
||
e.stack = captureStack(2)
|
||
}
|
||
return e
|
||
}
|
||
|
||
// Transform applies transformations to a copy of the error.
|
||
// Returns the transformed copy or the original if no changes are needed.
|
||
func (e *Error) Transform(fn func(*Error)) *Error {
|
||
if e == nil || fn == nil {
|
||
return e
|
||
}
|
||
newErr := e.Copy()
|
||
fn(newErr)
|
||
return newErr
|
||
}
|
||
|
||
// Unwrap returns the underlying cause of the error, if any.
|
||
// Implements the errors.Unwrap interface for unwrapping chains.
|
||
func (e *Error) Unwrap() error {
|
||
return e.cause
|
||
}
|
||
|
||
// UnwrapAll returns a slice of all errors in the chain, starting with this error.
|
||
// Traverses the cause chain, creating isolated copies of each *Error.
|
||
func (e *Error) UnwrapAll() []error {
|
||
if e == nil {
|
||
return nil
|
||
}
|
||
var chain []error
|
||
current := error(e)
|
||
for current != nil {
|
||
if err, ok := current.(*Error); ok {
|
||
isolated := newError()
|
||
isolated.msg = err.msg
|
||
isolated.name = err.name
|
||
isolated.template = err.template
|
||
isolated.code = err.code
|
||
isolated.category = err.category
|
||
if err.smallCount > 0 {
|
||
isolated.smallCount = err.smallCount
|
||
for i := int32(0); i < err.smallCount; i++ {
|
||
isolated.smallContext[i] = err.smallContext[i]
|
||
}
|
||
}
|
||
if err.context != nil {
|
||
isolated.context = make(map[string]interface{}, len(err.context))
|
||
for k, v := range err.context {
|
||
isolated.context[k] = v
|
||
}
|
||
}
|
||
if err.stack != nil {
|
||
isolated.stack = append([]uintptr(nil), err.stack...)
|
||
}
|
||
chain = append(chain, isolated)
|
||
} else {
|
||
chain = append(chain, current)
|
||
}
|
||
if unwrapper, ok := current.(interface{ Unwrap() error }); ok {
|
||
current = unwrapper.Unwrap()
|
||
} else {
|
||
break
|
||
}
|
||
}
|
||
return chain
|
||
}
|
||
|
||
// Walk traverses the error chain, applying fn to each error.
|
||
// Starts with the current error and follows the cause chain.
|
||
func (e *Error) Walk(fn func(error)) {
|
||
if e == nil || fn == nil {
|
||
return
|
||
}
|
||
current := error(e)
|
||
for current != nil {
|
||
fn(current)
|
||
if unwrappable, ok := current.(interface{ Unwrap() error }); ok {
|
||
current = unwrappable.Unwrap()
|
||
} else {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
// With adds a key-value pair to the error's context.
|
||
// Uses smallContext for efficiency until full, then switches to map; thread-safe.
|
||
func (e *Error) With(key string, value interface{}) *Error {
|
||
// Fast path for small context (no map needed)
|
||
if e.smallCount < contextSize && e.context == nil {
|
||
e.mu.Lock()
|
||
// Double-check after acquiring lock
|
||
if e.smallCount < contextSize && e.context == nil {
|
||
e.smallContext[e.smallCount] = contextItem{key, value}
|
||
e.smallCount++
|
||
e.mu.Unlock()
|
||
return e
|
||
}
|
||
e.mu.Unlock()
|
||
}
|
||
|
||
// Slow path - requires map
|
||
e.mu.Lock()
|
||
defer e.mu.Unlock()
|
||
|
||
if e.context == nil {
|
||
e.context = make(map[string]interface{}, currentConfig.contextSize)
|
||
// Migrate existing items if any
|
||
for i := int32(0); i < e.smallCount; i++ {
|
||
e.context[e.smallContext[i].key] = e.smallContext[i].value
|
||
}
|
||
}
|
||
|
||
e.context[key] = value
|
||
return e
|
||
}
|
||
|
||
// WithCategory sets a category for the error and returns the error.
|
||
// Useful for classifying errors (e.g., "network", "validation").
|
||
func (e *Error) WithCategory(category ErrorCategory) *Error {
|
||
e.category = string(category)
|
||
return e
|
||
}
|
||
|
||
// WithCode sets an HTTP-like status code for the error and returns the error.
|
||
// Overwrites any existing code.
|
||
func (e *Error) WithCode(code int) *Error {
|
||
e.code = int32(code)
|
||
return e
|
||
}
|
||
|
||
// WithName sets the error's name and returns the error.
|
||
// Overwrites any existing name.
|
||
func (e *Error) WithName(name string) *Error {
|
||
e.name = name
|
||
return e
|
||
}
|
||
|
||
// WithRetryable marks the error as retryable in its context.
|
||
// Adds a "retry" key with value true; returns the error.
|
||
func (e *Error) WithRetryable() *Error {
|
||
return e.With(ctxRetry, true)
|
||
}
|
||
|
||
// WithStack captures the stack trace at call time and returns the error.
|
||
// Skips capturing if stack already exists or depth is 0.
|
||
func (e *Error) WithStack() *Error {
|
||
if e.stack == nil {
|
||
e.stack = captureStack(1) // Skip WithStack
|
||
}
|
||
return e
|
||
}
|
||
|
||
// WithTemplate sets a template string for the error and returns the error.
|
||
// Used as the error message if no explicit message is set.
|
||
func (e *Error) WithTemplate(template string) *Error {
|
||
e.template = template
|
||
return e
|
||
}
|
||
|
||
// WithTimeout marks the error as a timeout error in its context.
|
||
// Adds a "timeout" key with value true; returns the error.
|
||
func (e *Error) WithTimeout() *Error {
|
||
return e.With(ctxTimeout, true)
|
||
}
|
||
|
||
// Wrap associates a cause error with this error, creating an error chain.
|
||
// Returns the error for method chaining.
|
||
func (e *Error) Wrap(cause error) *Error {
|
||
if cause == nil {
|
||
return e
|
||
}
|
||
e.cause = cause
|
||
return e
|
||
}
|
||
|
||
// WrapNotNil wraps a cause error only if it is non-nil.
|
||
// Returns the error for method chaining; no-op if cause is nil.
|
||
func (e *Error) WrapNotNil(cause error) *Error {
|
||
if cause != nil {
|
||
e.cause = cause
|
||
}
|
||
return e
|
||
}
|
||
|
||
// WarmPool pre-populates the error pool with a specified number of instances.
|
||
// Reduces allocation overhead during initial usage; no effect if pooling is disabled.
|
||
func WarmPool(count int) {
|
||
if currentConfig.disablePooling {
|
||
return
|
||
}
|
||
for i := 0; i < count; i++ {
|
||
e := &Error{
|
||
smallContext: [contextSize]contextItem{},
|
||
stack: nil,
|
||
}
|
||
errorPool.Put(e)
|
||
stackPool.Put(make([]uintptr, 0, currentConfig.stackDepth))
|
||
}
|
||
}
|
||
|
||
// WarmStackPool pre-populates the stack pool with a specified number of slices.
|
||
// Reduces allocation overhead for stack traces; no effect if pooling is disabled.
|
||
func WarmStackPool(count int) {
|
||
if currentConfig.disablePooling {
|
||
return
|
||
}
|
||
for i := 0; i < count; i++ {
|
||
stackPool.Put(make([]uintptr, 0, currentConfig.stackDepth))
|
||
}
|
||
}
|