mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
				
	
	
		
			170 lines
		
	
	
		
			4.6 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			170 lines
		
	
	
		
			4.6 KiB
		
	
	
	
		
			Go
		
	
	
	
package frontendlogging
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"io"
 | 
						|
	"net/http"
 | 
						|
	"net/url"
 | 
						|
	"os"
 | 
						|
	"path/filepath"
 | 
						|
	"strings"
 | 
						|
	"sync"
 | 
						|
 | 
						|
	sourcemap "github.com/go-sourcemap/sourcemap"
 | 
						|
	"github.com/grafana/grafana/pkg/infra/log"
 | 
						|
	"github.com/grafana/grafana/pkg/plugins"
 | 
						|
	"github.com/grafana/grafana/pkg/setting"
 | 
						|
)
 | 
						|
 | 
						|
var logger = log.New("frontendlogging")
 | 
						|
 | 
						|
type sourceMapLocation struct {
 | 
						|
	dir      string
 | 
						|
	path     string
 | 
						|
	pluginID string
 | 
						|
}
 | 
						|
 | 
						|
type sourceMap struct {
 | 
						|
	consumer *sourcemap.Consumer
 | 
						|
	pluginID string
 | 
						|
}
 | 
						|
 | 
						|
type ReadSourceMapFn func(dir string, path string) ([]byte, error)
 | 
						|
 | 
						|
func ReadSourceMapFromFS(dir string, path string) ([]byte, error) {
 | 
						|
	file, err := http.Dir(dir).Open(path)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	defer func() {
 | 
						|
		if err := file.Close(); err != nil {
 | 
						|
			logger.Error("Failed to close source map file", "err", err)
 | 
						|
		}
 | 
						|
	}()
 | 
						|
	return io.ReadAll(file)
 | 
						|
}
 | 
						|
 | 
						|
type SourceMapStore struct {
 | 
						|
	sync.Mutex
 | 
						|
	cache         map[string]*sourceMap
 | 
						|
	cfg           *setting.Cfg
 | 
						|
	readSourceMap ReadSourceMapFn
 | 
						|
	routeResolver plugins.StaticRouteResolver
 | 
						|
}
 | 
						|
 | 
						|
func NewSourceMapStore(cfg *setting.Cfg, routeResolver plugins.StaticRouteResolver, readSourceMap ReadSourceMapFn) *SourceMapStore {
 | 
						|
	return &SourceMapStore{
 | 
						|
		cache:         make(map[string]*sourceMap),
 | 
						|
		cfg:           cfg,
 | 
						|
		routeResolver: routeResolver,
 | 
						|
		readSourceMap: readSourceMap,
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
/* guessSourceMapLocation will attempt to guess location of a source map on fs.
 | 
						|
 * it does not read the source file or make any web requests,
 | 
						|
 * just assumes that a [source filename].map file might exist in the same dir as the source file
 | 
						|
 * and only considers sources coming from grafana core or plugins`
 | 
						|
 */
 | 
						|
func (store *SourceMapStore) guessSourceMapLocation(ctx context.Context, sourceURL string) (*sourceMapLocation, error) {
 | 
						|
	u, err := url.Parse(sourceURL)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	// determine if source comes from grafana core, locally or CDN, look in public build dir on fs
 | 
						|
	if strings.HasPrefix(u.Path, "/public/build/") || (store.cfg.CDNRootURL != nil &&
 | 
						|
		strings.HasPrefix(sourceURL, store.cfg.CDNRootURL.String()) && strings.Contains(u.Path, "/public/build/")) {
 | 
						|
		pathParts := strings.SplitN(u.Path, "/public/build/", 2)
 | 
						|
		if len(pathParts) == 2 {
 | 
						|
			return &sourceMapLocation{
 | 
						|
				dir:      store.cfg.StaticRootPath,
 | 
						|
				path:     filepath.Join("build", pathParts[1]+".map"),
 | 
						|
				pluginID: "",
 | 
						|
			}, nil
 | 
						|
		}
 | 
						|
		// if source comes from a plugin, look in plugin dir
 | 
						|
	} else if strings.HasPrefix(u.Path, "/public/plugins/") {
 | 
						|
		for _, route := range store.routeResolver.Routes(ctx) {
 | 
						|
			pluginPrefix := filepath.Join("/public/plugins/", route.PluginID)
 | 
						|
			if strings.HasPrefix(u.Path, pluginPrefix) {
 | 
						|
				return &sourceMapLocation{
 | 
						|
					dir:      route.Directory,
 | 
						|
					path:     u.Path[len(pluginPrefix):] + ".map",
 | 
						|
					pluginID: route.PluginID,
 | 
						|
				}, nil
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return nil, nil
 | 
						|
}
 | 
						|
 | 
						|
func (store *SourceMapStore) getSourceMap(ctx context.Context, sourceURL string) (*sourceMap, error) {
 | 
						|
	store.Lock()
 | 
						|
	defer store.Unlock()
 | 
						|
 | 
						|
	if smap, ok := store.cache[sourceURL]; ok {
 | 
						|
		return smap, nil
 | 
						|
	}
 | 
						|
	sourceMapLocation, err := store.guessSourceMapLocation(ctx, sourceURL)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	if sourceMapLocation == nil {
 | 
						|
		// Cache nil value for sourceURL, since we want to flag that we couldn't guess the map location and not try again
 | 
						|
		store.cache[sourceURL] = nil
 | 
						|
		return nil, nil
 | 
						|
	}
 | 
						|
	path := strings.ReplaceAll(sourceMapLocation.path, "../", "") // just in case
 | 
						|
	content, err := store.readSourceMap(sourceMapLocation.dir, path)
 | 
						|
	if err != nil {
 | 
						|
		if os.IsNotExist(err) {
 | 
						|
			// Cache nil value for sourceURL, since we want to flag that it wasn't found in the filesystem and not try again
 | 
						|
			store.cache[sourceURL] = nil
 | 
						|
			return nil, nil
 | 
						|
		}
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	consumer, err := sourcemap.Parse(sourceURL+".map", content)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	smap := &sourceMap{
 | 
						|
		consumer: consumer,
 | 
						|
		pluginID: sourceMapLocation.pluginID,
 | 
						|
	}
 | 
						|
	store.cache[sourceURL] = smap
 | 
						|
	return smap, nil
 | 
						|
}
 | 
						|
 | 
						|
func (store *SourceMapStore) resolveSourceLocation(ctx context.Context, frame Frame) (*Frame, error) {
 | 
						|
	smap, err := store.getSourceMap(ctx, frame.Filename)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	if smap == nil {
 | 
						|
		return nil, nil
 | 
						|
	}
 | 
						|
	file, function, line, col, ok := smap.consumer.Source(frame.Lineno, frame.Colno)
 | 
						|
	if !ok {
 | 
						|
		return nil, nil
 | 
						|
	}
 | 
						|
	// unfortunately in many cases go-sourcemap fails to determine the original function name.
 | 
						|
	// not a big issue as long as file, line and column are correct
 | 
						|
	if len(function) == 0 {
 | 
						|
		function = "?"
 | 
						|
	}
 | 
						|
	module := "core"
 | 
						|
	if len(smap.pluginID) > 0 {
 | 
						|
		module = smap.pluginID
 | 
						|
	}
 | 
						|
	return &Frame{
 | 
						|
		Filename: file,
 | 
						|
		Lineno:   line,
 | 
						|
		Colno:    col,
 | 
						|
		Function: function,
 | 
						|
		Module:   module,
 | 
						|
	}, nil
 | 
						|
}
 |