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 }