github.com/grafana/pyroscope@v1.18.0/pkg/validation/usage_groups.go (about)

     1  // This file is a modified copy of the usage groups implementation in Mimir:
     2  //
     3  // https://github.com/grafana/mimir/blob/0e8c09f237649e95dc1bf3f7547fd279c24bdcf9/pkg/ingester/activeseries/custom_trackers_config.go#L48
     4  
     5  package validation
     6  
     7  import (
     8  	"encoding/json"
     9  	"fmt"
    10  	"strings"
    11  	"unicode/utf8"
    12  
    13  	"github.com/go-kit/log"
    14  	"github.com/go-kit/log/level"
    15  	"github.com/prometheus/client_golang/prometheus"
    16  	"github.com/prometheus/client_golang/prometheus/promauto"
    17  	"github.com/prometheus/prometheus/model/labels"
    18  	"github.com/prometheus/prometheus/promql/parser"
    19  	"gopkg.in/yaml.v3"
    20  
    21  	phlaremodel "github.com/grafana/pyroscope/pkg/model"
    22  )
    23  
    24  const (
    25  	// Maximum number of usage groups that can be configured (per tenant).
    26  	maxUsageGroups = 50
    27  
    28  	// The usage group name to use when no user-defined usage groups matched.
    29  	noMatchName = "other"
    30  )
    31  
    32  var (
    33  	// This is a duplicate of distributor_received_decompressed_bytes, but with
    34  	// usage_group as a label.
    35  	usageGroupReceivedDecompressedBytes = promauto.NewCounterVec(
    36  		prometheus.CounterOpts{
    37  			Namespace: "pyroscope",
    38  			Name:      "usage_group_received_decompressed_total",
    39  			Help:      "The total number of decompressed bytes per profile received by usage group.",
    40  		},
    41  		[]string{"type", "tenant", "usage_group"},
    42  	)
    43  
    44  	// This is a duplicate of discarded_bytes_total, but with usage_group as a
    45  	// label.
    46  	usageGroupDiscardedBytes = promauto.NewCounterVec(
    47  		prometheus.CounterOpts{
    48  			Namespace: "pyroscope",
    49  			Name:      "usage_group_discarded_bytes_total",
    50  			Help:      "The total number of bytes that were discarded by usage group.",
    51  		},
    52  		[]string{"reason", "tenant", "usage_group"},
    53  	)
    54  )
    55  
    56  // templatePart represents a part of a parsed usage group name template
    57  type templatePart struct {
    58  	isLiteral bool
    59  	value     string // literal text or label name for placeholder
    60  }
    61  
    62  // usageGroupEntry represents a single usage group configuration
    63  type usageGroupEntry struct {
    64  	matchers []*labels.Matcher
    65  	// For static names, template is nil and name is used
    66  	name string
    67  	// For dynamic names, template contains the parsed template parts
    68  	template []templatePart
    69  }
    70  
    71  type UsageGroupConfig struct {
    72  	config map[string][]*labels.Matcher
    73  
    74  	parsedEntries []usageGroupEntry
    75  }
    76  
    77  const dynamicLabelNamePrefix = "${labels."
    78  
    79  type UsageGroupEvaluator struct {
    80  	logger log.Logger
    81  }
    82  
    83  func NewUsageGroupEvaluator(logger log.Logger) *UsageGroupEvaluator {
    84  	return &UsageGroupEvaluator{
    85  		logger: logger,
    86  	}
    87  }
    88  
    89  func (e *UsageGroupEvaluator) GetMatch(tenantID string, c *UsageGroupConfig, lbls phlaremodel.Labels) UsageGroupMatch {
    90  	match := UsageGroupMatch{
    91  		tenantID: tenantID,
    92  		names:    make([]UsageGroupMatchName, 0, len(c.parsedEntries)),
    93  	}
    94  
    95  	for _, entry := range c.parsedEntries {
    96  		if c.matchesAll(entry.matchers, lbls) {
    97  			if entry.template != nil {
    98  				resolvedName, err := c.expandTemplate(entry.template, lbls)
    99  				if err != nil {
   100  					level.Warn(e.logger).Log(
   101  						"msg", "failed to expand usage group template, skipping usage group",
   102  						"err", err,
   103  						"usage_group", entry.name)
   104  					continue
   105  				}
   106  				if resolvedName == "" {
   107  					level.Warn(e.logger).Log(
   108  						"msg", "usage group template expanded to empty string, skipping usage group",
   109  						"usage_group", entry.name)
   110  					continue
   111  				}
   112  				match.names = append(match.names, UsageGroupMatchName{
   113  					ConfiguredName: entry.name,
   114  					ResolvedName:   resolvedName,
   115  				})
   116  			} else {
   117  				match.names = append(match.names, UsageGroupMatchName{
   118  					ConfiguredName: entry.name,
   119  					ResolvedName:   entry.name,
   120  				})
   121  			}
   122  		}
   123  	}
   124  
   125  	return match
   126  }
   127  
   128  func (c *UsageGroupConfig) UnmarshalYAML(value *yaml.Node) error {
   129  	m := make(map[string]string)
   130  	err := value.DecodeWithOptions(&m, yaml.DecodeOptions{
   131  		KnownFields: true,
   132  	})
   133  	if err != nil {
   134  		return fmt.Errorf("malformed usage group config: %w", err)
   135  	}
   136  
   137  	entries, rawData, err := parseUsageGroupEntries(m)
   138  	if err != nil {
   139  		return err
   140  	}
   141  	c.parsedEntries = entries
   142  	c.config = rawData
   143  	return nil
   144  }
   145  
   146  func (c *UsageGroupConfig) UnmarshalJSON(bytes []byte) error {
   147  	m := make(map[string]string)
   148  	err := json.Unmarshal(bytes, &m)
   149  	if err != nil {
   150  		return fmt.Errorf("malformed usage group config: %w", err)
   151  	}
   152  
   153  	entries, rawData, err := parseUsageGroupEntries(m)
   154  	if err != nil {
   155  		return err
   156  	}
   157  	c.parsedEntries = entries
   158  	c.config = rawData
   159  	return nil
   160  }
   161  
   162  type UsageGroupMatch struct {
   163  	tenantID string
   164  	names    []UsageGroupMatchName
   165  }
   166  
   167  type UsageGroupMatchName struct {
   168  	ConfiguredName string
   169  	ResolvedName   string
   170  }
   171  
   172  func (m *UsageGroupMatchName) IsMoreSpecificThan(other *UsageGroupMatchName) bool {
   173  	return !strings.Contains(m.ConfiguredName, dynamicLabelNamePrefix) && strings.Contains(other.ConfiguredName, dynamicLabelNamePrefix)
   174  }
   175  
   176  func (m *UsageGroupMatchName) String() string {
   177  	return fmt.Sprintf("{configured: %s, resolved: %s}", m.ConfiguredName, m.ResolvedName)
   178  }
   179  
   180  func (m UsageGroupMatch) CountReceivedBytes(profileType string, n int64) {
   181  	if len(m.names) == 0 {
   182  		usageGroupReceivedDecompressedBytes.WithLabelValues(profileType, m.tenantID, noMatchName).Add(float64(n))
   183  		return
   184  	}
   185  
   186  	for _, name := range m.names {
   187  		usageGroupReceivedDecompressedBytes.WithLabelValues(profileType, m.tenantID, name.ResolvedName).Add(float64(n))
   188  	}
   189  }
   190  
   191  func (m UsageGroupMatch) CountDiscardedBytes(reason string, n int64) {
   192  	if len(m.names) == 0 {
   193  		usageGroupDiscardedBytes.WithLabelValues(reason, m.tenantID, noMatchName).Add(float64(n))
   194  		return
   195  	}
   196  
   197  	for _, name := range m.names {
   198  		usageGroupDiscardedBytes.WithLabelValues(reason, m.tenantID, name.ResolvedName).Add(float64(n))
   199  	}
   200  }
   201  
   202  func (m UsageGroupMatch) Names() []UsageGroupMatchName {
   203  	return m.names
   204  }
   205  
   206  func NewUsageGroupConfig(m map[string]string) (*UsageGroupConfig, error) {
   207  	entries, rawData, err := parseUsageGroupEntries(m)
   208  	if err != nil {
   209  		return nil, err
   210  	}
   211  	config := &UsageGroupConfig{
   212  		parsedEntries: entries,
   213  		config:        rawData,
   214  	}
   215  	return config, nil
   216  }
   217  
   218  func parseUsageGroupEntries(m map[string]string) ([]usageGroupEntry, map[string][]*labels.Matcher, error) {
   219  	if len(m) > maxUsageGroups {
   220  		return nil, nil, fmt.Errorf("maximum number of usage groups is %d, got %d", maxUsageGroups, len(m))
   221  	}
   222  
   223  	rawData := make(map[string][]*labels.Matcher)
   224  	entries := make([]usageGroupEntry, 0, len(m))
   225  
   226  	for name, matchersText := range m {
   227  		if !utf8.ValidString(name) {
   228  			return nil, nil, fmt.Errorf("usage group name %q is not valid UTF-8", name)
   229  		}
   230  
   231  		name = strings.TrimSpace(name)
   232  		if name == "" {
   233  			return nil, nil, fmt.Errorf("usage group name cannot be empty")
   234  		}
   235  
   236  		if name == noMatchName {
   237  			return nil, nil, fmt.Errorf("usage group name %q is reserved", noMatchName)
   238  		}
   239  
   240  		matchers, err := parser.ParseMetricSelector(matchersText)
   241  		if err != nil {
   242  			return nil, nil, fmt.Errorf("failed to parse matchers for usage group %q: %w", name, err)
   243  		}
   244  
   245  		entry := usageGroupEntry{
   246  			matchers: matchers,
   247  			name:     name,
   248  		}
   249  
   250  		if strings.Contains(name, dynamicLabelNamePrefix) {
   251  			template, err := parseTemplate(name)
   252  			if err != nil {
   253  				return nil, nil, fmt.Errorf("failed to parse template for usage group %q: %w", name, err)
   254  			}
   255  			entry.template = template
   256  		}
   257  
   258  		entries = append(entries, entry)
   259  		rawData[name] = matchers
   260  	}
   261  
   262  	return entries, rawData, nil
   263  }
   264  
   265  // parseTemplate parses a usage group name template into parts
   266  func parseTemplate(name string) ([]templatePart, error) {
   267  	var parts []templatePart
   268  	remaining := name
   269  
   270  	for len(remaining) > 0 {
   271  		before, after, found := strings.Cut(remaining, dynamicLabelNamePrefix)
   272  
   273  		// add literal part before placeholder (if any)
   274  		if len(before) > 0 {
   275  			parts = append(parts, templatePart{
   276  				isLiteral: true,
   277  				value:     before,
   278  			})
   279  		}
   280  
   281  		if !found {
   282  			break
   283  		}
   284  
   285  		labelName, afterBrace, foundBrace := strings.Cut(after, "}")
   286  		if !foundBrace {
   287  			return nil, fmt.Errorf("unclosed placeholder")
   288  		}
   289  
   290  		if labelName == "" {
   291  			return nil, fmt.Errorf("empty label name in placeholder")
   292  		}
   293  
   294  		parts = append(parts, templatePart{
   295  			isLiteral: false,
   296  			value:     labelName,
   297  		})
   298  
   299  		remaining = afterBrace
   300  	}
   301  
   302  	return parts, nil
   303  }
   304  
   305  func (o *Overrides) DistributorUsageGroups(tenantID string) *UsageGroupConfig {
   306  	config := o.getOverridesForTenant(tenantID).DistributorUsageGroups
   307  
   308  	// It should never be nil, but check just in case!
   309  	if config == nil {
   310  		config = &UsageGroupConfig{}
   311  	}
   312  	return config
   313  }
   314  
   315  func (c *UsageGroupConfig) matchesAll(matchers []*labels.Matcher, lbls phlaremodel.Labels) bool {
   316  	if len(lbls) == 0 && len(matchers) > 0 {
   317  		return false
   318  	}
   319  
   320  	for _, m := range matchers {
   321  		if lbl, ok := lbls.GetLabel(m.Name); ok {
   322  			if !m.Matches(lbl.Value) {
   323  				return false
   324  			}
   325  			continue
   326  		}
   327  		return false
   328  	}
   329  	return true
   330  }
   331  
   332  func (c *UsageGroupConfig) expandTemplate(template []templatePart, lbls phlaremodel.Labels) (string, error) {
   333  	var result strings.Builder
   334  	result.Grow(len(template) * 8)
   335  
   336  	for _, part := range template {
   337  		if part.isLiteral {
   338  			result.WriteString(part.value)
   339  		} else {
   340  			value, found := lbls.GetLabel(part.value)
   341  			if !found {
   342  				return "", fmt.Errorf("label %q not found", part.value)
   343  			}
   344  			if value.Value == "" {
   345  				return "", fmt.Errorf("label %q is empty", part.value)
   346  			}
   347  			result.WriteString(value.Value)
   348  		}
   349  	}
   350  
   351  	return result.String(), nil
   352  }