parse_panic.go

  1package errors
  2
  3import (
  4	"strconv"
  5	"strings"
  6)
  7
  8type uncaughtPanic struct{ message string }
  9
 10func (p uncaughtPanic) Error() string {
 11	return p.message
 12}
 13
 14// ParsePanic allows you to get an error object from the output of a go program
 15// that panicked. This is particularly useful with https://github.com/mitchellh/panicwrap.
 16func ParsePanic(text string) (*Error, error) {
 17	lines := strings.Split(text, "\n")
 18
 19	state := "start"
 20
 21	var message string
 22	var stack []StackFrame
 23
 24	for i := 0; i < len(lines); i++ {
 25		line := lines[i]
 26
 27		if state == "start" {
 28			if strings.HasPrefix(line, "panic: ") {
 29				message = strings.TrimPrefix(line, "panic: ")
 30				state = "seek"
 31			} else {
 32				return nil, Errorf("bugsnag.panicParser: Invalid line (no prefix): %s", line)
 33			}
 34
 35		} else if state == "seek" {
 36			if strings.HasPrefix(line, "goroutine ") && strings.HasSuffix(line, "[running]:") {
 37				state = "parsing"
 38			}
 39
 40		} else if state == "parsing" {
 41			if line == "" {
 42				state = "done"
 43				break
 44			}
 45			createdBy := false
 46			if strings.HasPrefix(line, "created by ") {
 47				line = strings.TrimPrefix(line, "created by ")
 48				createdBy = true
 49			}
 50
 51			i++
 52
 53			if i >= len(lines) {
 54				return nil, Errorf("bugsnag.panicParser: Invalid line (unpaired): %s", line)
 55			}
 56
 57			frame, err := parsePanicFrame(line, lines[i], createdBy)
 58			if err != nil {
 59				return nil, err
 60			}
 61
 62			stack = append(stack, *frame)
 63			if createdBy {
 64				state = "done"
 65				break
 66			}
 67		}
 68	}
 69
 70	if state == "done" || state == "parsing" {
 71		return &Error{Err: uncaughtPanic{message}, frames: stack}, nil
 72	}
 73	return nil, Errorf("could not parse panic: %v", text)
 74}
 75
 76// The lines we're passing look like this:
 77//
 78//     main.(*foo).destruct(0xc208067e98)
 79//             /0/go/src/github.com/bugsnag/bugsnag-go/pan/main.go:22 +0x151
 80func parsePanicFrame(name string, line string, createdBy bool) (*StackFrame, error) {
 81	idx := strings.LastIndex(name, "(")
 82	if idx == -1 && !createdBy {
 83		return nil, Errorf("bugsnag.panicParser: Invalid line (no call): %s", name)
 84	}
 85	if idx != -1 {
 86		name = name[:idx]
 87	}
 88	pkg := ""
 89
 90	if lastslash := strings.LastIndex(name, "/"); lastslash >= 0 {
 91		pkg += name[:lastslash] + "/"
 92		name = name[lastslash+1:]
 93	}
 94	if period := strings.Index(name, "."); period >= 0 {
 95		pkg += name[:period]
 96		name = name[period+1:]
 97	}
 98
 99	name = strings.Replace(name, "·", ".", -1)
100
101	if !strings.HasPrefix(line, "\t") {
102		return nil, Errorf("bugsnag.panicParser: Invalid line (no tab): %s", line)
103	}
104
105	idx = strings.LastIndex(line, ":")
106	if idx == -1 {
107		return nil, Errorf("bugsnag.panicParser: Invalid line (no line number): %s", line)
108	}
109	file := line[1:idx]
110
111	number := line[idx+1:]
112	if idx = strings.Index(number, " +"); idx > -1 {
113		number = number[:idx]
114	}
115
116	lno, err := strconv.ParseInt(number, 10, 32)
117	if err != nil {
118		return nil, Errorf("bugsnag.panicParser: Invalid line (bad line number): %s", line)
119	}
120
121	return &StackFrame{
122		File:       file,
123		LineNumber: int(lno),
124		Package:    pkg,
125		Name:       name,
126	}, nil
127}