github.com/grafana/pyroscope@v1.18.0/pkg/frontend/vcs/source/find_python.go (about) 1 package source 2 3 import ( 4 "context" 5 "errors" 6 "path/filepath" 7 "regexp" 8 "strings" 9 10 "connectrpc.com/connect" 11 "github.com/go-kit/log/level" 12 "github.com/opentracing/opentracing-go" 13 14 vcsv1 "github.com/grafana/pyroscope/api/gen/proto/go/vcs/v1" 15 "github.com/grafana/pyroscope/pkg/frontend/vcs/client" 16 "github.com/grafana/pyroscope/pkg/frontend/vcs/config" 17 ) 18 19 const ( 20 ExtPython = ".py" 21 ) 22 23 var ( 24 // stdLibRegex matches Python version directories and captures the version. 25 // Example: "python3.12/" → version="3.12" 26 stdLibRegex = regexp.MustCompile(`python(\d+\.\d{1,2})/`) 27 ) 28 29 func (ff FileFinder) fetchPythonStdlib(ctx context.Context, path string, version string) (*vcsv1.GetFileResponse, error) { 30 sp, ctx := opentracing.StartSpanFromContext(ctx, "fetchPythonStdlib") 31 defer sp.Finish() 32 33 // use main branch as fallback 34 ref := "main" 35 if version != "" { 36 ref = version 37 } 38 39 content, err := ff.client.GetFile(ctx, client.FileRequest{ 40 Owner: "python", 41 Repo: "cpython", 42 Path: filepath.Join("Lib", path), 43 Ref: ref, 44 }) 45 if err != nil { 46 return nil, err 47 } 48 return newFileResponse(content.Content, content.URL) 49 } 50 51 // isPythonStdlibPath returns the cleaned path of the standard library package with version, if detected. 52 // For example, given "/path/to/lib/python3.12/difflib.py", 53 // it returns ("difflib.py", "3.12", true). 54 // Note that minor versions are not captured in this path, so there are 55 // future improvements that can be made to this logic. 56 func isPythonStdlibPath(path string) (string, string, bool) { 57 matches := stdLibRegex.FindAllStringSubmatchIndex(path, -1) 58 if len(matches) == 0 { 59 return "", "", false 60 } 61 62 // Take the last match to handle paths with multiple python version directories 63 m := matches[len(matches)-1] 64 version := path[m[2]:m[3]] 65 remaining := path[m[1]:] 66 if remaining == "" { 67 return "", "", false 68 } 69 return remaining, version, true 70 } 71 72 // findPythonFile finds a python file in a vcs repository. 73 // Currently only supports Python stdlib 74 func (ff FileFinder) findPythonFile(ctx context.Context, mappings ...*config.MappingConfig) (*vcsv1.GetFileResponse, error) { 75 sp, ctx := opentracing.StartSpanFromContext(ctx, "findPythonFile") 76 defer sp.Finish() 77 sp.SetTag("file.function_name", ff.file.FunctionName) 78 sp.SetTag("file.path", ff.file.Path) 79 80 if path, version, ok := isPythonStdlibPath(ff.file.Path); ok { 81 return ff.fetchPythonStdlib(ctx, path, version) 82 } 83 84 for _, m := range mappings { 85 // Strip the matched prefix from the runtime path to get the relative 86 // path within the mapped source (e.g., "/app/myproject/main.py" with 87 // prefix "/app/myproject" yields "main.py"). 88 pos := m.Match(ff.file) 89 if pos < 0 || pos > len(ff.file.Path) { 90 level.Warn(ff.logger).Log("msg", "mapping match out of bounds", "pos", pos, "file_path", ff.file.Path) 91 continue 92 } 93 path := strings.TrimLeft(ff.file.Path[pos:], "/") 94 95 resp, err := ff.fetchMappingFile(ctx, m, path) 96 if err != nil { 97 if errors.Is(err, client.ErrNotFound) { 98 continue 99 } 100 level.Warn(ff.logger).Log("msg", "failed to fetch mapping file", "err", err) 101 continue 102 } 103 return resp, nil 104 } 105 106 // Fallback to relative file path matching 107 f, err := ff.fetchRepoFile(ctx, ff.file.Path, ff.ref) 108 if err != nil { 109 level.Warn(ff.logger).Log("msg", "failed to fetch relative file", "err", err) 110 } else { 111 return f, nil 112 } 113 114 return nil, connect.NewError(connect.CodeNotFound, errors.New("stdlib not detected and no mappings provided, file not resolvable")) 115 }