├── .travis.yml ├── nop_test.go ├── mock ├── mock_test.go └── mock.go ├── sender_example_test.go ├── nop.go ├── handler_test.go ├── LICENSE ├── handler_pre17_test.go ├── expvar ├── expvar_test.go └── expvar.go ├── handler_example_test.go ├── sender_test.go ├── handler_pre17.go ├── handler.go ├── xstats_example_test.go ├── sender.go ├── statsd ├── statsd_test.go └── statsd.go ├── prometheus ├── prometheus_test.go └── prometheus.go ├── telegraf ├── telegraf_test.go └── telegraf.go ├── dogstatsd ├── dogstatsd_test.go └── dogstatsd.go ├── README.md ├── xstats_test.go └── xstats.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.7 4 | - tip 5 | matrix: 6 | allow_failures: 7 | - go: tip 8 | -------------------------------------------------------------------------------- /nop_test.go: -------------------------------------------------------------------------------- 1 | package xstats 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestNop(t *testing.T) { 9 | nop.AddTags("tag") 10 | nop.Gauge("metric", 1) 11 | nop.Count("metric", 1) 12 | nop.Histogram("metric", 1) 13 | nop.Timing("metric", 1*time.Second) 14 | } 15 | -------------------------------------------------------------------------------- /mock/mock_test.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "testing" 5 | "github.com/stretchr/testify/assert" 6 | "github.com/rs/xstats" 7 | "time" 8 | ) 9 | 10 | func TestNew(t *testing.T) { 11 | s := New() 12 | assert.Implements(t, (*xstats.Sender)(nil), s) 13 | } 14 | 15 | func TestSender(t *testing.T) { 16 | s := New() 17 | d := time.Second 18 | tags := []string{"2", "3"} 19 | s.On("Timing", "1", d, tags) 20 | s.Timing("1", d, tags...) 21 | s.On("Count", "1", 2.0, tags) 22 | s.Count("1", 2.0, tags...) 23 | s.On("Gauge", "1", 2.0, tags) 24 | s.Gauge("1", 2.0, tags...) 25 | s.On("Histogram", "1", 2.0, tags) 26 | s.Histogram("1", 2.0, tags...) 27 | s.AssertExpectations(t) 28 | } -------------------------------------------------------------------------------- /sender_example_test.go: -------------------------------------------------------------------------------- 1 | package xstats_test 2 | 3 | import ( 4 | "net" 5 | "time" 6 | 7 | "github.com/rs/xstats" 8 | "github.com/rs/xstats/dogstatsd" 9 | "github.com/rs/xstats/expvar" 10 | ) 11 | 12 | func ExampleMultiSender() { 13 | // Create an expvar sender 14 | s1 := expvar.New("stats") 15 | 16 | // Create the stats sender 17 | statsdWriter, _ := net.Dial("udp", "127.0.0.1:8126") 18 | s2 := dogstatsd.New(statsdWriter, 5*time.Second) 19 | 20 | // Create a xstats with a sender composed of the previous two. 21 | // You may also create a NewHandler() the same way. 22 | s := xstats.New(xstats.MultiSender{s1, s2}) 23 | 24 | // Send some observations 25 | s.Count("requests", 1, "tag") 26 | s.Timing("something", 5*time.Millisecond, "tag") 27 | } 28 | -------------------------------------------------------------------------------- /nop.go: -------------------------------------------------------------------------------- 1 | package xstats 2 | 3 | import "time" 4 | 5 | type nopS struct { 6 | } 7 | 8 | var nop = &nopS{} 9 | 10 | // AddTags implements XStats interface 11 | func (rc *nopS) AddTags(tags ...string) { 12 | } 13 | 14 | // GetTags implements XStats interface 15 | func (rc *nopS) GetTags() []string { 16 | return nil 17 | } 18 | 19 | // Gauge implements XStats interface 20 | func (rc *nopS) Gauge(stat string, value float64, tags ...string) { 21 | } 22 | 23 | // Count implements XStats interface 24 | func (rc *nopS) Count(stat string, count float64, tags ...string) { 25 | } 26 | 27 | // Histogram implements XStats interface 28 | func (rc *nopS) Histogram(stat string, value float64, tags ...string) { 29 | } 30 | 31 | // Timing implements XStats interface 32 | func (rc *nopS) Timing(stat string, duration time.Duration, tags ...string) { 33 | } 34 | -------------------------------------------------------------------------------- /handler_test.go: -------------------------------------------------------------------------------- 1 | // +build go1.7 2 | 3 | package xstats 4 | 5 | import ( 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestHandler(t *testing.T) { 13 | s := &fakeSender{} 14 | n := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 | xs, ok := FromRequest(r).(*xstats) 16 | assert.True(t, ok) 17 | assert.Equal(t, s, xs.s) 18 | assert.Equal(t, []string{"envtag"}, xs.tags) 19 | }) 20 | h := NewHandler(s, []string{"envtag"})(n) 21 | h.ServeHTTP(nil, &http.Request{}) 22 | } 23 | 24 | func TestHandlerPrefix(t *testing.T) { 25 | s := &fakeSender{} 26 | n := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 27 | xs, ok := FromRequest(r).(*xstats) 28 | assert.True(t, ok) 29 | assert.Equal(t, s, xs.s) 30 | assert.Equal(t, []string{"envtag"}, xs.tags) 31 | assert.Equal(t, "prefix.", xs.prefix) 32 | }) 33 | h := NewHandlerPrefix(s, []string{"envtag"}, "prefix.")(n) 34 | h.ServeHTTP(nil, &http.Request{}) 35 | } 36 | -------------------------------------------------------------------------------- /mock/mock.go: -------------------------------------------------------------------------------- 1 | // Package mock implements mock object for xstats Sender interface based on 2 | // github.com/stretchr/testify/mock package mock implementation 3 | package mock 4 | 5 | import ( 6 | "time" 7 | 8 | "github.com/stretchr/testify/mock" 9 | ) 10 | 11 | type sender struct { 12 | mock.Mock 13 | } 14 | 15 | // New creates an instance of type sender 16 | func New() *sender { 17 | return new(sender) 18 | } 19 | 20 | // Gauge implements xstats.Sender interface 21 | func (s *sender) Gauge(stat string, value float64, tags ...string) { 22 | s.Called(stat, value, tags) 23 | } 24 | 25 | // Count implements xstats.Sender interface 26 | func (s *sender) Count(stat string, count float64, tags ...string) { 27 | s.Called(stat, count, tags) 28 | } 29 | 30 | // Histogram implements xstats.Sender interface 31 | func (s *sender) Histogram(stat string, value float64, tags ...string) { 32 | s.Called(stat, value, tags) 33 | } 34 | 35 | // Timing implements xstats.Sender interface 36 | func (s *sender) Timing(stat string, value time.Duration, tags ...string) { 37 | s.Called(stat, value, tags) 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Olivier Poitrey 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /handler_pre17_test.go: -------------------------------------------------------------------------------- 1 | // +build !go1.7 2 | 3 | package xstats 4 | 5 | import ( 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/rs/xhandler" 10 | "github.com/stretchr/testify/assert" 11 | "golang.org/x/net/context" 12 | ) 13 | 14 | func TestHandler(t *testing.T) { 15 | s := &fakeSender{} 16 | n := xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { 17 | xs, ok := FromContext(ctx).(*xstats) 18 | assert.True(t, ok) 19 | assert.Equal(t, s, xs.s) 20 | assert.Equal(t, []string{"envtag"}, xs.tags) 21 | }) 22 | h := NewHandler(s, []string{"envtag"})(n) 23 | h.ServeHTTPC(context.Background(), nil, nil) 24 | } 25 | 26 | func TestHandlerPrefix(t *testing.T) { 27 | s := &fakeSender{} 28 | n := xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { 29 | xs, ok := FromContext(ctx).(*xstats) 30 | assert.True(t, ok) 31 | assert.Equal(t, s, xs.s) 32 | assert.Equal(t, []string{"envtag"}, xs.tags) 33 | assert.Equal(t, "prefix.", xs.prefix) 34 | }) 35 | h := NewHandlerPrefix(s, []string{"envtag"}, "prefix.")(n) 36 | h.ServeHTTPC(context.Background(), nil, nil) 37 | } 38 | -------------------------------------------------------------------------------- /expvar/expvar_test.go: -------------------------------------------------------------------------------- 1 | package expvar 2 | 3 | import ( 4 | "expvar" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestNew(t *testing.T) { 11 | // Publishes prefix in expvar, panics the second time 12 | assert.Nil(t, expvar.Get("name")) 13 | New("name") 14 | assert.NotNil(t, expvar.Get("name")) 15 | assert.IsType(t, expvar.Get("name"), &expvar.Map{}) 16 | assert.Panics(t, func() { 17 | New("name") 18 | }) 19 | } 20 | 21 | func TestGauge(t *testing.T) { 22 | s := New("gauge") 23 | v := expvar.Get("gauge").(*expvar.Map) 24 | s.Gauge("test", 1) 25 | assert.Equal(t, "1", v.Get("test").String()) 26 | s.Gauge("test", -1) 27 | assert.Equal(t, "-1", v.Get("test").String()) 28 | } 29 | 30 | func TestCount(t *testing.T) { 31 | s := New("count") 32 | v := expvar.Get("count").(*expvar.Map) 33 | s.Count("test", 1) 34 | assert.Equal(t, "1", v.Get("test").String()) 35 | s.Count("test", -1) 36 | assert.Equal(t, "0", v.Get("test").String()) 37 | } 38 | 39 | func TestHistogram(t *testing.T) { 40 | s := New("histogram") 41 | s.Histogram("test", 1) 42 | } 43 | 44 | func TestTiming(t *testing.T) { 45 | s := New("timing") 46 | s.Timing("test", 1) 47 | } 48 | -------------------------------------------------------------------------------- /handler_example_test.go: -------------------------------------------------------------------------------- 1 | // +build go1.7 2 | 3 | package xstats_test 4 | 5 | import ( 6 | "log" 7 | "net" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/rs/xhandler" 12 | "github.com/rs/xstats" 13 | "github.com/rs/xstats/dogstatsd" 14 | ) 15 | 16 | func ExampleNewHandler() { 17 | c := xhandler.Chain{} 18 | 19 | // Install the metric handler with dogstatsd backend client and some env tags 20 | flushInterval := 5 * time.Second 21 | tags := []string{"role:my-service"} 22 | statsdWriter, err := net.Dial("udp", "127.0.0.1:8126") 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | c.Use(xstats.NewHandler(dogstatsd.New(statsdWriter, flushInterval), tags)) 27 | 28 | // Here is your handler 29 | h := c.HandlerH(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 30 | // Get the xstats request's instance from the context. You can safely assume it will 31 | // be always there, if the handler is removed, xstats.FromContext will return a nop 32 | // instance. 33 | m := xstats.FromRequest(r) 34 | 35 | // Count something 36 | m.Count("requests", 1, "route:index") 37 | })) 38 | 39 | http.Handle("/", h) 40 | 41 | if err := http.ListenAndServe(":8080", nil); err != nil { 42 | log.Fatal(err) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /expvar/expvar.go: -------------------------------------------------------------------------------- 1 | package expvar 2 | 3 | import ( 4 | "expvar" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/rs/xstats" 9 | ) 10 | 11 | type sender struct { 12 | vars *expvar.Map 13 | } 14 | 15 | // A expvar.Var static float 16 | type float float64 17 | 18 | // String implements the expvar.Var 19 | func (f float) String() string { 20 | return strconv.FormatFloat(float64(f), 'g', -1, 64) 21 | } 22 | 23 | // New creates a statsd sender that publish observations in expvar under 24 | // the given prefix "path". Will panic if the prefix is already used. 25 | // 26 | // Tags are ignored. Histogram and Timing methods are not supported. 27 | func New(prefix string) xstats.Sender { 28 | return &sender{expvar.NewMap(prefix)} 29 | } 30 | 31 | // Gauge implements xstats.Sender interface 32 | func (s sender) Gauge(stat string, value float64, tags ...string) { 33 | s.vars.Set(stat, float(value)) 34 | } 35 | 36 | // Count implements xstats.Sender interface 37 | func (s sender) Count(stat string, count float64, tags ...string) { 38 | s.vars.AddFloat(stat, count) 39 | } 40 | 41 | // Histogram implements xstats.Sender interface 42 | func (s sender) Histogram(stat string, value float64, tags ...string) { 43 | // Not supported, just ignored 44 | } 45 | 46 | // Timing implements xstats.Sender interface 47 | func (s sender) Timing(stat string, duration time.Duration, tags ...string) { 48 | // Not supported, just ignored 49 | } 50 | -------------------------------------------------------------------------------- /sender_test.go: -------------------------------------------------------------------------------- 1 | package xstats 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestMultiSender(t *testing.T) { 12 | fs1 := &fakeSender{} 13 | fs2 := &fakeSendCloser{err: errors.New("foo")} 14 | fs3 := &fakeSendCloser{err: errors.New("bar")} 15 | m := MultiSender{fs1, fs2, fs3} 16 | 17 | m.Count("foo", 1, "bar", "baz") 18 | countCmd := cmd{"Count", "foo", 1, []string{"bar", "baz"}} 19 | assert.Equal(t, countCmd, fs1.last) 20 | assert.Equal(t, countCmd, fs2.last) 21 | assert.Equal(t, countCmd, fs3.last) 22 | 23 | m.Gauge("foo", 1, "bar", "baz") 24 | gaugeCmd := cmd{"Gauge", "foo", 1, []string{"bar", "baz"}} 25 | assert.Equal(t, gaugeCmd, fs1.last) 26 | assert.Equal(t, gaugeCmd, fs2.last) 27 | assert.Equal(t, gaugeCmd, fs3.last) 28 | 29 | m.Histogram("foo", 1, "bar", "baz") 30 | histoCmd := cmd{"Histogram", "foo", 1, []string{"bar", "baz"}} 31 | assert.Equal(t, histoCmd, fs1.last) 32 | assert.Equal(t, histoCmd, fs2.last) 33 | assert.Equal(t, histoCmd, fs3.last) 34 | 35 | m.Timing("foo", 1*time.Second, "bar", "baz") 36 | timingCmd := cmd{"Timing", "foo", 1, []string{"bar", "baz"}} 37 | assert.Equal(t, timingCmd, fs1.last) 38 | assert.Equal(t, timingCmd, fs2.last) 39 | assert.Equal(t, timingCmd, fs3.last) 40 | 41 | assert.Equal(t, fs2.err, CloseSender(m)) 42 | assert.Equal(t, timingCmd, fs1.last) 43 | assert.Equal(t, cmd{name: "Close"}, fs2.last) 44 | assert.Equal(t, cmd{name: "Close"}, fs3.last) 45 | } 46 | -------------------------------------------------------------------------------- /handler_pre17.go: -------------------------------------------------------------------------------- 1 | // +build !go1.7 2 | 3 | package xstats 4 | 5 | import ( 6 | "net/http" 7 | 8 | "github.com/rs/xhandler" 9 | "golang.org/x/net/context" 10 | ) 11 | 12 | // Handler injects a per request metrics client in the net/context which can be 13 | // retrived using xstats.FromContext(ctx) 14 | type Handler struct { 15 | s Sender 16 | tags []string 17 | prefix string 18 | } 19 | 20 | type key int 21 | 22 | const xstatsKey key = 0 23 | 24 | // NewContext returns a copy of the parent context and associates it with passed stats. 25 | func NewContext(ctx context.Context, xs XStater) context.Context { 26 | return context.WithValue(ctx, xstatsKey, xs) 27 | } 28 | 29 | // FromContext retreives the request's xstats client from a given context if any. 30 | // If no xstats is embeded in the context, a nop instance is returned so you can 31 | // use it safely without having to test for it's presence. 32 | func FromContext(ctx context.Context) XStater { 33 | rc, ok := ctx.Value(xstatsKey).(XStater) 34 | if ok { 35 | return rc 36 | } 37 | return nop 38 | } 39 | 40 | // NewHandler creates a new handler with the provided metric client. 41 | // If some tags are provided, the will be added to all logged metrics. 42 | func NewHandler(s Sender, tags []string) func(xhandler.HandlerC) xhandler.HandlerC { 43 | return NewHandlerPrefix(s, tags, "") 44 | } 45 | 46 | // NewHandlerPrefix creates a new handler with the provided metric client. 47 | // If some tags are provided, the will be added to all logged metrics. 48 | // If the prefix argument is provided, all produced metrics will have this 49 | // prefix prepended. 50 | func NewHandlerPrefix(s Sender, tags []string, prefix string) func(xhandler.HandlerC) xhandler.HandlerC { 51 | return func(next xhandler.HandlerC) xhandler.HandlerC { 52 | return xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { 53 | xs := NewPrefix(s, prefix).(*xstats) 54 | xs.AddTags(tags...) 55 | ctx = NewContext(ctx, xs) 56 | next.ServeHTTPC(ctx, w, r) 57 | xs.Close() 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | // +build go1.7 2 | 3 | package xstats 4 | 5 | import ( 6 | "net/http" 7 | 8 | "context" 9 | ) 10 | 11 | // Handler injects a per request metrics client in the net/context which can be 12 | // retrived using xstats.FromContext(ctx) 13 | type Handler struct { 14 | s Sender 15 | tags []string 16 | prefix string 17 | } 18 | 19 | type key int 20 | 21 | const xstatsKey key = 0 22 | 23 | // NewContext returns a copy of the parent context and associates it with passed stats. 24 | func NewContext(ctx context.Context, xs XStater) context.Context { 25 | return context.WithValue(ctx, xstatsKey, xs) 26 | } 27 | 28 | // FromContext retreives the request's xstats client from a given context if any. 29 | // If no xstats is embeded in the context, a nop instance is returned so you can 30 | // use it safely without having to test for it's presence. 31 | func FromContext(ctx context.Context) XStater { 32 | rc, ok := ctx.Value(xstatsKey).(XStater) 33 | if ok { 34 | return rc 35 | } 36 | return nop 37 | } 38 | 39 | // FromRequest gets the xstats client in the request's context. 40 | // This is a shortcut for xstats.FromContext(r.Context()) 41 | func FromRequest(r *http.Request) XStater { 42 | if r == nil { 43 | return nop 44 | } 45 | return FromContext(r.Context()) 46 | } 47 | 48 | // NewHandler creates a new handler with the provided metric client. 49 | // If some tags are provided, the will be added to all logged metrics. 50 | func NewHandler(s Sender, tags []string) func(http.Handler) http.Handler { 51 | return NewHandlerPrefix(s, tags, "") 52 | } 53 | 54 | // NewHandlerPrefix creates a new handler with the provided metric client. 55 | // If some tags are provided, the will be added to all logged metrics. 56 | // If the prefix argument is provided, all produced metrics will have this 57 | // prefix prepended. 58 | func NewHandlerPrefix(s Sender, tags []string, prefix string) func(http.Handler) http.Handler { 59 | return func(next http.Handler) http.Handler { 60 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 61 | xs := NewPrefix(s, prefix).(*xstats) 62 | xs.AddTags(tags...) 63 | ctx := NewContext(r.Context(), xs) 64 | next.ServeHTTP(w, r.WithContext(ctx)) 65 | xs.Close() 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /xstats_example_test.go: -------------------------------------------------------------------------------- 1 | package xstats_test 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "time" 7 | 8 | "github.com/rs/xstats" 9 | "github.com/rs/xstats/dogstatsd" 10 | ) 11 | 12 | func ExampleNew() { 13 | // Defines interval between flushes to statsd server 14 | flushInterval := 5 * time.Second 15 | 16 | // Connection to the statsd server 17 | statsdWriter, err := net.Dial("udp", "127.0.0.1:8126") 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | 22 | // Create the stats client 23 | s := xstats.New(dogstatsd.New(statsdWriter, flushInterval)) 24 | 25 | // Global tags sent with all metrics (only with supported clients like datadog's) 26 | s.AddTags("role:my-service", "dc:sv6") 27 | 28 | // Send some observations 29 | s.Count("requests", 1, "tag") 30 | s.Timing("something", 5*time.Millisecond, "tag") 31 | } 32 | 33 | func ExampleNewScoping() { 34 | // Defines interval between flushes to statsd server 35 | flushInterval := 5 * time.Second 36 | 37 | // Connection to the statsd server 38 | statsdWriter, err := net.Dial("udp", "127.0.0.1:8126") 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | // Create the stats client 44 | s := xstats.NewScoping(dogstatsd.New(statsdWriter, flushInterval), ".", "my-thing") 45 | 46 | // Global tags sent with all metrics (only with supported clients like datadog's) 47 | s.AddTags("role:my-service", "dc:sv6") 48 | 49 | // Send some observations 50 | s.Count("requests", 1, "tag") 51 | s.Timing("something", 5*time.Millisecond, "tag") 52 | 53 | // Scope the client 54 | ss := xstats.Scope(s, "my-sub-thing") 55 | ss.Histogram("latency", 50, "tag") 56 | } 57 | 58 | func ExampleNewMaxPacket() { 59 | // Defines interval between flushes to statsd server 60 | flushInterval := 5 * time.Second 61 | 62 | // Defines the largest packet sent to the statsd server 63 | maxPacketLen := 8192 64 | 65 | // Connection to the statsd server 66 | statsdWriter, err := net.Dial("udp", "127.0.0.1:8126") 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | 71 | // Create the stats client 72 | s := xstats.New(dogstatsd.NewMaxPacket(statsdWriter, flushInterval, maxPacketLen)) 73 | 74 | // Global tags sent with all metrics (only with supported clients like datadog's) 75 | s.AddTags("role:my-service", "dc:sv6") 76 | 77 | // Send some observations 78 | s.Count("requests", 1, "tag") 79 | s.Timing("something", 5*time.Millisecond, "tag") 80 | } 81 | -------------------------------------------------------------------------------- /sender.go: -------------------------------------------------------------------------------- 1 | package xstats 2 | 3 | import ( 4 | "io" 5 | "time" 6 | ) 7 | 8 | // Sender define an interface to a stats system like statsd or datadog to send 9 | // service's metrics. 10 | type Sender interface { 11 | // Gauge measure the value of a particular thing at a particular time, 12 | // like the amount of fuel in a car’s gas tank or the number of users 13 | // connected to a system. 14 | Gauge(stat string, value float64, tags ...string) 15 | 16 | // Count track how many times something happened per second, like 17 | // the number of database requests or page views. 18 | Count(stat string, count float64, tags ...string) 19 | 20 | // Histogram track the statistical distribution of a set of values, 21 | // like the duration of a number of database queries or the size of 22 | // files uploaded by users. Each histogram will track the average, 23 | // the minimum, the maximum, the median, the 95th percentile and the count. 24 | Histogram(stat string, value float64, tags ...string) 25 | 26 | // Timing mesures the elapsed time 27 | Timing(stat string, value time.Duration, tags ...string) 28 | } 29 | 30 | // CloseSender will call Close() on any xstats.Sender that implements io.Closer 31 | func CloseSender(s Sender) error { 32 | if c, ok := s.(io.Closer); ok { 33 | return c.Close() 34 | } 35 | return nil 36 | } 37 | 38 | // MultiSender lets you assign more than one sender to xstats in order to 39 | // multicast observeration to different systems. 40 | type MultiSender []Sender 41 | 42 | // Gauge implements the xstats.Sender interface 43 | func (s MultiSender) Gauge(stat string, value float64, tags ...string) { 44 | for _, ss := range s { 45 | ss.Gauge(stat, value, tags...) 46 | } 47 | } 48 | 49 | // Count implements the xstats.Sender interface 50 | func (s MultiSender) Count(stat string, count float64, tags ...string) { 51 | for _, ss := range s { 52 | ss.Count(stat, count, tags...) 53 | } 54 | } 55 | 56 | // Histogram implements the xstats.Sender interface 57 | func (s MultiSender) Histogram(stat string, value float64, tags ...string) { 58 | for _, ss := range s { 59 | ss.Histogram(stat, value, tags...) 60 | } 61 | } 62 | 63 | // Timing implements the xstats.Sender interface 64 | func (s MultiSender) Timing(stat string, duration time.Duration, tags ...string) { 65 | for _, ss := range s { 66 | ss.Timing(stat, duration, tags...) 67 | } 68 | } 69 | 70 | // Close implements the io.Closer interface 71 | func (s MultiSender) Close() error { 72 | var firstErr error 73 | // attempt to close all senders, return first error encountered 74 | for _, ss := range s { 75 | err := CloseSender(ss) 76 | if err != nil && firstErr == nil { 77 | firstErr = err 78 | } 79 | } 80 | return firstErr 81 | } 82 | -------------------------------------------------------------------------------- /statsd/statsd_test.go: -------------------------------------------------------------------------------- 1 | package statsd 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "log" 7 | "os" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | var tickC = make(chan time.Time) 15 | var fakeTick = func(time.Duration) <-chan time.Time { return tickC } 16 | 17 | func wait(buf *bytes.Buffer) { 18 | for i := 0; i < 10 && buf.Len() == 0; i++ { 19 | tickC <- time.Now() 20 | time.Sleep(10 * time.Millisecond) 21 | } 22 | } 23 | 24 | func TestCounter(t *testing.T) { 25 | tick = fakeTick 26 | defer func() { tick = time.Tick }() 27 | 28 | buf := &bytes.Buffer{} 29 | c := New(buf, time.Second) 30 | 31 | c.Count("metric1", 1, "tag1") 32 | c.Count("metric2", 2, "tag1", "tag2") 33 | wait(buf) 34 | 35 | assert.Equal(t, "metric1:1.000000|c\nmetric2:2.000000|c\n", buf.String()) 36 | } 37 | 38 | func TestGauge(t *testing.T) { 39 | tick = fakeTick 40 | defer func() { tick = time.Tick }() 41 | 42 | buf := &bytes.Buffer{} 43 | c := New(buf, time.Second) 44 | 45 | c.Gauge("metric1", 1, "tag1") 46 | c.Gauge("metric2", -2.0, "tag1", "tag2") 47 | wait(buf) 48 | 49 | assert.Equal(t, "metric1:1.000000|g\nmetric2:-2.000000|g\n", buf.String()) 50 | } 51 | 52 | func TestHistogram(t *testing.T) { 53 | tick = fakeTick 54 | defer func() { tick = time.Tick }() 55 | 56 | buf := &bytes.Buffer{} 57 | c := New(buf, time.Second) 58 | 59 | c.Histogram("metric1", 1, "tag1") 60 | c.Histogram("metric2", 2, "tag1", "tag2") 61 | wait(buf) 62 | 63 | assert.Equal(t, "metric1:1.000000|h\nmetric2:2.000000|h\n", buf.String()) 64 | } 65 | 66 | func TestTiming(t *testing.T) { 67 | tick = fakeTick 68 | defer func() { tick = time.Tick }() 69 | 70 | buf := &bytes.Buffer{} 71 | c := New(buf, time.Second) 72 | 73 | c.Timing("metric1", time.Second, "tag1") 74 | c.Timing("metric2", 2*time.Second, "tag1", "tag2") 75 | wait(buf) 76 | 77 | assert.Equal(t, "metric1:1000.000000|ms\nmetric2:2000.000000|ms\n", buf.String()) 78 | } 79 | 80 | func TestMaxPacketLen(t *testing.T) { 81 | buf := &bytes.Buffer{} 82 | c := NewMaxPacket(buf, time.Hour, 32) 83 | 84 | c.Count("metric1", 1.0) // len("metric1:1.000000|c\n") == 19 85 | c.Count("met2", 1.0) // len == 16 86 | 87 | for i := 0; i < 10 && buf.Len() == 0; i++ { 88 | time.Sleep(10 * time.Millisecond) 89 | } 90 | 91 | assert.Equal(t, "metric1:1.000000|c\n", buf.String()) 92 | buf.Reset() 93 | 94 | c.Count("met3", 1.0) 95 | for i := 0; i < 10 && buf.Len() == 0; i++ { 96 | time.Sleep(10 * time.Millisecond) 97 | } 98 | 99 | assert.Equal(t, "met2:1.000000|c\nmet3:1.000000|c\n", buf.String()) 100 | } 101 | 102 | type errWriter struct{} 103 | 104 | func (w errWriter) Write(p []byte) (n int, err error) { 105 | return 0, errors.New("i/o error") 106 | } 107 | 108 | func TestInvalidBuffer(t *testing.T) { 109 | tick = fakeTick 110 | defer func() { tick = time.Tick }() 111 | 112 | buf := &bytes.Buffer{} 113 | log.SetOutput(buf) 114 | defer func() { log.SetOutput(os.Stderr) }() 115 | 116 | c := New(&errWriter{}, time.Second) 117 | 118 | c.Count("metric", 1) 119 | wait(buf) 120 | 121 | assert.Contains(t, buf.String(), "error: could not write to statsd: i/o error") 122 | } 123 | -------------------------------------------------------------------------------- /prometheus/prometheus_test.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "net/http/httptest" 10 | "net/url" 11 | "testing" 12 | "time" 13 | 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func get(w io.Writer, h http.Handler, mark byte) { 18 | rr := httptest.NewRecorder() 19 | h.ServeHTTP(rr, &http.Request{Method: "GET", URL: &url.URL{Path: "/metrics"}}) 20 | if rr.Code > 299 { 21 | fmt.Fprintf(w, "%d\n%v\n\n", rr.Code, rr.HeaderMap) 22 | io.Copy(w, rr.Body) 23 | return 24 | } 25 | scanner := bufio.NewScanner(rr.Body) 26 | for scanner.Scan() { 27 | line := scanner.Bytes() 28 | if bytes.HasPrefix(line, []byte("metric")) && 29 | line[bytes.IndexByte(line, '_')+1] == mark { 30 | w.Write(line) 31 | w.Write([]byte{'\n'}) 32 | } 33 | } 34 | } 35 | 36 | func TestCounter(t *testing.T) { 37 | c := NewHandler() 38 | c.Count("metric1_c", 1, "tag:1") 39 | c.Count("metric2_c", 2, "tag:1", "gat:2") 40 | buf := &bytes.Buffer{} 41 | get(buf, c, 'c') 42 | 43 | assert.Equal(t, "metric1_c{tag=\"1\"} 1\nmetric2_c{gat=\"2\",tag=\"1\"} 2\n", buf.String()) 44 | } 45 | 46 | func TestGauge(t *testing.T) { 47 | c := NewHandler() 48 | c.Gauge("metric1_g", 1, "tag:1") 49 | c.Gauge("metric2_g", -2.0, "tag:1", "gat:2") 50 | buf := &bytes.Buffer{} 51 | get(buf, c, 'g') 52 | 53 | assert.Equal(t, "metric1_g{tag=\"1\"} 1\nmetric2_g{gat=\"2\",tag=\"1\"} -2\n", buf.String()) 54 | } 55 | 56 | func TestHistogram(t *testing.T) { 57 | c := NewHandler() 58 | c.Histogram("metric1_h", 1, "tag:1") 59 | c.Histogram("metric2_h", 2, "tag:1", "gat:2") 60 | buf := &bytes.Buffer{} 61 | get(buf, c, 'h') 62 | 63 | assert.Equal(t, "metric1_h_bucket{tag=\"1\",le=\"0.005\"} 0\nmetric1_h_bucket{tag=\"1\",le=\"0.01\"} 0\nmetric1_h_bucket{tag=\"1\",le=\"0.025\"} 0\nmetric1_h_bucket{tag=\"1\",le=\"0.05\"} 0\nmetric1_h_bucket{tag=\"1\",le=\"0.1\"} 0\nmetric1_h_bucket{tag=\"1\",le=\"0.25\"} 0\nmetric1_h_bucket{tag=\"1\",le=\"0.5\"} 0\nmetric1_h_bucket{tag=\"1\",le=\"1\"} 1\nmetric1_h_bucket{tag=\"1\",le=\"2.5\"} 1\nmetric1_h_bucket{tag=\"1\",le=\"5\"} 1\nmetric1_h_bucket{tag=\"1\",le=\"10\"} 1\nmetric1_h_bucket{tag=\"1\",le=\"+Inf\"} 1\nmetric1_h_sum{tag=\"1\"} 1\nmetric1_h_count{tag=\"1\"} 1\nmetric2_h_bucket{gat=\"2\",tag=\"1\",le=\"0.005\"} 0\nmetric2_h_bucket{gat=\"2\",tag=\"1\",le=\"0.01\"} 0\nmetric2_h_bucket{gat=\"2\",tag=\"1\",le=\"0.025\"} 0\nmetric2_h_bucket{gat=\"2\",tag=\"1\",le=\"0.05\"} 0\nmetric2_h_bucket{gat=\"2\",tag=\"1\",le=\"0.1\"} 0\nmetric2_h_bucket{gat=\"2\",tag=\"1\",le=\"0.25\"} 0\nmetric2_h_bucket{gat=\"2\",tag=\"1\",le=\"0.5\"} 0\nmetric2_h_bucket{gat=\"2\",tag=\"1\",le=\"1\"} 0\nmetric2_h_bucket{gat=\"2\",tag=\"1\",le=\"2.5\"} 1\nmetric2_h_bucket{gat=\"2\",tag=\"1\",le=\"5\"} 1\nmetric2_h_bucket{gat=\"2\",tag=\"1\",le=\"10\"} 1\nmetric2_h_bucket{gat=\"2\",tag=\"1\",le=\"+Inf\"} 1\nmetric2_h_sum{gat=\"2\",tag=\"1\"} 2\nmetric2_h_count{gat=\"2\",tag=\"1\"} 1\n", buf.String()) 64 | } 65 | 66 | func TestTiming(t *testing.T) { 67 | c := NewHandler() 68 | c.Timing("metric1_t", time.Second, "tag:1") 69 | c.Timing("metric2_t", 2*time.Second, "tag:1", "gat:2") 70 | buf := &bytes.Buffer{} 71 | get(buf, c, 't') 72 | 73 | assert.Equal(t, "metric1_t{tag=\"1\"} 1000\nmetric2_t{gat=\"2\",tag=\"1\"} 2000\n", buf.String()) 74 | } 75 | -------------------------------------------------------------------------------- /telegraf/telegraf_test.go: -------------------------------------------------------------------------------- 1 | package telegraf 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "log" 7 | "os" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | var tickC = make(chan time.Time) 15 | var fakeTick = func(time.Duration) <-chan time.Time { return tickC } 16 | 17 | func wait(buf *bytes.Buffer) { 18 | for i := 0; i < 10 && buf.Len() == 0; i++ { 19 | tickC <- time.Now() 20 | time.Sleep(10 * time.Millisecond) 21 | } 22 | } 23 | 24 | func TestCounter(t *testing.T) { 25 | tick = fakeTick 26 | defer func() { tick = time.Tick }() 27 | 28 | buf := &bytes.Buffer{} 29 | c := New(buf, time.Second) 30 | 31 | c.Count("metric1", 1, "tag1") 32 | c.Count("metric2", 2, "tag1", "tag2") 33 | wait(buf) 34 | 35 | assert.Equal(t, "metric1,tag1:1.000000|c\nmetric2,tag1,tag2:2.000000|c\n", buf.String()) 36 | } 37 | 38 | func TestGauge(t *testing.T) { 39 | tick = fakeTick 40 | defer func() { tick = time.Tick }() 41 | 42 | buf := &bytes.Buffer{} 43 | c := New(buf, time.Second) 44 | 45 | c.Gauge("metric1", 1, "tag1") 46 | c.Gauge("metric2", -2.0, "tag1", "tag2") 47 | wait(buf) 48 | 49 | assert.Equal(t, "metric1,tag1:1.000000|g\nmetric2,tag1,tag2:-2.000000|g\n", buf.String()) 50 | } 51 | 52 | func TestHistogram(t *testing.T) { 53 | tick = fakeTick 54 | defer func() { tick = time.Tick }() 55 | 56 | buf := &bytes.Buffer{} 57 | c := New(buf, time.Second) 58 | 59 | c.Histogram("metric1", 1, "tag1") 60 | c.Histogram("metric2", 2, "tag1", "tag2") 61 | wait(buf) 62 | 63 | assert.Equal(t, "metric1,tag1:1.000000|h\nmetric2,tag1,tag2:2.000000|h\n", buf.String()) 64 | } 65 | 66 | func TestTiming(t *testing.T) { 67 | tick = fakeTick 68 | defer func() { tick = time.Tick }() 69 | 70 | buf := &bytes.Buffer{} 71 | c := New(buf, time.Second) 72 | 73 | c.Timing("metric1", time.Second, "tag1") 74 | c.Timing("metric2", 2*time.Second, "tag1", "tag2") 75 | wait(buf) 76 | 77 | assert.Equal(t, "metric1,tag1:1.000000|ms\nmetric2,tag1,tag2:2.000000|ms\n", buf.String()) 78 | } 79 | 80 | func TestMaxPacketLen(t *testing.T) { 81 | buf := &bytes.Buffer{} 82 | c := NewMaxPacket(buf, time.Hour, 32) 83 | 84 | c.Count("metric1", 1.0) // len("metric1,:1.000000|c\n") == 20 85 | c.Count("mt2", 1.0) // len == 16 86 | 87 | for i := 0; i < 10 && buf.Len() == 0; i++ { 88 | time.Sleep(10 * time.Millisecond) 89 | } 90 | 91 | assert.Equal(t, "metric1,:1.000000|c\n", buf.String()) 92 | buf.Reset() 93 | 94 | c.Count("mt3", 1.0) 95 | for i := 0; i < 10 && buf.Len() == 0; i++ { 96 | time.Sleep(10 * time.Millisecond) 97 | } 98 | 99 | assert.Equal(t, "mt2,:1.000000|c\nmt3,:1.000000|c\n", buf.String()) 100 | } 101 | 102 | type errWriter struct{} 103 | 104 | func (w errWriter) Write(p []byte) (n int, err error) { 105 | return 0, errors.New("i/o error") 106 | } 107 | 108 | func TestInvalidBuffer(t *testing.T) { 109 | tick = fakeTick 110 | defer func() { tick = time.Tick }() 111 | 112 | buf := &bytes.Buffer{} 113 | log.SetOutput(buf) 114 | defer func() { log.SetOutput(os.Stderr) }() 115 | 116 | c := New(&errWriter{}, time.Second) 117 | 118 | c.Count("metric", 1) 119 | wait(buf) 120 | 121 | assert.Contains(t, buf.String(), "error: could not write to statsd: i/o error") 122 | } 123 | -------------------------------------------------------------------------------- /dogstatsd/dogstatsd_test.go: -------------------------------------------------------------------------------- 1 | package dogstatsd 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "log" 7 | "os" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | var tickC = make(chan time.Time) 15 | var fakeTick = func(time.Duration) <-chan time.Time { return tickC } 16 | 17 | func wait(buf *bytes.Buffer) { 18 | for i := 0; i < 10 && buf.Len() == 0; i++ { 19 | tickC <- time.Now() 20 | time.Sleep(10 * time.Millisecond) 21 | } 22 | } 23 | 24 | func TestCounter(t *testing.T) { 25 | tick = fakeTick 26 | defer func() { tick = time.Tick }() 27 | 28 | buf := &bytes.Buffer{} 29 | c := New(buf, time.Second) 30 | 31 | c.Count("metric1", 1, "tag1") 32 | c.Count("metric2", 2, "tag1", "tag2") 33 | wait(buf) 34 | 35 | assert.Equal(t, "metric1:1.000000|c|#tag1\nmetric2:2.000000|c|#tag1,tag2\n", buf.String()) 36 | } 37 | 38 | func TestGauge(t *testing.T) { 39 | tick = fakeTick 40 | defer func() { tick = time.Tick }() 41 | 42 | buf := &bytes.Buffer{} 43 | c := New(buf, time.Second) 44 | 45 | c.Gauge("metric1", 1, "tag1") 46 | c.Gauge("metric2", -2.0, "tag1", "tag2") 47 | wait(buf) 48 | 49 | assert.Equal(t, "metric1:1.000000|g|#tag1\nmetric2:-2.000000|g|#tag1,tag2\n", buf.String()) 50 | } 51 | 52 | func TestHistogram(t *testing.T) { 53 | tick = fakeTick 54 | defer func() { tick = time.Tick }() 55 | 56 | buf := &bytes.Buffer{} 57 | c := New(buf, time.Second) 58 | 59 | c.Histogram("metric1", 1, "tag1") 60 | c.Histogram("metric2", 2, "tag1", "tag2") 61 | wait(buf) 62 | 63 | assert.Equal(t, "metric1:1.000000|h|#tag1\nmetric2:2.000000|h|#tag1,tag2\n", buf.String()) 64 | } 65 | 66 | func TestTiming(t *testing.T) { 67 | tick = fakeTick 68 | defer func() { tick = time.Tick }() 69 | 70 | buf := &bytes.Buffer{} 71 | c := New(buf, time.Second) 72 | 73 | c.Timing("metric1", time.Second, "tag1") 74 | c.Timing("metric2", 2*time.Second, "tag1", "tag2") 75 | wait(buf) 76 | 77 | assert.Equal(t, "metric1:1000.000000|ms|#tag1\nmetric2:2000.000000|ms|#tag1,tag2\n", buf.String()) 78 | } 79 | 80 | func TestMaxPacketLen(t *testing.T) { 81 | buf := &bytes.Buffer{} 82 | c := NewMaxPacket(buf, time.Hour, 32) 83 | 84 | c.Count("metric1", 1.0) // len("metric1:1.000000|c\n") == 19 85 | c.Count("met2", 1.0) // len == 16 86 | 87 | for i := 0; i < 10 && buf.Len() == 0; i++ { 88 | time.Sleep(10 * time.Millisecond) 89 | } 90 | 91 | assert.Equal(t, "metric1:1.000000|c\n", buf.String()) 92 | buf.Reset() 93 | 94 | c.Count("met3", 1.0) 95 | for i := 0; i < 10 && buf.Len() == 0; i++ { 96 | time.Sleep(10 * time.Millisecond) 97 | } 98 | 99 | assert.Equal(t, "met2:1.000000|c\nmet3:1.000000|c\n", buf.String()) 100 | } 101 | 102 | type errWriter struct{} 103 | 104 | func (w errWriter) Write(p []byte) (n int, err error) { 105 | return 0, errors.New("i/o error") 106 | } 107 | 108 | func TestInvalidBuffer(t *testing.T) { 109 | tick = fakeTick 110 | defer func() { tick = time.Tick }() 111 | 112 | buf := &bytes.Buffer{} 113 | log.SetOutput(buf) 114 | defer func() { log.SetOutput(os.Stderr) }() 115 | 116 | c := New(&errWriter{}, time.Second) 117 | 118 | c.Count("metric", 1) 119 | wait(buf) 120 | 121 | assert.Contains(t, buf.String(), "error: could not write to statsd: i/o error") 122 | } 123 | -------------------------------------------------------------------------------- /statsd/statsd.go: -------------------------------------------------------------------------------- 1 | // Package statsd implement the StatsD protocol for github.com/rs/xstats 2 | package statsd 3 | 4 | import ( 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "log" 9 | "time" 10 | 11 | "github.com/rs/xstats" 12 | ) 13 | 14 | // Inspired by https://github.com/streadway/handy statsd package 15 | 16 | type sender struct { 17 | c chan string 18 | quit chan struct{} 19 | done chan struct{} 20 | } 21 | 22 | // defaultMaxPacketLen is the default number of bytes filled before a packet is 23 | // flushed before the reporting interval. 24 | const defaultMaxPacketLen = 1 << 15 25 | 26 | var tick = time.Tick 27 | 28 | // New creates a statsd sender that emits observations in the statsd 29 | // protocol to the passed writer. Observations are buffered for the report 30 | // interval or until the buffer exceeds a max packet size, whichever comes 31 | // first. 32 | func New(w io.Writer, reportInterval time.Duration) xstats.Sender { 33 | return NewMaxPacket(w, reportInterval, defaultMaxPacketLen) 34 | } 35 | 36 | // NewMaxPacket creates a statsd sender that emits observations in the statsd 37 | // protocol to the passed writer. Observations are buffered for the report 38 | // interval or until the buffer exceeds the max packet size, whichever comes 39 | // first. Tags are ignored. 40 | func NewMaxPacket(w io.Writer, reportInterval time.Duration, maxPacketLen int) xstats.Sender { 41 | s := &sender{ 42 | c: make(chan string), 43 | quit: make(chan struct{}), 44 | done: make(chan struct{}), 45 | } 46 | go s.fwd(w, reportInterval, maxPacketLen) 47 | return s 48 | } 49 | 50 | // Gauge implements xstats.Sender interface 51 | func (s *sender) Gauge(stat string, value float64, tags ...string) { 52 | s.c <- fmt.Sprintf("%s:%f|g\n", stat, value) 53 | } 54 | 55 | // Count implements xstats.Sender interface 56 | func (s *sender) Count(stat string, count float64, tags ...string) { 57 | s.c <- fmt.Sprintf("%s:%f|c\n", stat, count) 58 | } 59 | 60 | // Histogram implements xstats.Sender interface 61 | func (s *sender) Histogram(stat string, value float64, tags ...string) { 62 | s.c <- fmt.Sprintf("%s:%f|h\n", stat, value) 63 | } 64 | 65 | // Timing implements xstats.Sender interface 66 | func (s *sender) Timing(stat string, duration time.Duration, tags ...string) { 67 | s.c <- fmt.Sprintf("%s:%f|ms\n", stat, duration.Seconds()*1000) 68 | } 69 | 70 | // Close implements xstats.Sender interface 71 | func (s *sender) Close() error { 72 | close(s.quit) 73 | <-s.done 74 | close(s.c) 75 | 76 | return nil 77 | } 78 | 79 | func (s *sender) fwd(w io.Writer, reportInterval time.Duration, maxPacketLen int) { 80 | defer close(s.done) 81 | 82 | buf := &bytes.Buffer{} 83 | tick := tick(reportInterval) 84 | for { 85 | select { 86 | case m := <-s.c: 87 | newLen := buf.Len() + len(m) 88 | if newLen > maxPacketLen { 89 | flush(w, buf) 90 | } 91 | 92 | buf.Write([]byte(m)) 93 | 94 | if newLen == maxPacketLen { 95 | flush(w, buf) 96 | } 97 | 98 | case <-tick: 99 | flush(w, buf) 100 | case <-s.quit: 101 | flush(w, buf) 102 | return 103 | } 104 | } 105 | } 106 | 107 | func flush(w io.Writer, buf *bytes.Buffer) { 108 | if buf.Len() <= 0 { 109 | return 110 | } 111 | if _, err := w.Write(buf.Bytes()); err != nil { 112 | log.Printf("error: could not write to statsd: %v", err) 113 | } 114 | buf.Reset() 115 | } 116 | -------------------------------------------------------------------------------- /prometheus/prometheus.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | "sync" 7 | "time" 8 | 9 | "github.com/prometheus/client_golang/prometheus" 10 | "github.com/rs/xstats" 11 | ) 12 | 13 | type sender struct { 14 | http.Handler 15 | 16 | counters map[string]*prometheus.CounterVec 17 | gauges map[string]*prometheus.GaugeVec 18 | histograms map[string]*prometheus.HistogramVec 19 | sync.RWMutex 20 | } 21 | 22 | // New creates a prometheus publisher at the given HTTP address. 23 | func New(addr string) xstats.Sender { 24 | s := NewHandler() 25 | go func() { 26 | http.ListenAndServe(addr, s) 27 | }() 28 | return s 29 | } 30 | 31 | // NewHandler creates a prometheus publisher - a http.Handler and an xstats.Sender. 32 | func NewHandler() *sender { 33 | return &sender{ 34 | Handler: prometheus.Handler(), 35 | counters: make(map[string]*prometheus.CounterVec), 36 | gauges: make(map[string]*prometheus.GaugeVec), 37 | histograms: make(map[string]*prometheus.HistogramVec), 38 | } 39 | } 40 | 41 | // Gauge implements xstats.Sender interface 42 | // 43 | // Mark the tags as "key:value". 44 | func (s *sender) Gauge(stat string, value float64, tags ...string) { 45 | s.RLock() 46 | m, ok := s.gauges[stat] 47 | s.RUnlock() 48 | keys, values := splitTags(tags) 49 | if !ok { 50 | s.Lock() 51 | if m, ok = s.gauges[stat]; !ok { 52 | m = prometheus.NewGaugeVec( 53 | prometheus.GaugeOpts{Name: stat, Help: stat}, 54 | keys) 55 | prometheus.MustRegister(m) 56 | s.gauges[stat] = m 57 | } 58 | s.Unlock() 59 | } 60 | m.WithLabelValues(values...).Set(value) 61 | } 62 | 63 | // Count implements xstats.Sender interface 64 | // 65 | // Mark the tags as "key:value". 66 | func (s *sender) Count(stat string, count float64, tags ...string) { 67 | s.RLock() 68 | m, ok := s.counters[stat] 69 | s.RUnlock() 70 | keys, values := splitTags(tags) 71 | if !ok { 72 | s.Lock() 73 | if m, ok = s.counters[stat]; !ok { 74 | m = prometheus.NewCounterVec( 75 | prometheus.CounterOpts{Name: stat, Help: stat}, 76 | keys) 77 | prometheus.MustRegister(m) 78 | s.counters[stat] = m 79 | } 80 | s.Unlock() 81 | } 82 | m.WithLabelValues(values...).Add(count) 83 | } 84 | 85 | // Histogram implements xstats.Sender interface 86 | // 87 | // Mark the tags as "key:value". 88 | func (s *sender) Histogram(stat string, value float64, tags ...string) { 89 | s.RLock() 90 | m, ok := s.histograms[stat] 91 | s.RUnlock() 92 | keys, values := splitTags(tags) 93 | if !ok { 94 | s.Lock() 95 | if m, ok = s.histograms[stat]; !ok { 96 | m = prometheus.NewHistogramVec( 97 | prometheus.HistogramOpts{Name: stat, Help: stat}, 98 | keys) 99 | prometheus.MustRegister(m) 100 | s.histograms[stat] = m 101 | } 102 | s.Unlock() 103 | } 104 | m.WithLabelValues(values...).Observe(value) 105 | } 106 | 107 | // Timing implements xstats.Sender interface - simulates Timing with Gauge. 108 | // 109 | // Mark the tags as "key:value". 110 | func (s *sender) Timing(stat string, duration time.Duration, tags ...string) { 111 | s.Gauge(stat, float64(duration/time.Millisecond), tags...) 112 | } 113 | 114 | func splitTags(tags []string) ([]string, []string) { 115 | keys, values := make([]string, len(tags)), make([]string, len(tags)) 116 | for i, t := range tags { 117 | if j := strings.IndexByte(t, ':'); j >= 0 { 118 | keys[i] = t[:j] 119 | values[i] = t[j+1:] 120 | } else { 121 | keys[i] = t 122 | } 123 | } 124 | return keys, values 125 | } 126 | -------------------------------------------------------------------------------- /dogstatsd/dogstatsd.go: -------------------------------------------------------------------------------- 1 | // Package dogstatsd implement Datadog extended StatsD protocol for github.com/rs/xstats 2 | package dogstatsd 3 | 4 | import ( 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "log" 9 | "strings" 10 | "time" 11 | 12 | "github.com/rs/xstats" 13 | ) 14 | 15 | // Inspired by https://github.com/streadway/handy statsd package 16 | 17 | type sender struct { 18 | c chan string 19 | quit chan struct{} 20 | done chan struct{} 21 | } 22 | 23 | // defaultMaxPacketLen is the default number of bytes filled before a packet is 24 | // flushed before the reporting interval. 25 | const defaultMaxPacketLen = 1 << 15 26 | 27 | var tick = time.Tick 28 | 29 | // New creates a datadog statsd sender that emits observations in the statsd 30 | // protocol to the passed writer. Observations are buffered for the report 31 | // interval or until the buffer exceeds a max packet size, whichever comes 32 | // first. 33 | func New(w io.Writer, reportInterval time.Duration) xstats.Sender { 34 | return NewMaxPacket(w, reportInterval, defaultMaxPacketLen) 35 | } 36 | 37 | // NewMaxPacket creates a datadog statsd sender that emits observations in the 38 | // statsd protocol to the passed writer. Observations are buffered for the 39 | // report interval or until the buffer exceeds the max packet size, whichever 40 | // comes first. 41 | func NewMaxPacket(w io.Writer, reportInterval time.Duration, maxPacketLen int) xstats.Sender { 42 | s := &sender{ 43 | c: make(chan string), 44 | quit: make(chan struct{}), 45 | done: make(chan struct{}), 46 | } 47 | go s.fwd(w, reportInterval, maxPacketLen) 48 | return s 49 | } 50 | 51 | // Gauge implements xstats.Sender interface 52 | func (s *sender) Gauge(stat string, value float64, tags ...string) { 53 | s.c <- fmt.Sprintf("%s:%f|g%s\n", stat, value, t(tags)) 54 | } 55 | 56 | // Count implements xstats.Sender interface 57 | func (s *sender) Count(stat string, count float64, tags ...string) { 58 | s.c <- fmt.Sprintf("%s:%f|c%s\n", stat, count, t(tags)) 59 | } 60 | 61 | // Histogram implements xstats.Sender interface 62 | func (s *sender) Histogram(stat string, value float64, tags ...string) { 63 | s.c <- fmt.Sprintf("%s:%f|h%s\n", stat, value, t(tags)) 64 | } 65 | 66 | // Timing implements xstats.Sender interface 67 | func (s *sender) Timing(stat string, duration time.Duration, tags ...string) { 68 | s.c <- fmt.Sprintf("%s:%f|ms%s\n", stat, duration.Seconds()*1000, t(tags)) 69 | } 70 | 71 | // Close implements xstats.Sender interface 72 | func (s *sender) Close() error { 73 | close(s.quit) 74 | <-s.done 75 | close(s.c) 76 | 77 | return nil 78 | } 79 | 80 | // Generate a DogStatsD tag suffix 81 | func t(tags []string) string { 82 | t := "" 83 | if len(tags) > 0 { 84 | t = "|#" + strings.Join(tags, ",") 85 | } 86 | return t 87 | } 88 | 89 | func (s *sender) fwd(w io.Writer, reportInterval time.Duration, maxPacketLen int) { 90 | defer close(s.done) 91 | 92 | buf := &bytes.Buffer{} 93 | tick := tick(reportInterval) 94 | for { 95 | select { 96 | case m := <-s.c: 97 | newLen := buf.Len() + len(m) 98 | if newLen > maxPacketLen { 99 | flush(w, buf) 100 | } 101 | 102 | buf.Write([]byte(m)) 103 | 104 | if newLen == maxPacketLen { 105 | flush(w, buf) 106 | } 107 | 108 | case <-tick: 109 | flush(w, buf) 110 | case <-s.quit: 111 | flush(w, buf) 112 | return 113 | } 114 | } 115 | } 116 | 117 | func flush(w io.Writer, buf *bytes.Buffer) { 118 | if buf.Len() <= 0 { 119 | return 120 | } 121 | if _, err := w.Write(buf.Bytes()); err != nil { 122 | log.Printf("error: could not write to statsd: %v", err) 123 | } 124 | buf.Reset() 125 | } 126 | -------------------------------------------------------------------------------- /telegraf/telegraf.go: -------------------------------------------------------------------------------- 1 | // Package telegrafstatsd implement telegraf extended StatsD protocol for github.com/rs/xstats 2 | package telegraf 3 | 4 | import ( 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "log" 9 | "strings" 10 | "time" 11 | 12 | "github.com/rs/xstats" 13 | ) 14 | 15 | // Inspired by https://github.com/streadway/handy statsd package 16 | 17 | type sender struct { 18 | c chan string 19 | quit chan struct{} 20 | done chan struct{} 21 | } 22 | 23 | // defaultMaxPacketLen is the default number of bytes filled before a packet is 24 | // flushed before the reporting interval. 25 | const defaultMaxPacketLen = 1 << 15 26 | 27 | var tick = time.Tick 28 | 29 | // New creates a telegraf statsd sender that emits observations in the statsd 30 | // protocol to the passed writer. Observations are buffered for the report 31 | // interval or until the buffer exceeds a max packet size, whichever comes 32 | // first. 33 | func New(w io.Writer, reportInterval time.Duration) xstats.Sender { 34 | return NewMaxPacket(w, reportInterval, defaultMaxPacketLen) 35 | } 36 | 37 | // NewMaxPacket creates a telegraf statsd sender that emits observations in the 38 | // statsd protocol to the passed writer. Observations are buffered for the 39 | // report interval or until the buffer exceeds the max packet size, whichever 40 | // comes first. 41 | func NewMaxPacket(w io.Writer, reportInterval time.Duration, maxPacketLen int) xstats.Sender { 42 | s := &sender{ 43 | c: make(chan string), 44 | quit: make(chan struct{}), 45 | done: make(chan struct{}), 46 | } 47 | go s.fwd(w, reportInterval, maxPacketLen) 48 | return s 49 | } 50 | 51 | // Gauge implements xstats.Sender interface 52 | func (s *sender) Gauge(stat string, value float64, tags ...string) { 53 | s.c <- fmt.Sprintf("%s,%s:%f|g\n", stat, t(tags), value) 54 | } 55 | 56 | // Count implements xstats.Sender interface 57 | func (s *sender) Count(stat string, count float64, tags ...string) { 58 | s.c <- fmt.Sprintf("%s,%s:%f|c\n", stat, t(tags), count) 59 | } 60 | 61 | // Histogram implements xstats.Sender interface 62 | func (s *sender) Histogram(stat string, value float64, tags ...string) { 63 | s.c <- fmt.Sprintf("%s,%s:%f|h\n", stat, t(tags), value) 64 | } 65 | 66 | // Timing implements xstats.Sender interface 67 | func (s *sender) Timing(stat string, duration time.Duration, tags ...string) { 68 | s.c <- fmt.Sprintf("%s,%s:%f|ms\n", stat, t(tags), duration.Seconds()) 69 | } 70 | 71 | // Close implements xstats.Sender interface 72 | func (s *sender) Close() error { 73 | close(s.quit) 74 | <-s.done 75 | close(s.c) 76 | 77 | return nil 78 | } 79 | 80 | // Generate a telegraf tag suffix 81 | func t(tags []string) string { 82 | for i, v := range tags { 83 | tags[i] = strings.Replace(v, ":", "=", 1) 84 | } 85 | t := "" 86 | if len(tags) > 0 { 87 | t = "" + strings.Join(tags, ",") 88 | } 89 | return t 90 | } 91 | 92 | func (s *sender) fwd(w io.Writer, reportInterval time.Duration, maxPacketLen int) { 93 | defer close(s.done) 94 | 95 | buf := &bytes.Buffer{} 96 | tick := tick(reportInterval) 97 | for { 98 | select { 99 | case m := <-s.c: 100 | newLen := buf.Len() + len(m) 101 | if newLen > maxPacketLen { 102 | flush(w, buf) 103 | } 104 | 105 | buf.Write([]byte(m)) 106 | 107 | if newLen == maxPacketLen { 108 | flush(w, buf) 109 | } 110 | 111 | case <-tick: 112 | flush(w, buf) 113 | case <-s.quit: 114 | flush(w, buf) 115 | return 116 | } 117 | } 118 | } 119 | 120 | func flush(w io.Writer, buf *bytes.Buffer) { 121 | if buf.Len() <= 0 { 122 | return 123 | } 124 | if _, err := w.Write(buf.Bytes()); err != nil { 125 | log.Printf("error: could not write to statsd: %v", err) 126 | } 127 | buf.Reset() 128 | } 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XStats 2 | 3 | [![godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/rs/xstats) [![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/rs/xstats/master/LICENSE) [![Build Status](https://travis-ci.org/rs/xstats.svg?branch=master)](https://travis-ci.org/rs/xstats) [![Coverage](http://gocover.io/_badge/github.com/rs/xstats)](http://gocover.io/github.com/rs/xstats) 4 | 5 | Package `xstats` is a generic client for service instrumentation. 6 | 7 | `xstats` is inspired from Go-kit's [metrics](https://github.com/go-kit/kit/tree/master/metrics) package but it takes a slightly different path. Instead of having to create an instance for each metric, `xstats` use a single instance to log every metrics you want. This reduces the boiler plate when you have a lot a metrics in your app. It's also easier in term of dependency injection. 8 | 9 | Talking about dependency injection, `xstats` comes with a [xhandler.Handler](https://github.com/rs/xhandler) integration so it can automatically inject the `xstats` client within the `net/context` of each request. Each request's `xstats` instance have its own tags storage ; This let you inject some per request contextual tags to be included with all observations sent within the lifespan of the request. 10 | 11 | `xstats` is pluggable and comes with integration for `expvar`, `StatsD` and `DogStatsD`, the [Datadog](http://datadoghq.com) augmented version of StatsD with support for tags. More integration may come later (PR welcome). 12 | 13 | ## Supported Clients 14 | 15 | - [StatsD](https://github.com/b/statsd_spec) 16 | - [DogStatsD](http://docs.datadoghq.com/guides/dogstatsd/#datagram-format) 17 | - [expvar](https://golang.org/pkg/expvar/) 18 | - [prometheus](https://github.com/prometheus/client_golang) 19 | - [telegraf](https://influxdata.com/blog/getting-started-with-sending-statsd-metrics-to-telegraf-influxdb) 20 | - [mock](https://github.com/stretchr/testify) 21 | 22 | ## Install 23 | 24 | go get github.com/rs/xstats 25 | 26 | ## Usage 27 | 28 | ```go 29 | // Defines interval between flushes to statsd server 30 | flushInterval := 5 * time.Second 31 | 32 | // Connection to the statsd server 33 | statsdWriter, err := net.Dial("udp", "127.0.0.1:8126") 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | // Create the stats client 39 | s := xstats.New(dogstatsd.New(statsdWriter, flushInterval)) 40 | 41 | // Global tags sent with all metrics (only with supported clients like datadog's) 42 | s.AddTags("role:my-service", "dc:sv6") 43 | 44 | // Send some observations 45 | s.Count("requests", 1, "tag") 46 | s.Timing("something", 5*time.Millisecond, "tag") 47 | ``` 48 | 49 | Integration with [github.com/rs/xhandler](https://github.com/rs/xhandler): 50 | 51 | ```go 52 | var xh xhandler.HandlerC 53 | 54 | // Here is your handler 55 | xh = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 56 | // Get the xstats request's instance from the context. You can safely assume it will 57 | // be always there, if the handler is removed, xstats.FromContext will return a nop 58 | // instance. 59 | m := xstats.FromRequest(r) 60 | 61 | // Count something 62 | m.Count("requests", 1, "route:index") 63 | }) 64 | 65 | // Install the metric handler with dogstatsd backend client and some env tags 66 | flushInterval := 5 * time.Second 67 | tags := []string{"role:my-service"} 68 | statsdWriter, err := net.Dial("udp", "127.0.0.1:8126") 69 | if err != nil { 70 | log.Fatal(err) 71 | } 72 | xh = xstats.NewHandler(dogstatsd.New(statsdWriter, flushInterval), tags, xh) 73 | 74 | // Root context 75 | ctx := context.Background() 76 | h := xhandler.New(ctx, xh) 77 | http.Handle("/", h) 78 | 79 | if err := http.ListenAndServe(":8080", nil); err != nil { 80 | log.Fatal(err) 81 | } 82 | ``` 83 | 84 | ## Testing 85 | ```go 86 | func TestFunc(t *testing.T) { 87 | m := mock.New() 88 | s := xstats.New(m) 89 | m.On("Timing", "something", 5*time.Millisecond, "tag") 90 | s.Timing("something", 5*time.Millisecond, "tag") 91 | s.AssertExpectations(t) 92 | } 93 | ``` 94 | 95 | ## Licenses 96 | 97 | All source code is licensed under the [MIT License](https://raw.github.com/rs/xstats/master/LICENSE). 98 | -------------------------------------------------------------------------------- /xstats_test.go: -------------------------------------------------------------------------------- 1 | package xstats 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "golang.org/x/net/context" 10 | ) 11 | 12 | type fakeSender struct { 13 | last cmd 14 | } 15 | 16 | type fakeSendCloser struct { 17 | fakeSender 18 | err error 19 | } 20 | 21 | type cmd struct { 22 | name string 23 | stat string 24 | value float64 25 | tags []string 26 | } 27 | 28 | func (s *fakeSender) Gauge(stat string, value float64, tags ...string) { 29 | s.last = cmd{"Gauge", stat, value, tags} 30 | } 31 | 32 | func (s *fakeSender) Count(stat string, count float64, tags ...string) { 33 | s.last = cmd{"Count", stat, count, tags} 34 | } 35 | 36 | func (s *fakeSender) Histogram(stat string, value float64, tags ...string) { 37 | s.last = cmd{"Histogram", stat, value, tags} 38 | } 39 | 40 | func (s *fakeSender) Timing(stat string, duration time.Duration, tags ...string) { 41 | s.last = cmd{"Timing", stat, duration.Seconds(), tags} 42 | } 43 | 44 | func (s *fakeSendCloser) Close() error { 45 | s.fakeSender.last = cmd{name: "Close"} 46 | return s.err 47 | } 48 | 49 | func TestContext(t *testing.T) { 50 | ctx := context.Background() 51 | s := FromContext(ctx) 52 | assert.Equal(t, nop, s) 53 | 54 | ctx = context.Background() 55 | xs := &xstats{} 56 | ctx = NewContext(ctx, xs) 57 | ctxs := FromContext(ctx) 58 | assert.Equal(t, xs, ctxs) 59 | } 60 | 61 | func TestNew(t *testing.T) { 62 | xs := New(&fakeSender{}) 63 | _, ok := xs.(*xstats) 64 | assert.True(t, ok) 65 | } 66 | 67 | func TestNewPoolingDisable(t *testing.T) { 68 | originalValue := DisablePooling 69 | originalPool := xstatsPool 70 | defer func(value bool, pool *sync.Pool) { 71 | DisablePooling = value 72 | xstatsPool = pool 73 | }(originalValue, originalPool) 74 | DisablePooling = true 75 | xstatsPool = &sync.Pool{ 76 | New: func() interface{} { 77 | assert.Fail(t, "pool used while disabled") 78 | return nil 79 | }, 80 | } 81 | xs := New(&fakeSender{}) 82 | x, ok := xs.(*xstats) 83 | assert.True(t, ok) 84 | x.AddTags("test:true") 85 | x.Close() 86 | // Ensure that state is maintained when pooling is disabled. 87 | tags := x.GetTags() 88 | assert.Contains(t, tags, "test:true") 89 | } 90 | 91 | func TestNewPrefix(t *testing.T) { 92 | xs := NewPrefix(&fakeSender{}, "prefix.") 93 | x, ok := xs.(*xstats) 94 | assert.True(t, ok) 95 | assert.Equal(t, "prefix.", x.prefix) 96 | } 97 | 98 | func TestNewScoping(t *testing.T) { 99 | xs := NewScoping(&fakeSender{}, "/") 100 | x, ok := xs.(*xstats) 101 | assert.True(t, ok) 102 | assert.Equal(t, "", x.prefix) 103 | 104 | xs = NewScoping(&fakeSender{}, "/", "prefix", "infix", "suffix") 105 | x, ok = xs.(*xstats) 106 | assert.True(t, ok) 107 | assert.Equal(t, "prefix/infix/suffix/", x.prefix) 108 | } 109 | 110 | func TestCopy(t *testing.T) { 111 | xs := NewPrefix(&fakeSender{}, "prefix.").(*xstats) 112 | xs.AddTags("foo") 113 | xs2 := Copy(xs).(*xstats) 114 | assert.Equal(t, xs.s, xs2.s) 115 | assert.Equal(t, xs.tags, xs2.tags) 116 | assert.Equal(t, xs.prefix, xs2.prefix) 117 | xs2.AddTags("bar", "baz") 118 | assert.Equal(t, []string{"foo"}, xs.tags) 119 | assert.Equal(t, []string{"foo", "bar", "baz"}, xs2.tags) 120 | 121 | assert.Equal(t, nop, Copy(nop)) 122 | assert.Equal(t, nop, Copy(nil)) 123 | } 124 | 125 | func TestScope(t *testing.T) { 126 | xs := NewScoping(&fakeSender{}, ".").(*xstats) 127 | xs.AddTags("foo") 128 | 129 | xs2 := Scope(xs, "prefix").(*xstats) 130 | assert.Equal(t, xs.s, xs2.s) 131 | assert.Equal(t, xs.tags, xs2.tags) 132 | assert.Equal(t, "prefix.", xs2.prefix) 133 | 134 | xs3 := Scope(xs2, "infix", "suffix").(*xstats) 135 | assert.Equal(t, xs2.s, xs3.s) 136 | assert.Equal(t, xs2.tags, xs3.tags) 137 | assert.Equal(t, "prefix.infix.suffix.", xs3.prefix) 138 | 139 | xs2.AddTags("bar", "baz") 140 | xs3.AddTags("blegga") 141 | assert.Equal(t, []string{"foo"}, xs.tags) 142 | assert.Equal(t, []string{"foo", "bar", "baz"}, xs2.tags) 143 | assert.Equal(t, []string{"foo", "blegga"}, xs3.tags) 144 | 145 | assert.Equal(t, nop, Scope(nop, "prefix")) 146 | assert.Equal(t, nop, Scope(nil, "prefix")) 147 | } 148 | 149 | func TestAddTag(t *testing.T) { 150 | xs := &xstats{s: &fakeSender{}} 151 | xs.AddTags("foo") 152 | assert.Equal(t, []string{"foo"}, xs.tags) 153 | } 154 | 155 | func TestGauge(t *testing.T) { 156 | s := &fakeSender{} 157 | xs := &xstats{s: s, prefix: "p."} 158 | xs.AddTags("foo") 159 | xs.Gauge("bar", 1, "baz") 160 | assert.Equal(t, cmd{"Gauge", "p.bar", 1, []string{"baz", "foo"}}, s.last) 161 | } 162 | 163 | func TestCount(t *testing.T) { 164 | s := &fakeSender{} 165 | xs := &xstats{s: s, prefix: "p."} 166 | xs.AddTags("foo") 167 | xs.Count("bar", 1, "baz") 168 | assert.Equal(t, cmd{"Count", "p.bar", 1, []string{"baz", "foo"}}, s.last) 169 | } 170 | 171 | func TestHistogram(t *testing.T) { 172 | s := &fakeSender{} 173 | xs := &xstats{s: s, prefix: "p."} 174 | xs.AddTags("foo") 175 | xs.Histogram("bar", 1, "baz") 176 | assert.Equal(t, cmd{"Histogram", "p.bar", 1, []string{"baz", "foo"}}, s.last) 177 | } 178 | 179 | func TestTiming(t *testing.T) { 180 | s := &fakeSender{} 181 | xs := &xstats{s: s, prefix: "p."} 182 | xs.AddTags("foo") 183 | xs.Timing("bar", 1, "baz") 184 | assert.Equal(t, cmd{"Timing", "p.bar", 1 / float64(time.Second), []string{"baz", "foo"}}, s.last) 185 | } 186 | 187 | func TestNilSender(t *testing.T) { 188 | xs := &xstats{} 189 | xs.Gauge("foo", 1) 190 | xs.Count("foo", 1) 191 | xs.Histogram("foo", 1) 192 | xs.Timing("foo", 1) 193 | } 194 | -------------------------------------------------------------------------------- /xstats.go: -------------------------------------------------------------------------------- 1 | // Package xstats is a generic client for service instrumentation. 2 | // 3 | // xstats is inspired from Go-kit's metrics (https://github.com/go-kit/kit/tree/master/metrics) 4 | // package but it takes a slightly different path. Instead of having to create 5 | // an instance for each metric, xstats use a single instance to log every metrics 6 | // you want. This reduces the boiler plate when you have a lot a metrics in your app. 7 | // It's also easier in term of dependency injection. 8 | // 9 | // Talking about dependency injection, xstats comes with a xhandler.Handler 10 | // integration so it can automatically inject the xstats client within the net/context 11 | // of each request. Each request's xstats instance have its own tags storage ; 12 | // This let you inject some per request contextual tags to be included with all 13 | // observations sent within the lifespan of the request. 14 | // 15 | // xstats is pluggable and comes with integration for StatsD and DogStatsD, 16 | // the Datadog (http://datadoghq.com) augmented version of StatsD with support for tags. 17 | // More integration may come later (PR welcome). 18 | package xstats // import "github.com/rs/xstats" 19 | 20 | import ( 21 | "io" 22 | "strings" 23 | "sync" 24 | "time" 25 | ) 26 | 27 | const ( 28 | defaultDelimiter = "." 29 | ) 30 | 31 | var ( 32 | // DisablePooling will disable the use of sync.Pool fo resource management when true. 33 | // This allows for XStater instances to persist beyond the scope of an HTTP request 34 | // handler. However, using this option puts a greater pressure on GC and changes 35 | // the memory usage patterns of the library. Use only if there is a requirement 36 | // for persistent stater references. 37 | DisablePooling = false 38 | ) 39 | 40 | // XStater is a wrapper around a Sender to inject env tags within all observations. 41 | type XStater interface { 42 | Sender 43 | 44 | // AddTag adds a tag to the request client, this tag will be sent with all 45 | // subsequent stats queries. 46 | AddTags(tags ...string) 47 | 48 | // GetTags returns the tags associated with the XStater, all the tags that 49 | // will be sent along with all the stats queries. 50 | GetTags() []string 51 | } 52 | 53 | // Copier is an interface to an XStater that supports coping 54 | type Copier interface { 55 | Copy() XStater 56 | } 57 | 58 | // Scoper is an interface to an XStater, that supports scoping 59 | type Scoper interface { 60 | Scope(scope string, scopes ...string) XStater 61 | } 62 | 63 | var xstatsPool = &sync.Pool{ 64 | New: func() interface{} { 65 | return &xstats{} 66 | }, 67 | } 68 | 69 | // New returns a new xstats client with the provided backend sender. 70 | func New(s Sender) XStater { 71 | return NewPrefix(s, "") 72 | } 73 | 74 | // NewPrefix returns a new xstats client with the provided backend sender. 75 | // The prefix is prepended to all metric names. 76 | func NewPrefix(s Sender, prefix string) XStater { 77 | return NewScoping(s, "", prefix) 78 | } 79 | 80 | // NewScoping returns a new xstats client with the provided backend sender. 81 | // The delimiter is used to delimit scopes. Initial scopes can be provided. 82 | func NewScoping(s Sender, delimiter string, scopes ...string) XStater { 83 | var xs *xstats 84 | if DisablePooling { 85 | xs = &xstats{} 86 | } else { 87 | xs = xstatsPool.Get().(*xstats) 88 | } 89 | xs.s = s 90 | if len(scopes) > 0 { 91 | xs.prefix = strings.Join(scopes, delimiter) + delimiter 92 | } else { 93 | xs.prefix = "" 94 | } 95 | xs.delimiter = delimiter 96 | return xs 97 | } 98 | 99 | // Copy makes a copy of the given XStater if it implements the Copier 100 | // interface. Otherwise it returns a nop stats. 101 | func Copy(xs XStater) XStater { 102 | if c, ok := xs.(Copier); ok { 103 | return c.Copy() 104 | } 105 | return nop 106 | } 107 | 108 | // Scope makes a scoped copy of the given XStater if it implements the Scoper 109 | // interface. Otherwise it returns a nop stats. 110 | func Scope(xs XStater, scope string, scopes ...string) XStater { 111 | if c, ok := xs.(Scoper); ok { 112 | return c.Scope(scope, scopes...) 113 | } 114 | return nop 115 | } 116 | 117 | // Close will call Close() on any xstats.XStater that implements io.Closer 118 | func Close(xs XStater) error { 119 | if c, ok := xs.(io.Closer); ok { 120 | return c.Close() 121 | } 122 | return nil 123 | } 124 | 125 | type xstats struct { 126 | s Sender 127 | // tags are appended to the tags provided to commands 128 | tags []string 129 | // prefix is prepended to all metric 130 | prefix string 131 | // delimiter is used to delimit scopes 132 | delimiter string 133 | } 134 | 135 | // Copy implements the Copier interface 136 | func (xs *xstats) Copy() XStater { 137 | xs2 := NewScoping(xs.s, xs.delimiter, xs.prefix).(*xstats) 138 | xs2.tags = xs.tags 139 | return xs2 140 | } 141 | 142 | // Scope implements Scoper interface 143 | func (xs *xstats) Scope(scope string, scopes ...string) XStater { 144 | var scs []string 145 | if xs.prefix == "" { 146 | scs = make([]string, 0, 1+len(scopes)) 147 | } else { 148 | scs = make([]string, 0, 2+len(scopes)) 149 | scs = append(scs, strings.TrimRight(xs.prefix, xs.delimiter)) 150 | } 151 | scs = append(scs, scope) 152 | scs = append(scs, scopes...) 153 | xs2 := NewScoping(xs.s, xs.delimiter, scs...).(*xstats) 154 | xs2.tags = xs.tags 155 | return xs2 156 | } 157 | 158 | // Close returns the xstats to the sync.Pool 159 | func (xs *xstats) Close() error { 160 | if !DisablePooling { 161 | xs.s = nil 162 | xs.tags = nil 163 | xs.prefix = "" 164 | xs.delimiter = "" 165 | xstatsPool.Put(xs) 166 | } 167 | return nil 168 | } 169 | 170 | // AddTag implements XStater interface 171 | func (xs *xstats) AddTags(tags ...string) { 172 | if xs.tags == nil { 173 | xs.tags = tags 174 | } else { 175 | xs.tags = append(xs.tags, tags...) 176 | } 177 | } 178 | 179 | // AddTag implements XStater interface 180 | func (xs *xstats) GetTags() []string { 181 | return xs.tags 182 | } 183 | 184 | // Gauge implements XStater interface 185 | func (xs *xstats) Gauge(stat string, value float64, tags ...string) { 186 | if xs.s == nil { 187 | return 188 | } 189 | tags = append(tags, xs.tags...) 190 | xs.s.Gauge(xs.prefix+stat, value, tags...) 191 | } 192 | 193 | // Count implements XStater interface 194 | func (xs *xstats) Count(stat string, count float64, tags ...string) { 195 | if xs.s == nil { 196 | return 197 | } 198 | tags = append(tags, xs.tags...) 199 | xs.s.Count(xs.prefix+stat, count, tags...) 200 | } 201 | 202 | // Histogram implements XStater interface 203 | func (xs *xstats) Histogram(stat string, value float64, tags ...string) { 204 | if xs.s == nil { 205 | return 206 | } 207 | tags = append(tags, xs.tags...) 208 | xs.s.Histogram(xs.prefix+stat, value, tags...) 209 | } 210 | 211 | // Timing implements XStater interface 212 | func (xs *xstats) Timing(stat string, duration time.Duration, tags ...string) { 213 | if xs.s == nil { 214 | return 215 | } 216 | tags = append(tags, xs.tags...) 217 | xs.s.Timing(xs.prefix+stat, duration, tags...) 218 | } 219 | --------------------------------------------------------------------------------