github.com/lulzWill/go-agent@v2.1.2+incompatible/internal/metric_rules.go (about)

     1  package internal
     2  
     3  import (
     4  	"encoding/json"
     5  	"regexp"
     6  	"sort"
     7  	"strings"
     8  )
     9  
    10  type ruleResult int
    11  
    12  const (
    13  	ruleMatched ruleResult = iota
    14  	ruleUnmatched
    15  	ruleIgnore
    16  )
    17  
    18  type metricRule struct {
    19  	// 'Ignore' indicates if the entire transaction should be discarded if
    20  	// there is a match.  This field is only used by "url_rules" and
    21  	// "transaction_name_rules", not "metric_name_rules".
    22  	Ignore              bool   `json:"ignore"`
    23  	EachSegment         bool   `json:"each_segment"`
    24  	ReplaceAll          bool   `json:"replace_all"`
    25  	Terminate           bool   `json:"terminate_chain"`
    26  	Order               int    `json:"eval_order"`
    27  	OriginalReplacement string `json:"replacement"`
    28  	RawExpr             string `json:"match_expression"`
    29  
    30  	// Go's regexp backreferences use '${1}' instead of the Perlish '\1', so
    31  	// we transform the replacement string into the Go syntax and store it
    32  	// here.
    33  	TransformedReplacement string
    34  	re                     *regexp.Regexp
    35  }
    36  
    37  type metricRules []*metricRule
    38  
    39  // Go's regexp backreferences use `${1}` instead of the Perlish `\1`, so we must
    40  // transform the replacement string.  This is non-trivial: `\1` is a
    41  // backreference but `\\1` is not.  Rather than count the number of back slashes
    42  // preceding the digit, we simply skip rules with tricky replacements.
    43  var (
    44  	transformReplacementAmbiguous   = regexp.MustCompile(`\\\\([0-9]+)`)
    45  	transformReplacementRegex       = regexp.MustCompile(`\\([0-9]+)`)
    46  	transformReplacementReplacement = "$${${1}}"
    47  )
    48  
    49  func (rules *metricRules) UnmarshalJSON(data []byte) (err error) {
    50  	var raw []*metricRule
    51  
    52  	if err := json.Unmarshal(data, &raw); nil != err {
    53  		return err
    54  	}
    55  
    56  	valid := make(metricRules, 0, len(raw))
    57  
    58  	for _, r := range raw {
    59  		re, err := regexp.Compile("(?i)" + r.RawExpr)
    60  		if err != nil {
    61  			// TODO
    62  			// Warn("unable to compile rule", {
    63  			// 	"match_expression": r.RawExpr,
    64  			// 	"error":            err.Error(),
    65  			// })
    66  			continue
    67  		}
    68  
    69  		if transformReplacementAmbiguous.MatchString(r.OriginalReplacement) {
    70  			// TODO
    71  			// Warn("unable to transform replacement", {
    72  			// 	"match_expression": r.RawExpr,
    73  			// 	"replacement":      r.OriginalReplacement,
    74  			// })
    75  			continue
    76  		}
    77  
    78  		r.re = re
    79  		r.TransformedReplacement = transformReplacementRegex.ReplaceAllString(r.OriginalReplacement,
    80  			transformReplacementReplacement)
    81  		valid = append(valid, r)
    82  	}
    83  
    84  	sort.Sort(valid)
    85  
    86  	*rules = valid
    87  	return nil
    88  }
    89  
    90  func (rules metricRules) Len() int {
    91  	return len(rules)
    92  }
    93  
    94  // Rules should be applied in increasing order
    95  func (rules metricRules) Less(i, j int) bool {
    96  	return rules[i].Order < rules[j].Order
    97  }
    98  func (rules metricRules) Swap(i, j int) {
    99  	rules[i], rules[j] = rules[j], rules[i]
   100  }
   101  
   102  func replaceFirst(re *regexp.Regexp, s string, replacement string) (ruleResult, string) {
   103  	// Note that ReplaceAllStringFunc cannot be used here since it does
   104  	// not replace $1 placeholders.
   105  	loc := re.FindStringIndex(s)
   106  	if nil == loc {
   107  		return ruleUnmatched, s
   108  	}
   109  	firstMatch := s[loc[0]:loc[1]]
   110  	firstMatchReplaced := re.ReplaceAllString(firstMatch, replacement)
   111  	return ruleMatched, s[0:loc[0]] + firstMatchReplaced + s[loc[1]:]
   112  }
   113  
   114  func (r *metricRule) apply(s string) (ruleResult, string) {
   115  	// Rules are strange, and there is no spec.
   116  	// This code attempts to duplicate the logic of the PHP agent.
   117  	// Ambiguity abounds.
   118  
   119  	if r.Ignore {
   120  		if r.re.MatchString(s) {
   121  			return ruleIgnore, ""
   122  		}
   123  		return ruleUnmatched, s
   124  	}
   125  
   126  	if r.ReplaceAll {
   127  		if r.re.MatchString(s) {
   128  			return ruleMatched, r.re.ReplaceAllString(s, r.TransformedReplacement)
   129  		}
   130  		return ruleUnmatched, s
   131  	} else if r.EachSegment {
   132  		segments := strings.Split(s, "/")
   133  		applied := make([]string, len(segments))
   134  		result := ruleUnmatched
   135  		for i, segment := range segments {
   136  			var segmentMatched ruleResult
   137  			segmentMatched, applied[i] = replaceFirst(r.re, segment, r.TransformedReplacement)
   138  			if segmentMatched == ruleMatched {
   139  				result = ruleMatched
   140  			}
   141  		}
   142  		return result, strings.Join(applied, "/")
   143  	} else {
   144  		return replaceFirst(r.re, s, r.TransformedReplacement)
   145  	}
   146  }
   147  
   148  func (rules metricRules) Apply(input string) string {
   149  	var res ruleResult
   150  	s := input
   151  
   152  	for _, rule := range rules {
   153  		res, s = rule.apply(s)
   154  
   155  		if ruleIgnore == res {
   156  			return ""
   157  		}
   158  		if (ruleMatched == res) && rule.Terminate {
   159  			break
   160  		}
   161  	}
   162  
   163  	return s
   164  }