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  }