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