metric_storeview_routing.go (406 lines of code) (raw):

package sls import ( "encoding/json" "errors" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/promql/parser" "regexp" "regexp/syntax" "sort" "strings" ) var ( prefixRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-]+\.\*$`) suffixRegex = regexp.MustCompile(`^\.\*[a-zA-Z0-9_\-]+$`) containsRegex = regexp.MustCompile(`^\.\*[a-zA-Z0-9_\-]+\.\*$`) // except "+", e.g., ".+ABC.+" -> ABC literalExtractRegex = regexp.MustCompile(`[a-zA-Z0-9_\-]+`) ) type MatchType int func (m MatchType) String() string { switch m { case MatchEqual: return "EQUAL" case MatchPrefix: return "PREFIX" case MatchContains: return "CONTAINS" case MatchSuffix: return "SUFFIX" case MatchInclude: return "INCLUDE" case MatchRegexp: return "REGEXP" default: return "UNKNOWN" } } const ( MatchEqual MatchType = iota MatchPrefix MatchContains MatchSuffix MatchInclude MatchRegexp ) type Condition struct { MatchAll bool // ".*" ".+" MatchType MatchType MatchValue string IncludeValuesMap map[string]struct{} RegexPattern *regexp.Regexp MatchFunc func(cond *Condition, val string) bool } func (c *Condition) Match(valueStr string) bool { if c.MatchAll { return true } return c.MatchFunc(c, valueStr) } func (c *Condition) String() string { if c.MatchAll { return "ALL" } matchType := c.MatchType var strBuilder strings.Builder strBuilder.WriteString(matchType.String()) strBuilder.WriteByte(':') switch matchType { case MatchRegexp: strBuilder.WriteString(c.RegexPattern.String()) strBuilder.WriteByte('#') strBuilder.WriteString(c.MatchValue) break case MatchInclude: for k, _ := range c.IncludeValuesMap { strBuilder.WriteString(k) strBuilder.WriteByte(',') } break default: strBuilder.WriteString(c.MatchValue) } return strBuilder.String() } func equalFunc(cond *Condition, val string) bool { return cond.MatchValue == val } func prefixFunc(cond *Condition, val string) bool { return strings.HasPrefix(val, cond.MatchValue) } func suffixFunc(cond *Condition, val string) bool { return strings.HasSuffix(val, cond.MatchValue) } func containsFunc(cond *Condition, val string) bool { return strings.Contains(val, cond.MatchValue) } func includeFunc(cond *Condition, val string) bool { _, ok := cond.IncludeValuesMap[val] return ok } func regexpFunc(cond *Condition, val string) bool { return cond.RegexPattern.MatchString(val) } func isPlainRegex(re *syntax.Regexp) bool { switch re.Op { case syntax.OpLiteral: if re.Flags != syntax.Perl { return false } return true case syntax.OpConcat: for _, sub := range re.Sub { if !isPlainRegex(sub) { return false } } if len(re.Sub) == 0 && re.Sub0[0] != nil { return isPlainRegex(re.Sub0[0]) } return true default: return false } } func special(b byte) bool { _, ok := specialChars[b] return ok } var specialChars = map[byte]struct{}{ '\\': {}, '.': {}, '+': {}, '*': {}, '?': {}, '(': {}, ')': {}, '|': {}, '[': {}, ']': {}, '{': {}, '}': {}, '^': {}, '$': {}, } // unescapeRegexStr: delete all escape characters func unescapeRegexStr(s string) string { var i int for i = 0; i < len(s); i++ { if special(s[i]) { break } } if i >= len(s) { return s } res := make([]byte, len(s)) j := 0 for idx := 0; idx < len(s); j++ { if s[idx] == '\\' && idx+1 < len(s) && special(s[idx+1]) { // delete escaped character: '\' res[j] = s[idx+1] idx += 2 continue } res[j] = s[idx] idx++ } return string(res[:j]) } func isSimpleStrRegex(regexStr string) (bool, string) { re, err := syntax.Parse(regexStr, syntax.Perl) if err != nil { return false, "" } if !isPlainRegex(re) { return false, "" } str := unescapeRegexStr(regexStr) return true, str } func strictPrefixRegex(regexStr string) string { if prefixRegex.MatchString(regexStr) { return literalExtractRegex.FindStringSubmatch(regexStr)[0] } return "" } func strictSuffixRegex(regexStr string) string { if suffixRegex.MatchString(regexStr) { return literalExtractRegex.FindStringSubmatch(regexStr)[0] } return "" } func strictContainsRegex(regexStr string) string { if containsRegex.MatchString(regexStr) { return literalExtractRegex.FindStringSubmatch(regexStr)[0] } return "" } func transRegexToInclude(labelValue string) []string { valLen := len(labelValue) if valLen > 2 && labelValue[0] == '(' && labelValue[valLen-1] == ')' { labelValue = labelValue[1 : valLen-1] } splits := strings.Split(labelValue, "|") sort.Strings(splits) values := make([]string, 0, len(splits)) for _, val := range splits { if val == "" { return nil } if ok, newVal := isSimpleStrRegex(val); ok { values = append(values, newVal) continue } return nil } return values } func checkIncludeCond(val string) []string { if !strings.Contains(val, "|") { return nil } return transRegexToInclude(val) } func generateMatchCondition(matchStr string) (*Condition, error) { if matchStr == "" { return nil, errors.New("empty match string") } if matchStr == ".*" || matchStr == ".+" { return &Condition{MatchAll: true, MatchType: MatchEqual, MatchValue: matchStr}, nil } if val := strictPrefixRegex(matchStr); val != "" { return &Condition{MatchAll: false, MatchType: MatchPrefix, MatchValue: val, MatchFunc: prefixFunc}, nil } else if val = strictContainsRegex(matchStr); val != "" { return &Condition{MatchAll: false, MatchType: MatchContains, MatchValue: val, MatchFunc: containsFunc}, nil } else if val = strictSuffixRegex(matchStr); val != "" { return &Condition{MatchAll: false, MatchType: MatchSuffix, MatchValue: val, MatchFunc: suffixFunc}, nil } else if includes := checkIncludeCond(matchStr); len(includes) > 0 { valuesMap := make(map[string]struct{}, len(includes)) for _, v := range includes { valuesMap[v] = struct{}{} } return &Condition{MatchAll: false, MatchType: MatchInclude, MatchValue: matchStr, IncludeValuesMap: valuesMap, MatchFunc: includeFunc}, nil } else if ok, _ := isSimpleStrRegex(matchStr); ok { return &Condition{MatchAll: false, MatchType: MatchEqual, MatchValue: matchStr, MatchFunc: equalFunc}, nil } else { regex, cErr := regexp.Compile(matchStr) if cErr == nil { return &Condition{MatchAll: false, MatchType: MatchRegexp, MatchValue: matchStr, RegexPattern: regex, MatchFunc: regexpFunc}, nil } return nil, cErr } } func generateMatchConditions(matchStrs []string, matchConds []*Condition) []*Condition { for _, name := range matchStrs { newCond, err := generateMatchCondition(name) if err != nil { continue } if newCond.MatchAll { matchConds = matchConds[:0] matchConds = append(matchConds, newCond) break } matchConds = append(matchConds, newCond) } return matchConds } type ProjectStoreCond struct { ProjectCond *Condition MetricStoreCond *Condition } func (p *ProjectStoreCond) String() string { return p.ProjectCond.String() + "__" + p.MetricStoreCond.String() } func (p *ProjectStoreCond) Match(project, store string) bool { return (project == "" || p.ProjectCond.Match(project)) && p.MetricStoreCond.Match(store) } type StoreViewRoutingMatchStrategy struct { MetricNameConds []*Condition ProjectStoreConds []*ProjectStoreCond } func (s *StoreViewRoutingMatchStrategy) String() string { var strBuilder strings.Builder for _, cond := range s.MetricNameConds { strBuilder.WriteString(cond.String()) strBuilder.WriteByte('|') } strBuilder.WriteString(";;;") for _, cond := range s.ProjectStoreConds { strBuilder.WriteString(cond.ProjectCond.String()) strBuilder.WriteString("_") strBuilder.WriteString(cond.MetricStoreCond.String()) strBuilder.WriteByte('|') } return strBuilder.String() } type StoreViewRoutingStategiesItem struct { HashVal uint64 Strategies []*StoreViewRoutingMatchStrategy } type StoreViewRoutingConfigs []*MetricStoreViewRoutingConfig func generateStoreviewRoutingStrategyOnConfigs(configsBuf []byte, newHashVal uint64) (*StoreViewRoutingStategiesItem, error) { strategyList := StoreViewRoutingConfigs{} err := json.Unmarshal(configsBuf, &strategyList) if err != nil { return nil, err } matchStrategies := make([]*StoreViewRoutingMatchStrategy, 0, len(strategyList)) for _, strategy := range strategyList { matchStrategy := &StoreViewRoutingMatchStrategy{ MetricNameConds: make([]*Condition, 0, len(strategy.MetricNames)+1), ProjectStoreConds: make([]*ProjectStoreCond, 0, len(strategy.ProjectStores)), } // metric names if len(strategy.MetricNames) == 0 { matchStrategy.MetricNameConds = append(matchStrategy.MetricNameConds, &Condition{MatchAll: true, MatchType: MatchEqual}) } matchStrategy.MetricNameConds = generateMatchConditions(strategy.MetricNames, matchStrategy.MetricNameConds) for _, projectStore := range strategy.ProjectStores { projCond, err := generateMatchCondition(projectStore.ProjectName) if err != nil { continue } storeCond, err := generateMatchCondition(projectStore.MetricStore) if err != nil { continue } matchStrategy.ProjectStoreConds = append(matchStrategy.ProjectStoreConds, &ProjectStoreCond{ ProjectCond: projCond, MetricStoreCond: storeCond, }) } matchStrategies = append(matchStrategies, matchStrategy) } return &StoreViewRoutingStategiesItem{Strategies: matchStrategies, HashVal: newHashVal}, nil } type StoreViewRoutingChecker struct { strategyItem *StoreViewRoutingStategiesItem } func NewStoreViewRoutingChecker(routingConf []byte) (*StoreViewRoutingChecker, error) { strategyItem, err := generateStoreviewRoutingStrategyOnConfigs(routingConf, 0) if err != nil { return nil, err } return &StoreViewRoutingChecker{strategyItem: strategyItem}, nil } type MetricRoutingResult struct { MetricName string ProjectStores []ProjectStore } func (s *StoreViewRoutingChecker) CheckPromQlQuery(query string, sourceProjects []ProjectStore) ([]MetricRoutingResult, error) { expr, err := parser.ParseExpr(query) if err != nil { return nil, err } dstProjects := make([]MetricRoutingResult, 0, 4) parser.Inspect(expr, func(node parser.Node, path []parser.Node) error { switch n := node.(type) { case *parser.VectorSelector: metricNameOnEqual := "" for _, matcher := range n.LabelMatchers { if matcher.Name == "__name__" && matcher.Type == labels.MatchEqual { metricNameOnEqual = matcher.Value break } } if metricNameOnEqual == "" { // default: all stores matched dstProjects = append(dstProjects, MetricRoutingResult{MetricName: n.Name, ProjectStores: sourceProjects}) return nil } matchedStrategies := make([]*StoreViewRoutingMatchStrategy, 0, len(s.strategyItem.Strategies)) for _, strategy := range s.strategyItem.Strategies { metricNameMatched := false for _, metricNameCond := range strategy.MetricNameConds { if metricNameCond.Match(metricNameOnEqual) { metricNameMatched = true break } } if metricNameMatched { matchedStrategies = append(matchedStrategies, strategy) } } if (len(matchedStrategies)) == 0 { // default: all stores matched dstProjects = append(dstProjects, MetricRoutingResult{MetricName: n.Name, ProjectStores: sourceProjects}) return nil } routingResult := MetricRoutingResult{MetricName: n.Name} for _, projectStore := range sourceProjects { storeMatched := false for _, strategy := range matchedStrategies { for _, storeCond := range strategy.ProjectStoreConds { if storeCond.Match(projectStore.ProjectName, projectStore.MetricStore) { storeMatched = true break } } if storeMatched { routingResult.ProjectStores = append(routingResult.ProjectStores, projectStore) break } } } dstProjects = append(dstProjects, routingResult) } return nil }) return dstProjects, nil }