diff --git a/errorcontext.go b/errorcontext.go new file mode 100644 index 0000000..e07197b --- /dev/null +++ b/errorcontext.go @@ -0,0 +1,93 @@ +package highlight + +import ( + "bytes" + "io/ioutil" + "sync" + + "github.com/pkg/errors" +) + +type FrameWithContext struct { + errors.Frame + LinesBefore string + LineContent string + LinesAfter string +} + +type sourceReader struct { + mu sync.Mutex + cache map[string][][]byte +} + +func newSourceReader() sourceReader { + return sourceReader{ + cache: make(map[string][][]byte), + } +} + +func (sr *sourceReader) readContextLines(filename string, line, context int) ([][]byte, int) { + // context stands for the number of lines +/- we're returning + sr.mu.Lock() + defer sr.mu.Unlock() + + lines, ok := sr.cache[filename] + + if !ok { + data, err := ioutil.ReadFile(filename) + if err != nil { + sr.cache[filename] = nil + return nil, 0 + } + lines = bytes.Split(data, []byte{'\n'}) + sr.cache[filename] = lines + } + + return sr.calculateContextLines(lines, line, context) +} + +func (sr *sourceReader) calculateContextLines(lines [][]byte, line, context int) ([][]byte, int) { + // Stacktrace lines are 1-indexed, slices are 0-indexed + line-- + + // contextLine points to a line that caused an issue itself, in relation to + // returned slice. + contextLine := context + + if lines == nil || line >= len(lines) || line < 0 { + return nil, 0 + } + + if context < 0 { + context = 0 + contextLine = 0 + } + + start := line - context + if start < 0 { + contextLine += start + start = 0 + } + + end := line + context + 1 + + if end > len(lines) { + end = len(lines) + } + + return lines[start:end], contextLine +} + +func (sr *sourceReader) addContextLinesToFrame(frame FrameWithContext, lines [][]byte, contextLine int) FrameWithContext { + for i, line := range lines { + switch { + case i < contextLine: + frame.LinesBefore = frame.LinesBefore + string(line) + case i == contextLine: + frame.LineContent = string(line) + default: + frame.LinesAfter = frame.LinesAfter + string(line) + } + } + return frame +} diff --git a/highlight.go b/highlight.go index c4b4926..b816e81 100644 --- a/highlight.go +++ b/highlight.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "os/signal" + "strconv" "strings" "sync" "syscall" @@ -24,6 +25,7 @@ var ( signalChan chan os.Signal wg sync.WaitGroup graphqlClientAddress string + sr sourceReader ) // contextKey represents the keys that highlight may store in the users' context @@ -62,6 +64,8 @@ var ( const backendSetupCooldown = 15 +const contextLines = 5 + var ( lastBackendSetupTimestamp time.Time ) @@ -144,6 +148,7 @@ func init() { errorChan = make(chan BackendErrorObjectInput, 128) interruptChan = make(chan bool, 1) signalChan = make(chan os.Signal, 1) + sr = newSourceReader() signal.Notify(signalChan, syscall.SIGABRT, syscall.SIGTERM, syscall.SIGINT) SetGraphqlClientAddress("https://pub.highlight.run") @@ -254,6 +259,21 @@ func MarkBackendSetup(ctx context.Context) { } } +func getFileNameAndLineNumber(s string) (string, int) { + splitSpace := strings.Fields(s) + if len(splitSpace) == 2 { + splitData := strings.Split(splitSpace[1], ":") + if len(splitData) == 2 { + lineNumber, err := strconv.Atoi(splitData[1]) + if err != nil { + return "", 0 + } + return splitData[0], lineNumber + } + } + return "", 0 +} + // ConsumeError adds an error to the queue of errors to be sent to our backend. // the provided context must have the injected highlight keys from InterceptRequestWithContext. func ConsumeError(ctx context.Context, errorInput interface{}, tags ...string) { @@ -300,6 +320,19 @@ func ConsumeError(ctx context.Context, errorInput interface{}, tags ...string) { } var stackFrames []string for _, frame := range stack { + frame := FrameWithContext{ + Frame: frame, + } + initialFrame, err := frame.MarshalText() + if err != nil { + logger.Errorf("[highlight-go] %v", errors.Wrap(err, "error marshaling stack frames")) + return + } + frameFileName, frameLineNumber := getFileNameAndLineNumber(string(initialFrame)) + if frameFileName != "" && frameLineNumber != 0 { + lines, contextLine := sr.readContextLines(frameFileName, frameLineNumber, contextLines) + frame = sr.addContextLinesToFrame(frame, lines, contextLine) + } frameBytes, err := frame.MarshalText() if err != nil { logger.Errorf("[highlight-go] %v", errors.Wrap(err, "error marshaling frame text"))