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 }