Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 25 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ true

## Index

- [Constants](<#constants>)
- [func SetBaseFilePath\(path string\)](<#SetBaseFilePath>)
- [func SetMaxStackDepth\(n int\)](<#SetMaxStackDepth>)
- [type ErrorExt](<#ErrorExt>)
Expand All @@ -124,8 +125,16 @@ true
- [type StackFrame](<#StackFrame>)


## Constants

<a name="SupportPackageIsVersion1"></a>SupportPackageIsVersion1 is a compile\-time assertion constant. Downstream packages reference this to enforce version compatibility.

```go
const SupportPackageIsVersion1 = true
```

<a name="SetBaseFilePath"></a>
## func [SetBaseFilePath](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L257>)
## func [SetBaseFilePath](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L281>)

```go
func SetBaseFilePath(path string)
Expand All @@ -134,16 +143,16 @@ func SetBaseFilePath(path string)
SetBaseFilePath sets the base file path for linking source code with reported stack information

<a name="SetMaxStackDepth"></a>
## func [SetMaxStackDepth](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L240>)
## func [SetMaxStackDepth](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L264>)

```go
func SetMaxStackDepth(n int)
```

SetMaxStackDepth sets the maximum number of stack frames captured when creating errors. Default is 64. Must be called during initialization.
SetMaxStackDepth sets the maximum number of stack frames captured when creating errors. Accepts values in \[1, 256\]; out\-of\-range values are ignored. Default is 16. Safe for concurrent use.

<a name="ErrorExt"></a>
## type [ErrorExt](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L26-L37>)
## type [ErrorExt](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L34-L45>)

ErrorExt is the interface that defines a error, any ErrorExt implementors can use and override errors and notifier package

Expand All @@ -163,7 +172,7 @@ type ErrorExt interface {
```

<a name="New"></a>
### func [New](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L156>)
### func [New](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L180>)

```go
func New(msg string) ErrorExt
Expand Down Expand Up @@ -201,7 +210,7 @@ something went wrong
</details>

<a name="NewWithSkip"></a>
### func [NewWithSkip](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L166>)
### func [NewWithSkip](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L190>)

```go
func NewWithSkip(msg string, skip int) ErrorExt
Expand All @@ -210,7 +219,7 @@ func NewWithSkip(msg string, skip int) ErrorExt
NewWithSkip creates a new error skipping the number of function on the stack

<a name="NewWithSkipAndStatus"></a>
### func [NewWithSkipAndStatus](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L171>)
### func [NewWithSkipAndStatus](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L195>)

```go
func NewWithSkipAndStatus(msg string, skip int, status *grpcstatus.Status) ErrorExt
Expand All @@ -219,7 +228,7 @@ func NewWithSkipAndStatus(msg string, skip int, status *grpcstatus.Status) Error
NewWithSkipAndStatus creates a new error skipping the number of function on the stack and GRPC status

<a name="NewWithStatus"></a>
### func [NewWithStatus](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L161>)
### func [NewWithStatus](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L185>)

```go
func NewWithStatus(msg string, status *grpcstatus.Status) ErrorExt
Expand All @@ -228,7 +237,7 @@ func NewWithStatus(msg string, status *grpcstatus.Status) ErrorExt
NewWithStatus creates a new error with statck information and GRPC status

<a name="Newf"></a>
### func [Newf](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L247>)
### func [Newf](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L271>)

```go
func Newf(format string, args ...any) ErrorExt
Expand Down Expand Up @@ -266,7 +275,7 @@ user alice not found
</details>

<a name="Wrap"></a>
### func [Wrap](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L176>)
### func [Wrap](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L200>)

```go
func Wrap(err error, msg string) ErrorExt
Expand Down Expand Up @@ -340,7 +349,7 @@ true
</details>

<a name="WrapWithSkip"></a>
### func [WrapWithSkip](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L186>)
### func [WrapWithSkip](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L210>)

```go
func WrapWithSkip(err error, msg string, skip int) ErrorExt
Expand All @@ -349,7 +358,7 @@ func WrapWithSkip(err error, msg string, skip int) ErrorExt
WrapWithSkip wraps an existing error and appends stack information if it does not exists skipping the number of function on the stack

<a name="WrapWithSkipAndStatus"></a>
### func [WrapWithSkipAndStatus](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L191>)
### func [WrapWithSkipAndStatus](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L215>)

```go
func WrapWithSkipAndStatus(err error, msg string, skip int, status *grpcstatus.Status) ErrorExt
Expand All @@ -358,7 +367,7 @@ func WrapWithSkipAndStatus(err error, msg string, skip int, status *grpcstatus.S
WrapWithSkip wraps an existing error and appends stack information if it does not exists skipping the number of function on the stack along with GRPC status

<a name="WrapWithStatus"></a>
### func [WrapWithStatus](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L181>)
### func [WrapWithStatus](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L205>)

```go
func WrapWithStatus(err error, msg string, status *grpcstatus.Status) ErrorExt
Expand Down Expand Up @@ -403,7 +412,7 @@ gRPC code: NotFound
</details>

<a name="Wrapf"></a>
### func [Wrapf](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L252>)
### func [Wrapf](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L276>)

```go
func Wrapf(err error, format string, args ...any) ErrorExt
Expand Down Expand Up @@ -442,7 +451,7 @@ failed to connect to port 5432: connection refused
</details>

<a name="NotifyExt"></a>
## type [NotifyExt](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L40-L45>)
## type [NotifyExt](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L48-L53>)

NotifyExt is the interface definition for notifier related options

Expand All @@ -456,7 +465,7 @@ type NotifyExt interface {
```

<a name="StackFrame"></a>
## type [StackFrame](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L19-L23>)
## type [StackFrame](<https://github.com/go-coldbrew/errors/blob/main/errors.go#L27-L31>)

StackFrame represents the stackframe for tracing exception

Expand Down
43 changes: 43 additions & 0 deletions bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package errors

import (
"testing"
)

func BenchmarkNew(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
_ = New("benchmark error")
}
}

func BenchmarkNewAndStackFrame(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
e := New("benchmark error")
_ = e.StackFrame()
}
}

func BenchmarkWrap(b *testing.B) {
base := New("base error")
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
_ = Wrap(base, "wrapped")
}
}

func BenchmarkNewDeepStack(b *testing.B) {
b.ReportAllocs()
var recurse func(depth int) ErrorExt
recurse = func(depth int) ErrorExt {
if depth == 0 {
return New("deep error")
}
return recurse(depth - 1)
}
for b.Loop() {
_ = recurse(32)
}
}
98 changes: 61 additions & 37 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"runtime"
"strings"
"sync"
"sync/atomic"

"google.golang.org/grpc/codes"
grpcstatus "google.golang.org/grpc/status"
Expand All @@ -14,9 +16,11 @@ import (
// Downstream packages reference this to enforce version compatibility.
const SupportPackageIsVersion1 = true

const defaultStackDepth = 16

var (
basePath = ""
maxStackDepth = 64
atomicBasePath atomic.Value // stores string
atomicStackDepth atomic.Int32
)

// StackFrame represents the stackframe for tracing exception
Expand Down Expand Up @@ -52,6 +56,8 @@ type customError struct {
Msg string
stack []uintptr
frame []StackFrame
frameOnce sync.Once
basePath string // snapshot of basePath at capture time
cause error
wrapped error // immediate parent for Unwrap() chain; may differ from cause
shouldNotify bool
Expand All @@ -69,32 +75,38 @@ func (c *customError) Notified(status bool) {
}

// Error returns the error message.
func (c customError) Error() string {
func (c *customError) Error() string {
return c.Msg
}

// Callers returns the program counters of the call stack when the error was created.
func (c customError) Callers() []uintptr {
func (c *customError) Callers() []uintptr {
return c.stack[:]
}

// StackTrace returns the program counters of the call stack (alias for Callers).
func (c customError) StackTrace() []uintptr {
func (c *customError) StackTrace() []uintptr {
return c.Callers()
Comment thread
ankurs marked this conversation as resolved.
}

// StackFrame returns the structured stack frames for the error.
func (c customError) StackFrame() []StackFrame {
// Frames are resolved lazily from program counters on first access.
func (c *customError) StackFrame() []StackFrame {
c.frameOnce.Do(func() {
if len(c.stack) > 0 {
c.frame = resolveFrames(c.stack, c.basePath)
}
})
return c.frame
Comment thread
ankurs marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// Cause returns the root cause error that originated this error chain.
func (c customError) Cause() error {
func (c *customError) Cause() error {
return c.cause
}

// GRPCStatus returns the gRPC status for this error.
func (c customError) GRPCStatus() *grpcstatus.Status {
func (c *customError) GRPCStatus() *grpcstatus.Status {
if c.status != nil {
// use latest error message and keep other data (e.g. details)
newStatus := c.status.Proto()
Expand All @@ -106,43 +118,52 @@ func (c customError) GRPCStatus() *grpcstatus.Status {
return grpcstatus.New(codes.Internal, c.Error())
}

func (c *customError) generateStack(skip int) []StackFrame {
stack := []StackFrame{}
trace := []uintptr{}
for i := skip + 1; i < skip+1+maxStackDepth; i++ {
pc, file, line, ok := runtime.Caller(i)
if !ok {
break
}
_, funcName := packageFuncName(pc)
if basePath != "" {
file = strings.Replace(file, basePath, "", 1)
// captureStack records program counters for the call stack.
// Symbolization is deferred until StackFrame() is called.
func (c *customError) captureStack(skip int) {
depth := int(atomicStackDepth.Load())
if depth == 0 {
depth = defaultStackDepth
}
pcs := make([]uintptr, depth)
n := runtime.Callers(skip+2, pcs)
c.stack = pcs[:n]
c.basePath, _ = atomicBasePath.Load().(string)
}
Comment thread
ankurs marked this conversation as resolved.

// resolveFrames converts program counters to structured stack frames.
func resolveFrames(pcs []uintptr, base string) []StackFrame {
frames := runtime.CallersFrames(pcs)
maxFrames := len(pcs)
stack := make([]StackFrame, 0, maxFrames)
for {
frame, more := frames.Next()
file := frame.File
if base != "" {
file = strings.TrimPrefix(file, base)
}
Comment thread
ankurs marked this conversation as resolved.
_, funcName := splitFuncName(frame.Function)
stack = append(stack, StackFrame{
File: file,
Line: line,
Line: frame.Line,
Func: funcName,
})
trace = append(trace, pc)
if !more || len(stack) >= maxFrames {
break
}
}
c.frame = stack
c.stack = trace
return stack
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// Unwrap returns the immediate parent error for use with errors.Is and errors.As.
func (c customError) Unwrap() error {
func (c *customError) Unwrap() error {
return c.wrapped
}

func packageFuncName(pc uintptr) (string, string) {
f := runtime.FuncForPC(pc)
if f == nil {
return "", ""
}

// splitFuncName splits a fully qualified function name into package and function parts.
func splitFuncName(qualifiedName string) (string, string) {
packageName := ""
funcName := f.Name()
funcName := qualifiedName

if ind := strings.LastIndex(funcName, "/"); ind > 0 {
packageName += funcName[:ind+1]
Expand Down Expand Up @@ -220,7 +241,9 @@ func WrapWithSkipAndStatus(err error, msg string, skip int, status *grpcstatus.S
}

c.stack = e.Callers()
Comment thread
ankurs marked this conversation as resolved.
c.frame = e.StackFrame()
if ce, ok := e.(*customError); ok {
c.basePath = ce.basePath
Comment thread
ankurs marked this conversation as resolved.
}
if n, ok := e.(NotifyExt); ok {
c.shouldNotify = n.ShouldNotify()
}
Expand All @@ -234,16 +257,17 @@ func WrapWithSkipAndStatus(err error, msg string, skip int, status *grpcstatus.S
shouldNotify: true,
status: status,
}
c.generateStack(skip + 1)
c.captureStack(skip + 1)
return c

}

// SetMaxStackDepth sets the maximum number of stack frames captured when creating errors.
// Default is 64. Must be called during initialization.
// Accepts values in [1, 256]; out-of-range values are ignored. Default is 16.
// Safe for concurrent use.
func SetMaxStackDepth(n int) {
if n > 0 {
maxStackDepth = n
if n > 0 && n <= 256 {
atomicStackDepth.Store(int32(n))
}
Comment thread
ankurs marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Comment thread
ankurs marked this conversation as resolved.

Expand All @@ -261,6 +285,6 @@ func Wrapf(err error, format string, args ...any) ErrorExt {
func SetBaseFilePath(path string) {
path = strings.TrimSpace(path)
if path != "" {
basePath = path
atomicBasePath.Store(path)
}
}
Loading
Loading