pax_global_header00006660000000000000000000000064135415661770014531gustar00rootroot0000000000000052 comment=5ca90424ceb7e5e9affff2765da00e9dd737f274 go-statsd-client-3.2.0/000077500000000000000000000000001354156617700147145ustar00rootroot00000000000000go-statsd-client-3.2.0/.travis.yml000066400000000000000000000001311354156617700170200ustar00rootroot00000000000000language: go sudo: false script: go test -v -cpu=1,2 ./... go: - "1.11.x" - "1.12.x" go-statsd-client-3.2.0/CHANGELOG.md000066400000000000000000000037501354156617700165320ustar00rootroot00000000000000Changelog ========= ## head ## 3.2.0 2019-09-21 * A new client constructor with "config style" semantics. "legacy" client construction still supported, to retain backwards compat. * Add an optional re-resolving client configuration. This sets a schedule for having the client periodically re-resolve the addr to ip. This does add some overhead, so best used only when necessary. ## 3.1.1 2018-01-19 * avoid some overhead by not using defer for two "hot" path funcs * Fix leak on sender create with unresolvable destination (GH-34). ## 3.1.0 2016-05-30 * `NewClientWithSender(Sender, string) (Statter, error)` method added to enable building a Client from a prefix and an already created Sender. * Add stat recording sender in submodule statsdtest (GH-32). * Add an example helper stat validation function. * Change the way scope joins are done (GH-26). * Reorder some structs to avoid middle padding. ## 3.0.3 2016-02-18 * make sampler function tunable (GH-24) ## 3.0.2 2016-01-13 * reduce memory allocations * improve performance of buffered clients ## 3.0.1 2016-01-01 * documentation typo fixes * fix possible race condition with `buffered_sender` send/close. ## 3.0.0 2015-12-04 * add substatter support ## 2.0.2 2015-10-16 * remove trailing newline in buffered sends to avoid etsy statsd log messages * minor internal code reorganization for clarity (no api changes) ## 2.0.1 2015-07-12 * Add Set and SetInt funcs to support Sets * Properly flush BufferedSender on close (bugfix) * Add TimingDuration with support for sub-millisecond timing * fewer allocations, better performance of BufferedClient ## 2.0.0 2015-03-19 * BufferedClient - send multiple stats at once * clean up godocs * clean up interfaces -- BREAKING CHANGE: for users who previously defined types as *Client instead of the Statter interface type. ## 1.0.1 2015-03-19 * BufferedClient - send multiple stats at once ## 1.0.0 2015-02-04 * tag a version as fix for GH-8 go-statsd-client-3.2.0/LICENSE.md000066400000000000000000000020441354156617700163200ustar00rootroot00000000000000Copyright (c) 2012-2016 Eli Janssen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. go-statsd-client-3.2.0/README.md000066400000000000000000000103041354156617700161710ustar00rootroot00000000000000go-statsd-client ================ [![Build Status](https://travis-ci.org/cactus/go-statsd-client.png?branch=master)](https://travis-ci.org/cactus/go-statsd-client) [![GoDoc](https://godoc.org/github.com/cactus/go-statsd-client/statsd?status.png)](https://godoc.org/github.com/cactus/go-statsd-client/statsd) [![Go Report Card](https://goreportcard.com/badge/cactus/go-statsd-client)](https://goreportcard.com/report/cactus/go-statsd-client) [![License](https://img.shields.io/github/license/cactus/go-statsd-client.svg)](https://github.com/cactus/go-statsd-client/blob/master/LICENSE.md) ## About A [StatsD][1] client for Go. ## Docs Viewable online at [godoc.org][2]. ## Example Some examples: ``` go import ( "log" "github.com/cactus/go-statsd-client/statsd" ) func main() { // First create a client config. Here is a simple config that sends one // stat per packet (for compatibility). config := &statsd.ClientConfig{ Address: "127.0.0.1:8125", Prefix: "test-client", } /* // This one is for a client that re-resolves the hostname ever 30 seconds. // Useful if the address of a hostname changes frequently. Note that this // type of client has some additional locking overhead for safety. // As such, leave ResInetval as the zero value (previous exmaple) if you // don't specifically need this functionality. config := &statsd.ClientConfig{ Address: "127.0.0.1:8125", Prefix: "test-client", ResInterval: 30 * time.Second, } // This one is for a buffered client, which sends multiple stats in one // packet, is recommended when your server supports it (better performance). config := &statsd.ClientConfig{ Address: "127.0.0.1:8125", Prefix: "test-client", UseBuffered: true, // interval to force flush buffer. full buffers will flush on their own, // but for data not frequently sent, a max threshold is useful FlushInterval: 300*time.Millisecond, } // This one is for a buffered resolving client, which sends multiple stats // in one packet (like previous example), as well as re-resolving the // hostname every 30 seconds. config := &statsd.ClientConfig{ Address: "127.0.0.1:8125", Prefix: "test-client", ResInterval: 30 * time.Second, UseBuffered: true, FlushInterval: 300*time.Millisecond, } */ // Now create the client client, err := statsd.NewClientWithConfig(config) // and handle any initialization errors if err != nil { log.Fatal(err) } // make sure to close to clean up when done, to avoid leaks. defer client.Close() // Send a stat client.Inc("stat1", 42, 1.0) } ``` ### Legacy Example A legacy client creation method is still supported. This is retained so as not to break or interrupt existing integrations. ``` go import ( "log" "github.com/cactus/go-statsd-client/statsd" ) func main() { // first create a client // The basic client sends one stat per packet (for compatibility). client, err := statsd.NewClient("127.0.0.1:8125", "test-client") // A buffered client, which sends multiple stats in one packet, is // recommended when your server supports it (better performance). // client, err := statsd.NewBufferedClient("127.0.0.1:8125", "test-client", 300*time.Millisecond, 0) // handle any errors if err != nil { log.Fatal(err) } // make sure to close to clean up when done, to avoid leaks. defer client.Close() // Send a stat client.Inc("stat1", 42, 1.0) } ``` See [docs][2] for more info. There is also some additional example code in the `test-client` directory. ## Contributors See [here][4]. ## Alternative Implementations See the [statsd wiki][5] for some additional client implementations (scroll down to the Go section). ## License Released under the [MIT license][3]. See `LICENSE.md` file for details. [1]: https://github.com/etsy/statsd [2]: http://godoc.org/github.com/cactus/go-statsd-client/statsd [3]: http://www.opensource.org/licenses/mit-license.php [4]: https://github.com/cactus/go-statsd-client/graphs/contributors [5]: https://github.com/etsy/statsd/wiki#client-implementations go-statsd-client-3.2.0/SECURITY.md000066400000000000000000000017551354156617700165150ustar00rootroot00000000000000# Security Policy ## Supported Versions Either of the following are currently supported: * The HEAD of the master branch. * The most recent tagged "release" version to appear on the [releases][1] page. ## Reporting a Vulnerability To report a vulnerability, please open a github Issue stating only that you have found a security vulnerability or other problem that you would like to report, and requesting a [Draft Security Advisory][2] be created. A Draft Security Advisory will then be created, and the user that opened the Issue will be invited to collaborate on the Draft Advisory. If the issue is accepted, a comment to that effect will be made in the original issue. If the Security Advistory leaves draft state, it will eventually be linked from the original issue. If the issue is declined, a comment to that effect will be made in the original issue. [1]: https://github.com/cactus/go-statsd-client/releases [2]: https://help.github.com/en/articles/about-maintainer-security-advisories go-statsd-client-3.2.0/statsd/000077500000000000000000000000001354156617700162165ustar00rootroot00000000000000go-statsd-client-3.2.0/statsd/bench_buffered_test.go000066400000000000000000000035071354156617700225320ustar00rootroot00000000000000// Copyright (c) 2012-2016 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package statsd import ( "testing" "time" ) func BenchmarkBufferedClientInc(b *testing.B) { l, err := newUDPListener("127.0.0.1:0") if err != nil { b.Fatal(err) } defer l.Close() c, err := NewBufferedClient(l.LocalAddr().String(), "test", 1*time.Second, 0) if err != nil { b.Fatal(err) } defer c.Close() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { //i := 0; i < b.N; i++ { c.Inc("benchinc", 1, 1) } }) } func BenchmarkBufferedClientIncSample(b *testing.B) { l, err := newUDPListener("127.0.0.1:0") if err != nil { b.Fatal(err) } defer l.Close() c, err := NewBufferedClient(l.LocalAddr().String(), "test", 1*time.Second, 0) if err != nil { b.Fatal(err) } defer c.Close() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { //i := 0; i < b.N; i++ { c.Inc("benchinc", 1, 0.3) } }) } func BenchmarkBufferedClientSetInt(b *testing.B) { l, err := newUDPListener("127.0.0.1:0") if err != nil { b.Fatal(err) } defer l.Close() c, err := NewBufferedClient(l.LocalAddr().String(), "test", 1*time.Second, 0) if err != nil { b.Fatal(err) } defer c.Close() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { //i := 0; i < b.N; i++ { c.SetInt("setint", 1, 1) } }) } func BenchmarkBufferedClientSetIntSample(b *testing.B) { l, err := newUDPListener("127.0.0.1:0") if err != nil { b.Fatal(err) } defer l.Close() c, err := NewBufferedClient(l.LocalAddr().String(), "test", 1*time.Second, 0) if err != nil { b.Fatal(err) } defer c.Close() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { //i := 0; i < b.N; i++ { c.SetInt("setint", 1, 0.3) } }) } go-statsd-client-3.2.0/statsd/bench_client_test.go000066400000000000000000000032671354156617700222310ustar00rootroot00000000000000// Copyright (c) 2012-2016 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package statsd import ( "testing" ) func BenchmarkClientInc(b *testing.B) { l, err := newUDPListener("127.0.0.1:0") if err != nil { b.Fatal(err) } defer l.Close() c, err := NewClient(l.LocalAddr().String(), "test") if err != nil { b.Fatal(err) } defer c.Close() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { //i := 0; i < b.N; i++ { c.Inc("benchinc", 1, 1) } }) } func BenchmarkClientIncSample(b *testing.B) { l, err := newUDPListener("127.0.0.1:0") if err != nil { b.Fatal(err) } defer l.Close() c, err := NewClient(l.LocalAddr().String(), "test") if err != nil { b.Fatal(err) } defer c.Close() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { //i := 0; i < b.N; i++ { c.Inc("benchinc", 1, 0.3) } }) } func BenchmarkClientSetInt(b *testing.B) { l, err := newUDPListener("127.0.0.1:0") if err != nil { b.Fatal(err) } defer l.Close() c, err := NewClient(l.LocalAddr().String(), "test") if err != nil { b.Fatal(err) } defer c.Close() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { //i := 0; i < b.N; i++ { c.SetInt("setint", 1, 1) } }) } func BenchmarkClientSetIntSample(b *testing.B) { l, err := newUDPListener("127.0.0.1:0") if err != nil { b.Fatal(err) } defer l.Close() c, err := NewClient(l.LocalAddr().String(), "test") if err != nil { b.Fatal(err) } defer c.Close() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { //i := 0; i < b.N; i++ { c.SetInt("setint", 1, 0.3) } }) } go-statsd-client-3.2.0/statsd/bench_sender_test.go000066400000000000000000000036031354156617700222250ustar00rootroot00000000000000// Copyright (c) 2012-2016 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package statsd import ( "bytes" "testing" "time" ) func BenchmarkSenderSmall(b *testing.B) { l, err := newUDPListener("127.0.0.1:0") if err != nil { b.Fatal(err) } defer l.Close() s, err := NewSimpleSender(l.LocalAddr().String()) if err != nil { b.Fatal(err) } defer s.Close() data := []byte("test.gauge:1|g\n") b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { //i := 0; i < b.N; i++ { s.Send(data) } }) } func BenchmarkSenderLarge(b *testing.B) { l, err := newUDPListener("127.0.0.1:0") if err != nil { b.Fatal(err) } defer l.Close() s, err := NewSimpleSender(l.LocalAddr().String()) if err != nil { b.Fatal(err) } defer s.Close() data := bytes.Repeat([]byte("test.gauge:1|g\n"), 50) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { //i := 0; i < b.N; i++ { s.Send(data) } }) } func BenchmarkBufferedSenderSmall(b *testing.B) { l, err := newUDPListener("127.0.0.1:0") if err != nil { b.Fatal(err) } defer l.Close() s, err := NewBufferedSender(l.LocalAddr().String(), 300*time.Millisecond, 1432) if err != nil { b.Fatal(err) } defer s.Close() data := []byte("test.gauge:1|g\n") b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { //i := 0; i < b.N; i++ { s.Send(data) } }) } func BenchmarkBufferedSenderLarge(b *testing.B) { l, err := newUDPListener("127.0.0.1:0") if err != nil { b.Fatal(err) } defer l.Close() s, err := NewBufferedSender(l.LocalAddr().String(), 300*time.Millisecond, 1432) if err != nil { b.Fatal(err) } defer s.Close() data := bytes.Repeat([]byte("test.gauge:1|g\n"), 50) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { //i := 0; i < b.N; i++ { s.Send(data) } }) } go-statsd-client-3.2.0/statsd/buffer_pool.go000066400000000000000000000010471354156617700210510ustar00rootroot00000000000000// Copyright (c) 2012-2016 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package statsd import ( "bytes" "sync" ) type bufferPool struct { *sync.Pool } func newBufferPool() *bufferPool { return &bufferPool{ &sync.Pool{New: func() interface{} { return bytes.NewBuffer(make([]byte, 0, 1700)) }}, } } func (bp *bufferPool) Get() *bytes.Buffer { return (bp.Pool.Get()).(*bytes.Buffer) } func (bp *bufferPool) Put(b *bytes.Buffer) { b.Truncate(0) bp.Pool.Put(b) } go-statsd-client-3.2.0/statsd/client.go000066400000000000000000000163231354156617700200300ustar00rootroot00000000000000// Copyright (c) 2012-2016 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package statsd import ( "fmt" "math/rand" "strconv" "strings" "time" ) var bufPool = newBufferPool() // The StatSender interface wraps all the statsd metric methods type StatSender interface { Inc(string, int64, float32) error Dec(string, int64, float32) error Gauge(string, int64, float32) error GaugeDelta(string, int64, float32) error Timing(string, int64, float32) error TimingDuration(string, time.Duration, float32) error Set(string, string, float32) error SetInt(string, int64, float32) error Raw(string, string, float32) error } // The Statter interface defines the behavior of a stat client type Statter interface { StatSender NewSubStatter(string) SubStatter SetPrefix(string) Close() error } // The SubStatter interface defines the behavior of a stat child/subclient type SubStatter interface { StatSender SetSamplerFunc(SamplerFunc) NewSubStatter(string) SubStatter } // The SamplerFunc type defines a function that can serve // as a Client sampler function. type SamplerFunc func(float32) bool // DefaultSampler is the default rate sampler function func DefaultSampler(rate float32) bool { if rate < 1 { return rand.Float32() < rate } return true } // A Client is a statsd client. type Client struct { // prefix for statsd name prefix string // packet sender sender Sender // sampler method sampler SamplerFunc } // Close closes the connection and cleans up. func (s *Client) Close() error { if s == nil { return nil } err := s.sender.Close() return err } // Inc increments a statsd count type. // stat is a string name for the metric. // value is the integer value // rate is the sample rate (0.0 to 1.0) func (s *Client) Inc(stat string, value int64, rate float32) error { if !s.includeStat(rate) { return nil } return s.submit(stat, "", value, "|c", rate) } // Dec decrements a statsd count type. // stat is a string name for the metric. // value is the integer value. // rate is the sample rate (0.0 to 1.0). func (s *Client) Dec(stat string, value int64, rate float32) error { if !s.includeStat(rate) { return nil } return s.submit(stat, "", -value, "|c", rate) } // Gauge submits/updates a statsd gauge type. // stat is a string name for the metric. // value is the integer value. // rate is the sample rate (0.0 to 1.0). func (s *Client) Gauge(stat string, value int64, rate float32) error { if !s.includeStat(rate) { return nil } return s.submit(stat, "", value, "|g", rate) } // GaugeDelta submits a delta to a statsd gauge. // stat is the string name for the metric. // value is the (positive or negative) change. // rate is the sample rate (0.0 to 1.0). func (s *Client) GaugeDelta(stat string, value int64, rate float32) error { if !s.includeStat(rate) { return nil } // if negative, the submit formatter will prefix with a - already // so only special case the positive value if value >= 0 { return s.submit(stat, "+", value, "|g", rate) } return s.submit(stat, "", value, "|g", rate) } // Timing submits a statsd timing type. // stat is a string name for the metric. // delta is the time duration value in milliseconds // rate is the sample rate (0.0 to 1.0). func (s *Client) Timing(stat string, delta int64, rate float32) error { if !s.includeStat(rate) { return nil } return s.submit(stat, "", delta, "|ms", rate) } // TimingDuration submits a statsd timing type. // stat is a string name for the metric. // delta is the timing value as time.Duration // rate is the sample rate (0.0 to 1.0). func (s *Client) TimingDuration(stat string, delta time.Duration, rate float32) error { if !s.includeStat(rate) { return nil } ms := float64(delta) / float64(time.Millisecond) return s.submit(stat, "", ms, "|ms", rate) } // Set submits a stats set type // stat is a string name for the metric. // value is the string value // rate is the sample rate (0.0 to 1.0). func (s *Client) Set(stat string, value string, rate float32) error { if !s.includeStat(rate) { return nil } return s.submit(stat, "", value, "|s", rate) } // SetInt submits a number as a stats set type. // stat is a string name for the metric. // value is the integer value // rate is the sample rate (0.0 to 1.0). func (s *Client) SetInt(stat string, value int64, rate float32) error { if !s.includeStat(rate) { return nil } return s.submit(stat, "", value, "|s", rate) } // Raw submits a preformatted value. // stat is the string name for the metric. // value is a preformatted "raw" value string. // rate is the sample rate (0.0 to 1.0). func (s *Client) Raw(stat string, value string, rate float32) error { if !s.includeStat(rate) { return nil } return s.submit(stat, "", value, "", rate) } // SetSamplerFunc sets a sampler function to something other than the default // sampler is a function that determines whether the metric is // to be accepted, or discarded. // An example use case is for submitted pre-sampled metrics. func (s *Client) SetSamplerFunc(sampler SamplerFunc) { s.sampler = sampler } // submit an already sampled raw stat func (s *Client) submit(stat, vprefix string, value interface{}, suffix string, rate float32) error { data := bufPool.Get() defer bufPool.Put(data) if s.prefix != "" { data.WriteString(s.prefix) data.WriteString(".") } data.WriteString(stat) data.WriteString(":") if vprefix != "" { data.WriteString(vprefix) } // sadly, no way to jam this back into the bytes.Buffer without // doing a few allocations... avoiding those is the whole point here... // so from here on out just use it as a raw []byte b := data.Bytes() switch v := value.(type) { case string: b = append(b, v...) case int64: b = strconv.AppendInt(b, v, 10) case float64: b = strconv.AppendFloat(b, v, 'f', -1, 64) default: return fmt.Errorf("No matching type format") } if suffix != "" { b = append(b, suffix...) } if rate < 1 { b = append(b, "|@"...) b = strconv.AppendFloat(b, float64(rate), 'f', 6, 32) } _, err := s.sender.Send(b) return err } // check for nil client, and perform sampling calculation func (s *Client) includeStat(rate float32) bool { if s == nil { return false } // test for nil in case someone builds their own // client without calling new (result is nil sampler) if s.sampler != nil { return s.sampler(rate) } return DefaultSampler(rate) } // SetPrefix sets/updates the statsd client prefix. // Note: Does not change the prefix of any SubStatters. func (s *Client) SetPrefix(prefix string) { if s == nil { return } s.prefix = prefix } // NewSubStatter returns a SubStatter with appended prefix func (s *Client) NewSubStatter(prefix string) SubStatter { var c *Client if s != nil { c = &Client{ prefix: joinPathComp(s.prefix, prefix), sender: s.sender, sampler: s.sampler, } } return c } // joinPathComp is a helper that ensures we combine path components with a dot // when it's appropriate to do so; prefix is the existing prefix and suffix is // the new component being added. // // It returns the joined prefix. func joinPathComp(prefix, suffix string) string { suffix = strings.TrimLeft(suffix, ".") if prefix != "" && suffix != "" { return prefix + "." + suffix } return prefix + suffix } go-statsd-client-3.2.0/statsd/client_buffered_test.go000066400000000000000000000126111354156617700227250ustar00rootroot00000000000000// Copyright (c) 2012-2016 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package statsd import ( "bytes" "fmt" "log" "reflect" "strings" "testing" "time" ) func TestBufferedClientFlushSize(t *testing.T) { l, err := newUDPListener("127.0.0.1:0") if err != nil { t.Fatal(err) } defer l.Close() for _, tt := range statsdPacketTests { // set flush length to the size of the expected output packet // so we can ensure a flush happens right away. // set flush time sufficiently high so that it never matters for this // test c, err := NewBufferedClient(l.LocalAddr().String(), tt.Prefix, 10*time.Second, len(tt.Expected)+1) if err != nil { c.Close() t.Fatal(err) } method := reflect.ValueOf(c).MethodByName(tt.Method) e := method.Call([]reflect.Value{ reflect.ValueOf(tt.Stat), reflect.ValueOf(tt.Value), reflect.ValueOf(tt.Rate)})[0] errInter := e.Interface() if errInter != nil { c.Close() t.Fatal(errInter.(error)) } data := make([]byte, len(tt.Expected)+16) _, _, err = l.ReadFrom(data) if err != nil { c.Close() t.Fatal(err) } data = bytes.TrimRight(data, "\x00\n") if !bytes.Equal(data, []byte(tt.Expected)) { t.Fatalf("%s got '%s' expected '%s'", tt.Method, data, tt.Expected) } c.Close() } } func TestBufferedClientFlushTime(t *testing.T) { l, err := newUDPListener("127.0.0.1:0") if err != nil { t.Fatal(err) } defer l.Close() for _, tt := range statsdPacketTests { // set flush length to the size of the expected output packet // so we can ensure a flush happens right away. // set flush time sufficiently high so that it never matters for this // test c, err := NewBufferedClient(l.LocalAddr().String(), tt.Prefix, 1*time.Microsecond, 1024) if err != nil { c.Close() t.Fatal(err) } method := reflect.ValueOf(c).MethodByName(tt.Method) e := method.Call([]reflect.Value{ reflect.ValueOf(tt.Stat), reflect.ValueOf(tt.Value), reflect.ValueOf(tt.Rate)})[0] errInter := e.Interface() if errInter != nil { c.Close() t.Fatal(errInter.(error)) } time.Sleep(1 * time.Millisecond) data := make([]byte, len(tt.Expected)+16) _, _, err = l.ReadFrom(data) if err != nil { c.Close() t.Fatal(err) } data = bytes.TrimRight(data, "\x00\n") if !bytes.Equal(data, []byte(tt.Expected)) { t.Fatalf("%s got '%s' expected '%s'", tt.Method, data, tt.Expected) } c.Close() } } func TestBufferedClientBigPacket(t *testing.T) { l, err := newUDPListener("127.0.0.1:0") if err != nil { t.Fatal(err) } defer l.Close() c, err := NewBufferedClient(l.LocalAddr().String(), "test", 10*time.Millisecond, 1024) if err != nil { t.Fatal(err) } defer c.Close() for _, tt := range statsdPacketTests { if tt.Prefix != "test" { continue } method := reflect.ValueOf(c).MethodByName(tt.Method) e := method.Call([]reflect.Value{ reflect.ValueOf(tt.Stat), reflect.ValueOf(tt.Value), reflect.ValueOf(tt.Rate)})[0] errInter := e.Interface() if errInter != nil { t.Fatal(errInter.(error)) } } expected := "" for _, tt := range statsdPacketTests { if tt.Prefix != "test" { continue } expected = expected + tt.Expected + "\n" } expected = strings.TrimSuffix(expected, "\n") time.Sleep(12 * time.Millisecond) data := make([]byte, 1024) _, _, err = l.ReadFrom(data) if err != nil { t.Fatal(err) } data = bytes.TrimRight(data, "\x00") if !bytes.Equal(data, []byte(expected)) { t.Fatalf("got '%s' expected '%s'", data, expected) } } func TestFlushOnClose(t *testing.T) { l, err := newUDPListener("127.0.0.1:0") if err != nil { t.Fatal(err) } defer l.Close() c, err := NewBufferedClient(l.LocalAddr().String(), "test", 1*time.Second, 1024) if err != nil { t.Fatal(err) } c.Inc("count", int64(1), 1.0) c.Close() expected := "test.count:1|c" data := make([]byte, 1024) _, _, err = l.ReadFrom(data) if err != nil { t.Fatal(err) } data = bytes.TrimRight(data, "\x00") if !bytes.Equal(data, []byte(expected)) { fmt.Println(data) fmt.Println([]byte(expected)) t.Fatalf("got '%s' expected '%s'", data, expected) } } func ExampleClient_buffered() { // This one is for a buffered client, which sends multiple stats in one // packet, is recommended when your server supports it (better performance). config := &ClientConfig{ Address: "127.0.0.1:8125", Prefix: "test-client", UseBuffered: true, // interval to force flush buffer. full buffers will flush on their own, // but for data not frequently sent, a max threshold is useful FlushInterval: 300 * time.Millisecond, } // Now create the client client, err := NewClientWithConfig(config) // and handle any initialization errors if err != nil { log.Fatal(err) } // make sure to close to clean up when done, to avoid leaks. defer client.Close() // Send a stat err = client.Inc("stat1", 42, 1.0) // handle any errors if err != nil { log.Printf("Error sending metric: %+v", err) } } func ExampleClient_legacyBuffered() { // first create a client client, err := NewBufferedClient("127.0.0.1:8125", "test-client", 10*time.Millisecond, 0) // handle any errors if err != nil { log.Fatal(err) } // make sure to close to clean up when done, to avoid leaks. defer client.Close() // Send a stat err = client.Inc("stat1", 42, 1.0) // handle any errors if err != nil { log.Printf("Error sending metric: %+v", err) } } go-statsd-client-3.2.0/statsd/client_config.go000066400000000000000000000064171354156617700213600ustar00rootroot00000000000000// Copyright (c) 2012-2016 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package statsd import ( "fmt" "time" ) type ClientConfig struct { // addr is a string of the format "hostname:port", and must be something // validly parsable by net.ResolveUDPAddr. Address string // prefix is the statsd client prefix. Can be "" if no prefix is desired. Prefix string // ResInterval is the interval over which the addr is re-resolved. // Do note that this /does/ add overhead! // If you need higher performance, leave unset (or set to 0), // in which case the address will not be re-resolved. // // Note that if Address is an {ip}:{port} and not a {hostname}:{port}, then // ResInterval will be ignored. ResInterval time.Duration // UseBuffered determines whether a buffered sender is used or not. // If a buffered sender is /not/ used, FlushInterval and FlushBytes values are // ignored. Default is false. UseBuffered bool // FlushInterval is a time.Duration, and specifies the maximum interval for // packet sending. Note that if you send lots of metrics, you will send more // often. This is just a maximal threshold. // If FlushInterval is 0, defaults to 300ms. FlushInterval time.Duration // If flushBytes is 0, defaults to 1432 bytes, which is considered safe // for local traffic. If sending over the public internet, 512 bytes is // the recommended value. FlushBytes int } // NewClientWithConfig returns a new BufferedClient // // config is a ClientConfig, which holds various configuration values. func NewClientWithConfig(config *ClientConfig) (Statter, error) { var sender Sender var err error // guard against nil config if config == nil { return nil, fmt.Errorf("config cannot be nil") } // Use a re-resolving simple sender iff: // * The time duration greater than 0 // * The Address is not an ip (eg. {ip}:{port}). // Otherwise, re-resolution is not required. if config.ResInterval > 0 && !mustBeIP(config.Address) { sender, err = NewResolvingSimpleSender(config.Address, config.ResInterval) } else { sender, err = NewSimpleSender(config.Address) } if err != nil { return nil, err } if config.UseBuffered { return newBufferedC(sender, config) } else { return NewClientWithSender(sender, config.Prefix) } } func newBufferedC(baseSender Sender, config *ClientConfig) (Statter, error) { flushBytes := config.FlushBytes if flushBytes <= 0 { // ref: // github.com/etsy/statsd/blob/master/docs/metric_types.md#multi-metric-packets flushBytes = 1432 } flushInterval := config.FlushInterval if flushInterval <= time.Duration(0) { flushInterval = 300 * time.Millisecond } bufsender, err := newBufferedSenderWithSender(baseSender, flushInterval, flushBytes) if err != nil { return nil, err } return NewClientWithSender(bufsender, config.Prefix) } // NewClientWithSender returns a pointer to a new Client and an error. // // sender is an instance of a statsd.Sender interface and may not be nil // // prefix is the stastd client prefix. Can be "" if no prefix is desired. func NewClientWithSender(sender Sender, prefix string) (Statter, error) { if sender == nil { return nil, fmt.Errorf("Client sender may not be nil") } return &Client{prefix: prefix, sender: sender}, nil } go-statsd-client-3.2.0/statsd/client_legacy.go000066400000000000000000000046361354156617700213600ustar00rootroot00000000000000// Copyright (c) 2012-2016 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package statsd import "time" // Deprecated stuff here... // NewBufferedClient returns a new BufferedClient // // addr is a string of the format "hostname:port", and must be parsable by // net.ResolveUDPAddr. // // prefix is the statsd client prefix. Can be "" if no prefix is desired. // // flushInterval is a time.Duration, and specifies the maximum interval for // packet sending. Note that if you send lots of metrics, you will send more // often. This is just a maximal threshold. // // If flushInterval is 0ms, defaults to 300ms. // // flushBytes specifies the maximum udp packet size you wish to send. If adding // a metric would result in a larger packet than flushBytes, the packet will // first be send, then the new data will be added to the next packet. // // If flushBytes is 0, defaults to 1432 bytes, which is considered safe // for local traffic. If sending over the public internet, 512 bytes is // the recommended value. // // Deprecated: This interface is "legacy", and it is recommented to migrate to // using NewClientWithConfig in the future. func NewBufferedClient(addr, prefix string, flushInterval time.Duration, flushBytes int) (Statter, error) { config := &ClientConfig{ Address: addr, Prefix: prefix, UseBuffered: true, FlushInterval: flushInterval, FlushBytes: flushBytes, } return NewClientWithConfig(config) } // NewClient returns a pointer to a new Client, and an error. // // addr is a string of the format "hostname:port", and must be parsable by // net.ResolveUDPAddr. // // prefix is the statsd client prefix. Can be "" if no prefix is desired. // // Deprecated: This interface is "legacy", and it is recommented to migrate to // using NewClientWithConfig in the future. func NewClient(addr, prefix string) (Statter, error) { config := &ClientConfig{ Address: addr, Prefix: prefix, UseBuffered: false, } return NewClientWithConfig(config) } // Dial is a compatibility alias for NewClient // // Deprecated: This interface is "legacy", and it is recommented to migrate to // using NewClientWithConfig in the future. var Dial = NewClient // New is a compatibility alias for NewClient // // Deprecated: This interface is "legacy", and it is recommented to migrate to // using NewClientWithConfig in the future. var New = NewClient go-statsd-client-3.2.0/statsd/client_noop_legacy.go000066400000000000000000000074131354156617700224070ustar00rootroot00000000000000// Copyright (c) 2012-2016 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package statsd import "time" // A NoopClient is a client that does nothing. // // Deprecated: This type is "legacy", and it is recommented to migrate to // using a nil *Client in the future. type NoopClient struct{} // Close closes the connection and cleans up. func (s *NoopClient) Close() error { return nil } // Inc increments a statsd count type. // stat is a string name for the metric. // value is the integer value // rate is the sample rate (0.0 to 1.0) func (s *NoopClient) Inc(stat string, value int64, rate float32) error { return nil } // Dec decrements a statsd count type. // stat is a string name for the metric. // value is the integer value. // rate is the sample rate (0.0 to 1.0). func (s *NoopClient) Dec(stat string, value int64, rate float32) error { return nil } // Gauge submits/Updates a statsd gauge type. // stat is a string name for the metric. // value is the integer value. // rate is the sample rate (0.0 to 1.0). func (s *NoopClient) Gauge(stat string, value int64, rate float32) error { return nil } // GaugeDelta submits a delta to a statsd gauge. // stat is the string name for the metric. // value is the (positive or negative) change. // rate is the sample rate (0.0 to 1.0). func (s *NoopClient) GaugeDelta(stat string, value int64, rate float32) error { return nil } // Timing submits a statsd timing type. // stat is a string name for the metric. // delta is the time duration value in milliseconds // rate is the sample rate (0.0 to 1.0). func (s *NoopClient) Timing(stat string, delta int64, rate float32) error { return nil } // TimingDuration submits a statsd timing type. // stat is a string name for the metric. // delta is the timing value as time.Duration // rate is the sample rate (0.0 to 1.0). func (s *NoopClient) TimingDuration(stat string, delta time.Duration, rate float32) error { return nil } // Set submits a stats set type. // stat is a string name for the metric. // value is the string value // rate is the sample rate (0.0 to 1.0). func (s *NoopClient) Set(stat string, value string, rate float32) error { return nil } // SetInt submits a number as a stats set type. // convenience method for Set with number. // stat is a string name for the metric. // value is the integer value // rate is the sample rate (0.0 to 1.0). func (s *NoopClient) SetInt(stat string, value int64, rate float32) error { return nil } // Raw formats the statsd event data, handles sampling, prepares it, // and sends it to the server. // stat is the string name for the metric. // value is the preformatted "raw" value string. // rate is the sample rate (0.0 to 1.0). func (s *NoopClient) Raw(stat string, value string, rate float32) error { return nil } // SetPrefix sets/updates the statsd client prefix func (s *NoopClient) SetPrefix(prefix string) {} // NewSubStatter returns a SubStatter with appended prefix func (s *NoopClient) NewSubStatter(prefix string) SubStatter { return &NoopClient{} } // SetSamplerFunc sets the sampler function func (s *NoopClient) SetSamplerFunc(sampler SamplerFunc) {} // NewNoopClient returns a pointer to a new NoopClient, and an error (always // nil, just supplied to support api convention). // Use variadic arguments to support identical format as NewClient, or a more // conventional no argument form. // // Deprecated: This type is "legacy", and it is recommented to migrate to // using a nil *Client in the future. func NewNoopClient(a ...interface{}) (Statter, error) { return &NoopClient{}, nil } // NewNoop is a compatibility alias for NewNoopClient // // Deprecated: This type is "legacy", and it is recommented to migrate to // using a nil *Client in the future. var NewNoop = NewNoopClient go-statsd-client-3.2.0/statsd/client_substatter_test.go000066400000000000000000000172101354156617700233430ustar00rootroot00000000000000// Copyright (c) 2012-2016 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package statsd import ( "bytes" "log" "reflect" "strings" "testing" "time" ) var statsdSubStatterPacketTests = []struct { Prefix string SubPrefix string Method string Stat string Value interface{} Rate float32 Expected string }{ {"test", "sub", "Gauge", "gauge", int64(1), 1.0, "test.sub.gauge:1|g"}, {"test", "sub", "Inc", "count", int64(1), 0.999999, "test.sub.count:1|c|@0.999999"}, {"test", "sub", "Inc", "count", int64(1), 1.0, "test.sub.count:1|c"}, {"test", "sub", "Dec", "count", int64(1), 1.0, "test.sub.count:-1|c"}, {"test", "sub", "Timing", "timing", int64(1), 1.0, "test.sub.timing:1|ms"}, {"test", "sub", "TimingDuration", "timing", 1500 * time.Microsecond, 1.0, "test.sub.timing:1.5|ms"}, {"test", "sub", "TimingDuration", "timing", 3 * time.Microsecond, 1.0, "test.sub.timing:0.003|ms"}, {"test", "sub", "Set", "strset", "pickle", 1.0, "test.sub.strset:pickle|s"}, {"test", "sub", "SetInt", "intset", int64(1), 1.0, "test.sub.intset:1|s"}, {"test", "sub", "GaugeDelta", "gauge", int64(1), 1.0, "test.sub.gauge:+1|g"}, {"test", "sub", "GaugeDelta", "gauge", int64(-1), 1.0, "test.sub.gauge:-1|g"}, // empty sub prefix -- note: not used in subsub tests {"test", "", "Inc", "count", int64(1), 1.0, "test.count:1|c"}, // empty base prefix {"", "sub", "Inc", "count", int64(1), 1.0, "sub.count:1|c"}, } func TestSubStatterClient(t *testing.T) { l, err := newUDPListener("127.0.0.1:0") if err != nil { t.Fatal(err) } defer l.Close() for _, tt := range statsdSubStatterPacketTests { c, err := NewClient(l.LocalAddr().String(), tt.Prefix) if err != nil { t.Fatal(err) } s := c.NewSubStatter(tt.SubPrefix) method := reflect.ValueOf(s).MethodByName(tt.Method) e := method.Call([]reflect.Value{ reflect.ValueOf(tt.Stat), reflect.ValueOf(tt.Value), reflect.ValueOf(tt.Rate)})[0] errInter := e.Interface() if errInter != nil { t.Fatal(errInter.(error)) } data := make([]byte, 128) _, _, err = l.ReadFrom(data) if err != nil { c.Close() t.Fatal(err) } data = bytes.TrimRight(data, "\x00") if !bytes.Equal(data, []byte(tt.Expected)) { c.Close() t.Fatalf("%s got '%s' expected '%s'", tt.Method, data, tt.Expected) } c.Close() } } func TestMultSubStatterClient(t *testing.T) { l, err := newUDPListener("127.0.0.1:0") if err != nil { t.Fatal(err) } defer l.Close() for _, tt := range statsdSubStatterPacketTests { // ignore empty sub test for this, as there is nothing to regex sub if tt.SubPrefix == "" { continue } c, err := NewClient(l.LocalAddr().String(), tt.Prefix) if err != nil { t.Fatal(err) } s1 := c.NewSubStatter("sub1") s2 := c.NewSubStatter("sub2") responses := [][]byte{} for _, s := range []SubStatter{s1, s2} { method := reflect.ValueOf(s).MethodByName(tt.Method) e := method.Call([]reflect.Value{ reflect.ValueOf(tt.Stat), reflect.ValueOf(tt.Value), reflect.ValueOf(tt.Rate)})[0] errInter := e.Interface() if errInter != nil { t.Fatal(errInter.(error)) } data := make([]byte, 128) _, _, err = l.ReadFrom(data) if err != nil { c.Close() t.Fatal(err) } data = bytes.TrimRight(data, "\x00") responses = append(responses, data) } expected := strings.Replace(tt.Expected, "sub.", "sub1.", -1) if !bytes.Equal(responses[0], []byte(expected)) { c.Close() t.Fatalf("%s got '%s' expected '%s'", tt.Method, responses[0], tt.Expected) } expected = strings.Replace(tt.Expected, "sub.", "sub2.", -1) if !bytes.Equal(responses[1], []byte(expected)) { c.Close() t.Fatalf("%s got '%s' expected '%s'", tt.Method, responses[1], tt.Expected) } c.Close() } } func TestSubSubStatterClient(t *testing.T) { l, err := newUDPListener("127.0.0.1:0") if err != nil { t.Fatal(err) } defer l.Close() for _, tt := range statsdSubStatterPacketTests { // ignore empty sub test for this, as there is nothing to regex sub if tt.SubPrefix == "" { continue } c, err := NewClient(l.LocalAddr().String(), tt.Prefix) if err != nil { t.Fatal(err) } s := c.NewSubStatter(tt.SubPrefix).NewSubStatter("sub2") method := reflect.ValueOf(s).MethodByName(tt.Method) e := method.Call([]reflect.Value{ reflect.ValueOf(tt.Stat), reflect.ValueOf(tt.Value), reflect.ValueOf(tt.Rate)})[0] errInter := e.Interface() if errInter != nil { t.Fatal(errInter.(error)) } data := make([]byte, 128) _, _, err = l.ReadFrom(data) if err != nil { c.Close() t.Fatal(err) } data = bytes.TrimRight(data, "\x00") expected := strings.Replace(tt.Expected, "sub.", "sub.sub2.", -1) if !bytes.Equal(data, []byte(expected)) { c.Close() t.Fatalf("%s got '%s' expected '%s'", tt.Method, data, tt.Expected) } c.Close() } } func TestSubStatterClosedClient(t *testing.T) { l, err := newUDPListener("127.0.0.1:0") if err != nil { t.Fatal(err) } defer l.Close() for _, tt := range statsdSubStatterPacketTests { c, err := NewClient(l.LocalAddr().String(), tt.Prefix) if err != nil { t.Fatal(err) } c.Close() s := c.NewSubStatter(tt.SubPrefix) method := reflect.ValueOf(s).MethodByName(tt.Method) e := method.Call([]reflect.Value{ reflect.ValueOf(tt.Stat), reflect.ValueOf(tt.Value), reflect.ValueOf(tt.Rate)})[0] errInter := e.Interface() if errInter == nil { t.Fatal("Expected error but got none") } } } func TestNilSubStatterClient(t *testing.T) { l, err := newUDPListener("127.0.0.1:0") if err != nil { t.Fatal(err) } defer l.Close() for _, tt := range statsdSubStatterPacketTests { var c *Client s := c.NewSubStatter(tt.SubPrefix) method := reflect.ValueOf(s).MethodByName(tt.Method) e := method.Call([]reflect.Value{ reflect.ValueOf(tt.Stat), reflect.ValueOf(tt.Value), reflect.ValueOf(tt.Rate)})[0] errInter := e.Interface() if errInter != nil { t.Fatal(errInter.(error)) } data := make([]byte, 128) n, _, err := l.ReadFrom(data) // this is expected to error, since there should // be no udp data sent, so the read will time out if err == nil || n != 0 { c.Close() t.Fatal(err) } c.Close() } } func TestNoopSubStatterClient(t *testing.T) { l, err := newUDPListener("127.0.0.1:0") if err != nil { t.Fatal(err) } defer l.Close() for _, tt := range statsdSubStatterPacketTests { c, err := NewNoopClient(l.LocalAddr().String(), tt.Prefix) if err != nil { t.Fatal(err) } s := c.NewSubStatter(tt.SubPrefix) method := reflect.ValueOf(s).MethodByName(tt.Method) e := method.Call([]reflect.Value{ reflect.ValueOf(tt.Stat), reflect.ValueOf(tt.Value), reflect.ValueOf(tt.Rate)})[0] errInter := e.Interface() if errInter != nil { t.Fatal(errInter.(error)) } data := make([]byte, 128) n, _, err := l.ReadFrom(data) // this is expected to error, since there should // be no udp data sent, so the read will time out if err == nil || n != 0 { c.Close() t.Fatal(err) } c.Close() } } func ExampleClient_substatter() { // First create a client config. Here is a simple config that sends one // stat per packet (for compatibility). config := &ClientConfig{ Address: "127.0.0.1:8125", Prefix: "test-client", } // Now create the client client, err := NewClientWithConfig(config) // handle any errors if err != nil { log.Fatal(err) } // make sure to close to clean up when done, to avoid leaks. defer client.Close() // create a substatter subclient := client.NewSubStatter("sub") // send a stat err = subclient.Inc("stat1", 42, 1.0) // handle any errors if err != nil { log.Printf("Error sending metric: %+v", err) } } go-statsd-client-3.2.0/statsd/client_test.go000066400000000000000000000142051354156617700210640ustar00rootroot00000000000000// Copyright (c) 2012-2016 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package statsd import ( "bytes" "log" "net" "reflect" "testing" "time" ) var statsdPacketTests = []struct { Prefix string Method string Stat string Value interface{} Rate float32 Expected string }{ {"test", "Gauge", "gauge", int64(1), 1.0, "test.gauge:1|g"}, {"test", "Inc", "count", int64(1), 0.999999, "test.count:1|c|@0.999999"}, {"test", "Inc", "count", int64(1), 1.0, "test.count:1|c"}, {"test", "Dec", "count", int64(1), 1.0, "test.count:-1|c"}, {"test", "Timing", "timing", int64(1), 1.0, "test.timing:1|ms"}, {"test", "TimingDuration", "timing", 1500 * time.Microsecond, 1.0, "test.timing:1.5|ms"}, {"test", "TimingDuration", "timing", 3 * time.Microsecond, 1.0, "test.timing:0.003|ms"}, {"test", "Set", "strset", "pickle", 1.0, "test.strset:pickle|s"}, {"test", "SetInt", "intset", int64(1), 1.0, "test.intset:1|s"}, {"test", "GaugeDelta", "gauge", int64(1), 1.0, "test.gauge:+1|g"}, {"test", "GaugeDelta", "gauge", int64(-1), 1.0, "test.gauge:-1|g"}, {"", "Gauge", "gauge", int64(1), 1.0, "gauge:1|g"}, {"", "Inc", "count", int64(1), 0.999999, "count:1|c|@0.999999"}, {"", "Inc", "count", int64(1), 1.0, "count:1|c"}, {"", "Dec", "count", int64(1), 1.0, "count:-1|c"}, {"", "Timing", "timing", int64(1), 1.0, "timing:1|ms"}, {"", "TimingDuration", "timing", 1500 * time.Microsecond, 1.0, "timing:1.5|ms"}, {"", "Set", "strset", "pickle", 1.0, "strset:pickle|s"}, {"", "SetInt", "intset", int64(1), 1.0, "intset:1|s"}, {"", "GaugeDelta", "gauge", int64(1), 1.0, "gauge:+1|g"}, {"", "GaugeDelta", "gauge", int64(-1), 1.0, "gauge:-1|g"}, } func TestClient(t *testing.T) { l, err := newUDPListener("127.0.0.1:0") if err != nil { t.Fatal(err) } defer l.Close() for _, tt := range statsdPacketTests { c, err := NewClient(l.LocalAddr().String(), tt.Prefix) if err != nil { t.Fatal(err) } method := reflect.ValueOf(c).MethodByName(tt.Method) e := method.Call([]reflect.Value{ reflect.ValueOf(tt.Stat), reflect.ValueOf(tt.Value), reflect.ValueOf(tt.Rate)})[0] errInter := e.Interface() if errInter != nil { t.Fatal(errInter.(error)) } data := make([]byte, 128) _, _, err = l.ReadFrom(data) if err != nil { c.Close() t.Fatal(err) } data = bytes.TrimRight(data, "\x00") if !bytes.Equal(data, []byte(tt.Expected)) { c.Close() t.Fatalf("%s got '%s' expected '%s'", tt.Method, data, tt.Expected) } c.Close() } } func TestNilClient(t *testing.T) { l, err := newUDPListener("127.0.0.1:0") if err != nil { t.Fatal(err) } defer l.Close() for _, tt := range statsdPacketTests { var c *Client method := reflect.ValueOf(c).MethodByName(tt.Method) e := method.Call([]reflect.Value{ reflect.ValueOf(tt.Stat), reflect.ValueOf(tt.Value), reflect.ValueOf(tt.Rate)})[0] errInter := e.Interface() if errInter != nil { t.Fatal(errInter.(error)) } data := make([]byte, 128) n, _, err := l.ReadFrom(data) // this is expected to error, since there should // be no udp data sent, so the read will time out if err == nil || n != 0 { c.Close() t.Fatal(err) } c.Close() } } func TestNoopClient(t *testing.T) { l, err := newUDPListener("127.0.0.1:0") if err != nil { t.Fatal(err) } defer l.Close() for _, tt := range statsdPacketTests { c, err := NewNoopClient(l.LocalAddr().String(), tt.Prefix) if err != nil { t.Fatal(err) } method := reflect.ValueOf(c).MethodByName(tt.Method) e := method.Call([]reflect.Value{ reflect.ValueOf(tt.Stat), reflect.ValueOf(tt.Value), reflect.ValueOf(tt.Rate)})[0] errInter := e.Interface() if errInter != nil { t.Fatal(errInter.(error)) } data := make([]byte, 128) n, _, err := l.ReadFrom(data) // this is expected to error, since there should // be no udp data sent, so the read will time out if err == nil || n != 0 { c.Close() t.Fatal(err) } c.Close() } } func newUDPListener(addr string) (*net.UDPConn, error) { l, err := net.ListenPacket("udp", addr) if err != nil { return nil, err } l.SetDeadline(time.Now().Add(100 * time.Millisecond)) l.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) l.SetWriteDeadline(time.Now().Add(100 * time.Millisecond)) return l.(*net.UDPConn), nil } func ExampleClient() { // First create a client config. Here is a simple config that sends one // stat per packet (for compatibility). config := &ClientConfig{ Address: "127.0.0.1:8125", Prefix: "test-client", } // Now create the client client, err := NewClientWithConfig(config) // and handle any initialization errors if err != nil { log.Fatal(err) } // make sure to clean up defer client.Close() // Send a stat err = client.Inc("stat1", 42, 1.0) // handle any errors if err != nil { log.Printf("Error sending metric: %+v", err) } } func ExampleClient_legacySimple() { // first create a client client, err := NewClient("127.0.0.1:8125", "test-client") // handle any errors if err != nil { log.Fatal(err) } // make sure to close to clean up when done, to avoid leaks. defer client.Close() // Send a stat err = client.Inc("stat1", 42, 1.0) // handle any errors if err != nil { log.Printf("Error sending metric: %+v", err) } } func ExampleClient_noop() { // use interface so we can sub noop client if needed var client Statter var err error // First create a client config. Here is a simple config that sends one // stat per packet (for compatibility). config := &ClientConfig{ Address: "not-resolvable:8125", Prefix: "test-client", } // Now create the client client, err = NewClientWithConfig(config) // Let us say real client creation fails, but you don't care enough about // stats that you don't want your program to run. Just log an error and // make a NoopClient instead if err != nil { log.Println("Remote endpoint did not resolve. Disabling stats", err) } // at this point, client is a nil *Client. This will work like a noop client. // It is ok to call close when client is nil. It will be a noop too. defer client.Close() // Sicne client is nil, this is a noop. err = client.Inc("stat1", 42, 1.0) } go-statsd-client-3.2.0/statsd/doc.go000066400000000000000000000017041354156617700173140ustar00rootroot00000000000000// Copyright (c) 2012-2016 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. /* Package statsd provides a StatsD client implementation that is safe for concurrent use by multiple goroutines and for efficiency can be created and reused. Example usage: // First create a client config. Here is a simple config that sends one // stat per packet (for compatibility). config := &statsd.ClientConfig{ Address: "127.0.0.1:8125", Prefix: "test-client", } // Now create the client client, err := statsd.NewClientWithConfig(config) // and handle any initialization errors if err != nil { log.Fatal(err) } // make sure to clean up defer client.Close() // Send a stat err = client.Inc("stat1", 42, 1.0) // handle any errors if err != nil { log.Printf("Error sending metric: %+v", err) } */ package statsd go-statsd-client-3.2.0/statsd/go.mod000066400000000000000000000000721354156617700173230ustar00rootroot00000000000000module github.com/cactus/go-statsd-client/statsd go 1.12 go-statsd-client-3.2.0/statsd/sender.go000066400000000000000000000026341354156617700200320ustar00rootroot00000000000000// Copyright (c) 2012-2016 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package statsd import ( "errors" "net" ) // The Sender interface wraps a Send and Close type Sender interface { Send(data []byte) (int, error) Close() error } // SimpleSender provides a socket send interface. type SimpleSender struct { // underlying connection c net.PacketConn // resolved udp address ra *net.UDPAddr } // Send sends the data to the server endpoint. func (s *SimpleSender) Send(data []byte) (int, error) { // no need for locking here, as the underlying fdNet // already serialized writes n, err := s.c.(*net.UDPConn).WriteToUDP(data, s.ra) if err != nil { return 0, err } if n == 0 { return n, errors.New("Wrote no bytes") } return n, nil } // Close closes the SimpleSender and cleans up. func (s *SimpleSender) Close() error { err := s.c.Close() return err } // NewSimpleSender returns a new SimpleSender for sending to the supplied // addresss. // // addr is a string of the format "hostname:port", and must be parsable by // net.ResolveUDPAddr. func NewSimpleSender(addr string) (Sender, error) { c, err := net.ListenPacket("udp", ":0") if err != nil { return nil, err } ra, err := net.ResolveUDPAddr("udp", addr) if err != nil { c.Close() return nil, err } sender := &SimpleSender{ c: c, ra: ra, } return sender, nil } go-statsd-client-3.2.0/statsd/sender_buffered.go000066400000000000000000000107041354156617700216710ustar00rootroot00000000000000// Copyright (c) 2012-2016 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package statsd import ( "bytes" "fmt" "sync" "time" ) var senderPool = newBufferPool() // BufferedSender provides a buffered statsd udp, sending multiple // metrics, where possible. type BufferedSender struct { sender Sender flushBytes int flushInterval time.Duration // buffers bufmx sync.Mutex buffer *bytes.Buffer bufs chan *bytes.Buffer // lifecycle runmx sync.RWMutex shutdown chan chan error running bool } // Send bytes. func (s *BufferedSender) Send(data []byte) (int, error) { s.runmx.RLock() if !s.running { s.runmx.RUnlock() return 0, fmt.Errorf("BufferedSender is not running") } s.withBufferLock(func() { blen := s.buffer.Len() if blen > 0 && blen+len(data)+1 >= s.flushBytes { s.swapnqueue() } s.buffer.Write(data) s.buffer.WriteByte('\n') if s.buffer.Len() >= s.flushBytes { s.swapnqueue() } }) s.runmx.RUnlock() return len(data), nil } // Close closes the Buffered Sender and cleans up. func (s *BufferedSender) Close() error { // since we are running, write lock during cleanup s.runmx.Lock() defer s.runmx.Unlock() if !s.running { return nil } errChan := make(chan error) s.running = false s.shutdown <- errChan return <-errChan } // Start Buffered Sender // Begins ticker and read loop func (s *BufferedSender) Start() { // write lock to start running s.runmx.Lock() defer s.runmx.Unlock() if s.running { return } s.running = true s.bufs = make(chan *bytes.Buffer, 32) go s.run() } func (s *BufferedSender) withBufferLock(fn func()) { s.bufmx.Lock() fn() s.bufmx.Unlock() } func (s *BufferedSender) swapnqueue() { if s.buffer.Len() == 0 { return } ob := s.buffer nb := senderPool.Get() s.buffer = nb s.bufs <- ob } func (s *BufferedSender) run() { ticker := time.NewTicker(s.flushInterval) defer ticker.Stop() doneChan := make(chan bool) go func() { for buf := range s.bufs { s.flush(buf) senderPool.Put(buf) } doneChan <- true }() for { select { case <-ticker.C: s.withBufferLock(func() { s.swapnqueue() }) case errChan := <-s.shutdown: s.withBufferLock(func() { s.swapnqueue() }) close(s.bufs) <-doneChan errChan <- s.sender.Close() return } } } // send to remove endpoint and truncate buffer func (s *BufferedSender) flush(b *bytes.Buffer) (int, error) { bb := b.Bytes() bbl := len(bb) if bb[bbl-1] == '\n' { bb = bb[:bbl-1] } //n, err := s.sender.Send(bytes.TrimSuffix(b.Bytes(), []byte("\n"))) n, err := s.sender.Send(bb) b.Truncate(0) // clear the buffer return n, err } // NewBufferedSender returns a new BufferedSender // // addr is a string of the format "hostname:port", and must be parsable by // net.ResolveUDPAddr. // // flushInterval is a time.Duration, and specifies the maximum interval for // packet sending. Note that if you send lots of metrics, you will send more // often. This is just a maximal threshold. // // flushBytes specifies the maximum udp packet size you wish to send. If adding // a metric would result in a larger packet than flushBytes, the packet will // first be send, then the new data will be added to the next packet. func NewBufferedSender(addr string, flushInterval time.Duration, flushBytes int) (Sender, error) { simpleSender, err := NewSimpleSender(addr) if err != nil { return nil, err } return newBufferedSenderWithSender(simpleSender, flushInterval, flushBytes) } // newBufferedSender returns a new BufferedSender // // sender is an instance of a statsd.Sender interface. Sender is required. // // flushInterval is a time.Duration, and specifies the maximum interval for // packet sending. Note that if you send lots of metrics, you will send more // often. This is just a maximal threshold. // // flushBytes specifies the maximum udp packet size you wish to send. If adding // a metric would result in a larger packet than flushBytes, the packet will // first be send, then the new data will be added to the next packet. func newBufferedSenderWithSender(sender Sender, flushInterval time.Duration, flushBytes int) (Sender, error) { if sender == nil { return nil, fmt.Errorf("sender may not be nil") } bufSender := &BufferedSender{ flushBytes: flushBytes, flushInterval: flushInterval, sender: sender, buffer: senderPool.Get(), shutdown: make(chan chan error), } bufSender.Start() return bufSender, nil } go-statsd-client-3.2.0/statsd/sender_buffered_test.go000066400000000000000000000045771354156617700227430ustar00rootroot00000000000000// Copyright (c) 2012-2016 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package statsd import ( "bytes" "testing" "time" ) type mockSender struct { closeCallCount int } func (m *mockSender) Send(data []byte) (int, error) { return 0, nil } func (m *mockSender) Close() error { m.closeCallCount++ return nil } func TestClose(t *testing.T) { mockSender := &mockSender{} sender := &BufferedSender{ flushBytes: 512, flushInterval: 1 * time.Second, sender: mockSender, buffer: bytes.NewBuffer(make([]byte, 0, 512)), shutdown: make(chan chan error), } sender.Close() if mockSender.closeCallCount != 0 { t.Fatalf("expected close to have been called zero times, but got %d", mockSender.closeCallCount) } sender.Start() if !sender.running { t.Fatal("sender failed to start") } sender.Close() if mockSender.closeCallCount != 1 { t.Fatalf("expected close to have been called once, but got %d", mockSender.closeCallCount) } } func TestCloseConcurrent(t *testing.T) { mockSender := &mockSender{} sender := &BufferedSender{ flushBytes: 512, flushInterval: 1 * time.Second, sender: mockSender, buffer: bytes.NewBuffer(make([]byte, 0, 512)), shutdown: make(chan chan error), } sender.Start() const N = 10 c := make(chan struct{}, N) for i := 0; i < N; i++ { go func() { sender.Close() c <- struct{}{} }() } for i := 0; i < N; i++ { <-c } if mockSender.closeCallCount != 1 { t.Errorf("expected close to have been called once, but got %d", mockSender.closeCallCount) } } func TestCloseDuringSendConcurrent(t *testing.T) { mockSender := &mockSender{} sender := &BufferedSender{ flushBytes: 512, flushInterval: 1 * time.Second, sender: mockSender, buffer: bytes.NewBuffer(make([]byte, 0, 512)), shutdown: make(chan chan error), } sender.Start() const N = 10 c := make(chan struct{}, N) for i := 0; i < N; i++ { go func() { for { _, err := sender.Send([]byte("stat:1|c")) if err != nil { c <- struct{}{} return } } }() } // senders should error out now // we should not receive any panics sender.Close() for i := 0; i < N; i++ { <-c } if mockSender.closeCallCount != 1 { t.Errorf("expected close to have been called once, but got %d", mockSender.closeCallCount) } } go-statsd-client-3.2.0/statsd/sender_resolving.go000066400000000000000000000070621354156617700221220ustar00rootroot00000000000000// Copyright (c) 2012-2016 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package statsd import ( "errors" "fmt" "net" "sync" "time" ) // ResolvingSimpleSender provides a socket send interface that re-resolves and // reconnects. type ResolvingSimpleSender struct { // underlying connection conn net.PacketConn // resolved udp address addrResolved *net.UDPAddr // unresolved addr addrUnresolved string // interval time reresolveInterval time.Duration // lifecycle mx sync.RWMutex doneChan chan struct{} running bool } // Send sends the data to the server endpoint. func (s *ResolvingSimpleSender) Send(data []byte) (int, error) { s.mx.RLock() if !s.running { s.mx.RUnlock() return 0, fmt.Errorf("ResolvingSimpleSender is not running") } // no need for locking here, as the underlying fdNet // already serialized writes n, err := s.conn.(*net.UDPConn).WriteToUDP(data, s.addrResolved) // unlock manually, and early (vs doing a defer) to avoid some overhead s.mx.RUnlock() if err != nil { return 0, err } if n == 0 { return n, errors.New("Wrote no bytes") } return n, nil } // Close closes the ResolvingSender and cleans up func (s *ResolvingSimpleSender) Close() error { // lock to guard against ra reconnection modification s.mx.Lock() defer s.mx.Unlock() if !s.running { return nil } s.running = false close(s.doneChan) err := s.conn.Close() return err } func (s *ResolvingSimpleSender) Reconnect() { // lock to guard against s.running mutation s.mx.RLock() if !s.running { s.mx.RUnlock() return } // get old addr for comparison, then release lock (asap) oldAddr := s.addrResolved.String() // done with rlock for now s.mx.RUnlock() // ro doesn't change, so no need to lock addrResolved, err := net.ResolveUDPAddr("udp", s.addrUnresolved) if err != nil { // no good new address.. so continue with old address return } if oldAddr == addrResolved.String() { // got same address.. so continue with old address return } // acquire write lock to both guard against s.running having been mutated in the // meantime, as well as for safely setting s.ra s.mx.Lock() // check running again, just to be sure nothing was terminated in the meantime... if s.running { s.addrResolved = addrResolved } s.mx.Unlock() } // Start Resolving Simple Sender // Begins ticker and read loop func (s *ResolvingSimpleSender) Start() { // write lock to start running s.mx.Lock() defer s.mx.Unlock() if s.running { return } s.running = true go s.run() } func (s *ResolvingSimpleSender) run() { ticker := time.NewTicker(s.reresolveInterval) defer ticker.Stop() for { select { case <-s.doneChan: return case <-ticker.C: // reconnect locks/checks running, so no need to do it here s.Reconnect() } } } // NewResolvingSimpleSender returns a new ResolvingSimpleSender for // sending to the supplied addresss. // // addr is a string of the format "hostname:port", and must be parsable by // net.ResolveUDPAddr. func NewResolvingSimpleSender(addr string, interval time.Duration) (Sender, error) { conn, err := net.ListenPacket("udp", ":0") if err != nil { return nil, err } addrResolved, err := net.ResolveUDPAddr("udp", addr) if err != nil { conn.Close() return nil, err } sender := &ResolvingSimpleSender{ conn: conn, addrResolved: addrResolved, addrUnresolved: addr, reresolveInterval: interval, doneChan: make(chan struct{}), running: false, } sender.Start() return sender, nil } go-statsd-client-3.2.0/statsd/statsdtest/000077500000000000000000000000001354156617700204205ustar00rootroot00000000000000go-statsd-client-3.2.0/statsd/statsdtest/recorder.go000066400000000000000000000040411354156617700225530ustar00rootroot00000000000000package 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)) copy(results[i].Raw, e.Raw) } 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 } go-statsd-client-3.2.0/statsd/statsdtest/recorder_test.go000066400000000000000000000032241354156617700236140ustar00rootroot00000000000000package 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 { // just dive out because everything else relies on ordering t.Fatalf("Did not capture all stats sent; got: %s", sent) } 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) } } go-statsd-client-3.2.0/statsd/statsdtest/stat.go000066400000000000000000000057111354156617700217260ustar00rootroot00000000000000package statsdtest import ( "bytes" "fmt" "strings" ) // 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 } // String fulfils the stringer interface func (s *Stat) String() string { return fmt.Sprintf("%s %s %s", s.Stat, s.Value, s.Rate) } // 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)) copy(d, src) // standard protocol 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 } // Stats is a slice of Stat 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 } // String fulfils the stringer interface func (s Stats) String() string { if len(s) == 0 { return "" } r := make([]string, len(s)) for i, e := range s { r[i] = e.String() } return strings.Join(r, "\n") } go-statsd-client-3.2.0/statsd/statsdtest/stat_test.go000066400000000000000000000065541354156617700227730ustar00rootroot00000000000000package statsdtest import ( "bytes" "reflect" "testing" ) type parsingTestCase struct { name string sent [][]byte expected Stats } var ( badStatNameOnly = []byte("foo.bar.baz:") bsnoStat = Stat{ Raw: badStatNameOnly, Stat: "foo.bar.baz", Parsed: false, } gaugeWithoutRate = []byte("foo.bar.baz:1.000|g") gworStat = Stat{ Raw: gaugeWithoutRate, Stat: "foo.bar.baz", Value: "1.000", Tag: "g", Parsed: true, } counterWithRate = []byte("foo.bar.baz:1.000|c|@0.75") cwrStat = Stat{ Raw: counterWithRate, Stat: "foo.bar.baz", Value: "1.000", Tag: "c", Rate: "0.75", Parsed: true, } stringStat = []byte(":some string value|s") sStat = Stat{ Raw: stringStat, Stat: "", Value: "some string value", Tag: "s", Parsed: true, } badValue = []byte("asoentuh") bvStat = Stat{Raw: badValue} testCases = []parsingTestCase{ {name: "no stat data", sent: [][]byte{badStatNameOnly}, expected: Stats{bsnoStat}}, {name: "trivial case", sent: [][]byte{gaugeWithoutRate}, expected: Stats{gworStat}}, {name: "multiple simple", sent: [][]byte{gaugeWithoutRate, counterWithRate}, expected: Stats{gworStat, cwrStat}}, {name: "mixed good and bad", sent: [][]byte{badValue, badValue, stringStat, badValue, counterWithRate, badValue}, expected: Stats{bvStat, bvStat, sStat, bvStat, cwrStat, bvStat}}, } ) func TestParseBytes(t *testing.T) { for _, tc := range testCases { got := ParseStats(bytes.Join(tc.sent, []byte("\n"))) want := tc.expected if !reflect.DeepEqual(got, want) { t.Errorf("%s: got: %+v, want: %+v", tc.name, got, want) } } } func TestStatsUnparsed(t *testing.T) { start := Stats{bsnoStat, gworStat, bsnoStat, bsnoStat, cwrStat} got := start.Unparsed() want := Stats{bsnoStat, bsnoStat, bsnoStat} if !reflect.DeepEqual(got, want) { t.Errorf("got: %+v, want: %+v", got, want) } } func TestStatsCollectNamed(t *testing.T) { type test struct { name string start Stats want Stats matchOn string } cases := []test{ {"No matches", Stats{bsnoStat, cwrStat}, nil, "foo"}, {"One match", Stats{bsnoStat, Stat{Stat: "foo"}, cwrStat}, Stats{Stat{Stat: "foo"}}, "foo"}, {"Two matches", Stats{bsnoStat, Stat{Stat: "foo"}, cwrStat}, Stats{bsnoStat, cwrStat}, "foo.bar.baz"}, } for _, c := range cases { got := c.start.CollectNamed(c.matchOn) if !reflect.DeepEqual(got, c.want) { t.Errorf("%s: got: %+v, want: %+v", c.name, got, c.want) } } } func TestStatsCollect(t *testing.T) { type test struct { name string start Stats want Stats pred func(Stat) bool } cases := []test{ {"Not called", Stats{}, nil, func(_ Stat) bool { t.Errorf("should not be called"); return true }}, {"Matches value = 1.000", Stats{bsnoStat, gworStat, cwrStat, sStat, bsnoStat}, Stats{gworStat, cwrStat}, func(s Stat) bool { return s.Value == "1.000" }}, } for _, c := range cases { got := c.start.Collect(c.pred) if !reflect.DeepEqual(got, c.want) { t.Errorf("%s: got: %+v, want: %+v", c.name, got, c.want) } } } func TestStatsValues(t *testing.T) { start := Stats{bsnoStat, sStat, gworStat} got := start.Values() want := []string{bsnoStat.Value, sStat.Value, gworStat.Value} if !reflect.DeepEqual(got, want) { t.Errorf("got: %+v, want: %+v", got, want) } } go-statsd-client-3.2.0/statsd/validator.go000066400000000000000000000015271354156617700205370ustar00rootroot00000000000000// Copyright (c) 2012-2016 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package statsd import ( "fmt" "net" "regexp" ) // The ValidatorFunc type defines a function that can serve // as a stat name validation function. type ValidatorFunc func(string) error var safeName = regexp.MustCompile(`^[a-zA-Z0-9\-_.]+$`) // CheckName may be used to validate whether a stat name contains invalid // characters. If invalid characters are found, the function will return an // error. func CheckName(stat string) error { if !safeName.MatchString(stat) { return fmt.Errorf("invalid stat name: %s", stat) } return nil } func mustBeIP(hostport string) bool { host, _, err := net.SplitHostPort(hostport) if err != nil { return false } ip := net.ParseIP(host) return ip != nil } go-statsd-client-3.2.0/statsd/validator_test.go000066400000000000000000000011371354156617700215730ustar00rootroot00000000000000// Copyright (c) 2012-2016 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package statsd import "testing" var validatorTests = []struct { Stat string Valid bool }{ {"test.one", true}, {"test#two", false}, {"test|three", false}, {"test@four", false}, } func TestValidator(t *testing.T) { var err error for _, tt := range validatorTests { err = CheckName(tt.Stat) switch { case err != nil && tt.Valid: t.Fatal(err) case err == nil && !tt.Valid: t.Fatalf("validation should have failed for %s", tt.Stat) } } } go-statsd-client-3.2.0/test-client/000077500000000000000000000000001354156617700171475ustar00rootroot00000000000000go-statsd-client-3.2.0/test-client/main.go000066400000000000000000000065251354156617700204320ustar00rootroot00000000000000// Copyright (c) 2012-2016 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package main import ( "fmt" "log" "os" "time" "github.com/cactus/go-statsd-client/statsd" flags "github.com/jessevdk/go-flags" ) func main() { // command line flags var opts struct { HostPort string `long:"host" default:"127.0.0.1:8125" description:"host:port of statsd server"` Prefix string `long:"prefix" default:"test-client" description:"Statsd prefix"` StatType string `long:"type" default:"count" description:"stat type to send. Can be one of: timing, count, gauge"` StatValue int64 `long:"value" default:"1" description:"Value to send"` Name string `short:"n" long:"name" default:"counter" description:"stat name"` Rate float32 `short:"r" long:"rate" default:"1.0" description:"sample rate"` Volume int `short:"c" long:"count" default:"1000" description:"Number of stats to send. Volume."` Nil bool `long:"nil" description:"Use nil client"` Buffered bool `long:"buffered" description:"Use a buffered client"` Duration time.Duration `short:"d" long:"duration" default:"10s" description:"How long to spread the volume across. For each second of duration, volume/seconds events will be sent."` } // parse said flags _, err := flags.Parse(&opts) if err != nil { if e, ok := err.(*flags.Error); ok { if e.Type == flags.ErrHelp { os.Exit(0) } } fmt.Printf("Error: %+v\n", err) os.Exit(1) } if opts.Nil && opts.Buffered { fmt.Printf("Specifying both nil and buffered together is invalid\n") os.Exit(1) } if opts.Name == "" || statsd.CheckName(opts.Name) != nil { fmt.Printf("Stat name contains invalid characters\n") os.Exit(1) } if statsd.CheckName(opts.Prefix) != nil { fmt.Printf("Stat prefix contains invalid characters\n") os.Exit(1) } config := &statsd.ClientConfig{ Address: opts.HostPort, Prefix: opts.Prefix, ResInterval: time.Duration(0), } var client statsd.Statter if !opts.Nil { if opts.Buffered { config.UseBuffered = true config.FlushInterval = opts.Duration / time.Duration(4) config.FlushBytes = 0 } client, err = statsd.NewClientWithConfig(config) if err != nil { log.Fatal(err) } defer client.Close() } var stat func(stat string, value int64, rate float32) error switch opts.StatType { case "count": stat = func(stat string, value int64, rate float32) error { return client.Inc(stat, value, rate) } case "gauge": stat = func(stat string, value int64, rate float32) error { return client.Gauge(stat, value, rate) } case "timing": stat = func(stat string, value int64, rate float32) error { return client.Timing(stat, value, rate) } default: log.Fatal("Unsupported state type") } pertick := opts.Volume / int(opts.Duration.Seconds()) / 10 // add some extra time, because the first tick takes a while ender := time.After(opts.Duration + 100*time.Millisecond) c := time.Tick(time.Second / 10) count := 0 for { select { case <-c: for x := 0; x < pertick; x++ { err := stat(opts.Name, opts.StatValue, opts.Rate) if err != nil { log.Printf("Got Error: %+v\n", err) break } count++ } case <-ender: log.Printf("%d events called\n", count) return } } }