From cc268c7555272829721db7110993f90e26aaa3dc Mon Sep 17 00:00:00 2001 From: Anthony Date: Wed, 13 Apr 2022 16:18:01 -0500 Subject: [PATCH] Added stack trace context --- errorcontext.go | 93 +++++++++++++++++++++++++++++++++++++++++++++++++ highlight.go | 61 ++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 errorcontext.go 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 89743e9..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 @@ -60,6 +62,14 @@ var ( state appState // 0 is idle, 1 is started, 2 is stopped ) +const backendSetupCooldown = 15 + +const contextLines = 5 + +var ( + lastBackendSetupTimestamp time.Time +) + const ( consumeErrorSessionIDMissing = "context does not contain highlightSessionSecureID; context must have injected values from highlight.InterceptRequest" consumeErrorRequestIDMissing = "context does not contain highlightRequestID; context must have injected values from highlight.InterceptRequest" @@ -138,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") @@ -226,6 +237,43 @@ func InterceptRequestWithContext(ctx context.Context, r *http.Request) context.C return ctx } +func MarkBackendSetup(ctx context.Context) { + if lastBackendSetupTimestamp.IsZero() { + currentTime := time.Now() + if currentTime.Sub(lastBackendSetupTimestamp).Minutes() > backendSetupCooldown { + lastBackendSetupTimestamp = currentTime + var mutation struct { + MarkBackendSetup string `graphql:"markBackendSetup(session_secure_id: $session_secure_id)"` + } + sessionSecureID := ctx.Value(ContextKeys.SessionSecureID) + variables := map[string]interface{}{ + "session_secure_id": graphql.String(fmt.Sprintf("%v", sessionSecureID)), + } + + err := client.Mutate(context.Background(), &mutation, variables) + if err != nil { + logger.Errorf("[highlight-go] %v", errors.Wrap(err, "error marking backend setup")) + return + } + } + } +} + +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) { @@ -272,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"))