forked from vikunja/vikunja
konrad
d02d413c5e
Use sentry echo integration to send errors Only capture errors not already handled by echo Add sentry panic handler Add sentry library Add sentry init Add sentry config Co-authored-by: kolaente <k@knt.li> Reviewed-on: vikunja/api#591
272 lines
6.4 KiB
Go
272 lines
6.4 KiB
Go
package sentry
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"runtime"
|
|
"runtime/debug"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// ================================
|
|
// Modules Integration
|
|
// ================================
|
|
|
|
type modulesIntegration struct {
|
|
once sync.Once
|
|
modules map[string]string
|
|
}
|
|
|
|
func (mi *modulesIntegration) Name() string {
|
|
return "Modules"
|
|
}
|
|
|
|
func (mi *modulesIntegration) SetupOnce(client *Client) {
|
|
client.AddEventProcessor(mi.processor)
|
|
}
|
|
|
|
func (mi *modulesIntegration) processor(event *Event, hint *EventHint) *Event {
|
|
if len(event.Modules) == 0 {
|
|
mi.once.Do(func() {
|
|
info, ok := debug.ReadBuildInfo()
|
|
if !ok {
|
|
Logger.Print("The Modules integration is not available in binaries built without module support.")
|
|
return
|
|
}
|
|
mi.modules = extractModules(info)
|
|
})
|
|
}
|
|
event.Modules = mi.modules
|
|
return event
|
|
}
|
|
|
|
func extractModules(info *debug.BuildInfo) map[string]string {
|
|
modules := map[string]string{
|
|
info.Main.Path: info.Main.Version,
|
|
}
|
|
for _, dep := range info.Deps {
|
|
ver := dep.Version
|
|
if dep.Replace != nil {
|
|
ver += fmt.Sprintf(" => %s %s", dep.Replace.Path, dep.Replace.Version)
|
|
}
|
|
modules[dep.Path] = strings.TrimSuffix(ver, " ")
|
|
}
|
|
return modules
|
|
}
|
|
|
|
// ================================
|
|
// Environment Integration
|
|
// ================================
|
|
|
|
type environmentIntegration struct{}
|
|
|
|
func (ei *environmentIntegration) Name() string {
|
|
return "Environment"
|
|
}
|
|
|
|
func (ei *environmentIntegration) SetupOnce(client *Client) {
|
|
client.AddEventProcessor(ei.processor)
|
|
}
|
|
|
|
func (ei *environmentIntegration) processor(event *Event, hint *EventHint) *Event {
|
|
if event.Contexts == nil {
|
|
event.Contexts = make(map[string]interface{})
|
|
}
|
|
|
|
event.Contexts["device"] = map[string]interface{}{
|
|
"arch": runtime.GOARCH,
|
|
"num_cpu": runtime.NumCPU(),
|
|
}
|
|
|
|
event.Contexts["os"] = map[string]interface{}{
|
|
"name": runtime.GOOS,
|
|
}
|
|
|
|
event.Contexts["runtime"] = map[string]interface{}{
|
|
"name": "go",
|
|
"version": runtime.Version(),
|
|
"go_numroutines": runtime.NumGoroutine(),
|
|
"go_maxprocs": runtime.GOMAXPROCS(0),
|
|
"go_numcgocalls": runtime.NumCgoCall(),
|
|
}
|
|
|
|
return event
|
|
}
|
|
|
|
// ================================
|
|
// Ignore Errors Integration
|
|
// ================================
|
|
|
|
type ignoreErrorsIntegration struct {
|
|
ignoreErrors []*regexp.Regexp
|
|
}
|
|
|
|
func (iei *ignoreErrorsIntegration) Name() string {
|
|
return "IgnoreErrors"
|
|
}
|
|
|
|
func (iei *ignoreErrorsIntegration) SetupOnce(client *Client) {
|
|
iei.ignoreErrors = transformStringsIntoRegexps(client.Options().IgnoreErrors)
|
|
client.AddEventProcessor(iei.processor)
|
|
}
|
|
|
|
func (iei *ignoreErrorsIntegration) processor(event *Event, hint *EventHint) *Event {
|
|
suspects := getIgnoreErrorsSuspects(event)
|
|
|
|
for _, suspect := range suspects {
|
|
for _, pattern := range iei.ignoreErrors {
|
|
if pattern.Match([]byte(suspect)) {
|
|
Logger.Printf("Event dropped due to being matched by `IgnoreErrors` option."+
|
|
"| Value matched: %s | Filter used: %s", suspect, pattern)
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return event
|
|
}
|
|
|
|
func transformStringsIntoRegexps(strings []string) []*regexp.Regexp {
|
|
var exprs []*regexp.Regexp
|
|
|
|
for _, s := range strings {
|
|
r, err := regexp.Compile(s)
|
|
if err == nil {
|
|
exprs = append(exprs, r)
|
|
}
|
|
}
|
|
|
|
return exprs
|
|
}
|
|
|
|
func getIgnoreErrorsSuspects(event *Event) []string {
|
|
suspects := []string{}
|
|
|
|
if event.Message != "" {
|
|
suspects = append(suspects, event.Message)
|
|
}
|
|
|
|
for _, ex := range event.Exception {
|
|
suspects = append(suspects, ex.Type)
|
|
suspects = append(suspects, ex.Value)
|
|
}
|
|
|
|
return suspects
|
|
}
|
|
|
|
// ================================
|
|
// Contextify Frames Integration
|
|
// ================================
|
|
|
|
type contextifyFramesIntegration struct {
|
|
sr sourceReader
|
|
contextLines int
|
|
cachedLocations sync.Map
|
|
}
|
|
|
|
func (cfi *contextifyFramesIntegration) Name() string {
|
|
return "ContextifyFrames"
|
|
}
|
|
|
|
func (cfi *contextifyFramesIntegration) SetupOnce(client *Client) {
|
|
cfi.sr = newSourceReader()
|
|
cfi.contextLines = 5
|
|
|
|
client.AddEventProcessor(cfi.processor)
|
|
}
|
|
|
|
func (cfi *contextifyFramesIntegration) processor(event *Event, hint *EventHint) *Event {
|
|
// Range over all exceptions
|
|
for _, ex := range event.Exception {
|
|
// If it has no stacktrace, just bail out
|
|
if ex.Stacktrace == nil {
|
|
continue
|
|
}
|
|
|
|
// If it does, it should have frames, so try to contextify them
|
|
ex.Stacktrace.Frames = cfi.contextify(ex.Stacktrace.Frames)
|
|
}
|
|
|
|
// Range over all threads
|
|
for _, th := range event.Threads {
|
|
// If it has no stacktrace, just bail out
|
|
if th.Stacktrace == nil {
|
|
continue
|
|
}
|
|
|
|
// If it does, it should have frames, so try to contextify them
|
|
th.Stacktrace.Frames = cfi.contextify(th.Stacktrace.Frames)
|
|
}
|
|
|
|
return event
|
|
}
|
|
|
|
func (cfi *contextifyFramesIntegration) contextify(frames []Frame) []Frame {
|
|
contextifiedFrames := make([]Frame, 0, len(frames))
|
|
|
|
for _, frame := range frames {
|
|
if !frame.InApp {
|
|
contextifiedFrames = append(contextifiedFrames, frame)
|
|
continue
|
|
}
|
|
|
|
var path string
|
|
|
|
if cachedPath, ok := cfi.cachedLocations.Load(frame.AbsPath); ok {
|
|
if p, ok := cachedPath.(string); ok {
|
|
path = p
|
|
}
|
|
} else {
|
|
// Optimize for happy path here
|
|
if fileExists(frame.AbsPath) {
|
|
path = frame.AbsPath
|
|
} else {
|
|
path = cfi.findNearbySourceCodeLocation(frame.AbsPath)
|
|
}
|
|
}
|
|
|
|
if path == "" {
|
|
contextifiedFrames = append(contextifiedFrames, frame)
|
|
continue
|
|
}
|
|
|
|
lines, contextLine := cfi.sr.readContextLines(path, frame.Lineno, cfi.contextLines)
|
|
contextifiedFrames = append(contextifiedFrames, cfi.addContextLinesToFrame(frame, lines, contextLine))
|
|
}
|
|
|
|
return contextifiedFrames
|
|
}
|
|
|
|
func (cfi *contextifyFramesIntegration) findNearbySourceCodeLocation(originalPath string) string {
|
|
trimmedPath := strings.TrimPrefix(originalPath, "/")
|
|
components := strings.Split(trimmedPath, "/")
|
|
|
|
for len(components) > 0 {
|
|
components = components[1:]
|
|
possibleLocation := strings.Join(components, "/")
|
|
|
|
if fileExists(possibleLocation) {
|
|
cfi.cachedLocations.Store(originalPath, possibleLocation)
|
|
return possibleLocation
|
|
}
|
|
}
|
|
|
|
cfi.cachedLocations.Store(originalPath, "")
|
|
return ""
|
|
}
|
|
|
|
func (cfi *contextifyFramesIntegration) addContextLinesToFrame(frame Frame, lines [][]byte, contextLine int) Frame {
|
|
for i, line := range lines {
|
|
switch {
|
|
case i < contextLine:
|
|
frame.PreContext = append(frame.PreContext, string(line))
|
|
case i == contextLine:
|
|
frame.ContextLine = string(line)
|
|
default:
|
|
frame.PostContext = append(frame.PostContext, string(line))
|
|
}
|
|
}
|
|
return frame
|
|
}
|