github.com/hellobchain/third_party@v0.0.0-20230331131523-deb0478a2e52/go-kit/kit/metrics/statsd/statsd.go (about)

     1  // Package statsd provides a StatsD backend for package metrics. StatsD has no
     2  // concept of arbitrary key-value tagging, so label values are not supported,
     3  // and With is a no-op on all metrics.
     4  //
     5  // This package batches observations and emits them on some schedule to the
     6  // remote server. This is useful even if you connect to your StatsD server over
     7  // UDP. Emitting one network packet per observation can quickly overwhelm even
     8  // the fastest internal network.
     9  package statsd
    10  
    11  import (
    12  	"fmt"
    13  	"io"
    14  	"time"
    15  
    16  	"github.com/hellobchain/third_party/go-kit/kit/log"
    17  	"github.com/hellobchain/third_party/go-kit/kit/metrics"
    18  	"github.com/hellobchain/third_party/go-kit/kit/metrics/internal/lv"
    19  	"github.com/hellobchain/third_party/go-kit/kit/metrics/internal/ratemap"
    20  	"github.com/hellobchain/third_party/go-kit/kit/util/conn"
    21  )
    22  
    23  // Statsd receives metrics observations and forwards them to a StatsD server.
    24  // Create a Statsd object, use it to create metrics, and pass those metrics as
    25  // dependencies to the components that will use them.
    26  //
    27  // All metrics are buffered until WriteTo is called. Counters and gauges are
    28  // aggregated into a single observation per timeseries per write. Timings are
    29  // buffered but not aggregated.
    30  //
    31  // To regularly report metrics to an io.Writer, use the WriteLoop helper method.
    32  // To send to a StatsD server, use the SendLoop helper method.
    33  type Statsd struct {
    34  	prefix string
    35  	rates  *ratemap.RateMap
    36  
    37  	// The observations are collected in an N-dimensional vector space, even
    38  	// though they only take advantage of a single dimension (name). This is an
    39  	// implementation detail born purely from convenience. It would be more
    40  	// accurate to collect them in a map[string][]float64, but we already have
    41  	// this nice data structure and helper methods.
    42  	counters *lv.Space
    43  	gauges   *lv.Space
    44  	timings  *lv.Space
    45  
    46  	logger log.Logger
    47  }
    48  
    49  // New returns a Statsd object that may be used to create metrics. Prefix is
    50  // applied to all created metrics. Callers must ensure that regular calls to
    51  // WriteTo are performed, either manually or with one of the helper methods.
    52  func New(prefix string, logger log.Logger) *Statsd {
    53  	return &Statsd{
    54  		prefix:   prefix,
    55  		rates:    ratemap.New(),
    56  		counters: lv.NewSpace(),
    57  		gauges:   lv.NewSpace(),
    58  		timings:  lv.NewSpace(),
    59  		logger:   logger,
    60  	}
    61  }
    62  
    63  // NewCounter returns a counter, sending observations to this Statsd object.
    64  func (s *Statsd) NewCounter(name string, sampleRate float64) *Counter {
    65  	s.rates.Set(s.prefix+name, sampleRate)
    66  	return &Counter{
    67  		name: s.prefix + name,
    68  		obs:  s.counters.Observe,
    69  	}
    70  }
    71  
    72  // NewGauge returns a gauge, sending observations to this Statsd object.
    73  func (s *Statsd) NewGauge(name string) *Gauge {
    74  	return &Gauge{
    75  		name: s.prefix + name,
    76  		obs:  s.gauges.Observe,
    77  		add:  s.gauges.Add,
    78  	}
    79  }
    80  
    81  // NewTiming returns a histogram whose observations are interpreted as
    82  // millisecond durations, and are forwarded to this Statsd object.
    83  func (s *Statsd) NewTiming(name string, sampleRate float64) *Timing {
    84  	s.rates.Set(s.prefix+name, sampleRate)
    85  	return &Timing{
    86  		name: s.prefix + name,
    87  		obs:  s.timings.Observe,
    88  	}
    89  }
    90  
    91  // WriteLoop is a helper method that invokes WriteTo to the passed writer every
    92  // time the passed channel fires. This method blocks until the channel is
    93  // closed, so clients probably want to run it in its own goroutine. For typical
    94  // usage, create a time.Ticker and pass its C channel to this method.
    95  func (s *Statsd) WriteLoop(c <-chan time.Time, w io.Writer) {
    96  	for range c {
    97  		if _, err := s.WriteTo(w); err != nil {
    98  			s.logger.Log("during", "WriteTo", "err", err)
    99  		}
   100  	}
   101  }
   102  
   103  // SendLoop is a helper method that wraps WriteLoop, passing a managed
   104  // connection to the network and address. Like WriteLoop, this method blocks
   105  // until the channel is closed, so clients probably want to start it in its own
   106  // goroutine. For typical usage, create a time.Ticker and pass its C channel to
   107  // this method.
   108  func (s *Statsd) SendLoop(c <-chan time.Time, network, address string) {
   109  	s.WriteLoop(c, conn.NewDefaultManager(network, address, s.logger))
   110  }
   111  
   112  // WriteTo flushes the buffered content of the metrics to the writer, in
   113  // StatsD format. WriteTo abides best-effort semantics, so observations are
   114  // lost if there is a problem with the write. Clients should be sure to call
   115  // WriteTo regularly, ideally through the WriteLoop or SendLoop helper methods.
   116  func (s *Statsd) WriteTo(w io.Writer) (count int64, err error) {
   117  	var n int
   118  
   119  	s.counters.Reset().Walk(func(name string, _ lv.LabelValues, values []float64) bool {
   120  		n, err = fmt.Fprintf(w, "%s:%f|c%s\n", name, sum(values), sampling(s.rates.Get(name)))
   121  		if err != nil {
   122  			return false
   123  		}
   124  		count += int64(n)
   125  		return true
   126  	})
   127  	if err != nil {
   128  		return count, err
   129  	}
   130  
   131  	s.gauges.Reset().Walk(func(name string, _ lv.LabelValues, values []float64) bool {
   132  		n, err = fmt.Fprintf(w, "%s:%f|g\n", name, last(values))
   133  		if err != nil {
   134  			return false
   135  		}
   136  		count += int64(n)
   137  		return true
   138  	})
   139  	if err != nil {
   140  		return count, err
   141  	}
   142  
   143  	s.timings.Reset().Walk(func(name string, _ lv.LabelValues, values []float64) bool {
   144  		sampleRate := s.rates.Get(name)
   145  		for _, value := range values {
   146  			n, err = fmt.Fprintf(w, "%s:%f|ms%s\n", name, value, sampling(sampleRate))
   147  			if err != nil {
   148  				return false
   149  			}
   150  			count += int64(n)
   151  		}
   152  		return true
   153  	})
   154  	if err != nil {
   155  		return count, err
   156  	}
   157  
   158  	return count, err
   159  }
   160  
   161  func sum(a []float64) float64 {
   162  	var v float64
   163  	for _, f := range a {
   164  		v += f
   165  	}
   166  	return v
   167  }
   168  
   169  func last(a []float64) float64 {
   170  	return a[len(a)-1]
   171  }
   172  
   173  func sampling(r float64) string {
   174  	var sv string
   175  	if r < 1.0 {
   176  		sv = fmt.Sprintf("|@%f", r)
   177  	}
   178  	return sv
   179  }
   180  
   181  type observeFunc func(name string, lvs lv.LabelValues, value float64)
   182  
   183  // Counter is a StatsD counter. Observations are forwarded to a Statsd object,
   184  // and aggregated (summed) per timeseries.
   185  type Counter struct {
   186  	name string
   187  	obs  observeFunc
   188  }
   189  
   190  // With is a no-op.
   191  func (c *Counter) With(...string) metrics.Counter {
   192  	return c
   193  }
   194  
   195  // Add implements metrics.Counter.
   196  func (c *Counter) Add(delta float64) {
   197  	c.obs(c.name, lv.LabelValues{}, delta)
   198  }
   199  
   200  // Gauge is a StatsD gauge. Observations are forwarded to a Statsd object, and
   201  // aggregated (the last observation selected) per timeseries.
   202  type Gauge struct {
   203  	name string
   204  	obs  observeFunc
   205  	add  observeFunc
   206  }
   207  
   208  // With is a no-op.
   209  func (g *Gauge) With(...string) metrics.Gauge {
   210  	return g
   211  }
   212  
   213  // Set implements metrics.Gauge.
   214  func (g *Gauge) Set(value float64) {
   215  	g.obs(g.name, lv.LabelValues{}, value)
   216  }
   217  
   218  // Add implements metrics.Gauge.
   219  func (g *Gauge) Add(delta float64) {
   220  	g.add(g.name, lv.LabelValues{}, delta)
   221  }
   222  
   223  // Timing is a StatsD timing, or metrics.Histogram. Observations are
   224  // forwarded to a Statsd object, and collected (but not aggregated) per
   225  // timeseries.
   226  type Timing struct {
   227  	name string
   228  	obs  observeFunc
   229  }
   230  
   231  // With is a no-op.
   232  func (t *Timing) With(...string) metrics.Histogram {
   233  	return t
   234  }
   235  
   236  // Observe implements metrics.Histogram. Value is interpreted as milliseconds.
   237  func (t *Timing) Observe(value float64) {
   238  	t.obs(t.name, lv.LabelValues{}, value)
   239  }