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 }