hanayo/vendor/github.com/getsentry/raven-go/stacktrace.go

255 lines
7.0 KiB
Go
Raw Normal View History

2019-02-23 13:29:15 +00:00
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Some code from the runtime/debug package of the Go standard library.
package raven
import (
"bytes"
"go/build"
"io/ioutil"
"path/filepath"
"runtime"
"strings"
"sync"
"github.com/pkg/errors"
)
// https://docs.getsentry.com/hosted/clientdev/interfaces/#failure-interfaces
type Stacktrace struct {
// Required
Frames []*StacktraceFrame `json:"frames"`
}
func (s *Stacktrace) Class() string { return "stacktrace" }
func (s *Stacktrace) Culprit() string {
for i := len(s.Frames) - 1; i >= 0; i-- {
frame := s.Frames[i]
if frame.InApp == true && frame.Module != "" && frame.Function != "" {
return frame.Module + "." + frame.Function
}
}
return ""
}
type StacktraceFrame struct {
// At least one required
Filename string `json:"filename,omitempty"`
Function string `json:"function,omitempty"`
Module string `json:"module,omitempty"`
// Optional
Lineno int `json:"lineno,omitempty"`
Colno int `json:"colno,omitempty"`
AbsolutePath string `json:"abs_path,omitempty"`
ContextLine string `json:"context_line,omitempty"`
PreContext []string `json:"pre_context,omitempty"`
PostContext []string `json:"post_context,omitempty"`
InApp bool `json:"in_app"`
}
// Try to get stacktrace from err as an interface of github.com/pkg/errors, or else NewStacktrace()
func GetOrNewStacktrace(err error, skip int, context int, appPackagePrefixes []string) *Stacktrace {
stacktracer, errHasStacktrace := err.(interface {
StackTrace() errors.StackTrace
})
if errHasStacktrace {
var frames []*StacktraceFrame
for _, f := range stacktracer.StackTrace() {
pc := uintptr(f) - 1
fn := runtime.FuncForPC(pc)
var file string
var line int
if fn != nil {
file, line = fn.FileLine(pc)
} else {
file = "unknown"
}
frame := NewStacktraceFrame(pc, file, line, context, appPackagePrefixes)
if frame != nil {
frames = append([]*StacktraceFrame{frame}, frames...)
}
}
return &Stacktrace{Frames: frames}
} else {
return NewStacktrace(skip + 1, context, appPackagePrefixes)
}
}
// Intialize and populate a new stacktrace, skipping skip frames.
//
// context is the number of surrounding lines that should be included for context.
// Setting context to 3 would try to get seven lines. Setting context to -1 returns
// one line with no surrounding context, and 0 returns no context.
//
// appPackagePrefixes is a list of prefixes used to check whether a package should
// be considered "in app".
func NewStacktrace(skip int, context int, appPackagePrefixes []string) *Stacktrace {
var frames []*StacktraceFrame
for i := 1 + skip; ; i++ {
pc, file, line, ok := runtime.Caller(i)
if !ok {
break
}
frame := NewStacktraceFrame(pc, file, line, context, appPackagePrefixes)
if frame != nil {
frames = append(frames, frame)
}
}
// If there are no frames, the entire stacktrace is nil
if len(frames) == 0 {
return nil
}
// Optimize the path where there's only 1 frame
if len(frames) == 1 {
return &Stacktrace{frames}
}
// Sentry wants the frames with the oldest first, so reverse them
for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 {
frames[i], frames[j] = frames[j], frames[i]
}
return &Stacktrace{frames}
}
// Build a single frame using data returned from runtime.Caller.
//
// context is the number of surrounding lines that should be included for context.
// Setting context to 3 would try to get seven lines. Setting context to -1 returns
// one line with no surrounding context, and 0 returns no context.
//
// appPackagePrefixes is a list of prefixes used to check whether a package should
// be considered "in app".
func NewStacktraceFrame(pc uintptr, file string, line, context int, appPackagePrefixes []string) *StacktraceFrame {
frame := &StacktraceFrame{AbsolutePath: file, Filename: trimPath(file), Lineno: line, InApp: false}
frame.Module, frame.Function = functionName(pc)
// `runtime.goexit` is effectively a placeholder that comes from
// runtime/asm_amd64.s and is meaningless.
if frame.Module == "runtime" && frame.Function == "goexit" {
return nil
}
if frame.Module == "main" {
frame.InApp = true
} else {
for _, prefix := range appPackagePrefixes {
if strings.HasPrefix(frame.Module, prefix) && !strings.Contains(frame.Module, "vendor") && !strings.Contains(frame.Module, "third_party") {
frame.InApp = true
}
}
}
if context > 0 {
contextLines, lineIdx := fileContext(file, line, context)
if len(contextLines) > 0 {
for i, line := range contextLines {
switch {
case i < lineIdx:
frame.PreContext = append(frame.PreContext, string(line))
case i == lineIdx:
frame.ContextLine = string(line)
default:
frame.PostContext = append(frame.PostContext, string(line))
}
}
}
} else if context == -1 {
contextLine, _ := fileContext(file, line, 0)
if len(contextLine) > 0 {
frame.ContextLine = string(contextLine[0])
}
}
return frame
}
// Retrieve the name of the package and function containing the PC.
func functionName(pc uintptr) (pack string, name string) {
fn := runtime.FuncForPC(pc)
if fn == nil {
return
}
name = fn.Name()
// We get this:
// runtime/debug.*T·ptrmethod
// and want this:
// pack = runtime/debug
// name = *T.ptrmethod
if idx := strings.LastIndex(name, "."); idx != -1 {
pack = name[:idx]
name = name[idx+1:]
}
name = strings.Replace(name, "·", ".", -1)
return
}
var fileCacheLock sync.Mutex
var fileCache = make(map[string][][]byte)
func fileContext(filename string, line, context int) ([][]byte, int) {
fileCacheLock.Lock()
defer fileCacheLock.Unlock()
lines, ok := fileCache[filename]
if !ok {
data, err := ioutil.ReadFile(filename)
if err != nil {
// cache errors as nil slice: code below handles it correctly
// otherwise when missing the source or running as a different user, we try
// reading the file on each error which is unnecessary
fileCache[filename] = nil
return nil, 0
}
lines = bytes.Split(data, []byte{'\n'})
fileCache[filename] = lines
}
if lines == nil {
// cached error from ReadFile: return no lines
return nil, 0
}
line-- // stack trace lines are 1-indexed
start := line - context
var idx int
if start < 0 {
start = 0
idx = line
} else {
idx = context
}
end := line + context + 1
if line >= len(lines) {
return nil, 0
}
if end > len(lines) {
end = len(lines)
}
return lines[start:end], idx
}
var trimPaths []string
// Try to trim the GOROOT or GOPATH prefix off of a filename
func trimPath(filename string) string {
for _, prefix := range trimPaths {
if trimmed := strings.TrimPrefix(filename, prefix); len(trimmed) < len(filename) {
return trimmed
}
}
return filename
}
func init() {
// Collect all source directories, and make sure they
// end in a trailing "separator"
for _, prefix := range build.Default.SrcDirs() {
if prefix[len(prefix)-1] != filepath.Separator {
prefix += string(filepath.Separator)
}
trimPaths = append(trimPaths, prefix)
}
}