github.com/hashicorp/go-metrics@v0.5.3/statsd.go (about)

     1  package metrics
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"log"
     7  	"net"
     8  	"net/url"
     9  	"strings"
    10  	"time"
    11  )
    12  
    13  const (
    14  	// statsdMaxLen is the maximum size of a packet
    15  	// to send to statsd
    16  	statsdMaxLen = 1400
    17  )
    18  
    19  // StatsdSink provides a MetricSink that can be used
    20  // with a statsite or statsd metrics server. It uses
    21  // only UDP packets, while StatsiteSink uses TCP.
    22  type StatsdSink struct {
    23  	addr        string
    24  	metricQueue chan string
    25  }
    26  
    27  // NewStatsdSinkFromURL creates an StatsdSink from a URL. It is used
    28  // (and tested) from NewMetricSinkFromURL.
    29  func NewStatsdSinkFromURL(u *url.URL) (MetricSink, error) {
    30  	return NewStatsdSink(u.Host)
    31  }
    32  
    33  // NewStatsdSink is used to create a new StatsdSink
    34  func NewStatsdSink(addr string) (*StatsdSink, error) {
    35  	s := &StatsdSink{
    36  		addr:        addr,
    37  		metricQueue: make(chan string, 4096),
    38  	}
    39  	go s.flushMetrics()
    40  	return s, nil
    41  }
    42  
    43  // Close is used to stop flushing to statsd
    44  func (s *StatsdSink) Shutdown() {
    45  	close(s.metricQueue)
    46  }
    47  
    48  func (s *StatsdSink) SetGauge(key []string, val float32) {
    49  	flatKey := s.flattenKey(key)
    50  	s.pushMetric(fmt.Sprintf("%s:%f|g\n", flatKey, val))
    51  }
    52  
    53  func (s *StatsdSink) SetGaugeWithLabels(key []string, val float32, labels []Label) {
    54  	flatKey := s.flattenKeyLabels(key, labels)
    55  	s.pushMetric(fmt.Sprintf("%s:%f|g\n", flatKey, val))
    56  }
    57  
    58  func (s *StatsdSink) SetPrecisionGauge(key []string, val float64) {
    59  	flatKey := s.flattenKey(key)
    60  	s.pushMetric(fmt.Sprintf("%s:%f|g\n", flatKey, val))
    61  }
    62  
    63  func (s *StatsdSink) SetPrecisionGaugeWithLabels(key []string, val float64, labels []Label) {
    64  	flatKey := s.flattenKeyLabels(key, labels)
    65  	s.pushMetric(fmt.Sprintf("%s:%f|g\n", flatKey, val))
    66  }
    67  
    68  func (s *StatsdSink) EmitKey(key []string, val float32) {
    69  	flatKey := s.flattenKey(key)
    70  	s.pushMetric(fmt.Sprintf("%s:%f|kv\n", flatKey, val))
    71  }
    72  
    73  func (s *StatsdSink) IncrCounter(key []string, val float32) {
    74  	flatKey := s.flattenKey(key)
    75  	s.pushMetric(fmt.Sprintf("%s:%f|c\n", flatKey, val))
    76  }
    77  
    78  func (s *StatsdSink) IncrCounterWithLabels(key []string, val float32, labels []Label) {
    79  	flatKey := s.flattenKeyLabels(key, labels)
    80  	s.pushMetric(fmt.Sprintf("%s:%f|c\n", flatKey, val))
    81  }
    82  
    83  func (s *StatsdSink) AddSample(key []string, val float32) {
    84  	flatKey := s.flattenKey(key)
    85  	s.pushMetric(fmt.Sprintf("%s:%f|ms\n", flatKey, val))
    86  }
    87  
    88  func (s *StatsdSink) AddSampleWithLabels(key []string, val float32, labels []Label) {
    89  	flatKey := s.flattenKeyLabels(key, labels)
    90  	s.pushMetric(fmt.Sprintf("%s:%f|ms\n", flatKey, val))
    91  }
    92  
    93  // Flattens the key for formatting, removes spaces
    94  func (s *StatsdSink) flattenKey(parts []string) string {
    95  	joined := strings.Join(parts, ".")
    96  	return strings.Map(func(r rune) rune {
    97  		switch r {
    98  		case ':':
    99  			fallthrough
   100  		case ' ':
   101  			return '_'
   102  		default:
   103  			return r
   104  		}
   105  	}, joined)
   106  }
   107  
   108  // Flattens the key along with labels for formatting, removes spaces
   109  func (s *StatsdSink) flattenKeyLabels(parts []string, labels []Label) string {
   110  	for _, label := range labels {
   111  		parts = append(parts, label.Value)
   112  	}
   113  	return s.flattenKey(parts)
   114  }
   115  
   116  // Does a non-blocking push to the metrics queue
   117  func (s *StatsdSink) pushMetric(m string) {
   118  	select {
   119  	case s.metricQueue <- m:
   120  	default:
   121  	}
   122  }
   123  
   124  // Flushes metrics
   125  func (s *StatsdSink) flushMetrics() {
   126  	var sock net.Conn
   127  	var err error
   128  	var wait <-chan time.Time
   129  	ticker := time.NewTicker(flushInterval)
   130  	defer ticker.Stop()
   131  
   132  CONNECT:
   133  	// Create a buffer
   134  	buf := bytes.NewBuffer(nil)
   135  
   136  	// Attempt to connect
   137  	sock, err = net.Dial("udp", s.addr)
   138  	if err != nil {
   139  		log.Printf("[ERR] Error connecting to statsd! Err: %s", err)
   140  		goto WAIT
   141  	}
   142  
   143  	for {
   144  		select {
   145  		case metric, ok := <-s.metricQueue:
   146  			// Get a metric from the queue
   147  			if !ok {
   148  				goto QUIT
   149  			}
   150  
   151  			// Check if this would overflow the packet size
   152  			if len(metric)+buf.Len() > statsdMaxLen {
   153  				_, err := sock.Write(buf.Bytes())
   154  				buf.Reset()
   155  				if err != nil {
   156  					log.Printf("[ERR] Error writing to statsd! Err: %s", err)
   157  					goto WAIT
   158  				}
   159  			}
   160  
   161  			// Append to the buffer
   162  			buf.WriteString(metric)
   163  
   164  		case <-ticker.C:
   165  			if buf.Len() == 0 {
   166  				continue
   167  			}
   168  
   169  			_, err := sock.Write(buf.Bytes())
   170  			buf.Reset()
   171  			if err != nil {
   172  				log.Printf("[ERR] Error flushing to statsd! Err: %s", err)
   173  				goto WAIT
   174  			}
   175  		}
   176  	}
   177  
   178  WAIT:
   179  	// Wait for a while
   180  	wait = time.After(time.Duration(5) * time.Second)
   181  	for {
   182  		select {
   183  		// Dequeue the messages to avoid backlog
   184  		case _, ok := <-s.metricQueue:
   185  			if !ok {
   186  				goto QUIT
   187  			}
   188  		case <-wait:
   189  			goto CONNECT
   190  		}
   191  	}
   192  QUIT:
   193  	s.metricQueue = nil
   194  }