github.com/grafana/pyroscope@v1.18.0/pkg/frontend/vcs/source/find_go.go (about) 1 package source 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "path" 8 "path/filepath" 9 "strings" 10 "time" 11 12 "github.com/go-kit/log/level" 13 "github.com/opentracing/opentracing-go" 14 "golang.org/x/mod/modfile" 15 "golang.org/x/mod/module" 16 17 vcsv1 "github.com/grafana/pyroscope/api/gen/proto/go/vcs/v1" 18 "github.com/grafana/pyroscope/pkg/frontend/vcs/client" 19 "github.com/grafana/pyroscope/pkg/frontend/vcs/config" 20 "github.com/grafana/pyroscope/pkg/frontend/vcs/source/golang" 21 ) 22 23 const ( 24 ExtGo = ".go" 25 ExtAsm = ".s" // Assembler files in go 26 ) 27 28 // findGoFile finds a go file in a vcs repository. 29 func (ff FileFinder) findGoFile(ctx context.Context, mappings ...*config.MappingConfig) (*vcsv1.GetFileResponse, error) { 30 sp, ctx := opentracing.StartSpanFromContext(ctx, "findGoFile") 31 defer sp.Finish() 32 sp.SetTag("file.path", ff.file.Path) 33 sp.SetTag("file.function_name", ff.file.FunctionName) 34 35 // if we have mappings try those first 36 for _, m := range mappings { 37 pos := m.Match(ff.file) 38 if pos < 0 || pos > len(ff.file.Path) { 39 level.Warn(ff.logger).Log("msg", "mapping cut off out of bounds", "pos", pos, "file_path", ff.file.Path) 40 continue 41 } 42 resp, err := ff.fetchMappingFile(ctx, m, strings.TrimLeft(ff.file.Path[pos:], "/")) 43 if err != nil { 44 if errors.Is(err, client.ErrNotFound) { 45 continue 46 } 47 level.Warn(ff.logger).Log("msg", "failed to fetch mapping file", "err", err) 48 continue 49 } 50 return resp, nil 51 } 52 53 if path, version, ok := golang.IsStandardLibraryPath(ff.file.Path); ok { 54 return ff.fetchGoStdlib(ctx, path, version) 55 } 56 57 if relativePath, ok := golang.VendorRelativePath(ff.file.Path); ok { 58 return ff.fetchRepoFile(ctx, relativePath, ff.ref) 59 } 60 61 modFile, ok := golang.ParseModuleFromPath(ff.file.Path) 62 if ok { 63 mainModule := module.Version{ 64 Path: path.Join(ff.repo.GetHostName(), ff.repo.GetOwnerName(), ff.repo.GetRepoName()), 65 Version: module.PseudoVersion("", "", time.Time{}, ff.ref), 66 } 67 modf, err := ff.fetchGoMod(ctx) 68 if err != nil { 69 level.Warn(ff.logger).Log("msg", "failed to fetch go.mod file", "err", err) 70 } 71 if err := modFile.Resolve(ctx, mainModule, modf, ff.httpClient); err != nil { 72 return nil, err 73 } 74 return ff.fetchGoDependencyFile(ctx, modFile) 75 } 76 return ff.tryFindGoFile(ctx, 30) 77 } 78 79 func (ff FileFinder) fetchGoStdlib(ctx context.Context, path string, version string) (*vcsv1.GetFileResponse, error) { 80 sp, ctx := opentracing.StartSpanFromContext(ctx, "fetchGoStdlib") 81 defer sp.Finish() 82 83 // if there is no version detected, use the one from .pyroscope.yaml 84 if version == "" && ff.config != nil { 85 mapping := ff.config.FindMapping(config.FileSpec{Path: "$GOROOT/src"}) 86 if mapping != nil { 87 return ff.fetchMappingFile(ctx, mapping, path) 88 } 89 } 90 91 // use master branch as fallback 92 ref := "master" 93 if version != "" { 94 ref = "go" + version 95 } 96 97 content, err := ff.client.GetFile(ctx, client.FileRequest{ 98 Owner: "golang", 99 Repo: "go", 100 Path: filepath.Join("src", path), 101 Ref: ref, 102 }) 103 if err != nil { 104 return nil, err 105 } 106 return newFileResponse(content.Content, content.URL) 107 } 108 109 func (ff FileFinder) fetchGoMod(ctx context.Context) (*modfile.File, error) { 110 sp, ctx := opentracing.StartSpanFromContext(ctx, "fetchGoMod") 111 defer sp.Finish() 112 sp.SetTag("owner", ff.repo.GetOwnerName()) 113 sp.SetTag("repo", ff.repo.GetRepoName()) 114 sp.SetTag("ref", ff.ref) 115 116 content, err := ff.client.GetFile(ctx, client.FileRequest{ 117 Owner: ff.repo.GetOwnerName(), 118 Repo: ff.repo.GetRepoName(), 119 Path: golang.GoMod, 120 Ref: ff.ref, 121 }) 122 if err != nil { 123 return nil, err 124 } 125 return modfile.Parse(golang.GoMod, []byte(content.Content), nil) 126 } 127 128 func (ff FileFinder) fetchGoDependencyFile(ctx context.Context, module golang.Module) (*vcsv1.GetFileResponse, error) { 129 sp, ctx := opentracing.StartSpanFromContext(ctx, "fetchGoDependencyFile") 130 defer sp.Finish() 131 sp.SetTag("module_path", module.Path) 132 133 switch { 134 case module.IsGitHub(): 135 return ff.fetchGithubModuleFile(ctx, module) 136 case module.IsGoogleSource(): 137 return ff.fetchGoogleSourceDependencyFile(ctx, module) 138 } 139 return nil, fmt.Errorf("unsupported module path: %s", module.Path) 140 } 141 142 func (ff FileFinder) fetchGithubModuleFile(ctx context.Context, mod golang.Module) (*vcsv1.GetFileResponse, error) { 143 sp, ctx := opentracing.StartSpanFromContext(ctx, "fetchGithubModuleFile") 144 defer sp.Finish() 145 sp.SetTag("module_path", mod.Path) 146 147 // todo: what if this is not a github repo? 148 // VSClient should support querying multiple repo providers. 149 githubFile, err := mod.GithubFile() 150 if err != nil { 151 return nil, err 152 } 153 sp.SetTag("owner", githubFile.Owner) 154 sp.SetTag("repo", githubFile.Repo) 155 sp.SetTag("path", githubFile.Path) 156 sp.SetTag("ref", githubFile.Ref) 157 158 content, err := ff.client.GetFile(ctx, client.FileRequest{ 159 Owner: githubFile.Owner, 160 Repo: githubFile.Repo, 161 Path: githubFile.Path, 162 Ref: githubFile.Ref, 163 }) 164 if err != nil { 165 return nil, err 166 } 167 return newFileResponse(content.Content, content.URL) 168 } 169 170 func (ff FileFinder) fetchGoogleSourceDependencyFile(ctx context.Context, mod golang.Module) (*vcsv1.GetFileResponse, error) { 171 sp, ctx := opentracing.StartSpanFromContext(ctx, "fetchGoogleSourceDependencyFile") 172 defer sp.Finish() 173 sp.SetTag("module_path", mod.Path) 174 175 url, err := mod.GoogleSourceURL() 176 if err != nil { 177 return nil, err 178 } 179 sp.SetTag("url", url) 180 return ff.fetchURL(ctx, url, true) 181 } 182 183 // tryFindGoFile tries to find the go file in the repo, under the rootPath. 184 // It tries to find the file in the rootPath inside the repo by removing path segment after path segment. 185 // maxAttempts is the maximum number of attempts to try to find the file in case the file path is very long. 186 // For example, if the path is "github.com/grafana/grafana/pkg/infra/log/log.go" and rootPath is "path/to/module1", it 187 // will try to find the file at: 188 // - "path/to/module1/pkg/infra/log/log.go" 189 // - "path/to/module1/infra/log/log.go" 190 // - "path/to/module1/log/log.go" 191 // - "path/to/module1/log.go" 192 func (ff FileFinder) tryFindGoFile(ctx context.Context, maxAttempts int) (*vcsv1.GetFileResponse, error) { 193 if maxAttempts <= 0 { 194 return nil, errors.New("invalid max attempts") 195 } 196 197 // trim repo path (e.g. "github.com/grafana/pyroscope/") in path 198 path := ff.file.Path 199 repoPath := strings.Join([]string{ff.repo.GetHostName(), ff.repo.GetOwnerName(), ff.repo.GetRepoName(), ""}, "/") 200 if pos := strings.Index(path, repoPath); pos != -1 { 201 path = path[len(repoPath)+pos:] 202 } 203 204 // now try to find file in repo 205 path = strings.TrimLeft(path, "/") 206 attempts := 0 207 for { 208 reqPath := path 209 if ff.rootPath != "" { 210 reqPath = strings.Join([]string{ff.rootPath, path}, "/") 211 } 212 content, err := ff.client.GetFile(ctx, client.FileRequest{ 213 Owner: ff.repo.GetOwnerName(), 214 Repo: ff.repo.GetRepoName(), 215 Path: reqPath, 216 Ref: ff.ref, 217 }) 218 attempts++ 219 if err != nil && errors.Is(err, client.ErrNotFound) && attempts < maxAttempts { 220 i := strings.Index(path, "/") 221 if i < 0 { 222 return nil, err 223 } 224 // remove the first path segment 225 path = path[i+1:] 226 continue 227 } 228 if err != nil { 229 return nil, err 230 } 231 return newFileResponse(content.Content, content.URL) 232 } 233 }