Path: blob/main/pkg/integrations/v2/app_agent_receiver/sourcemaps.go
5333 views
package app_agent_receiver12import (3"bytes"4"fmt"5"io"6"io/fs"7"net/http"8"net/url"9"os"10"path/filepath"11"regexp"12"strings"13"sync"14"text/template"1516"github.com/go-kit/log"17"github.com/go-kit/log/level"18"github.com/go-sourcemap/sourcemap"19"github.com/prometheus/client_golang/prometheus"20"github.com/vincent-petithory/dataurl"21)2223// SourceMapStore is interface for a sourcemap service capable of transforming24// minified source locations to original source location25type SourceMapStore interface {26GetSourceMap(sourceURL string, release string) (*SourceMap, error)27}2829type httpClient interface {30Get(url string) (resp *http.Response, err error)31}3233// FileService is interface for a service that can be used to load source maps34// from file system35type fileService interface {36Stat(name string) (fs.FileInfo, error)37ReadFile(name string) ([]byte, error)38}3940type osFileService struct{}4142func (s *osFileService) Stat(name string) (fs.FileInfo, error) {43return os.Stat(name)44}4546func (s *osFileService) ReadFile(name string) ([]byte, error) {47return os.ReadFile(name)48}4950var reSourceMap = "//[#@]\\s(source(?:Mapping)?URL)=\\s*(?P<url>\\S+)\r?\n?$"5152// SourceMap is a wrapper for go-sourcemap consumer53type SourceMap struct {54consumer *sourcemap.Consumer55}5657type sourceMapMetrics struct {58cacheSize *prometheus.CounterVec59downloads *prometheus.CounterVec60fileReads *prometheus.CounterVec61}6263type sourcemapFileLocation struct {64SourceMapFileLocation65pathTemplate *template.Template66}6768// RealSourceMapStore is an implementation of SourceMapStore69// that can download source maps or read them from file system70type RealSourceMapStore struct {71sync.Mutex72l log.Logger73httpClient httpClient74fileService fileService75config SourceMapConfig76cache map[string]*SourceMap77fileLocations []*sourcemapFileLocation78metrics *sourceMapMetrics79}8081// NewSourceMapStore creates an instance of SourceMapStore.82// httpClient and fileService will be instantiated to defaults if nil is provided83func NewSourceMapStore(l log.Logger, config SourceMapConfig, reg prometheus.Registerer, httpClient httpClient, fileService fileService) SourceMapStore {84if httpClient == nil {85httpClient = &http.Client{86Timeout: config.DownloadTimeout,87}88}8990if fileService == nil {91fileService = &osFileService{}92}9394metrics := &sourceMapMetrics{95cacheSize: prometheus.NewCounterVec(prometheus.CounterOpts{96Name: "app_agent_receiver_sourcemap_cache_size",97Help: "number of items in source map cache, per origin",98}, []string{"origin"}),99downloads: prometheus.NewCounterVec(prometheus.CounterOpts{100Name: "app_agent_receiver_sourcemap_downloads_total",101Help: "downloads by the source map service",102}, []string{"origin", "http_status"}),103fileReads: prometheus.NewCounterVec(prometheus.CounterOpts{104Name: "app_agent_receiver_sourcemap_file_reads_total",105Help: "source map file reads from file system, by origin and status",106}, []string{"origin", "status"}),107}108reg.MustRegister(metrics.cacheSize, metrics.downloads, metrics.fileReads)109110fileLocations := []*sourcemapFileLocation{}111112for _, configLocation := range config.FileSystem {113tpl, err := template.New(configLocation.Path).Parse(configLocation.Path)114if err != nil {115panic(err)116}117118fileLocations = append(fileLocations, &sourcemapFileLocation{119SourceMapFileLocation: configLocation,120pathTemplate: tpl,121})122}123124return &RealSourceMapStore{125l: l,126httpClient: httpClient,127fileService: fileService,128config: config,129cache: make(map[string]*SourceMap),130metrics: metrics,131fileLocations: fileLocations,132}133}134135func (store *RealSourceMapStore) downloadFileContents(url string) ([]byte, error) {136resp, err := store.httpClient.Get(url)137if err != nil {138store.metrics.downloads.WithLabelValues(getOrigin(url), "?").Inc()139return nil, err140}141defer resp.Body.Close()142store.metrics.downloads.WithLabelValues(getOrigin(url), fmt.Sprint(resp.StatusCode)).Inc()143if resp.StatusCode != 200 {144return nil, fmt.Errorf("unexpected status %v", resp.StatusCode)145}146body, err := io.ReadAll(resp.Body)147if err != nil {148return nil, err149}150return body, nil151}152153func (store *RealSourceMapStore) downloadSourceMapContent(sourceURL string) (content []byte, resolvedSourceMapURL string, err error) {154level.Debug(store.l).Log("msg", "attempting to download source file", "url", sourceURL)155156result, err := store.downloadFileContents(sourceURL)157if err != nil {158level.Debug(store.l).Log("msg", "failed to download source file", "url", sourceURL, "err", err)159return nil, "", err160}161r := regexp.MustCompile(reSourceMap)162match := r.FindAllStringSubmatch(string(result), -1)163if len(match) == 0 {164level.Debug(store.l).Log("msg", "no source map url found in source", "url", sourceURL)165return nil, "", nil166}167sourceMapURL := match[len(match)-1][2]168169// inline sourcemap170if strings.HasPrefix(sourceMapURL, "data:") {171dataURL, err := dataurl.DecodeString(sourceMapURL)172if err != nil {173level.Debug(store.l).Log("msg", "failed to parse inline source map data url", "url", sourceURL, "err", err)174return nil, "", err175}176177level.Info(store.l).Log("msg", "successfully parsed inline source map data url", "url", sourceURL)178return dataURL.Data, sourceURL + ".map", nil179}180// remote sourcemap181resolvedSourceMapURL = sourceMapURL182183// if url is relative, attempt to resolve absolute184if !strings.HasPrefix(resolvedSourceMapURL, "http") {185base, err := url.Parse(sourceURL)186if err != nil {187level.Debug(store.l).Log("msg", "failed to parse source url", "url", sourceURL, "err", err)188return nil, "", err189}190relative, err := url.Parse(sourceMapURL)191if err != nil {192level.Debug(store.l).Log("msg", "failed to parse source map url", "url", sourceURL, "sourceMapURL", sourceMapURL, "err", err)193return nil, "", err194}195resolvedSourceMapURL = base.ResolveReference(relative).String()196level.Debug(store.l).Log("msg", "resolved absolute source map url", "url", sourceURL, "sourceMapURL", resolvedSourceMapURL)197}198level.Debug(store.l).Log("msg", "attempting to download source map file", "url", resolvedSourceMapURL)199result, err = store.downloadFileContents(resolvedSourceMapURL)200if err != nil {201level.Debug(store.l).Log("failed to download source map file", "url", resolvedSourceMapURL, "err", err)202return nil, "", err203}204return result, resolvedSourceMapURL, nil205}206207func (store *RealSourceMapStore) getSourceMapFromFileSystem(sourceURL string, release string, fileconf *sourcemapFileLocation) (content []byte, sourceMapURL string, err error) {208if len(sourceURL) == 0 || !strings.HasPrefix(sourceURL, fileconf.MinifiedPathPrefix) || strings.HasSuffix(sourceURL, "/") {209return nil, "", nil210}211212var rootPath bytes.Buffer213214err = fileconf.pathTemplate.Execute(&rootPath, struct{ Release string }{Release: cleanFilePathPart(release)})215if err != nil {216return nil, "", err217}218219pathParts := []string{rootPath.String()}220for _, part := range strings.Split(strings.TrimPrefix(strings.Split(sourceURL, "?")[0], fileconf.MinifiedPathPrefix), "/") {221if len(part) > 0 && part != "." && part != ".." {222pathParts = append(pathParts, part)223}224}225mapFilePath := filepath.Join(pathParts...) + ".map"226227if _, err := store.fileService.Stat(mapFilePath); err != nil {228store.metrics.fileReads.WithLabelValues(getOrigin(sourceURL), "not_found").Inc()229level.Debug(store.l).Log("msg", "source map not found on filesystem", "url", sourceURL, "file_path", mapFilePath)230return nil, "", nil231}232level.Debug(store.l).Log("msg", "source map found on filesystem", "url", mapFilePath, "file_path", mapFilePath)233234content, err = store.fileService.ReadFile(mapFilePath)235if err != nil {236store.metrics.fileReads.WithLabelValues(getOrigin(sourceURL), "error").Inc()237} else {238store.metrics.fileReads.WithLabelValues(getOrigin(sourceURL), "ok").Inc()239}240return content, sourceURL, err241}242243func (store *RealSourceMapStore) getSourceMapContent(sourceURL string, release string) (content []byte, sourceMapURL string, err error) {244//attempt to find in fs245for _, fileconf := range store.fileLocations {246content, sourceMapURL, err = store.getSourceMapFromFileSystem(sourceURL, release, fileconf)247if content != nil || err != nil {248return content, sourceMapURL, err249}250}251252//attempt to download253if strings.HasPrefix(sourceURL, "http") && urlMatchesOrigins(sourceURL, store.config.DownloadFromOrigins) {254return store.downloadSourceMapContent(sourceURL)255}256return nil, "", nil257}258259// GetSourceMap returns sourcemap for a given source url260func (store *RealSourceMapStore) GetSourceMap(sourceURL string, release string) (*SourceMap, error) {261store.Lock()262defer store.Unlock()263264cacheKey := fmt.Sprintf("%s__%s", sourceURL, release)265266if smap, ok := store.cache[cacheKey]; ok {267return smap, nil268}269content, sourceMapURL, err := store.getSourceMapContent(sourceURL, release)270if err != nil || content == nil {271store.cache[cacheKey] = nil272return nil, err273}274if content != nil {275consumer, err := sourcemap.Parse(sourceMapURL, content)276if err != nil {277store.cache[cacheKey] = nil278level.Debug(store.l).Log("msg", "failed to parse source map", "url", sourceMapURL, "release", release, "err", err)279return nil, err280}281level.Info(store.l).Log("msg", "successfully parsed source map", "url", sourceMapURL, "release", release)282smap := &SourceMap{283consumer: consumer,284}285store.cache[cacheKey] = smap286store.metrics.cacheSize.WithLabelValues(getOrigin(sourceURL)).Inc()287return smap, nil288}289return nil, nil290}291292// ResolveSourceLocation resolves minified source location to original source location293func ResolveSourceLocation(store SourceMapStore, frame *Frame, release string) (*Frame, error) {294smap, err := store.GetSourceMap(frame.Filename, release)295if err != nil {296return nil, err297}298if smap == nil {299return nil, nil300}301302file, function, line, col, ok := smap.consumer.Source(frame.Lineno, frame.Colno)303if !ok {304return nil, nil305}306// unfortunately in many cases go-sourcemap fails to determine the original function name.307// not a big issue as long as file, line and column are correct308if len(function) == 0 {309function = "?"310}311return &Frame{312Filename: file,313Lineno: line,314Colno: col,315Function: function,316}, nil317}318319// TransformException will attempt to resolve all minified source locations in the stacktrace with original source locations320func TransformException(store SourceMapStore, log log.Logger, ex *Exception, release string) *Exception {321if ex.Stacktrace == nil {322return ex323}324frames := []Frame{}325326for _, frame := range ex.Stacktrace.Frames {327mappedFrame, err := ResolveSourceLocation(store, &frame, release)328if err != nil {329level.Error(log).Log("msg", "Error resolving stack trace frame source location", "err", err)330frames = append(frames, frame)331} else if mappedFrame != nil {332frames = append(frames, *mappedFrame)333} else {334frames = append(frames, frame)335}336}337338return &Exception{339Type: ex.Type,340Value: ex.Value,341Stacktrace: &Stacktrace{Frames: frames},342Timestamp: ex.Timestamp,343}344}345346func cleanFilePathPart(x string) string {347return strings.TrimLeft(strings.ReplaceAll(strings.ReplaceAll(x, "\\", ""), "/", ""), ".")348}349350func getOrigin(URL string) string {351parsed, err := url.Parse(URL)352if err != nil {353return "?"354}355return fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host)356}357358359