github.com/Jeffail/benthos/v3@v3.65.0/lib/metrics/rename.go (about)

     1  package metrics
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  	"regexp"
     9  	"sort"
    10  
    11  	"github.com/Jeffail/benthos/v3/internal/docs"
    12  	"github.com/Jeffail/benthos/v3/lib/log"
    13  )
    14  
    15  //------------------------------------------------------------------------------
    16  
    17  func init() {
    18  	Constructors[TypeRename] = TypeSpec{
    19  		constructor: NewRename,
    20  		Status:      docs.StatusDeprecated,
    21  		Summary: `
    22  Rename metric paths as they are registered.`,
    23  		FieldSpecs: docs.FieldSpecs{
    24  			docs.FieldCommon("by_regexp", `
    25  A list of objects, each specifying an RE2 regular expression which will be
    26  tested against each metric path registered. Each occurrence of the expression
    27  will be replaced with the specified value. Inside the value $ signs are
    28  interpreted as submatch expansions, e.g. $1 represents the first submatch.
    29  
    30  The field `+"`to_label`"+` may contain any number of key/value pairs to be
    31  added to a metric as labels, where the value may contain submatches from the
    32  provided pattern. This allows you to extract (left-most) matched segments of the
    33  renamed path into the label values.`,
    34  				[]interface{}{
    35  					map[string]interface{}{
    36  						"pattern": "foo\\.([a-z]*)\\.([a-z]*)",
    37  						"value":   "foo.$1",
    38  						"to_label": map[string]interface{}{
    39  							"bar": "$2",
    40  						},
    41  					},
    42  				},
    43  			).WithChildren(
    44  				docs.FieldDeprecated("pattern"),
    45  				docs.FieldDeprecated("value"),
    46  				docs.FieldDeprecated("to_label").Map().HasType(docs.FieldTypeString),
    47  			).Array(),
    48  			docs.FieldCommon("child", "A child metric type, this is where renamed metrics will be routed.").HasType(docs.FieldTypeMetrics),
    49  		},
    50  		Description: `
    51  Metrics must be matched using dot notation even if the chosen output uses a
    52  different form. For example, the path would be 'foo.bar' rather than 'foo_bar'
    53  even when sending metrics to Prometheus. A full list of metrics paths that
    54  Benthos registers can be found in [this list](/docs/components/metrics/about#paths).`,
    55  		Footnotes: `
    56  ## Examples
    57  
    58  In order to replace the paths 'foo.bar.0.zap' and 'foo.baz.1.zap' with 'zip.bar'
    59  and 'zip.baz' respectively, and store the respective values '0' and '1' under
    60  the label key 'index' we could use this config:
    61  
    62  ` + "```yaml" + `
    63  metrics:
    64    rename:
    65      by_regexp:
    66        - pattern: "foo\\.([a-z]*)\\.([a-z]*)\\.zap"
    67          value: "zip.$1"
    68          to_label:
    69            index: $2
    70      child:
    71        statsd:
    72          prefix: foo
    73          address: localhost:8125
    74  ` + "```" + `
    75  
    76  These labels will only be injected into metrics registered without pre-existing
    77  labels. Therefore it's currently not possible to combine labels registered from
    78  the ` + "[`metric` processor](/docs/components/processors/metric)" + ` with labels
    79  set via renaming.
    80  
    81  ## Debugging
    82  
    83  In order to see logs breaking down which metrics are registered and whether they
    84  are renamed enable logging at the TRACE level.`,
    85  	}
    86  }
    87  
    88  //------------------------------------------------------------------------------
    89  
    90  // RenameByRegexpConfig contains config fields for a rename by regular
    91  // expression pattern.
    92  type RenameByRegexpConfig struct {
    93  	Pattern string            `json:"pattern" yaml:"pattern"`
    94  	Value   string            `json:"value" yaml:"value"`
    95  	Labels  map[string]string `json:"to_label" yaml:"to_label"`
    96  }
    97  
    98  // RenameConfig contains config fields for the Rename metric type.
    99  type RenameConfig struct {
   100  	ByRegexp []RenameByRegexpConfig `json:"by_regexp" yaml:"by_regexp"`
   101  	Child    *Config                `json:"child" yaml:"child"`
   102  }
   103  
   104  // NewRenameConfig returns a RenameConfig with default values.
   105  func NewRenameConfig() RenameConfig {
   106  	return RenameConfig{
   107  		ByRegexp: []RenameByRegexpConfig{},
   108  		Child:    nil,
   109  	}
   110  }
   111  
   112  //------------------------------------------------------------------------------
   113  
   114  type dummyRenameConfig struct {
   115  	ByRegexp []RenameByRegexpConfig `json:"by_regexp" yaml:"by_regexp"`
   116  	Child    interface{}            `json:"child" yaml:"child"`
   117  }
   118  
   119  // MarshalJSON prints an empty object instead of nil.
   120  func (w RenameConfig) MarshalJSON() ([]byte, error) {
   121  	dummy := dummyRenameConfig{
   122  		ByRegexp: w.ByRegexp,
   123  		Child:    w.Child,
   124  	}
   125  	if w.Child == nil {
   126  		dummy.Child = struct{}{}
   127  	}
   128  	return json.Marshal(dummy)
   129  }
   130  
   131  // MarshalYAML prints an empty object instead of nil.
   132  func (w RenameConfig) MarshalYAML() (interface{}, error) {
   133  	dummy := dummyRenameConfig{
   134  		ByRegexp: w.ByRegexp,
   135  		Child:    w.Child,
   136  	}
   137  	if w.Child == nil {
   138  		dummy.Child = struct{}{}
   139  	}
   140  	return dummy, nil
   141  }
   142  
   143  //------------------------------------------------------------------------------
   144  
   145  type renameByRegexp struct {
   146  	expression *regexp.Regexp
   147  	value      string
   148  	labels     map[string]string
   149  }
   150  
   151  // Rename is a statistics object that wraps a separate statistics object
   152  // and only permits statistics that pass through the whitelist to be recorded.
   153  type Rename struct {
   154  	byRegexp []renameByRegexp
   155  	s        Type
   156  	log      log.Modular
   157  }
   158  
   159  // NewRename creates and returns a new Rename object
   160  func NewRename(config Config, opts ...func(Type)) (Type, error) {
   161  	if config.Rename.Child == nil {
   162  		return nil, errors.New("cannot create a rename metric without a child")
   163  	}
   164  
   165  	child, err := New(*config.Rename.Child)
   166  	if err != nil {
   167  		return nil, err
   168  	}
   169  
   170  	r := &Rename{
   171  		s:   child,
   172  		log: log.Noop(),
   173  	}
   174  
   175  	for _, opt := range opts {
   176  		opt(r)
   177  	}
   178  
   179  	for _, p := range config.Rename.ByRegexp {
   180  		re, err := regexp.Compile(p.Pattern)
   181  		if err != nil {
   182  			return nil, fmt.Errorf("invalid regular expression: '%s': %v", p, err)
   183  		}
   184  		r.byRegexp = append(r.byRegexp, renameByRegexp{
   185  			expression: re,
   186  			value:      p.Value,
   187  			labels:     p.Labels,
   188  		})
   189  	}
   190  
   191  	return r, nil
   192  }
   193  
   194  //------------------------------------------------------------------------------
   195  
   196  // renamePath checks whether or not a given path is in the allowed set of
   197  // paths for the Rename metrics stat.
   198  func (r *Rename) renamePath(path string) (outPath string, labels map[string]string) {
   199  	renamed := false
   200  	labels = make(map[string]string)
   201  	for _, rr := range r.byRegexp {
   202  		newPath := rr.expression.ReplaceAllString(path, rr.value)
   203  		if newPath != path {
   204  			renamed = true
   205  			r.log.Tracef("Renamed metric path '%v' to '%v' as per regexp '%v'\n", path, newPath, rr.expression.String())
   206  		}
   207  		if rr.labels != nil && len(rr.labels) > 0 {
   208  			// Extract only the matching segment of the path (left-most)
   209  			leftPath := rr.expression.FindString(path)
   210  			if len(leftPath) > 0 {
   211  				for k, v := range rr.labels {
   212  					v = rr.expression.ReplaceAllString(leftPath, v)
   213  					labels[k] = v
   214  					r.log.Tracef("Renamed label '%v' to '%v' as per regexp '%v'\n", k, v, rr.expression.String())
   215  				}
   216  			}
   217  		}
   218  		path = newPath
   219  	}
   220  	if !renamed {
   221  		r.log.Tracef("Registered metric path '%v' unchanged\n", path)
   222  	}
   223  	return path, labels
   224  }
   225  
   226  //------------------------------------------------------------------------------
   227  
   228  func labelsFromMap(labels map[string]string) (names, values []string) {
   229  	names = make([]string, 0, len(labels))
   230  	for k := range labels {
   231  		names = append(names, k)
   232  	}
   233  	sort.Strings(names)
   234  	values = make([]string, 0, len(names))
   235  	for _, k := range names {
   236  		values = append(values, labels[k])
   237  	}
   238  	return names, values
   239  }
   240  
   241  // GetCounter returns a stat counter object for a path.
   242  func (r *Rename) GetCounter(path string) StatCounter {
   243  	rpath, labels := r.renamePath(path)
   244  	if len(labels) == 0 {
   245  		return r.s.GetCounter(rpath)
   246  	}
   247  	names, values := labelsFromMap(labels)
   248  	return r.s.GetCounterVec(rpath, names).With(values...)
   249  }
   250  
   251  // GetCounterVec returns a stat counter object for a path with the labels
   252  // and values.
   253  func (r *Rename) GetCounterVec(path string, n []string) StatCounterVec {
   254  	rpath, _ := r.renamePath(path)
   255  	return r.s.GetCounterVec(rpath, n)
   256  }
   257  
   258  // GetTimer returns a stat timer object for a path.
   259  func (r *Rename) GetTimer(path string) StatTimer {
   260  	rpath, labels := r.renamePath(path)
   261  	if len(labels) == 0 {
   262  		return r.s.GetTimer(rpath)
   263  	}
   264  
   265  	names, values := labelsFromMap(labels)
   266  	return r.s.GetTimerVec(rpath, names).With(values...)
   267  }
   268  
   269  // GetTimerVec returns a stat timer object for a path with the labels
   270  // and values.
   271  func (r *Rename) GetTimerVec(path string, n []string) StatTimerVec {
   272  	rpath, _ := r.renamePath(path)
   273  	return r.s.GetTimerVec(rpath, n)
   274  }
   275  
   276  // GetGauge returns a stat gauge object for a path.
   277  func (r *Rename) GetGauge(path string) StatGauge {
   278  	rpath, labels := r.renamePath(path)
   279  	if len(labels) == 0 {
   280  		return r.s.GetGauge(rpath)
   281  	}
   282  
   283  	names, values := labelsFromMap(labels)
   284  	return r.s.GetGaugeVec(rpath, names).With(values...)
   285  }
   286  
   287  // GetGaugeVec returns a stat timer object for a path with the labels
   288  // discarded.
   289  func (r *Rename) GetGaugeVec(path string, n []string) StatGaugeVec {
   290  	rpath, _ := r.renamePath(path)
   291  	return r.s.GetGaugeVec(rpath, n)
   292  }
   293  
   294  // SetLogger sets the logger used to print connection errors.
   295  func (r *Rename) SetLogger(log log.Modular) {
   296  	r.log = log.NewModule(".rename")
   297  	r.s.SetLogger(log)
   298  }
   299  
   300  // Close stops the Statsd object from aggregating metrics and cleans up
   301  // resources.
   302  func (r *Rename) Close() error {
   303  	return r.s.Close()
   304  }
   305  
   306  //------------------------------------------------------------------------------
   307  
   308  // HandlerFunc returns an http.HandlerFunc for accessing metrics for appropriate
   309  // child types
   310  func (r *Rename) HandlerFunc() http.HandlerFunc {
   311  	if wHandlerFunc, ok := r.s.(WithHandlerFunc); ok {
   312  		return wHandlerFunc.HandlerFunc()
   313  	}
   314  
   315  	return func(w http.ResponseWriter, r *http.Request) {
   316  		w.WriteHeader(501)
   317  		w.Write([]byte("The child of this rename does not support HTTP metrics."))
   318  	}
   319  }
   320  
   321  //------------------------------------------------------------------------------