-
Notifications
You must be signed in to change notification settings - Fork 320
/
stacktraces.go
170 lines (161 loc) · 6.05 KB
/
stacktraces.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
package stacktraces
import (
"context"
"encoding/json"
"regexp"
"strconv"
"strings"
"github.com/openlyinc/pointy"
log "github.com/sirupsen/logrus"
publicModel "github.com/highlight-run/highlight/backend/private-graph/graph/model"
)
type Language string
const Javascript Language = "js"
const Python Language = "python"
const Golang Language = "golang"
const DotNET Language = "dotnet"
// StructureOTELStackTrace processes a backend opentelemetry stacktrace into a structured ErrorTraces.
// The operation returns the deepest frame first (reversing the order of the incoming stacktrace).
func StructureOTELStackTrace(stackTrace string) ([]*publicModel.ErrorTrace, error) {
jsPattern := regexp.MustCompile(` {4}at ((.+) )?\(?(.+):(\d+):(\d+)\)?`)
jsAnonPattern := regexp.MustCompile(` {4}at (.+) \((.+)\)`)
pyPattern := regexp.MustCompile(` {2}File "(.+)", line (\d+), in (\w+)`)
pyExcPattern := regexp.MustCompile(`^(\S.+)`)
pyUnderPattern := regexp.MustCompile(`^\s*[\^~]+\s*$`)
pyMultiPattern := regexp.MustCompile(`^During handling of the above exception, another exception occurred:$`)
goLinePattern := regexp.MustCompile(`\t(.+):(\d+)( 0x[0-f]+)?`)
goFuncPattern := regexp.MustCompile(`^(.+)\.(.+?)(\([^()]*\))?$`)
goRecoveredPanicPattern := regexp.MustCompile(`^\s*runtime\.gopanic\s*$`)
dotnetExceptionPattern := regexp.MustCompile(`^System\.Exception: (.+)$`)
dotnetFilePattern := regexp.MustCompile(`^\s*at (.+?)(?: in (.+?)(?::line (\d+))?)?$`)
generalPattern := regexp.MustCompile(`^(.+)`)
var jsonStr string
if err := json.Unmarshal([]byte(stackTrace), &jsonStr); err == nil {
stackTrace = jsonStr
}
var language Language
var errMsg string
var frame *publicModel.ErrorTrace
frames := []*publicModel.ErrorTrace{}
lines := strings.Split(stackTrace, "\n")
for idx, line := range lines {
// frames explicitly set to nil means that this is part of a frame that is resetting the stacktrace
if frames == nil {
frames = []*publicModel.ErrorTrace{}
continue
}
if line == "Traceback (most recent call last):" {
language = Python
continue
}
if idx == 0 {
if line == "" {
language = Golang
continue
} else if matches := dotnetExceptionPattern.FindSubmatch([]byte(line)); matches != nil {
language = DotNET
errMsg = string(matches[1])
continue
}
errMsg = line
continue
}
if line == "" {
continue
}
if language == Python && idx == len(lines)-2 {
errMsg = line
continue
}
if matches := pyUnderPattern.FindSubmatch([]byte(line)); language == Python && matches != nil {
continue
}
if matches := pyMultiPattern.FindSubmatch([]byte(line)); language == Python && matches != nil {
continue
}
if errMsg == "" {
errMsg = line
}
if frame == nil {
frame = &publicModel.ErrorTrace{
Error: &errMsg,
}
}
if matches := dotnetFilePattern.FindSubmatch([]byte(line)); language == DotNET && matches != nil {
frame.FunctionName = pointy.String(string(matches[1]))
frame.FileName = pointy.String(string(matches[2]))
line, _ := strconv.ParseInt(string(matches[3]), 10, 32)
frame.LineNumber = pointy.Int(int(line))
} else if matches := jsPattern.FindSubmatch([]byte(line)); matches != nil {
language = Javascript
if matches[2] != nil {
frame.FunctionName = pointy.String(string(matches[2]))
}
frame.FileName = pointy.String(string(matches[3]))
l, _ := strconv.ParseInt(string(matches[4]), 10, 32)
col, _ := strconv.ParseInt(string(matches[5]), 10, 32)
frame.LineNumber = pointy.Int(int(l))
frame.ColumnNumber = pointy.Int(int(col))
} else if matches := jsAnonPattern.FindSubmatch([]byte(line)); matches != nil {
language = Javascript
frame.FunctionName = pointy.String(string(matches[1]))
frame.FileName = pointy.String(string(matches[2]))
frame.LineContent = pointy.String(string(matches[2]))
} else if matches := pyPattern.FindSubmatch([]byte(line)); matches != nil {
language = Python
frame.FunctionName = pointy.String(string(matches[3]))
frame.FileName = pointy.String(string(matches[1]))
line, _ := strconv.ParseInt(string(matches[2]), 10, 32)
frame.LineNumber = pointy.Int(int(line))
continue
} else if matches := goRecoveredPanicPattern.FindSubmatch([]byte(line)); matches != nil {
language = Golang
frames = nil
frame = nil
errMsg = ""
continue
} else if matches := goLinePattern.FindSubmatch([]byte(line)); matches != nil {
language = Golang
frame.FileName = pointy.String(string(matches[1]))
line, _ := strconv.ParseInt(string(matches[2]), 10, 32)
frame.LineNumber = pointy.Int(int(line))
} else if matches := goFuncPattern.FindSubmatch([]byte(line)); language == Golang && matches != nil {
frame.FunctionName = pointy.String(string(matches[2]))
continue
} else if matches := generalPattern.FindSubmatch([]byte(line)); matches != nil {
if language == Golang {
frame.FunctionName = pointy.String(string(matches[1]))
continue
} else if language == Python {
if m := pyExcPattern.FindSubmatch([]byte(line)); m != nil {
errMsg = line
continue
}
}
frame.LineContent = pointy.String(string(matches[1]))
}
frames = append(frames, frame)
frame = nil
}
// for otel non go/.net errors, stacktraces are sent top-down (top frame is most outer; bottom frame is most inner)
// our backend expects to store stack traces in the opposite order, so we have to reverse it before returning.
if language != Golang && language != DotNET {
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, nil
}
func FormatStructureStackTrace(ctx context.Context, stackTrace string) string {
frames, err := StructureOTELStackTrace(stackTrace)
if err != nil {
log.WithContext(ctx).WithField("StackTrace", stackTrace).WithError(err).Warnf("otel failed to structure stacktrace")
return stackTrace
}
output, err := json.Marshal(frames)
if err != nil {
log.WithContext(ctx).WithField("Frames", frames).WithField("StackTrace", stackTrace).WithError(err).Warnf("otel failed to json stringify frames")
return stackTrace
}
return string(output)
}