Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aos
GitHub Repository: aos/grafana-agent
Path: blob/main/pkg/integrations/v2/app_agent_receiver/sourcemaps.go
5333 views
1
package app_agent_receiver
2
3
import (
4
"bytes"
5
"fmt"
6
"io"
7
"io/fs"
8
"net/http"
9
"net/url"
10
"os"
11
"path/filepath"
12
"regexp"
13
"strings"
14
"sync"
15
"text/template"
16
17
"github.com/go-kit/log"
18
"github.com/go-kit/log/level"
19
"github.com/go-sourcemap/sourcemap"
20
"github.com/prometheus/client_golang/prometheus"
21
"github.com/vincent-petithory/dataurl"
22
)
23
24
// SourceMapStore is interface for a sourcemap service capable of transforming
25
// minified source locations to original source location
26
type SourceMapStore interface {
27
GetSourceMap(sourceURL string, release string) (*SourceMap, error)
28
}
29
30
type httpClient interface {
31
Get(url string) (resp *http.Response, err error)
32
}
33
34
// FileService is interface for a service that can be used to load source maps
35
// from file system
36
type fileService interface {
37
Stat(name string) (fs.FileInfo, error)
38
ReadFile(name string) ([]byte, error)
39
}
40
41
type osFileService struct{}
42
43
func (s *osFileService) Stat(name string) (fs.FileInfo, error) {
44
return os.Stat(name)
45
}
46
47
func (s *osFileService) ReadFile(name string) ([]byte, error) {
48
return os.ReadFile(name)
49
}
50
51
var reSourceMap = "//[#@]\\s(source(?:Mapping)?URL)=\\s*(?P<url>\\S+)\r?\n?$"
52
53
// SourceMap is a wrapper for go-sourcemap consumer
54
type SourceMap struct {
55
consumer *sourcemap.Consumer
56
}
57
58
type sourceMapMetrics struct {
59
cacheSize *prometheus.CounterVec
60
downloads *prometheus.CounterVec
61
fileReads *prometheus.CounterVec
62
}
63
64
type sourcemapFileLocation struct {
65
SourceMapFileLocation
66
pathTemplate *template.Template
67
}
68
69
// RealSourceMapStore is an implementation of SourceMapStore
70
// that can download source maps or read them from file system
71
type RealSourceMapStore struct {
72
sync.Mutex
73
l log.Logger
74
httpClient httpClient
75
fileService fileService
76
config SourceMapConfig
77
cache map[string]*SourceMap
78
fileLocations []*sourcemapFileLocation
79
metrics *sourceMapMetrics
80
}
81
82
// NewSourceMapStore creates an instance of SourceMapStore.
83
// httpClient and fileService will be instantiated to defaults if nil is provided
84
func NewSourceMapStore(l log.Logger, config SourceMapConfig, reg prometheus.Registerer, httpClient httpClient, fileService fileService) SourceMapStore {
85
if httpClient == nil {
86
httpClient = &http.Client{
87
Timeout: config.DownloadTimeout,
88
}
89
}
90
91
if fileService == nil {
92
fileService = &osFileService{}
93
}
94
95
metrics := &sourceMapMetrics{
96
cacheSize: prometheus.NewCounterVec(prometheus.CounterOpts{
97
Name: "app_agent_receiver_sourcemap_cache_size",
98
Help: "number of items in source map cache, per origin",
99
}, []string{"origin"}),
100
downloads: prometheus.NewCounterVec(prometheus.CounterOpts{
101
Name: "app_agent_receiver_sourcemap_downloads_total",
102
Help: "downloads by the source map service",
103
}, []string{"origin", "http_status"}),
104
fileReads: prometheus.NewCounterVec(prometheus.CounterOpts{
105
Name: "app_agent_receiver_sourcemap_file_reads_total",
106
Help: "source map file reads from file system, by origin and status",
107
}, []string{"origin", "status"}),
108
}
109
reg.MustRegister(metrics.cacheSize, metrics.downloads, metrics.fileReads)
110
111
fileLocations := []*sourcemapFileLocation{}
112
113
for _, configLocation := range config.FileSystem {
114
tpl, err := template.New(configLocation.Path).Parse(configLocation.Path)
115
if err != nil {
116
panic(err)
117
}
118
119
fileLocations = append(fileLocations, &sourcemapFileLocation{
120
SourceMapFileLocation: configLocation,
121
pathTemplate: tpl,
122
})
123
}
124
125
return &RealSourceMapStore{
126
l: l,
127
httpClient: httpClient,
128
fileService: fileService,
129
config: config,
130
cache: make(map[string]*SourceMap),
131
metrics: metrics,
132
fileLocations: fileLocations,
133
}
134
}
135
136
func (store *RealSourceMapStore) downloadFileContents(url string) ([]byte, error) {
137
resp, err := store.httpClient.Get(url)
138
if err != nil {
139
store.metrics.downloads.WithLabelValues(getOrigin(url), "?").Inc()
140
return nil, err
141
}
142
defer resp.Body.Close()
143
store.metrics.downloads.WithLabelValues(getOrigin(url), fmt.Sprint(resp.StatusCode)).Inc()
144
if resp.StatusCode != 200 {
145
return nil, fmt.Errorf("unexpected status %v", resp.StatusCode)
146
}
147
body, err := io.ReadAll(resp.Body)
148
if err != nil {
149
return nil, err
150
}
151
return body, nil
152
}
153
154
func (store *RealSourceMapStore) downloadSourceMapContent(sourceURL string) (content []byte, resolvedSourceMapURL string, err error) {
155
level.Debug(store.l).Log("msg", "attempting to download source file", "url", sourceURL)
156
157
result, err := store.downloadFileContents(sourceURL)
158
if err != nil {
159
level.Debug(store.l).Log("msg", "failed to download source file", "url", sourceURL, "err", err)
160
return nil, "", err
161
}
162
r := regexp.MustCompile(reSourceMap)
163
match := r.FindAllStringSubmatch(string(result), -1)
164
if len(match) == 0 {
165
level.Debug(store.l).Log("msg", "no source map url found in source", "url", sourceURL)
166
return nil, "", nil
167
}
168
sourceMapURL := match[len(match)-1][2]
169
170
// inline sourcemap
171
if strings.HasPrefix(sourceMapURL, "data:") {
172
dataURL, err := dataurl.DecodeString(sourceMapURL)
173
if err != nil {
174
level.Debug(store.l).Log("msg", "failed to parse inline source map data url", "url", sourceURL, "err", err)
175
return nil, "", err
176
}
177
178
level.Info(store.l).Log("msg", "successfully parsed inline source map data url", "url", sourceURL)
179
return dataURL.Data, sourceURL + ".map", nil
180
}
181
// remote sourcemap
182
resolvedSourceMapURL = sourceMapURL
183
184
// if url is relative, attempt to resolve absolute
185
if !strings.HasPrefix(resolvedSourceMapURL, "http") {
186
base, err := url.Parse(sourceURL)
187
if err != nil {
188
level.Debug(store.l).Log("msg", "failed to parse source url", "url", sourceURL, "err", err)
189
return nil, "", err
190
}
191
relative, err := url.Parse(sourceMapURL)
192
if err != nil {
193
level.Debug(store.l).Log("msg", "failed to parse source map url", "url", sourceURL, "sourceMapURL", sourceMapURL, "err", err)
194
return nil, "", err
195
}
196
resolvedSourceMapURL = base.ResolveReference(relative).String()
197
level.Debug(store.l).Log("msg", "resolved absolute source map url", "url", sourceURL, "sourceMapURL", resolvedSourceMapURL)
198
}
199
level.Debug(store.l).Log("msg", "attempting to download source map file", "url", resolvedSourceMapURL)
200
result, err = store.downloadFileContents(resolvedSourceMapURL)
201
if err != nil {
202
level.Debug(store.l).Log("failed to download source map file", "url", resolvedSourceMapURL, "err", err)
203
return nil, "", err
204
}
205
return result, resolvedSourceMapURL, nil
206
}
207
208
func (store *RealSourceMapStore) getSourceMapFromFileSystem(sourceURL string, release string, fileconf *sourcemapFileLocation) (content []byte, sourceMapURL string, err error) {
209
if len(sourceURL) == 0 || !strings.HasPrefix(sourceURL, fileconf.MinifiedPathPrefix) || strings.HasSuffix(sourceURL, "/") {
210
return nil, "", nil
211
}
212
213
var rootPath bytes.Buffer
214
215
err = fileconf.pathTemplate.Execute(&rootPath, struct{ Release string }{Release: cleanFilePathPart(release)})
216
if err != nil {
217
return nil, "", err
218
}
219
220
pathParts := []string{rootPath.String()}
221
for _, part := range strings.Split(strings.TrimPrefix(strings.Split(sourceURL, "?")[0], fileconf.MinifiedPathPrefix), "/") {
222
if len(part) > 0 && part != "." && part != ".." {
223
pathParts = append(pathParts, part)
224
}
225
}
226
mapFilePath := filepath.Join(pathParts...) + ".map"
227
228
if _, err := store.fileService.Stat(mapFilePath); err != nil {
229
store.metrics.fileReads.WithLabelValues(getOrigin(sourceURL), "not_found").Inc()
230
level.Debug(store.l).Log("msg", "source map not found on filesystem", "url", sourceURL, "file_path", mapFilePath)
231
return nil, "", nil
232
}
233
level.Debug(store.l).Log("msg", "source map found on filesystem", "url", mapFilePath, "file_path", mapFilePath)
234
235
content, err = store.fileService.ReadFile(mapFilePath)
236
if err != nil {
237
store.metrics.fileReads.WithLabelValues(getOrigin(sourceURL), "error").Inc()
238
} else {
239
store.metrics.fileReads.WithLabelValues(getOrigin(sourceURL), "ok").Inc()
240
}
241
return content, sourceURL, err
242
}
243
244
func (store *RealSourceMapStore) getSourceMapContent(sourceURL string, release string) (content []byte, sourceMapURL string, err error) {
245
//attempt to find in fs
246
for _, fileconf := range store.fileLocations {
247
content, sourceMapURL, err = store.getSourceMapFromFileSystem(sourceURL, release, fileconf)
248
if content != nil || err != nil {
249
return content, sourceMapURL, err
250
}
251
}
252
253
//attempt to download
254
if strings.HasPrefix(sourceURL, "http") && urlMatchesOrigins(sourceURL, store.config.DownloadFromOrigins) {
255
return store.downloadSourceMapContent(sourceURL)
256
}
257
return nil, "", nil
258
}
259
260
// GetSourceMap returns sourcemap for a given source url
261
func (store *RealSourceMapStore) GetSourceMap(sourceURL string, release string) (*SourceMap, error) {
262
store.Lock()
263
defer store.Unlock()
264
265
cacheKey := fmt.Sprintf("%s__%s", sourceURL, release)
266
267
if smap, ok := store.cache[cacheKey]; ok {
268
return smap, nil
269
}
270
content, sourceMapURL, err := store.getSourceMapContent(sourceURL, release)
271
if err != nil || content == nil {
272
store.cache[cacheKey] = nil
273
return nil, err
274
}
275
if content != nil {
276
consumer, err := sourcemap.Parse(sourceMapURL, content)
277
if err != nil {
278
store.cache[cacheKey] = nil
279
level.Debug(store.l).Log("msg", "failed to parse source map", "url", sourceMapURL, "release", release, "err", err)
280
return nil, err
281
}
282
level.Info(store.l).Log("msg", "successfully parsed source map", "url", sourceMapURL, "release", release)
283
smap := &SourceMap{
284
consumer: consumer,
285
}
286
store.cache[cacheKey] = smap
287
store.metrics.cacheSize.WithLabelValues(getOrigin(sourceURL)).Inc()
288
return smap, nil
289
}
290
return nil, nil
291
}
292
293
// ResolveSourceLocation resolves minified source location to original source location
294
func ResolveSourceLocation(store SourceMapStore, frame *Frame, release string) (*Frame, error) {
295
smap, err := store.GetSourceMap(frame.Filename, release)
296
if err != nil {
297
return nil, err
298
}
299
if smap == nil {
300
return nil, nil
301
}
302
303
file, function, line, col, ok := smap.consumer.Source(frame.Lineno, frame.Colno)
304
if !ok {
305
return nil, nil
306
}
307
// unfortunately in many cases go-sourcemap fails to determine the original function name.
308
// not a big issue as long as file, line and column are correct
309
if len(function) == 0 {
310
function = "?"
311
}
312
return &Frame{
313
Filename: file,
314
Lineno: line,
315
Colno: col,
316
Function: function,
317
}, nil
318
}
319
320
// TransformException will attempt to resolve all minified source locations in the stacktrace with original source locations
321
func TransformException(store SourceMapStore, log log.Logger, ex *Exception, release string) *Exception {
322
if ex.Stacktrace == nil {
323
return ex
324
}
325
frames := []Frame{}
326
327
for _, frame := range ex.Stacktrace.Frames {
328
mappedFrame, err := ResolveSourceLocation(store, &frame, release)
329
if err != nil {
330
level.Error(log).Log("msg", "Error resolving stack trace frame source location", "err", err)
331
frames = append(frames, frame)
332
} else if mappedFrame != nil {
333
frames = append(frames, *mappedFrame)
334
} else {
335
frames = append(frames, frame)
336
}
337
}
338
339
return &Exception{
340
Type: ex.Type,
341
Value: ex.Value,
342
Stacktrace: &Stacktrace{Frames: frames},
343
Timestamp: ex.Timestamp,
344
}
345
}
346
347
func cleanFilePathPart(x string) string {
348
return strings.TrimLeft(strings.ReplaceAll(strings.ReplaceAll(x, "\\", ""), "/", ""), ".")
349
}
350
351
func getOrigin(URL string) string {
352
parsed, err := url.Parse(URL)
353
if err != nil {
354
return "?"
355
}
356
return fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host)
357
}
358
359