github.com/blend/go-sdk@v1.20220411.3/statsd/client.go (about) 1 /* 2 3 Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file. 5 6 */ 7 8 package statsd 9 10 import ( 11 "io" 12 "math/rand" 13 "net" 14 "strconv" 15 "strings" 16 "sync" 17 "time" 18 19 "github.com/blend/go-sdk/ex" 20 "github.com/blend/go-sdk/stats" 21 ) 22 23 // Error classes. 24 const ( 25 ErrAddrUnset ex.Class = "statsd client address unset" 26 ErrMaxPacketSize ex.Class = "statsd max packet size exceeded" 27 ErrSampleRateInvalid ex.Class = "statsd invalid sample rate" 28 ) 29 30 var ( 31 _ stats.Collector = (*Client)(nil) 32 ) 33 34 // New creates a new statsd client and opens 35 // the underlying UDP connection. 36 func New(opts ...ClientOpt) (*Client, error) { 37 client := Client{ 38 DialTimeout: DefaultDialTimeout, 39 MaxPacketSize: DefaultMaxPacketSize, 40 MaxBufferSize: DefaultMaxBufferSize, 41 } 42 43 var err error 44 for _, opt := range opts { 45 if err = opt(&client); err != nil { 46 return nil, err 47 } 48 } 49 if client.Addr == "" { 50 return nil, ex.New(ErrAddrUnset) 51 } 52 client.conn, err = net.DialTimeout("udp", client.Addr, client.DialTimeout) 53 if err != nil { 54 return nil, err 55 } 56 return &client, nil 57 } 58 59 // OptAddr sets the client address. 60 func OptAddr(addr string) ClientOpt { 61 return func(c *Client) error { 62 c.Addr = addr 63 return nil 64 } 65 } 66 67 // OptDialTimeout sets the client dial timeout. 68 func OptDialTimeout(timeout time.Duration) ClientOpt { 69 return func(c *Client) error { 70 c.DialTimeout = timeout 71 return nil 72 } 73 } 74 75 // OptMaxPacketSize sets the client max dial size. 76 func OptMaxPacketSize(sizeBytes int) ClientOpt { 77 return func(c *Client) error { 78 c.MaxPacketSize = sizeBytes 79 return nil 80 } 81 } 82 83 // OptMaxBufferSize sets the client max buffer size in messages. 84 func OptMaxBufferSize(count int) ClientOpt { 85 return func(c *Client) error { 86 c.MaxBufferSize = count 87 return nil 88 } 89 } 90 91 // OptConfig sets fields on a client from a given config. 92 func OptConfig(cfg Config) ClientOpt { 93 return func(c *Client) error { 94 c.Addr = cfg.Addr 95 c.DialTimeout = cfg.DialTimeout 96 c.MaxPacketSize = cfg.MaxPacketSize 97 c.MaxBufferSize = cfg.MaxBufferSize 98 for key, value := range cfg.DefaultTags { 99 c.AddDefaultTags(stats.Tag(key, value)) 100 } 101 return OptSampleRate(cfg.SampleRate)(c) 102 } 103 } 104 105 // OptSampleRate sets the sample rate on the client or the percent of packets to send 106 // on the interval [0,1.0). 107 // A value of `0.0` will drop all packets, a value of `1.0` will send all packets. 108 func OptSampleRate(rate float64) ClientOpt { 109 return func(c *Client) error { 110 if rate < 0 || rate > 1.0 { 111 return ex.New(ErrSampleRateInvalid, ex.OptMessagef("rate: %0.2f", rate)) 112 } 113 if rate == 1.0 { // unset on 100% 114 c.SampleProvider = nil 115 } else { 116 c.SampleProvider = func() bool { 117 return rand.Float64() <= rate 118 } 119 } 120 return nil 121 } 122 } 123 124 // ClientOpt is an option for a client. 125 type ClientOpt func(*Client) error 126 127 // Client is a statsd client. 128 type Client struct { 129 Addr string 130 DialTimeout time.Duration 131 MaxPacketSize int 132 SampleProvider func() bool 133 MaxBufferSize int 134 135 defaultTags []string 136 137 conn io.WriteCloser 138 connMu sync.Mutex 139 140 bufferMu sync.Mutex 141 buffer []byte 142 bufferCount int 143 } 144 145 // AddDefaultTag adds a new default tag. 146 func (c *Client) AddDefaultTag(name, value string) { 147 c.defaultTags = append(c.defaultTags, stats.Tag(name, value)) 148 } 149 150 // AddDefaultTags adds default tags. 151 func (c *Client) AddDefaultTags(tags ...string) { 152 c.defaultTags = append(c.defaultTags, tags...) 153 } 154 155 // DefaultTags returns the default tags. 156 func (c *Client) DefaultTags() []string { 157 return c.defaultTags 158 } 159 160 // Count sends a count message. 161 func (c *Client) Count(name string, value int64, tags ...string) error { 162 return c.sendInt(MetricTypeCount, name, value, tags...) 163 } 164 165 // Increment sends a count message with a value of (1). 166 func (c *Client) Increment(name string, tags ...string) error { 167 return c.sendInt(MetricTypeCount, name, 1, tags...) 168 } 169 170 // Gauge sends a point in time value. 171 func (c *Client) Gauge(name string, value float64, tags ...string) error { 172 return c.sendFloat(MetricTypeGauge, name, value, tags...) 173 } 174 175 // TimeInMilliseconds sends a gauge method with a given value represented in milliseconds. 176 func (c *Client) TimeInMilliseconds(name string, value time.Duration, tags ...string) error { 177 return c.sendFloat(MetricTypeTimer, name, float64(value)/float64(time.Millisecond), tags...) 178 } 179 180 // Histogram is an no-op for raw statsd. 181 func (c *Client) Histogram(name string, value float64, tags ...string) error { 182 return c.sendFloat(MetricTypeHistogram, name, value, tags...) 183 } 184 185 // Distribution is an no-op for raw statsd. 186 func (c *Client) Distribution(name string, value float64, tags ...string) error { 187 return c.sendFloat(MetricTypeDistribution, name, value, tags...) 188 } 189 190 // Flush is a no-op. 191 func (c *Client) Flush() error { 192 c.bufferMu.Lock() 193 defer c.bufferMu.Unlock() 194 return c.flushBuffer() 195 } 196 197 // Close closes the underlying connection. 198 func (c *Client) Close() error { 199 return c.conn.Close() 200 } 201 202 func (c *Client) sendInt(metricType, name string, value int64, tags ...string) error { 203 if !c.shouldSend() { 204 return nil 205 } 206 if c.MaxBufferSize == 0 { 207 return c.send(c.appendInt(nil, metricType, name, value, tags...)) 208 } 209 c.bufferMu.Lock() 210 defer c.bufferMu.Unlock() 211 212 c.bufferCount++ 213 c.buffer = c.appendInt(c.buffer, metricType, name, value, tags...) 214 if c.bufferCount < c.MaxBufferSize { 215 c.buffer = c.appendMetricSeparator(c.buffer) 216 return nil 217 } 218 return c.flushBuffer() 219 } 220 221 func (c *Client) sendFloat(metricType, name string, value float64, tags ...string) error { 222 if !c.shouldSend() { 223 return nil 224 } 225 if c.MaxBufferSize == 0 { 226 return c.send(c.appendFloat(nil, metricType, name, value, tags...)) 227 } 228 c.bufferMu.Lock() 229 defer c.bufferMu.Unlock() 230 231 c.bufferCount++ 232 c.buffer = c.appendFloat(c.buffer, metricType, name, value, tags...) 233 if c.bufferCount < c.MaxBufferSize { 234 c.buffer = c.appendMetricSeparator(c.buffer) 235 return nil 236 } 237 return c.flushBuffer() 238 } 239 240 func (c *Client) appendInt(data []byte, metricType, name string, value int64, tags ...string) []byte { 241 data = append(data, []byte(name)...) 242 data = append(data, ':') 243 data = strconv.AppendInt(data, value, 10) 244 data = append(data, '|') 245 data = append(data, []byte(metricType)...) 246 data = c.appendTags(data, append(c.defaultTags, tags...)...) 247 return data 248 } 249 250 func (c *Client) appendFloat(data []byte, metricType, name string, value float64, tags ...string) []byte { 251 data = append(data, []byte(name)...) 252 data = append(data, ':') 253 data = strconv.AppendFloat(data, value, 'f', -1, 64) 254 data = append(data, '|') 255 data = append(data, []byte(metricType)...) 256 data = c.appendTags(data, append(c.defaultTags, tags...)...) 257 return data 258 } 259 260 func (c *Client) appendTags(data []byte, tags ...string) []byte { 261 if len(tags) == 0 { 262 return data 263 } 264 data = append(data, "|#"...) 265 firstTag := true 266 for _, tag := range tags { 267 if !firstTag { 268 data = append(data, ',') 269 } 270 data = append(data, strings.TrimSpace(tag)...) 271 firstTag = false 272 } 273 return data 274 } 275 276 func (c *Client) appendMetricSeparator(data []byte) []byte { 277 return append(data, '\n') 278 } 279 280 func (c *Client) shouldSend() bool { 281 if c.SampleProvider == nil { 282 return true 283 } 284 return c.SampleProvider() 285 } 286 287 func (c *Client) flushBuffer() error { 288 if err := c.send(c.buffer); err != nil { 289 return err 290 } 291 c.bufferCount = 0 292 c.buffer = nil 293 return nil 294 } 295 296 func (c *Client) send(data []byte) error { 297 if c.MaxPacketSize > 0 && len(data) > c.MaxPacketSize { 298 return ex.New(ErrMaxPacketSize) 299 } 300 301 c.connMu.Lock() 302 defer c.connMu.Unlock() 303 304 _, err := c.conn.Write(append(data, '\n')) 305 if err != nil { 306 return ex.New(err) 307 } 308 return nil 309 }