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  }