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  }