Skip to content
This repository has been archived by the owner on Dec 1, 2021. It is now read-only.

Commit

Permalink
Introduce Stacktrace and Frame (#37)
Browse files Browse the repository at this point in the history
* Introduce Stacktrace and Frame

This PR is a continuation of a series aimed at exposing the stack trace
information embedded in each error value. The secondary effect is to
deprecated the `Fprintf` helper.

Taking cues from from @ChrisHines' `stack` package this PR introduces a
new interface `Stacktrace() []Frame` and a `Frame` type, similar in
function to the `runtime.Frame` type (although lacking its iterator
type). Each `Frame` implemnts `fmt.Formatter` allowing it to print
itself.

The older `Location` interface is still supported but also deprecated.
  • Loading branch information
davecheney committed Jun 7, 2016
1 parent f45f2b7 commit 3cdd332
Show file tree
Hide file tree
Showing 6 changed files with 367 additions and 81 deletions.
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,19 @@ if err != nil {
`New`, `Errorf`, `Wrap`, and `Wrapf` record a stack trace at the point they are invoked.
This information can be retrieved with the following interface.
```go
type Stack interface {
Stack() []uintptr
type Stacktrace interface {
Stacktrace() []Frame
}
```
The `Frame` type represents a call site in the stacktrace.
`Frame` supports the `fmt.Formatter` interface that can be used for printing information about the stacktrace of this error. For example
```
if err, ok := err.(Stacktrace); ok {
fmt.Printf("%+s:%d", err.Stacktrace())
}
```
See [the documentation for `Frame.Format`](https://godoc.org/github.com/pkg/errors#Frame_Format) for more details.

## Retrieving the cause of an error

Using `errors.Wrap` constructs a stack of errors, adding context to the preceding error. Depending on the nature of the error it may be necessary to recurse the operation of errors.Wrap to retrieve the original error for inspection. Any error value which implements this interface can be inspected by `errors.Cause`.
Expand Down
103 changes: 26 additions & 77 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,6 @@
// return errors.Wrap(err, "read failed")
// }
//
// Retrieving the stack trace of an error or wrapper
//
// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
// invoked. This information can be retrieved with the following interface.
//
// type Stack interface {
// Stack() []uintptr
// }
//
// Retrieving the cause of an error
//
// Using errors.Wrap constructs a stack of errors, adding context to the
Expand All @@ -51,25 +42,23 @@
// default:
// // unknown error
// }
//
// Retrieving the stack trace of an error or wrapper
//
// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
// invoked. This information can be retrieved with the following interface.
//
// type Stacktrace interface {
// Stacktrace() []Frame
// }
package errors

import (
"errors"
"fmt"
"io"
"runtime"
"strings"
)

// stack represents a stack of program counters.
type stack []uintptr

func (s *stack) Stack() []uintptr { return *s }

func (s *stack) Location() (string, int) {
return location((*s)[0] - 1)
}

// New returns an error that formats as the given text.
func New(text string) error {
return struct {
Expand Down Expand Up @@ -167,26 +156,41 @@ func Cause(err error) error {
// Fprint prints the error to the supplied writer.
// If the error implements the Causer interface described in Cause
// Print will recurse into the error's cause.
// If the error implements the inteface:
// If the error implements one of the following interfaces:
//
// type Stacktrace interface {
// Stacktrace() []Frame
// }
//
// type Location interface {
// Location() (file string, line int)
// }
//
// Print will also print the file and line of the error.
// If err is nil, nothing is printed.
//
// Deprecated: Fprint will be removed in version 0.7.
func Fprint(w io.Writer, err error) {
type location interface {
Location() (string, int)
}
type stacktrace interface {
Stacktrace() []Frame
}
type message interface {
Message() string
}

for err != nil {
if err, ok := err.(location); ok {
switch err := err.(type) {
case stacktrace:
frame := err.Stacktrace()[0]
fmt.Fprintf(w, "%+s:%d: ", frame, frame)
case location:
file, line := err.Location()
fmt.Fprintf(w, "%s:%d: ", file, line)
default:
// de nada
}
switch err := err.(type) {
case message:
Expand All @@ -202,58 +206,3 @@ func Fprint(w io.Writer, err error) {
err = cause.Cause()
}
}

func callers() *stack {
const depth = 32
var pcs [depth]uintptr
n := runtime.Callers(3, pcs[:])
var st stack = pcs[0:n]
return &st
}

// location returns the source file and line matching pc.
func location(pc uintptr) (string, int) {
fn := runtime.FuncForPC(pc)
if fn == nil {
return "unknown", 0
}

// Here we want to get the source file path relative to the compile time
// GOPATH. As of Go 1.6.x there is no direct way to know the compiled
// GOPATH at runtime, but we can infer the number of path segments in the
// GOPATH. We note that fn.Name() returns the function name qualified by
// the import path, which does not include the GOPATH. Thus we can trim
// segments from the beginning of the file path until the number of path
// separators remaining is one more than the number of path separators in
// the function name. For example, given:
//
// GOPATH /home/user
// file /home/user/src/pkg/sub/file.go
// fn.Name() pkg/sub.Type.Method
//
// We want to produce:
//
// pkg/sub/file.go
//
// From this we can easily see that fn.Name() has one less path separator
// than our desired output. We count separators from the end of the file
// path until it finds two more than in the function name and then move
// one character forward to preserve the initial path segment without a
// leading separator.
const sep = "/"
goal := strings.Count(fn.Name(), sep) + 2
file, line := fn.FileLine(pc)
i := len(file)
for n := 0; n < goal; n++ {
i = strings.LastIndex(file[:i], sep)
if i == -1 {
// not enough separators found, set i so that the slice expression
// below leaves file unmodified
i = -len(sep)
break
}
}
// get back to 0 or trim the leading separator
file = file[i+len(sep):]
return file, line
}
5 changes: 3 additions & 2 deletions errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func TestCause(t *testing.T) {
}
}

func TestFprint(t *testing.T) {
func TestFprintError(t *testing.T) {
x := New("error")
tests := []struct {
err error
Expand Down Expand Up @@ -234,7 +234,8 @@ func TestStack(t *testing.T) {
}
st := x.Stack()
for i, want := range tt.want {
file, line := location(st[i] - 1)
frame := Frame(st[i])
file, line := fmt.Sprintf("%+s", frame), frame.line()
if file != want.file || line != want.line {
t.Errorf("frame %d: expected %s:%d, got %s:%d", i, want.file, want.line, file, line)
}
Expand Down
16 changes: 16 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,19 @@ func ExampleErrorf() {

// Output: github.com/pkg/errors/example_test.go:67: whoops: foo
}

func ExampleError_Stacktrace() {
type Stacktrace interface {
Stacktrace() []errors.Frame
}

err, ok := errors.Cause(fn()).(Stacktrace)
if !ok {
panic("oops, err does not implement Stacktrace")
}

st := err.Stacktrace()
fmt.Printf("%+v", st[0:2]) // top two framces

// Output: [github.com/pkg/errors/example_test.go:33 github.com/pkg/errors/example_test.go:78]
}
148 changes: 148 additions & 0 deletions stack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package errors

import (
"fmt"
"io"
"path"
"runtime"
"strings"
)

// Frame repesents an activation record.
type Frame uintptr

// pc returns the program counter for this frame;
// multiple frames may have the same PC value.
func (f Frame) pc() uintptr { return uintptr(f) - 1 }

// file returns the full path to the file that contains the
// function for this Frame's pc.
func (f Frame) file() string {
fn := runtime.FuncForPC(f.pc())
if fn == nil {
return "unknown"
}
file, _ := fn.FileLine(f.pc())
return file
}

// line returns the line number of source code of the
// function for this Frame's pc.
func (f Frame) line() int {
fn := runtime.FuncForPC(f.pc())
if fn == nil {
return 0
}
_, line := fn.FileLine(f.pc())
return line
}

// Format formats the frame according to the fmt.Formatter interface.
//
// %s source file
// %d source line
// %n function name
// %v equivalent to %s:%d
//
// Format accepts flags that alter the printing of some verbs, as follows:
//
// %+s path of source file relative to the compile time GOPATH
// %+v equivalent to %+s:%d
func (f Frame) Format(s fmt.State, verb rune) {
switch verb {
case 's':
switch {
case s.Flag('+'):
pc := f.pc()
fn := runtime.FuncForPC(pc)
if fn == nil {
io.WriteString(s, "unknown")
} else {
file, _ := fn.FileLine(pc)
io.WriteString(s, trimGOPATH(fn.Name(), file))
}
default:
io.WriteString(s, path.Base(f.file()))
}
case 'd':
fmt.Fprintf(s, "%d", f.line())
case 'n':
name := runtime.FuncForPC(f.pc()).Name()
i := strings.LastIndex(name, "/")
name = name[i+1:]
i = strings.Index(name, ".")
io.WriteString(s, name[i+1:])
case 'v':
f.Format(s, 's')
io.WriteString(s, ":")
f.Format(s, 'd')
}
}

// stack represents a stack of program counters.
type stack []uintptr

// Deprecated: use Stacktrace()
func (s *stack) Stack() []uintptr { return *s }

// Deprecated: use Stacktrace()[0]
func (s *stack) Location() (string, int) {
frame := s.Stacktrace()[0]
return fmt.Sprintf("%+s", frame), frame.line()
}

func (s *stack) Stacktrace() []Frame {
f := make([]Frame, len(*s))
for i := 0; i < len(f); i++ {
f[i] = Frame((*s)[i])
}
return f
}

func callers() *stack {
const depth = 32
var pcs [depth]uintptr
n := runtime.Callers(3, pcs[:])
var st stack = pcs[0:n]
return &st
}

func trimGOPATH(name, file string) string {
// Here we want to get the source file path relative to the compile time
// GOPATH. As of Go 1.6.x there is no direct way to know the compiled
// GOPATH at runtime, but we can infer the number of path segments in the
// GOPATH. We note that fn.Name() returns the function name qualified by
// the import path, which does not include the GOPATH. Thus we can trim
// segments from the beginning of the file path until the number of path
// separators remaining is one more than the number of path separators in
// the function name. For example, given:
//
// GOPATH /home/user
// file /home/user/src/pkg/sub/file.go
// fn.Name() pkg/sub.Type.Method
//
// We want to produce:
//
// pkg/sub/file.go
//
// From this we can easily see that fn.Name() has one less path separator
// than our desired output. We count separators from the end of the file
// path until it finds two more than in the function name and then move
// one character forward to preserve the initial path segment without a
// leading separator.
const sep = "/"
goal := strings.Count(name, sep) + 2
i := len(file)
for n := 0; n < goal; n++ {
i = strings.LastIndex(file[:i], sep)
if i == -1 {
// not enough separators found, set i so that the slice expression
// below leaves file unmodified
i = -len(sep)
break
}
}
// get back to 0 or trim the leading separator
file = file[i+len(sep):]
return file
}

0 comments on commit 3cdd332

Please sign in to comment.