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

     1  package metrics
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"log"
     7  	"net"
     8  	"net/url"
     9  	"strings"
    10  	"time"
    11  )
    12  
    13  const (
    14  	// We force flush the statsite metrics after this period of
    15  	// inactivity. Prevents stats from getting stuck in a buffer
    16  	// forever.
    17  	flushInterval = 100 * time.Millisecond
    18  )
    19  
    20  // NewStatsiteSinkFromURL creates an StatsiteSink from a URL. It is used
    21  // (and tested) from NewMetricSinkFromURL.
    22  func NewStatsiteSinkFromURL(u *url.URL) (MetricSink, error) {
    23  	return NewStatsiteSink(u.Host)
    24  }
    25  
    26  // StatsiteSink provides a MetricSink that can be used with a
    27  // statsite metrics server
    28  type StatsiteSink struct {
    29  	addr        string
    30  	metricQueue chan string
    31  }
    32  
    33  // NewStatsiteSink is used to create a new StatsiteSink
    34  func NewStatsiteSink(addr string) (*StatsiteSink, error) {
    35  	s := &StatsiteSink{
    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 statsite
    44  func (s *StatsiteSink) Shutdown() {
    45  	close(s.metricQueue)
    46  }
    47  
    48  func (s *StatsiteSink) 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 *StatsiteSink) 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 *StatsiteSink) 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 *StatsiteSink) 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 *StatsiteSink) 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 *StatsiteSink) 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 *StatsiteSink) 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 *StatsiteSink) 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 *StatsiteSink) 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 *StatsiteSink) 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 *StatsiteSink) 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 *StatsiteSink) pushMetric(m string) {
   118  	select {
   119  	case s.metricQueue <- m:
   120  	default:
   121  	}
   122  }
   123  
   124  // Flushes metrics
   125  func (s *StatsiteSink) flushMetrics() {
   126  	var sock net.Conn
   127  	var err error
   128  	var wait <-chan time.Time
   129  	var buffered *bufio.Writer
   130  	ticker := time.NewTicker(flushInterval)
   131  	defer ticker.Stop()
   132  
   133  CONNECT:
   134  	// Attempt to connect
   135  	sock, err = net.Dial("tcp", s.addr)
   136  	if err != nil {
   137  		log.Printf("[ERR] Error connecting to statsite! Err: %s", err)
   138  		goto WAIT
   139  	}
   140  
   141  	// Create a buffered writer
   142  	buffered = bufio.NewWriter(sock)
   143  
   144  	for {
   145  		select {
   146  		case metric, ok := <-s.metricQueue:
   147  			// Get a metric from the queue
   148  			if !ok {
   149  				goto QUIT
   150  			}
   151  
   152  			// Try to send to statsite
   153  			_, err := buffered.Write([]byte(metric))
   154  			if err != nil {
   155  				log.Printf("[ERR] Error writing to statsite! Err: %s", err)
   156  				goto WAIT
   157  			}
   158  		case <-ticker.C:
   159  			if err := buffered.Flush(); err != nil {
   160  				log.Printf("[ERR] Error flushing to statsite! Err: %s", err)
   161  				goto WAIT
   162  			}
   163  		}
   164  	}
   165  
   166  WAIT:
   167  	// Wait for a while
   168  	wait = time.After(time.Duration(5) * time.Second)
   169  	for {
   170  		select {
   171  		// Dequeue the messages to avoid backlog
   172  		case _, ok := <-s.metricQueue:
   173  			if !ok {
   174  				goto QUIT
   175  			}
   176  		case <-wait:
   177  			goto CONNECT
   178  		}
   179  	}
   180  QUIT:
   181  	s.metricQueue = nil
   182  }