package sentry import ( "go/build" "path/filepath" "reflect" "runtime" "strings" ) const unknown string = "unknown" // The module download is split into two parts: downloading the go.mod and downloading the actual code. // If you have dependencies only needed for tests, then they will show up in your go.mod, // and go get will download their go.mods, but it will not download their code. // The test-only dependencies get downloaded only when you need it, such as the first time you run go test. // // https://github.com/golang/go/issues/26913#issuecomment-411976222 // Stacktrace holds information about the frames of the stack. type Stacktrace struct { Frames []Frame `json:"frames,omitempty"` FramesOmitted []uint `json:"frames_omitted,omitempty"` } // NewStacktrace creates a stacktrace using runtime.Callers. func NewStacktrace() *Stacktrace { pcs := make([]uintptr, 100) n := runtime.Callers(1, pcs) if n == 0 { return nil } frames := extractFrames(pcs[:n]) frames = filterFrames(frames) stacktrace := Stacktrace{ Frames: frames, } return &stacktrace } // TODO: Make it configurable so that anyone can provide their own implementation? // Use of reflection allows us to not have a hard dependency on any given // package, so we don't have to import it. // ExtractStacktrace creates a new Stacktrace based on the given error. func ExtractStacktrace(err error) *Stacktrace { method := extractReflectedStacktraceMethod(err) var pcs []uintptr if method.IsValid() { pcs = extractPcs(method) } else { pcs = extractXErrorsPC(err) } if len(pcs) == 0 { return nil } frames := extractFrames(pcs) frames = filterFrames(frames) stacktrace := Stacktrace{ Frames: frames, } return &stacktrace } func extractReflectedStacktraceMethod(err error) reflect.Value { errValue := reflect.ValueOf(err) // https://github.com/go-errors/errors methodStackFrames := errValue.MethodByName("StackFrames") if methodStackFrames.IsValid() { return methodStackFrames } // https://github.com/pkg/errors methodStackTrace := errValue.MethodByName("StackTrace") if methodStackTrace.IsValid() { return methodStackTrace } // https://github.com/pingcap/errors methodGetStackTracer := errValue.MethodByName("GetStackTracer") if methodGetStackTracer.IsValid() { stacktracer := methodGetStackTracer.Call(nil)[0] stacktracerStackTrace := reflect.ValueOf(stacktracer).MethodByName("StackTrace") if stacktracerStackTrace.IsValid() { return stacktracerStackTrace } } return reflect.Value{} } func extractPcs(method reflect.Value) []uintptr { var pcs []uintptr stacktrace := method.Call(nil)[0] if stacktrace.Kind() != reflect.Slice { return nil } for i := 0; i < stacktrace.Len(); i++ { pc := stacktrace.Index(i) switch pc.Kind() { case reflect.Uintptr: pcs = append(pcs, uintptr(pc.Uint())) case reflect.Struct: for _, fieldName := range []string{"ProgramCounter", "PC"} { field := pc.FieldByName(fieldName) if !field.IsValid() { continue } if field.Kind() == reflect.Uintptr { pcs = append(pcs, uintptr(field.Uint())) break } } } } return pcs } // extractXErrorsPC extracts program counters from error values compatible with // the error types from golang.org/x/xerrors. // // It returns nil if err is not compatible with errors from that package or if // no program counters are stored in err. func extractXErrorsPC(err error) []uintptr { // This implementation uses the reflect package to avoid a hard dependency // on third-party packages. // We don't know if err matches the expected type. For simplicity, instead // of trying to account for all possible ways things can go wrong, some // assumptions are made and if they are violated the code will panic. We // recover from any panic and ignore it, returning nil. //nolint: errcheck defer func() { recover() }() field := reflect.ValueOf(err).Elem().FieldByName("frame") // type Frame struct{ frames [3]uintptr } field = field.FieldByName("frames") field = field.Slice(1, field.Len()) // drop first pc pointing to xerrors.New pc := make([]uintptr, field.Len()) for i := 0; i < field.Len(); i++ { pc[i] = uintptr(field.Index(i).Uint()) } return pc } // Frame represents a function call and it's metadata. Frames are associated // with a Stacktrace. type Frame struct { Function string `json:"function,omitempty"` Symbol string `json:"symbol,omitempty"` // Module is, despite the name, the Sentry protocol equivalent of a Go // package's import path. Module string `json:"module,omitempty"` // Package is not used for Go stack trace frames. In other platforms it // refers to a container where the Module can be found. For example, a // Java JAR, a .NET Assembly, or a native dynamic library. // It exists for completeness, allowing the construction and reporting // of custom event payloads. Package string `json:"package,omitempty"` Filename string `json:"filename,omitempty"` AbsPath string `json:"abs_path,omitempty"` Lineno int `json:"lineno,omitempty"` Colno int `json:"colno,omitempty"` PreContext []string `json:"pre_context,omitempty"` ContextLine string `json:"context_line,omitempty"` PostContext []string `json:"post_context,omitempty"` InApp bool `json:"in_app,omitempty"` Vars map[string]interface{} `json:"vars,omitempty"` } // NewFrame assembles a stacktrace frame out of runtime.Frame. func NewFrame(f runtime.Frame) Frame { var abspath, relpath string // NOTE: f.File paths historically use forward slash as path separator even // on Windows, though this is not yet documented, see // https://golang.org/issues/3335. In any case, filepath.IsAbs can work with // paths with either slash or backslash on Windows. switch { case f.File == "": relpath = unknown // Leave abspath as the empty string to be omitted when serializing // event as JSON. abspath = "" case filepath.IsAbs(f.File): abspath = f.File // TODO: in the general case, it is not trivial to come up with a // "project relative" path with the data we have in run time. // We shall not use filepath.Base because it creates ambiguous paths and // affects the "Suspect Commits" feature. // For now, leave relpath empty to be omitted when serializing the event // as JSON. Improve this later. relpath = "" default: // f.File is a relative path. This may happen when the binary is built // with the -trimpath flag. relpath = f.File // Omit abspath when serializing the event as JSON. abspath = "" } function := f.Function var pkg string if function != "" { pkg, function = splitQualifiedFunctionName(function) } frame := Frame{ AbsPath: abspath, Filename: relpath, Lineno: f.Line, Module: pkg, Function: function, } frame.InApp = isInAppFrame(frame) return frame } // splitQualifiedFunctionName splits a package path-qualified function name into // package name and function name. Such qualified names are found in // runtime.Frame.Function values. func splitQualifiedFunctionName(name string) (pkg string, fun string) { pkg = packageName(name) fun = strings.TrimPrefix(name, pkg+".") return } func extractFrames(pcs []uintptr) []Frame { var frames = make([]Frame, 0, len(pcs)) callersFrames := runtime.CallersFrames(pcs) for { callerFrame, more := callersFrames.Next() frames = append(frames, NewFrame(callerFrame)) if !more { break } } // reverse for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 { frames[i], frames[j] = frames[j], frames[i] } return frames } // filterFrames filters out stack frames that are not meant to be reported to // Sentry. Those are frames internal to the SDK or Go. func filterFrames(frames []Frame) []Frame { if len(frames) == 0 { return nil } // reuse filteredFrames := frames[:0] for _, frame := range frames { // Skip Go internal frames. if frame.Module == "runtime" || frame.Module == "testing" { continue } // Skip Sentry internal frames, except for frames in _test packages (for // testing). if strings.HasPrefix(frame.Module, "github.com/getsentry/sentry-go") && !strings.HasSuffix(frame.Module, "_test") { continue } filteredFrames = append(filteredFrames, frame) } return filteredFrames } func isInAppFrame(frame Frame) bool { if strings.HasPrefix(frame.AbsPath, build.Default.GOROOT) || strings.Contains(frame.Module, "vendor") || strings.Contains(frame.Module, "third_party") { return false } return true } func callerFunctionName() string { pcs := make([]uintptr, 1) runtime.Callers(3, pcs) callersFrames := runtime.CallersFrames(pcs) callerFrame, _ := callersFrames.Next() return baseName(callerFrame.Function) } // packageName returns the package part of the symbol name, or the empty string // if there is none. // It replicates https://golang.org/pkg/debug/gosym/#Sym.PackageName, avoiding a // dependency on debug/gosym. func packageName(name string) string { // A prefix of "type." and "go." is a compiler-generated symbol that doesn't belong to any package. // See variable reservedimports in cmd/compile/internal/gc/subr.go if strings.HasPrefix(name, "go.") || strings.HasPrefix(name, "type.") { return "" } pathend := strings.LastIndex(name, "/") if pathend < 0 { pathend = 0 } if i := strings.Index(name[pathend:], "."); i != -1 { return name[:pathend+i] } return "" } // baseName returns the symbol name without the package or receiver name. // It replicates https://golang.org/pkg/debug/gosym/#Sym.BaseName, avoiding a // dependency on debug/gosym. func baseName(name string) string { if i := strings.LastIndex(name, "."); i != -1 { return name[i+1:] } return name }