terraform/vendor/github.com/newrelic/go-agent/internal/metric_rules.go

164 lines
4.0 KiB
Go

package internal
import (
"encoding/json"
"regexp"
"sort"
"strings"
)
type ruleResult int
const (
ruleChanged ruleResult = iota
ruleUnchanged
ruleIgnore
)
type metricRule struct {
// 'Ignore' indicates if the entire transaction should be discarded if
// there is a match. This field is only used by "url_rules" and
// "transaction_name_rules", not "metric_name_rules".
Ignore bool `json:"ignore"`
EachSegment bool `json:"each_segment"`
ReplaceAll bool `json:"replace_all"`
Terminate bool `json:"terminate_chain"`
Order int `json:"eval_order"`
OriginalReplacement string `json:"replacement"`
RawExpr string `json:"match_expression"`
// Go's regexp backreferences use '${1}' instead of the Perlish '\1', so
// we transform the replacement string into the Go syntax and store it
// here.
TransformedReplacement string
re *regexp.Regexp
}
type metricRules []*metricRule
// Go's regexp backreferences use `${1}` instead of the Perlish `\1`, so we must
// transform the replacement string. This is non-trivial: `\1` is a
// backreference but `\\1` is not. Rather than count the number of back slashes
// preceding the digit, we simply skip rules with tricky replacements.
var (
transformReplacementAmbiguous = regexp.MustCompile(`\\\\([0-9]+)`)
transformReplacementRegex = regexp.MustCompile(`\\([0-9]+)`)
transformReplacementReplacement = "$${${1}}"
)
func (rules *metricRules) UnmarshalJSON(data []byte) (err error) {
var raw []*metricRule
if err := json.Unmarshal(data, &raw); nil != err {
return err
}
valid := make(metricRules, 0, len(raw))
for _, r := range raw {
re, err := regexp.Compile("(?i)" + r.RawExpr)
if err != nil {
// TODO
// Warn("unable to compile rule", {
// "match_expression": r.RawExpr,
// "error": err.Error(),
// })
continue
}
if transformReplacementAmbiguous.MatchString(r.OriginalReplacement) {
// TODO
// Warn("unable to transform replacement", {
// "match_expression": r.RawExpr,
// "replacement": r.OriginalReplacement,
// })
continue
}
r.re = re
r.TransformedReplacement = transformReplacementRegex.ReplaceAllString(r.OriginalReplacement,
transformReplacementReplacement)
valid = append(valid, r)
}
sort.Sort(valid)
*rules = valid
return nil
}
func (rules metricRules) Len() int {
return len(rules)
}
// Rules should be applied in increasing order
func (rules metricRules) Less(i, j int) bool {
return rules[i].Order < rules[j].Order
}
func (rules metricRules) Swap(i, j int) {
rules[i], rules[j] = rules[j], rules[i]
}
func replaceFirst(re *regexp.Regexp, s string, replacement string) string {
// Note that ReplaceAllStringFunc cannot be used here since it does
// not replace $1 placeholders.
loc := re.FindStringIndex(s)
if nil == loc {
return s
}
firstMatch := s[loc[0]:loc[1]]
firstMatchReplaced := re.ReplaceAllString(firstMatch, replacement)
return s[0:loc[0]] + firstMatchReplaced + s[loc[1]:]
}
func (r *metricRule) apply(s string) (ruleResult, string) {
// Rules are strange, and there is no spec.
// This code attempts to duplicate the logic of the PHP agent.
// Ambiguity abounds.
if r.Ignore {
if r.re.MatchString(s) {
return ruleIgnore, ""
}
return ruleUnchanged, s
}
var out string
if r.ReplaceAll {
out = r.re.ReplaceAllString(s, r.TransformedReplacement)
} else if r.EachSegment {
segments := strings.Split(string(s), "/")
applied := make([]string, len(segments))
for i, segment := range segments {
applied[i] = replaceFirst(r.re, segment, r.TransformedReplacement)
}
out = strings.Join(applied, "/")
} else {
out = replaceFirst(r.re, s, r.TransformedReplacement)
}
if out == s {
return ruleUnchanged, out
}
return ruleChanged, out
}
func (rules metricRules) Apply(input string) string {
var res ruleResult
s := input
for _, rule := range rules {
res, s = rule.apply(s)
if ruleIgnore == res {
return ""
}
if (ruleChanged == res) && rule.Terminate {
break
}
}
return s
}