github.com/grafana/pyroscope@v1.18.0/pkg/frontend/vcs/config/config.go (about) 1 package config 2 3 import ( 4 "errors" 5 "fmt" 6 "slices" 7 "strings" 8 9 "gopkg.in/yaml.v3" 10 ) 11 12 type Language string 13 14 const ( 15 PyroscopeConfigPath = ".pyroscope.yaml" 16 17 LanguageUnknown = Language("") 18 LanguageGo = Language("go") 19 LanguageJava = Language("java") 20 LanguagePython = Language("python") 21 ) 22 23 type Version string 24 25 const ( 26 VersionUnknown = Version("") 27 VersionV1 = Version("v1") 28 ) 29 30 var validLanguages = []Language{ 31 LanguageGo, 32 LanguageJava, 33 LanguagePython, 34 } 35 36 // PyroscopeConfig represents the structure of .pyroscope.yaml configuration file 37 type PyroscopeConfig struct { 38 Version Version 39 SourceCode SourceCodeConfig `yaml:"source_code"` 40 } 41 42 // SourceCodeConfig contains source code mapping configuration 43 type SourceCodeConfig struct { 44 Mappings []MappingConfig `yaml:"mappings"` 45 } 46 47 // MappingConfig represents a single source code path mapping 48 type MappingConfig struct { 49 Path []Match `yaml:"path"` 50 FunctionName []Match `yaml:"function_name"` 51 Language string `yaml:"language"` 52 53 Source Source `yaml:"source"` 54 } 55 56 // Match represents how mappings a single source code path mapping 57 type Match struct { 58 Prefix string `yaml:"prefix"` 59 } 60 61 // Source represents how mappings retrieve the source 62 type Source struct { 63 Local *LocalMappingConfig `yaml:"local,omitempty"` 64 GitHub *GitHubMappingConfig `yaml:"github,omitempty"` 65 } 66 67 // LocalMappingConfig contains configuration for local path mappings 68 type LocalMappingConfig struct { 69 Path string `yaml:"path"` 70 } 71 72 // GitHubMappingConfig contains configuration for GitHub repository mappings 73 type GitHubMappingConfig struct { 74 Owner string `yaml:"owner"` 75 Repo string `yaml:"repo"` 76 Ref string `yaml:"ref"` 77 Path string `yaml:"path"` 78 } 79 80 // ParsePyroscopeConfig parses a configuration from bytes 81 func ParsePyroscopeConfig(data []byte) (*PyroscopeConfig, error) { 82 var config PyroscopeConfig 83 if err := yaml.Unmarshal(data, &config); err != nil { 84 return nil, fmt.Errorf("failed to parse pyroscope config: %w", err) 85 } 86 87 // Validate the configuration 88 if err := config.Validate(); err != nil { 89 return nil, fmt.Errorf("invalid pyroscope config: %w", err) 90 } 91 92 return &config, nil 93 } 94 95 // Validate checks if the configuration is valid 96 func (c *PyroscopeConfig) Validate() error { 97 if c.Version == VersionUnknown { 98 c.Version = VersionV1 99 } 100 101 if c.Version != VersionV1 { 102 return fmt.Errorf("invalid version '%s', supported versions are '%s'", c.Version, VersionV1) 103 } 104 105 var errs []error 106 for i, mapping := range c.SourceCode.Mappings { 107 if err := mapping.Validate(); err != nil { 108 errs = append(errs, fmt.Errorf("mapping[%d]: %w", i, err)) 109 } 110 } 111 return errors.Join(errs...) 112 } 113 114 // Validate checks if a mapping configuration is valid 115 func (m *MappingConfig) Validate() error { 116 var errs []error 117 118 if len(m.Path) == 0 && len(m.FunctionName) == 0 { 119 errs = append(errs, fmt.Errorf("at least one path or a function_name match is required")) 120 } 121 122 if !slices.Contains(validLanguages, Language(m.Language)) { 123 errs = append(errs, fmt.Errorf("language '%s' unsupported, valid languages are %v", m.Language, validLanguages)) 124 } 125 126 if err := m.Source.Validate(); err != nil { 127 errs = append(errs, err) 128 } 129 130 return errors.Join(errs...) 131 } 132 133 // Validate checks if a source configuration is valid 134 func (m *Source) Validate() error { 135 var ( 136 instances int 137 errs []error 138 ) 139 140 if m.GitHub != nil { 141 instances++ 142 if err := m.GitHub.Validate(); err != nil { 143 errs = append(errs, err) 144 } 145 } 146 if m.Local != nil { 147 instances++ 148 if err := m.Local.Validate(); err != nil { 149 errs = append(errs, err) 150 } 151 } 152 153 if instances == 0 { 154 errs = append(errs, errors.New("no source type supplied, you need to supply exactly one source type")) 155 } else if instances != 1 { 156 errs = append(errs, errors.New("more than one source type supplied, you need to supply exactly one source type")) 157 } 158 159 return errors.Join(errs...) 160 } 161 162 func (m *GitHubMappingConfig) Validate() error { 163 return nil 164 } 165 166 func (m *LocalMappingConfig) Validate() error { 167 return nil 168 } 169 170 type FileSpec struct { 171 Path string 172 FunctionName string 173 } 174 175 // FindMapping finds a mapping configuration that matches the given FileSpec 176 // Returns nil if no matching mapping is found 177 func (c *PyroscopeConfig) FindMapping(file FileSpec) *MappingConfig { 178 if c == nil { 179 return nil 180 } 181 182 // Find the longest matching prefix 183 var bestMatch *MappingConfig 184 var bestMatchLen = -1 185 for _, m := range c.SourceCode.Mappings { 186 if result := m.Match(file); result > bestMatchLen { 187 bestMatch = &m 188 bestMatchLen = result 189 } 190 } 191 return bestMatch 192 } 193 194 // Returns -1 if no match, otherwise the number of characters that matched 195 func (m *MappingConfig) Match(file FileSpec) int { 196 result := -1 197 for _, fun := range m.FunctionName { 198 if strings.HasPrefix(file.FunctionName, fun.Prefix) { 199 if len(fun.Prefix) > result { 200 result = len(fun.Prefix) 201 } 202 } 203 } 204 for _, path := range m.Path { 205 if strings.HasPrefix(file.Path, path.Prefix) { 206 if len(path.Prefix) > result { 207 result = len(path.Prefix) 208 } 209 } 210 } 211 return result 212 }