Skip to content

Commit

Permalink
Merge pull request #52 from monzo/stack-string-causal-chain
Browse files Browse the repository at this point in the history
Add causal chain to the stack string
  • Loading branch information
cstorey-monzo authored Mar 12, 2024
2 parents c625f92 + acf7d7a commit 073654f
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 5 deletions.
42 changes: 37 additions & 5 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,45 @@ func (p *Error) StackTrace() []uintptr {
return out
}

// StackString formats the stack as a beautiful string with newlines
// StackString formats the stacks from the terror chain as a string. If we
// encounter more than one terror in the chain with a stack frame, we'll print
// each one, separated by three hyphens on their own line.
func (p *Error) StackString() string {
stackStr := ""
for _, frame := range p.StackFrames {
stackStr = fmt.Sprintf("%s\n %s:%d in %s", stackStr, frame.Filename, frame.Line, frame.Method)
// 32,000 seems like a reasonable limit for a stack trace. Otherwise, we risk
// overwhelming downstream systems.
return StackStringWithMaxSize(p, 32000)
}

func StackStringWithMaxSize(p *Error, sizeLimit int) string {
// if we run into this many causes, we've likely run into something absurd. Like
// a self causing error.
const maxCausalDepth = 1024
var buffer strings.Builder
terr := p
var causalDepth int
outer:
for terr != nil {
if buffer.Len() != 0 && len(terr.StackFrames) > 0 {
fmt.Fprintf(&buffer, "\n---")
}
for _, frame := range terr.StackFrames {
// 10 seems like a reasonable estimate of how large the rest of the line would be.
estimatedLineLen := len(frame.Filename) + len(frame.Method) + 16
if estimatedLineLen+buffer.Len() > sizeLimit {
break outer
}
fmt.Fprintf(&buffer, "\n %s:%d in %s", frame.Filename, frame.Line, frame.Method)
}

if tcause, ok := terr.cause.(*Error); ok && causalDepth < maxCausalDepth {
terr = tcause
causalDepth += 1
} else {
break outer
}
}
return stackStr

return buffer.String()
}

// VerboseString returns the error message, stack trace and params
Expand Down
33 changes: 33 additions & 0 deletions errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -583,3 +583,36 @@ func TestSetIsUnexpected(t *testing.T) {
err.SetIsUnexpected(false)
assert.False(t, *err.IsUnexpected)
}

func failyFunction() error {
return InternalService("halp", "I'm in trouble", nil)
}

func TestStackStringChasesCausalChain(t *testing.T) {
err := Augment(failyFunction(), "something may be up", nil)
terr := err.(*Error)
ss := terr.StackString()
t.Log(ss)
assert.Contains(t, ss, "failyFunction")
}

func TestCircularErrorProducesFiniteOutputWithStackFrames(t *testing.T) {
orig := failyFunction()
err := Augment(orig, "something may be up", nil)
terr := err.(*Error)
terr.cause = terr
terr.StackFrames = orig.(*Error).StackFrames
ss := terr.StackString()

// The default field size limit used in elastic-slog. It's kind of arbitrary, but it'll do for now.
assert.Less(t, len(ss), 32000)
assert.GreaterOrEqual(t, len(ss), 32000-1000)
}
func TestCircularErrorProducesFiniteOutputWithoutStackFrames(t *testing.T) {
err := Augment(failyFunction(), "something may be up", nil)
terr := err.(*Error)
terr.cause = terr
ss := terr.StackString()
// There's no actual stack in the causal cycle, so we don't render anything here.
assert.Empty(t, ss)
}

0 comments on commit 073654f

Please sign in to comment.