Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add recording sender and some convenience methods for test.
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
Showing
4 changed files
with
410 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
Oops, something went wrong.