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