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  }