Skip to content

Commit

Permalink
Add recording sender and some convenience methods for test.
Browse files Browse the repository at this point in the history
Squashed commit of the following:

commit 9a49bc17c779ec1a99004c66667c62fb88eeb5e4
Author: falun <github@magichappyplace.com>
Date:   Sun Apr 24 10:20:25 2016 -0700

    fix package name

commit 04ab59325709d1b334a30b7eb7759170e5ef9718
Author: falun <github@magichappyplace.com>
Date:   Sun Apr 24 10:17:51 2016 -0700

    move interface assertion into test code

commit 6b4acea
Author: falun <github@magichappyplace.com>
Date:   Sat Apr 23 10:49:27 2016 -0700

    Stats calls that result in no contents now return nil for easy of assertion in test code

commit 59b1887
Author: falun <github@magichappyplace.com>
Date:   Sat Apr 23 10:49:00 2016 -0700

    tweak docs

commit 5df5bc6
Author: falun <github@magichappyplace.com>
Date:   Sat Apr 23 10:42:49 2016 -0700

    convenience methods on Stats for writing tests

commit d95c145
Author: falun <github@magichappyplace.com>
Date:   Sat Apr 23 10:18:33 2016 -0700

    move files into statsdtest and rename some things

commit f20bc26
Author: falun <github@magichappyplace.com>
Date:   Sat Apr 23 02:38:21 2016 -0700

    remove old code

commit 64cb636
Author: falun <github@magichappyplace.com>
Date:   Sat Apr 23 02:37:29 2016 -0700

    move into recorder package

commit e2c5ea8
Author: falun <richard@turbinelabs.io>
Date:   Tue Apr 12 10:31:15 2016 -0700

    v1 ckpt
  • Loading branch information
falun committed Apr 24, 2016
1 parent 5517f30 commit 3c8a38c
Show file tree
Hide file tree
Showing 4 changed files with 410 additions and 0 deletions.
81 changes: 81 additions & 0 deletions statsd/statsdtest/recorder.go
@@ -0,0 +1,81 @@
package statsdtest

import (
"errors"
"sync"
)

// RecordingSender implements statsd.Sender but parses individual Stats into a
// buffer that can be later inspected instead of sending to some server. It
// should constructed with NewRecordingSender().
type RecordingSender struct {
m sync.Mutex
buffer Stats
closed bool
}

// NewRecordingSender creates a new RecordingSender for use by a statsd.Client.
func NewRecordingSender() *RecordingSender {
rs := &RecordingSender{}
rs.buffer = make(Stats, 0)
return rs
}

// GetSent returns the stats that have been sent. Locks and copies the current
// state of the sent Stats.
//
// The entire buffer of Stat objects (including Stat.Raw is copied).
func (rs *RecordingSender) GetSent() Stats {
rs.m.Lock()
defer rs.m.Unlock()

results := make(Stats, len(rs.buffer))
for i, e := range rs.buffer {
results[i] = e
results[i].Raw = make([]byte, len(e.Raw))
for j, b := range e.Raw {
results[i].Raw[j] = b
}
}

return results
}

// ClearSent locks the sender and clears any Stats that have been recorded.
func (rs *RecordingSender) ClearSent() {
rs.m.Lock()
defer rs.m.Unlock()

rs.buffer = rs.buffer[:0]
}

// Send parses the provided []byte into stat objects and then appends these to
// the buffer of sent stats. Buffer operations are synchronized so it is safe
// to call this from multiple goroutines (though contenion will impact
// performance so don't use this during a benchmark). Send treats '\n' as a
// delimiter between multiple sats in the same []byte.
//
// Calling after the Sender has been closed will return an error (and not
// mutate the buffer).
func (rs *RecordingSender) Send(data []byte) (int, error) {
sent := ParseStats(data)

rs.m.Lock()
defer rs.m.Unlock()
if rs.closed {
return 0, errors.New("writing to a closed sender")
}

rs.buffer = append(rs.buffer, sent...)
return len(data), nil
}

// Close marks this sender as closed. Subsequent attempts to Send stats will
// result in an error.
func (rs *RecordingSender) Close() error {
rs.m.Lock()
defer rs.m.Unlock()

rs.closed = true
return nil
}
58 changes: 58 additions & 0 deletions statsd/statsdtest/recorder_test.go
@@ -0,0 +1,58 @@
package statsdtest

import (
"fmt"
"reflect"
"strconv"
"testing"
"time"

"github.com/cactus/go-statsd-client/statsd"
)

