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  }