diff --git a/errors.go b/errors.go index bf876a7..737daf7 100644 --- a/errors.go +++ b/errors.go @@ -162,20 +162,37 @@ func (p *Error) StackTrace() []uintptr { // 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 { + // 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 { + if tcause, ok := terr.cause.(*Error); ok && causalDepth < maxCausalDepth { terr = tcause + causalDepth += 1 } else { - break + break outer } } diff --git a/errors_test.go b/errors_test.go index 9dd49e1..4aa9cd2 100644 --- a/errors_test.go +++ b/errors_test.go @@ -595,3 +595,24 @@ func TestStackStringChasesCausalChain(t *testing.T) { 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) +}