func TestRecordingSenderIsSender(t *testing.T) {
// This ensures that if the Sender interface changes in the future we'll get
// compile time failures should the RecordingSender not be updated to meet
// the new definition. This keeps changes from inadvertently breaking tests
// of folks that use go-statsd-client.
var _ statsd.Sender = NewRecordingSender()
}

func TestRecordingSender(t *testing.T) {
start := time.Now()
rs := new(RecordingSender)
statter, err := statsd.NewClientWithSender(rs, "test")
if err != nil {
t.Errorf("failed to construct client")
return
}

statter.Inc("stat", 4444, 1.0)
statter.Dec("stat", 5555, 1.0)
statter.Set("set-stat", "some string", 1.0)

d := time.Since(start)
statter.TimingDuration("timing", d, 1.0)

sent := rs.GetSent()
if len(sent) != 4 {
t.Errorf("Did not capture all stats sent; got: %s", sent)
// just dive out because everything else relies on ordering
return
}

ms := float64(d) / float64(time.Millisecond)
// somewhat fragile in that it assums float rendering within client *shrug*
msStr := string(strconv.AppendFloat([]byte(""), ms, 'f', -1, 64))

expected := Stats{
{[]byte("test.stat:4444|c"), "test.stat", "4444", "c", "", true},
{[]byte("test.stat:-5555|c"), "test.stat", "-5555", "c", "", true},
{[]byte("test.set-stat:some string|s"), "test.set-stat", "some string", "s", "", true},
{[]byte(fmt.Sprintf("test.timing:%s|ms", msStr)), "test.timing", msStr, "ms", "", true},
}

if !reflect.DeepEqual(sent, expected) {
t.Errorf("got: %s, want: %s", sent, expected)
}
}
117 changes: 117 additions & 0 deletions statsd/statsdtest/stat.go
@@ -0,0 +1,117 @@
package statsdtest

import "bytes"

// Stat contains the raw and extracted stat information from a stat that was
// sent by the RecordingSender. Raw will always have the content that was
// consumed for this specific stat and Parsed will be set if no errors were hit
// pulling information out of it.
type Stat struct {
Raw []byte
Stat string
Value string
Tag string
Rate string
Parsed bool
}

// ParseStats takes a sequence of bytes destined for a Statsd server and parses
// it out into one or more Stat structs. Each struct includes both the raw
// bytes (copied, so the src []byte may be reused if desired) as well as each
// component it was able to parse out. If parsing was incomplete Stat.Parsed
// will be set to false but no error is returned / kept.
func ParseStats(src []byte) Stats {
d := make([]byte, len(src))
for i, b := range src {
d[i] = b
}
// standard protocal indicates one stat per line
entries := bytes.Split(d, []byte{'\n'})

result := make(Stats, len(entries))

for i, e := range entries {
result[i] = Stat{Raw: e}
ss := &result[i]

// : deliniates the stat name from the stat data
marker := bytes.IndexByte(e, ':')
if marker == -1 {
continue
}
ss.Stat = string(e[0:marker])

// stat data folows ':' with the form {value}|{type tag}[|@{sample rate}]
e = e[marker+1:]
marker = bytes.IndexByte(e, '|')
if marker == -1 {
continue
}

ss.Value = string(e[:marker])

e = e[marker+1:]
marker = bytes.IndexByte(e, '|')
if marker == -1 {
// no sample rate
ss.Tag = string(e)
} else {
ss.Tag = string(e[:marker])
e = e[marker+1:]
if len(e) == 0 || e[0] != '@' {
// sample rate should be prefixed with '@'; bail otherwise
continue
}
ss.Rate = string(e[1:])
}

ss.Parsed = true
}

return result
}

type Stats []Stat

// Unparsed returns any stats that were unable to be completely parsed.
func (s Stats) Unparsed() Stats {
var r Stats
for _, e := range s {
if !e.Parsed {
r = append(r, e)
}
}

return r
}

// CollectNamed returns all data sent for a given stat name.
func (s Stats) CollectNamed(statName string) Stats {
return s.Collect(func(e Stat) bool {
return e.Stat == statName
})
}

// Collect gathers all stats that make some predicate true.
func (s Stats) Collect(pred func(Stat) bool) Stats {
var r Stats
for _, e := range s {
if pred(e) {
r = append(r, e)
}
}
return r
}

// Values returns the values associated with this Stats object.
func (s Stats) Values() []string {
if len(s) == 0 {
return nil
}

r := make([]string, len(s))
for i, e := range s {
r[i] = e.Value
}
return r
}

0 comments on commit 3c8a38c

Please sign in to comment.