github.com/Tyktechnologies/tyk@v2.9.5+incompatible/gateway/instrumentation_statsd_sink.go (about) 1 package gateway 2 3 import ( 4 "bytes" 5 "net" 6 "strconv" 7 "time" 8 9 "github.com/gocraft/health" 10 ) 11 12 type StatsDSinkSanitizationFunc func(*bytes.Buffer, string) 13 14 type eventKey struct { 15 job string 16 event string 17 suffix string 18 } 19 20 type prefixBuffer struct { 21 *bytes.Buffer 22 prefixLen int 23 } 24 25 type StatsDSinkOptions struct { 26 // Prefix is something like "metroid" 27 // Events emitted to StatsD would be metroid.myevent.wat 28 // Eg, don't include a trailing dot in the prefix. 29 // It can be "", that's fine. 30 Prefix string 31 32 // SanitizationFunc sanitizes jobs and events before sending them to statsd 33 SanitizationFunc StatsDSinkSanitizationFunc 34 35 // SkipNestedEvents will skip {events,timers,gauges} from sending the job.event version 36 // and will only send the event version. 37 SkipNestedEvents bool 38 39 // SkipTopLevelEvents will skip {events,timers,gauges} from sending the event version 40 // and will only send the job.event version. 41 SkipTopLevelEvents bool 42 } 43 44 var defaultStatsDOptions = StatsDSinkOptions{SanitizationFunc: sanitizeKey} 45 46 type StatsDSink struct { 47 options StatsDSinkOptions 48 49 cmdChan chan statsdEmitCmd 50 drainDoneChan chan struct{} 51 stopDoneChan chan struct{} 52 53 flushPeriod time.Duration 54 55 udpBuf bytes.Buffer 56 timingBuf []byte 57 58 udpConn *net.UDPConn 59 udpAddr *net.UDPAddr 60 61 // map of {job,event,suffix} to a re-usable buffer prefixed with the key. 62 // Since each timing/gauge has a unique component (the time), we'll truncate to the prefix, write the timing, 63 // and write the statsD suffix (eg, "|ms\n"). Then copy that to the UDP buffer. 64 prefixBuffers map[eventKey]prefixBuffer 65 } 66 67 type statsdCmdKind int 68 69 const ( 70 statsdCmdKindEvent statsdCmdKind = iota 71 statsdCmdKindEventErr 72 statsdCmdKindTiming 73 statsdCmdKindGauge 74 statsdCmdKindComplete 75 statsdCmdKindFlush 76 statsdCmdKindDrain 77 statsdCmdKindStop 78 ) 79 80 type statsdEmitCmd struct { 81 Kind statsdCmdKind 82 Job string 83 Event string 84 Nanos int64 85 Value float64 86 Status health.CompletionStatus 87 } 88 89 const cmdChanBuffSize = 8192 // random-ass-guess 90 const maxUdpBytes = 1440 // 1500(Ethernet MTU) - 60(Max UDP header size 91 92 func NewStatsDSink(addr string, options *StatsDSinkOptions) (*StatsDSink, error) { 93 c, err := net.ListenPacket("udp", ":0") 94 if err != nil { 95 return nil, err 96 } 97 98 ra, err := net.ResolveUDPAddr("udp", addr) 99 if err != nil { 100 return nil, err 101 } 102 103 s := &StatsDSink{ 104 udpConn: c.(*net.UDPConn), 105 udpAddr: ra, 106 cmdChan: make(chan statsdEmitCmd, cmdChanBuffSize), 107 drainDoneChan: make(chan struct{}), 108 stopDoneChan: make(chan struct{}), 109 flushPeriod: 100 * time.Millisecond, 110 prefixBuffers: map[eventKey]prefixBuffer{}, 111 } 112 113 if options != nil { 114 s.options = *options 115 if s.options.SanitizationFunc == nil { 116 s.options.SanitizationFunc = sanitizeKey 117 } 118 } else { 119 s.options = defaultStatsDOptions 120 } 121 122 go s.loop() 123 124 return s, nil 125 } 126 127 func (s *StatsDSink) Stop() { 128 s.cmdChan <- statsdEmitCmd{Kind: statsdCmdKindStop} 129 <-s.stopDoneChan 130 } 131 132 func (s *StatsDSink) Drain() { 133 s.cmdChan <- statsdEmitCmd{Kind: statsdCmdKindDrain} 134 <-s.drainDoneChan 135 } 136 137 func (s *StatsDSink) EmitEvent(job, event string, kvs map[string]string) { 138 s.cmdChan <- statsdEmitCmd{Kind: statsdCmdKindEvent, Job: job, Event: event} 139 } 140 141 func (s *StatsDSink) EmitEventErr(job, event string, inputErr error, kvs map[string]string) { 142 s.cmdChan <- statsdEmitCmd{Kind: statsdCmdKindEventErr, Job: job, Event: event} 143 } 144 145 func (s *StatsDSink) EmitTiming(job, event string, nanos int64, kvs map[string]string) { 146 s.cmdChan <- statsdEmitCmd{Kind: statsdCmdKindTiming, Job: job, Event: event, Nanos: nanos} 147 } 148 149 func (s *StatsDSink) EmitGauge(job, event string, value float64, kvs map[string]string) { 150 s.cmdChan <- statsdEmitCmd{Kind: statsdCmdKindGauge, Job: job, Event: event, Value: value} 151 } 152 153 func (s *StatsDSink) EmitComplete(job string, status health.CompletionStatus, nanos int64, kvs map[string]string) { 154 s.cmdChan <- statsdEmitCmd{Kind: statsdCmdKindComplete, Job: job, Status: status, Nanos: nanos} 155 } 156 157 func (s *StatsDSink) loop() { 158 cmdChan := s.cmdChan 159 160 ticker := time.NewTicker(s.flushPeriod) 161 go func() { 162 for range ticker.C { 163 cmdChan <- statsdEmitCmd{Kind: statsdCmdKindFlush} 164 } 165 }() 166 167 loop: 168 for cmd := range cmdChan { 169 switch cmd.Kind { 170 case statsdCmdKindDrain: 171 drainLoop: 172 for { 173 select { 174 case cmd := <-cmdChan: 175 s.processCmd(&cmd) 176 default: 177 s.flush() 178 s.drainDoneChan <- struct{}{} 179 break drainLoop 180 } 181 } 182 case statsdCmdKindStop: 183 s.stopDoneChan <- struct{}{} 184 break loop 185 case statsdCmdKindFlush: 186 s.flush() 187 default: 188 s.processCmd(&cmd) 189 } 190 } 191 192 ticker.Stop() 193 } 194 195 func (s *StatsDSink) processCmd(cmd *statsdEmitCmd) { 196 switch cmd.Kind { 197 case statsdCmdKindEvent: 198 s.processEvent(cmd.Job, cmd.Event, "") 199 case statsdCmdKindEventErr: 200 s.processEvent(cmd.Job, cmd.Event, "error") 201 case statsdCmdKindTiming: 202 s.processTiming(cmd.Job, cmd.Event, cmd.Nanos) 203 case statsdCmdKindGauge: 204 s.processGauge(cmd.Job, cmd.Event, cmd.Value) 205 case statsdCmdKindComplete: 206 s.processComplete(cmd.Job, cmd.Status, cmd.Nanos) 207 } 208 } 209 210 func (s *StatsDSink) processEvent(job, event, extra string) { 211 if !s.options.SkipTopLevelEvents { 212 pb := s.getPrefixBuffer("", event, extra) 213 pb.WriteString("1|c\n") 214 s.writeStatsDMetric(pb.Bytes()) 215 } 216 217 if !s.options.SkipNestedEvents { 218 pb := s.getPrefixBuffer(job, event, extra) 219 pb.WriteString("1|c\n") 220 s.writeStatsDMetric(pb.Bytes()) 221 } 222 } 223 224 func (s *StatsDSink) processTiming(job, event string, nanos int64) { 225 s.writeNanosToTimingBuf(nanos) 226 227 if !s.options.SkipTopLevelEvents { 228 pb := s.getPrefixBuffer("", event, "") 229 pb.Write(s.timingBuf) 230 pb.WriteString("|ms\n") 231 s.writeStatsDMetric(pb.Bytes()) 232 } 233 234 if !s.options.SkipNestedEvents { 235 pb := s.getPrefixBuffer(job, event, "") 236 pb.Write(s.timingBuf) 237 pb.WriteString("|ms\n") 238 s.writeStatsDMetric(pb.Bytes()) 239 } 240 } 241 242 func (s *StatsDSink) processGauge(job, event string, value float64) { 243 s.timingBuf = s.timingBuf[0:0] 244 prec := 2 245 if value < 0.1 && value > -0.1 { 246 prec = -1 247 } 248 s.timingBuf = strconv.AppendFloat(s.timingBuf, value, 'f', prec, 64) 249 250 if !s.options.SkipTopLevelEvents { 251 pb := s.getPrefixBuffer("", event, "") 252 pb.Write(s.timingBuf) 253 pb.WriteString("|g\n") 254 s.writeStatsDMetric(pb.Bytes()) 255 } 256 257 if !s.options.SkipNestedEvents { 258 pb := s.getPrefixBuffer(job, event, "") 259 pb.Write(s.timingBuf) 260 pb.WriteString("|g\n") 261 s.writeStatsDMetric(pb.Bytes()) 262 } 263 } 264 265 func (s *StatsDSink) processComplete(job string, status health.CompletionStatus, nanos int64) { 266 s.writeNanosToTimingBuf(nanos) 267 statusString := status.String() 268 269 pb := s.getPrefixBuffer(job, "", statusString) 270 pb.Write(s.timingBuf) 271 pb.WriteString("|ms\n") 272 s.writeStatsDMetric(pb.Bytes()) 273 } 274 275 func (s *StatsDSink) flush() { 276 if s.udpBuf.Len() > 0 { 277 s.udpConn.WriteToUDP(s.udpBuf.Bytes(), s.udpAddr) 278 s.udpBuf.Truncate(0) 279 } 280 } 281 282 // assumes b is a well-formed statsd metric like "job.event:1|c\n" (including newline) 283 func (s *StatsDSink) writeStatsDMetric(b []byte) { 284 lenb := len(b) 285 286 if lenb == 0 { 287 return 288 } 289 290 // single metric exceeds limit. sad day. 291 if lenb > maxUdpBytes { 292 return 293 } 294 295 lenUdpBuf := s.udpBuf.Len() 296 297 if lenb+lenUdpBuf > maxUdpBytes { 298 s.udpConn.WriteToUDP(s.udpBuf.Bytes(), s.udpAddr) 299 s.udpBuf.Truncate(0) 300 } 301 302 s.udpBuf.Write(b) 303 } 304 305 func (s *StatsDSink) getPrefixBuffer(job, event, suffix string) prefixBuffer { 306 key := eventKey{job, event, suffix} 307 308 b, ok := s.prefixBuffers[key] 309 if !ok { 310 b.Buffer = &bytes.Buffer{} 311 s.writeSanitizedKeys(b.Buffer, s.options.Prefix, job, event, suffix) 312 b.WriteByte(':') 313 b.prefixLen = b.Len() 314 315 // 123456789.99|ms\n 16 bytes. timing value represents 11 days max 316 b.Grow(16) 317 s.prefixBuffers[key] = b 318 } else { 319 b.Truncate(b.prefixLen) 320 } 321 322 return b 323 } 324 325 func (s *StatsDSink) writeSanitizedKeys(b *bytes.Buffer, keys ...string) { 326 needDot := false 327 for _, k := range keys { 328 if k != "" { 329 if needDot { 330 b.WriteByte('.') 331 } 332 s.options.SanitizationFunc(b, k) 333 needDot = true 334 } 335 } 336 } 337 338 func (s *StatsDSink) writeNanosToTimingBuf(nanos int64) { 339 s.timingBuf = s.timingBuf[0:0] 340 if nanos >= 10e6 { 341 // More than 10 milliseconds. We'll just print as an integer 342 s.timingBuf = strconv.AppendInt(s.timingBuf, nanos/1e6, 10) 343 } else { 344 s.timingBuf = strconv.AppendFloat(s.timingBuf, float64(nanos)/float64(time.Millisecond), 'f', 2, 64) 345 } 346 } 347 348 func sanitizeKey(b *bytes.Buffer, s string) { 349 b.Grow(len(s) + 1) 350 for _, r := range s { 351 switch { 352 case 'A' <= r && r <= 'Z', 353 'a' <= r && r <= 'z', 354 '0' <= r && r <= '9', 355 r == '_', r == '.', r == '-': 356 b.WriteRune(r) 357 default: 358 b.WriteByte('$') 359 } 360 } 361 }