├── .github ├── SECURITY.md ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ └── unit-tests.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── go.mod ├── go.sum ├── statsd ├── bench_buffered_test.go ├── bench_client_test.go ├── bench_sender_test.go ├── buffer_pool.go ├── client.go ├── client_buffered_test.go ├── client_config.go ├── client_legacy.go ├── client_substatter_test.go ├── client_test.go ├── doc.go ├── sender.go ├── sender_buffered.go ├── sender_buffered_test.go ├── sender_resolving.go ├── statsdtest │ ├── recorder.go │ ├── recorder_test.go │ ├── stat.go │ └── stat_test.go ├── tags.go ├── validator.go └── validator_test.go └── test-client └── main.go /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Either of the following are currently supported: 6 | 7 | * The HEAD of the master branch. 8 | * The most recent tagged "release" version to appear on the [releases][1] page. 9 | 10 | ## Reporting a Vulnerability 11 | 12 | To report a vulnerability, please use the 13 | [Privately reporting a security vulnerability][1] 14 | facility. 15 | 16 | [1]: https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability 17 | 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | labels: 11 | - dependencies 12 | schedule: 13 | interval: "weekly" 14 | day: "monday" 15 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | workflow_dispatch: 9 | branches: [master] 10 | schedule: 11 | - cron: '0 15 * * 4' 12 | 13 | # ensure testing on actual specified versions, and not auto-upgraded toolchain 14 | # versions 15 | env: 16 | GOTOOLCHAIN: local 17 | 18 | jobs: 19 | analyse: 20 | name: Analyse 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v4 26 | 27 | - name: Setup Go 28 | uses: actions/setup-go@v5 29 | with: 30 | go-version: '1.22.x' 31 | check-latest: true 32 | id: go 33 | 34 | - name: build 35 | env: 36 | GO111MODULE: "on" 37 | GOPROXY: "https://proxy.golang.org" 38 | run: go build ./... 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v2 43 | with: 44 | languages: go 45 | 46 | - name: Perform CodeQL Analysis 47 | uses: github/codeql-action/analyze@v2 48 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: unit-tests 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | workflow_dispatch: 9 | branches: [master] 10 | 11 | # ensure testing on actual specified versions, and not auto-upgraded toolchain 12 | # versions 13 | env: 14 | GOTOOLCHAIN: local 15 | 16 | jobs: 17 | build: 18 | name: Build 19 | strategy: 20 | matrix: 21 | goVer: ["1.19.x", "1.22.x"] 22 | platform: [ubuntu-latest] 23 | runs-on: ${{ matrix.platform }} 24 | 25 | steps: 26 | - name: Src Checkout 27 | uses: actions/checkout@v4 28 | with: 29 | fetch-depth: 1 30 | 31 | - name: Setup Go ${{ matrix.goVer }} 32 | uses: actions/setup-go@v5 33 | with: 34 | go-version: ${{ matrix.goVer }} 35 | check-latest: true 36 | id: go 37 | 38 | - name: Tests 39 | env: 40 | GO111MODULE: "on" 41 | GOPROXY: "https://proxy.golang.org" 42 | run: go test -v -cpu=1,2 ./... 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | ## head 5 | * update dependency 6 | 7 | ## 5.1.0 2023-07-19 8 | * Fix for tag format in substatter. (GH-55) 9 | * Add support for Floats in several situations. (GH-57) 10 | * Add new ExtendedStatSender interface for the new Float methods. 11 | 12 | ## 5.0.0 2021-01-13 13 | * Add Tag support: suffix-octothorpe, infix-comma, infix-semicolon (GH-53) 14 | * Remove previously deprecated NoopClient. Use a nil `*Client` Statter as a 15 | replacement, if needed. Ex: 16 | ``` 17 | var client Client 18 | // A nil *Client has noop behavior, so this is safe. 19 | // It will become a small overhead (just a couple function calls) noop. 20 | err = client.Inc("stat1", 42, 1.0) 21 | ``` 22 | 23 | ## 4.0.0 2020-11-05 24 | * Fix go.mod versioning. (GH-51,GH-52) 25 | * Bump major version for go.mod change, just in an attempt to be safer 26 | for existing users. 27 | 28 | ## 3.2.1 2020-06-23 29 | * Export NewBufferedSenderWithSender for direct use where needed. 30 | 31 | ## 3.2.0 2019-09-21 32 | * A new client constructor with "config style" semantics. 33 | "legacy" client construction still supported, to retain backwards compat. 34 | * Add an optional re-resolving client configuration. This sets a schedule for 35 | having the client periodically re-resolve the addr to ip. This does add some 36 | overhead, so best used only when necessary. 37 | 38 | ## 3.1.1 2018-01-19 39 | * avoid some overhead by not using defer for two "hot" path funcs 40 | * Fix leak on sender create with unresolvable destination (GH-34). 41 | 42 | ## 3.1.0 2016-05-30 43 | * `NewClientWithSender(Sender, string) (Statter, error)` method added to 44 | enable building a Client from a prefix and an already created Sender. 45 | * Add stat recording sender in submodule statsdtest (GH-32). 46 | * Add an example helper stat validation function. 47 | * Change the way scope joins are done (GH-26). 48 | * Reorder some structs to avoid middle padding. 49 | 50 | ## 3.0.3 2016-02-18 51 | * make sampler function tunable (GH-24) 52 | 53 | ## 3.0.2 2016-01-13 54 | * reduce memory allocations 55 | * improve performance of buffered clients 56 | 57 | ## 3.0.1 2016-01-01 58 | * documentation typo fixes 59 | * fix possible race condition with `buffered_sender` send/close. 60 | 61 | ## 3.0.0 2015-12-04 62 | * add substatter support 63 | 64 | ## 2.0.2 2015-10-16 65 | * remove trailing newline in buffered sends to avoid etsy statsd log messages 66 | * minor internal code reorganization for clarity (no api changes) 67 | 68 | ## 2.0.1 2015-07-12 69 | * Add Set and SetInt funcs to support Sets 70 | * Properly flush BufferedSender on close (bugfix) 71 | * Add TimingDuration with support for sub-millisecond timing 72 | * fewer allocations, better performance of BufferedClient 73 | 74 | ## 2.0.0 2015-03-19 75 | * BufferedClient - send multiple stats at once 76 | * clean up godocs 77 | * clean up interfaces -- BREAKING CHANGE: for users who previously defined 78 | types as *Client instead of the Statter interface type. 79 | 80 | ## 1.0.1 2015-03-19 81 | * BufferedClient - send multiple stats at once 82 | 83 | ## 1.0.0 2015-02-04 84 | * tag a version as fix for GH-8 85 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2016 Eli Janssen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | 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 THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-statsd-client 2 | ================ 3 | 4 | [![Build Status](https://github.com/cactus/go-statsd-client/workflows/unit-tests/badge.svg)](https://github.com/cactus/go-statsd-client/actions) 5 | [![GoDoc](https://godoc.org/github.com/cactus/go-statsd-client/statsd?status.png)](https://godoc.org/github.com/cactus/go-statsd-client/statsd) 6 | [![Go Report Card](https://goreportcard.com/badge/cactus/go-statsd-client)](https://goreportcard.com/report/cactus/go-statsd-client) 7 | [![License](https://img.shields.io/github/license/cactus/go-statsd-client.svg)](https://github.com/cactus/go-statsd-client/blob/master/LICENSE.md) 8 | 9 | ## About 10 | 11 | A [StatsD][1] client (UDP) for Go. 12 | 13 | ## Docs 14 | 15 | Viewable online at [godoc.org][2]. 16 | 17 | ## Example 18 | 19 | Some examples: 20 | 21 | ``` go 22 | import ( 23 | "log" 24 | 25 | "github.com/cactus/go-statsd-client/v5/statsd" 26 | ) 27 | 28 | func main() { 29 | // First create a client config. Here is a simple config that sends one 30 | // stat per packet (for compatibility). 31 | config := &statsd.ClientConfig{ 32 | Address: "127.0.0.1:8125", 33 | Prefix: "test-client", 34 | } 35 | 36 | /* 37 | // This one is for a client that re-resolves the hostname ever 30 seconds. 38 | // Useful if the address of a hostname changes frequently. Note that this 39 | // type of client has some additional locking overhead for safety. 40 | // As such, leave ResInetval as the zero value (previous exmaple) if you 41 | // don't specifically need this functionality. 42 | config := &statsd.ClientConfig{ 43 | Address: "127.0.0.1:8125", 44 | Prefix: "test-client", 45 | ResInterval: 30 * time.Second, 46 | } 47 | 48 | // This one is for a buffered client, which sends multiple stats in one 49 | // packet, is recommended when your server supports it (better performance). 50 | config := &statsd.ClientConfig{ 51 | Address: "127.0.0.1:8125", 52 | Prefix: "test-client", 53 | UseBuffered: true, 54 | // interval to force flush buffer. full buffers will flush on their own, 55 | // but for data not frequently sent, a max threshold is useful 56 | FlushInterval: 300*time.Millisecond, 57 | } 58 | 59 | // This one is for a buffered resolving client, which sends multiple stats 60 | // in one packet (like previous example), as well as re-resolving the 61 | // hostname every 30 seconds. 62 | config := &statsd.ClientConfig{ 63 | Address: "127.0.0.1:8125", 64 | Prefix: "test-client", 65 | ResInterval: 30 * time.Second, 66 | UseBuffered: true, 67 | FlushInterval: 300*time.Millisecond, 68 | } 69 | 70 | // This one is an example of configuring "Tag" support 71 | // Supported formats are: 72 | // InfixComma 73 | // InfixSemicolon 74 | // SuffixOctothorpe 75 | // The default, if not otherwise specified, is SuffixOctothorpe. 76 | config := &statsd.ClientConfig{ 77 | Address: "127.0.0.1:8125", 78 | Prefix: "test-client", 79 | ResInterval: 30 * time.Second, 80 | TagFormat: statsd.InfixSemicolon, 81 | } 82 | */ 83 | 84 | // Now create the client 85 | client, err := statsd.NewClientWithConfig(config) 86 | 87 | // and handle any initialization errors 88 | if err != nil { 89 | log.Fatal(err) 90 | } 91 | 92 | // make sure to close to clean up when done, to avoid leaks. 93 | defer client.Close() 94 | 95 | // Send a stat 96 | client.Inc("stat1", 42, 1.0) 97 | 98 | // Send a stat with "Tags" 99 | client.Inc("stat2", 41, 1.0, Tag{"mytag", "tagval"}) 100 | } 101 | ``` 102 | 103 | ### Legacy Example 104 | 105 | A legacy client creation method is still supported. This is retained so as not to break 106 | or interrupt existing integrations. 107 | 108 | ``` go 109 | import ( 110 | "log" 111 | 112 | "github.com/cactus/go-statsd-client/v5/statsd" 113 | ) 114 | 115 | func main() { 116 | // first create a client 117 | // The basic client sends one stat per packet (for compatibility). 118 | client, err := statsd.NewClient("127.0.0.1:8125", "test-client") 119 | 120 | // A buffered client, which sends multiple stats in one packet, is 121 | // recommended when your server supports it (better performance). 122 | // client, err := statsd.NewBufferedClient("127.0.0.1:8125", "test-client", 300*time.Millisecond, 0) 123 | 124 | // handle any errors 125 | if err != nil { 126 | log.Fatal(err) 127 | } 128 | // make sure to close to clean up when done, to avoid leaks. 129 | defer client.Close() 130 | 131 | // Send a stat 132 | client.Inc("stat1", 42, 1.0) 133 | } 134 | ``` 135 | 136 | 137 | See [docs][2] for more info. There is also some additional example code in the 138 | `test-client` directory. 139 | 140 | ## Contributors 141 | 142 | See [here][4]. 143 | 144 | ## Alternative Implementations 145 | 146 | See the [statsd wiki][5] for some additional client implementations 147 | (scroll down to the Go section). 148 | 149 | ## License 150 | 151 | Released under the [MIT license][3]. See `LICENSE.md` file for details. 152 | 153 | 154 | [1]: https://github.com/etsy/statsd 155 | [2]: http://godoc.org/github.com/cactus/go-statsd-client/ 156 | [3]: http://www.opensource.org/licenses/mit-license.php 157 | [4]: https://github.com/cactus/go-statsd-client/graphs/contributors 158 | [5]: https://github.com/etsy/statsd/wiki#client-implementations 159 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cactus/go-statsd-client/v5 2 | 3 | go 1.11 4 | 5 | require ( 6 | github.com/jessevdk/go-flags v1.6.1 7 | golang.org/x/sys v0.30.0 // indirect 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= 2 | github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= 3 | golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 4 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 5 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 6 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 7 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 8 | -------------------------------------------------------------------------------- /statsd/bench_buffered_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2016 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package statsd 6 | 7 | import ( 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func BenchmarkBufferedClientInc(b *testing.B) { 13 | l, err := newUDPListener("127.0.0.1:0") 14 | if err != nil { 15 | b.Fatal(err) 16 | } 17 | defer l.Close() 18 | c, err := NewBufferedClient(l.LocalAddr().String(), "test", 1*time.Second, 0) 19 | if err != nil { 20 | b.Fatal(err) 21 | } 22 | defer c.Close() 23 | 24 | b.ResetTimer() 25 | 26 | b.RunParallel(func(pb *testing.PB) { 27 | for pb.Next() { 28 | //i := 0; i < b.N; i++ { 29 | c.Inc("benchinc", 1, 1) 30 | } 31 | }) 32 | } 33 | 34 | func BenchmarkBufferedClientIncSample(b *testing.B) { 35 | l, err := newUDPListener("127.0.0.1:0") 36 | if err != nil { 37 | b.Fatal(err) 38 | } 39 | defer l.Close() 40 | c, err := NewBufferedClient(l.LocalAddr().String(), "test", 1*time.Second, 0) 41 | if err != nil { 42 | b.Fatal(err) 43 | } 44 | defer c.Close() 45 | 46 | b.ResetTimer() 47 | 48 | b.RunParallel(func(pb *testing.PB) { 49 | for pb.Next() { 50 | //i := 0; i < b.N; i++ { 51 | c.Inc("benchinc", 1, 0.3) 52 | } 53 | }) 54 | } 55 | 56 | func BenchmarkBufferedClientSetInt(b *testing.B) { 57 | l, err := newUDPListener("127.0.0.1:0") 58 | if err != nil { 59 | b.Fatal(err) 60 | } 61 | defer l.Close() 62 | c, err := NewBufferedClient(l.LocalAddr().String(), "test", 1*time.Second, 0) 63 | if err != nil { 64 | b.Fatal(err) 65 | } 66 | defer c.Close() 67 | 68 | b.ResetTimer() 69 | 70 | b.RunParallel(func(pb *testing.PB) { 71 | for pb.Next() { 72 | //i := 0; i < b.N; i++ { 73 | c.SetInt("setint", 1, 1) 74 | } 75 | }) 76 | } 77 | 78 | func BenchmarkBufferedClientSetIntSample(b *testing.B) { 79 | l, err := newUDPListener("127.0.0.1:0") 80 | if err != nil { 81 | b.Fatal(err) 82 | } 83 | defer l.Close() 84 | c, err := NewBufferedClient(l.LocalAddr().String(), "test", 1*time.Second, 0) 85 | if err != nil { 86 | b.Fatal(err) 87 | } 88 | defer c.Close() 89 | 90 | b.ResetTimer() 91 | 92 | b.RunParallel(func(pb *testing.PB) { 93 | for pb.Next() { 94 | //i := 0; i < b.N; i++ { 95 | c.SetInt("setint", 1, 0.3) 96 | } 97 | }) 98 | } 99 | -------------------------------------------------------------------------------- /statsd/bench_client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2016 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package statsd 6 | 7 | import ( 8 | "testing" 9 | ) 10 | 11 | func BenchmarkClientInc(b *testing.B) { 12 | l, err := newUDPListener("127.0.0.1:0") 13 | if err != nil { 14 | b.Fatal(err) 15 | } 16 | defer l.Close() 17 | c, err := NewClient(l.LocalAddr().String(), "test") 18 | if err != nil { 19 | b.Fatal(err) 20 | } 21 | defer c.Close() 22 | 23 | b.ResetTimer() 24 | 25 | b.RunParallel(func(pb *testing.PB) { 26 | for pb.Next() { 27 | //i := 0; i < b.N; i++ { 28 | c.Inc("benchinc", 1, 1) 29 | } 30 | }) 31 | } 32 | 33 | func BenchmarkClientIncSample(b *testing.B) { 34 | l, err := newUDPListener("127.0.0.1:0") 35 | if err != nil { 36 | b.Fatal(err) 37 | } 38 | defer l.Close() 39 | c, err := NewClient(l.LocalAddr().String(), "test") 40 | if err != nil { 41 | b.Fatal(err) 42 | } 43 | defer c.Close() 44 | 45 | b.ResetTimer() 46 | 47 | b.RunParallel(func(pb *testing.PB) { 48 | for pb.Next() { 49 | //i := 0; i < b.N; i++ { 50 | c.Inc("benchinc", 1, 0.3) 51 | } 52 | }) 53 | } 54 | 55 | func BenchmarkClientSetInt(b *testing.B) { 56 | l, err := newUDPListener("127.0.0.1:0") 57 | if err != nil { 58 | b.Fatal(err) 59 | } 60 | defer l.Close() 61 | c, err := NewClient(l.LocalAddr().String(), "test") 62 | if err != nil { 63 | b.Fatal(err) 64 | } 65 | defer c.Close() 66 | 67 | b.ResetTimer() 68 | 69 | b.RunParallel(func(pb *testing.PB) { 70 | for pb.Next() { 71 | //i := 0; i < b.N; i++ { 72 | c.SetInt("setint", 1, 1) 73 | } 74 | }) 75 | } 76 | 77 | func BenchmarkClientSetIntSample(b *testing.B) { 78 | l, err := newUDPListener("127.0.0.1:0") 79 | if err != nil { 80 | b.Fatal(err) 81 | } 82 | defer l.Close() 83 | c, err := NewClient(l.LocalAddr().String(), "test") 84 | if err != nil { 85 | b.Fatal(err) 86 | } 87 | defer c.Close() 88 | 89 | b.ResetTimer() 90 | 91 | b.RunParallel(func(pb *testing.PB) { 92 | for pb.Next() { 93 | //i := 0; i < b.N; i++ { 94 | c.SetInt("setint", 1, 0.3) 95 | } 96 | }) 97 | } 98 | -------------------------------------------------------------------------------- /statsd/bench_sender_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2016 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package statsd 6 | 7 | import ( 8 | "bytes" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func BenchmarkSenderSmall(b *testing.B) { 14 | l, err := newUDPListener("127.0.0.1:0") 15 | if err != nil { 16 | b.Fatal(err) 17 | } 18 | defer l.Close() 19 | s, err := NewSimpleSender(l.LocalAddr().String()) 20 | if err != nil { 21 | b.Fatal(err) 22 | } 23 | defer s.Close() 24 | 25 | data := []byte("test.gauge:1|g\n") 26 | b.ResetTimer() 27 | 28 | b.RunParallel(func(pb *testing.PB) { 29 | for pb.Next() { 30 | //i := 0; i < b.N; i++ { 31 | s.Send(data) 32 | } 33 | }) 34 | } 35 | 36 | func BenchmarkSenderLarge(b *testing.B) { 37 | l, err := newUDPListener("127.0.0.1:0") 38 | if err != nil { 39 | b.Fatal(err) 40 | } 41 | defer l.Close() 42 | s, err := NewSimpleSender(l.LocalAddr().String()) 43 | if err != nil { 44 | b.Fatal(err) 45 | } 46 | defer s.Close() 47 | 48 | data := bytes.Repeat([]byte("test.gauge:1|g\n"), 50) 49 | b.ResetTimer() 50 | 51 | b.RunParallel(func(pb *testing.PB) { 52 | for pb.Next() { 53 | //i := 0; i < b.N; i++ { 54 | s.Send(data) 55 | } 56 | }) 57 | } 58 | 59 | func BenchmarkBufferedSenderSmall(b *testing.B) { 60 | l, err := newUDPListener("127.0.0.1:0") 61 | if err != nil { 62 | b.Fatal(err) 63 | } 64 | defer l.Close() 65 | s, err := NewBufferedSender(l.LocalAddr().String(), 300*time.Millisecond, 1432) 66 | if err != nil { 67 | b.Fatal(err) 68 | } 69 | defer s.Close() 70 | 71 | data := []byte("test.gauge:1|g\n") 72 | b.ResetTimer() 73 | 74 | b.RunParallel(func(pb *testing.PB) { 75 | for pb.Next() { 76 | //i := 0; i < b.N; i++ { 77 | s.Send(data) 78 | } 79 | }) 80 | } 81 | func BenchmarkBufferedSenderLarge(b *testing.B) { 82 | l, err := newUDPListener("127.0.0.1:0") 83 | if err != nil { 84 | b.Fatal(err) 85 | } 86 | defer l.Close() 87 | s, err := NewBufferedSender(l.LocalAddr().String(), 300*time.Millisecond, 1432) 88 | if err != nil { 89 | b.Fatal(err) 90 | } 91 | defer s.Close() 92 | 93 | data := bytes.Repeat([]byte("test.gauge:1|g\n"), 50) 94 | b.ResetTimer() 95 | 96 | b.RunParallel(func(pb *testing.PB) { 97 | for pb.Next() { 98 | //i := 0; i < b.N; i++ { 99 | s.Send(data) 100 | } 101 | }) 102 | } 103 | -------------------------------------------------------------------------------- /statsd/buffer_pool.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2016 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package statsd 6 | 7 | import ( 8 | "bytes" 9 | "sync" 10 | ) 11 | 12 | type bufferPool struct { 13 | *sync.Pool 14 | } 15 | 16 | func newBufferPool() *bufferPool { 17 | return &bufferPool{ 18 | &sync.Pool{New: func() interface{} { 19 | return bytes.NewBuffer(make([]byte, 0, 1700)) 20 | }}, 21 | } 22 | } 23 | 24 | func (bp *bufferPool) Get() *bytes.Buffer { 25 | return (bp.Pool.Get()).(*bytes.Buffer) 26 | } 27 | 28 | func (bp *bufferPool) Put(b *bytes.Buffer) { 29 | b.Truncate(0) 30 | bp.Pool.Put(b) 31 | } 32 | -------------------------------------------------------------------------------- /statsd/client.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2016 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package statsd 6 | 7 | import ( 8 | "fmt" 9 | "math/rand" 10 | "strconv" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | var bufPool = newBufferPool() 16 | 17 | // The StatSender interface wraps all the statsd metric methods 18 | type StatSender interface { 19 | Inc(string, int64, float32, ...Tag) error 20 | Dec(string, int64, float32, ...Tag) error 21 | Gauge(string, int64, float32, ...Tag) error 22 | GaugeDelta(string, int64, float32, ...Tag) error 23 | Timing(string, int64, float32, ...Tag) error 24 | TimingDuration(string, time.Duration, float32, ...Tag) error 25 | Set(string, string, float32, ...Tag) error 26 | SetInt(string, int64, float32, ...Tag) error 27 | Raw(string, string, float32, ...Tag) error 28 | } 29 | 30 | // The ExtendedStatSender interface wraps a StatSender and adds some 31 | // methods that may be unsupported by some servers. 32 | type ExtendedStatSender interface { 33 | StatSender 34 | GaugeFloat(string, float64, float32, ...Tag) error 35 | GaugeFloatDelta(string, float64, float32, ...Tag) error 36 | SetFloat(string, float64, float32, ...Tag) error 37 | } 38 | 39 | // The Statter interface defines the behavior of a stat client 40 | type Statter interface { 41 | StatSender 42 | NewSubStatter(string) SubStatter 43 | SetPrefix(string) 44 | Close() error 45 | } 46 | 47 | // The SubStatter interface defines the behavior of a stat child/subclient 48 | type SubStatter interface { 49 | StatSender 50 | SetSamplerFunc(SamplerFunc) 51 | NewSubStatter(string) SubStatter 52 | } 53 | 54 | // The SamplerFunc type defines a function that can serve 55 | // as a Client sampler function. 56 | type SamplerFunc func(float32) bool 57 | 58 | // DefaultSampler is the default rate sampler function 59 | func DefaultSampler(rate float32) bool { 60 | if rate < 1 { 61 | return rand.Float32() < rate 62 | } 63 | return true 64 | } 65 | 66 | // A Client is a statsd client. 67 | type Client struct { 68 | // prefix for statsd name 69 | prefix string 70 | // packet sender 71 | sender Sender 72 | // sampler method 73 | sampler SamplerFunc 74 | // tag handler 75 | tagFormat TagFormat 76 | } 77 | 78 | // Close closes the connection and cleans up. 79 | func (s *Client) Close() error { 80 | if s == nil { 81 | return nil 82 | } 83 | 84 | err := s.sender.Close() 85 | return err 86 | } 87 | 88 | // Inc increments a statsd count type. 89 | // stat is a string name for the metric. 90 | // value is the integer value 91 | // rate is the sample rate (0.0 to 1.0) 92 | // tags is a []Tag 93 | func (s *Client) Inc(stat string, value int64, rate float32, tags ...Tag) error { 94 | if !s.includeStat(rate) { 95 | return nil 96 | } 97 | 98 | return s.submit(stat, "", value, "|c", rate, tags) 99 | } 100 | 101 | // Dec decrements a statsd count type. 102 | // stat is a string name for the metric. 103 | // value is the integer value. 104 | // rate is the sample rate (0.0 to 1.0). 105 | func (s *Client) Dec(stat string, value int64, rate float32, tags ...Tag) error { 106 | if !s.includeStat(rate) { 107 | return nil 108 | } 109 | 110 | return s.submit(stat, "", -value, "|c", rate, tags) 111 | } 112 | 113 | // Gauge submits/updates a statsd gauge type. 114 | // stat is a string name for the metric. 115 | // value is the integer value. 116 | // rate is the sample rate (0.0 to 1.0). 117 | func (s *Client) Gauge(stat string, value int64, rate float32, tags ...Tag) error { 118 | if !s.includeStat(rate) { 119 | return nil 120 | } 121 | 122 | return s.submit(stat, "", value, "|g", rate, tags) 123 | } 124 | 125 | // GaugeDelta submits a delta to a statsd gauge. 126 | // stat is the string name for the metric. 127 | // value is the (positive or negative) change. 128 | // rate is the sample rate (0.0 to 1.0). 129 | func (s *Client) GaugeDelta(stat string, value int64, rate float32, tags ...Tag) error { 130 | if !s.includeStat(rate) { 131 | return nil 132 | } 133 | 134 | // if negative, the submit formatter will prefix with a - already 135 | // so only special case the positive value. 136 | // don't pull out the prefix here, avoids some tiny amount of stack space by 137 | // inlining like this. performance 138 | if value >= 0 { 139 | return s.submit(stat, "+", value, "|g", rate, tags) 140 | } 141 | return s.submit(stat, "", value, "|g", rate, tags) 142 | } 143 | 144 | // GaugeFloat submits/updates a float statsd gauge type. 145 | // Note: May not be supported by all servers. 146 | // stat is a string name for the metric. 147 | // value is the float64 value. 148 | // rate is the sample rate (0.0 to 1.0). 149 | func (s *Client) GaugeFloat(stat string, value float64, rate float32, tags ...Tag) error { 150 | if !s.includeStat(rate) { 151 | return nil 152 | } 153 | 154 | return s.submit(stat, "", value, "|g", rate, tags) 155 | } 156 | 157 | // GaugeFloatDelta submits a float delta to a statsd gauge. 158 | // Note: May not be supported by all servers. 159 | // stat is the string name for the metric. 160 | // value is the (positive or negative) change. 161 | // rate is the sample rate (0.0 to 1.0). 162 | func (s *Client) GaugeFloatDelta(stat string, value float64, rate float32, tags ...Tag) error { 163 | if !s.includeStat(rate) { 164 | return nil 165 | } 166 | 167 | // if negative, the submit formatter will prefix with a - already 168 | // so only special case the positive value 169 | if value >= 0 { 170 | return s.submit(stat, "+", value, "|g", rate, tags) 171 | } 172 | return s.submit(stat, "", value, "|g", rate, tags) 173 | } 174 | 175 | // Timing submits a statsd timing type. 176 | // stat is a string name for the metric. 177 | // delta is the time duration value in milliseconds 178 | // rate is the sample rate (0.0 to 1.0). 179 | func (s *Client) Timing(stat string, delta int64, rate float32, tags ...Tag) error { 180 | if !s.includeStat(rate) { 181 | return nil 182 | } 183 | 184 | return s.submit(stat, "", delta, "|ms", rate, tags) 185 | } 186 | 187 | // TimingDuration submits a statsd timing type. 188 | // stat is a string name for the metric. 189 | // delta is the timing value as time.Duration 190 | // rate is the sample rate (0.0 to 1.0). 191 | func (s *Client) TimingDuration(stat string, delta time.Duration, rate float32, tags ...Tag) error { 192 | if !s.includeStat(rate) { 193 | return nil 194 | } 195 | 196 | ms := float64(delta) / float64(time.Millisecond) 197 | return s.submit(stat, "", ms, "|ms", rate, tags) 198 | } 199 | 200 | // Set submits a stats set type 201 | // stat is a string name for the metric. 202 | // value is the string value 203 | // rate is the sample rate (0.0 to 1.0). 204 | func (s *Client) Set(stat string, value string, rate float32, tags ...Tag) error { 205 | if !s.includeStat(rate) { 206 | return nil 207 | } 208 | 209 | return s.submit(stat, "", value, "|s", rate, tags) 210 | } 211 | 212 | // SetInt submits a number as a stats set type. 213 | // stat is a string name for the metric. 214 | // value is the integer value 215 | // rate is the sample rate (0.0 to 1.0). 216 | func (s *Client) SetInt(stat string, value int64, rate float32, tags ...Tag) error { 217 | if !s.includeStat(rate) { 218 | return nil 219 | } 220 | 221 | return s.submit(stat, "", value, "|s", rate, tags) 222 | } 223 | 224 | // SetFloat submits a number as a stats set type. 225 | // Note: May not be supported by all servers. 226 | // stat is a string name for the metric. 227 | // value is the integer value 228 | // rate is the sample rate (0.0 to 1.0). 229 | func (s *Client) SetFloat(stat string, value float64, rate float32, tags ...Tag) error { 230 | if !s.includeStat(rate) { 231 | return nil 232 | } 233 | 234 | return s.submit(stat, "", value, "|s", rate, tags) 235 | } 236 | 237 | // Raw submits a preformatted value. 238 | // stat is the string name for the metric. 239 | // value is a preformatted "raw" value string. 240 | // rate is the sample rate (0.0 to 1.0). 241 | func (s *Client) Raw(stat string, value string, rate float32, tags ...Tag) error { 242 | if !s.includeStat(rate) { 243 | return nil 244 | } 245 | 246 | return s.submit(stat, "", value, "", rate, tags) 247 | } 248 | 249 | // SetSamplerFunc sets a sampler function to something other than the default 250 | // sampler is a function that determines whether the metric is 251 | // to be accepted, or discarded. 252 | // An example use case is for submitted pre-sampled metrics. 253 | func (s *Client) SetSamplerFunc(sampler SamplerFunc) { 254 | s.sampler = sampler 255 | } 256 | 257 | // submit an already sampled raw stat 258 | func (s *Client) submit(stat, vprefix string, value interface{}, suffix string, rate float32, tags []Tag) error { 259 | skiptags := false 260 | if len(tags) == 0 { 261 | skiptags = true 262 | } 263 | 264 | buf := bufPool.Get() 265 | defer bufPool.Put(buf) 266 | // sadly, no way to jam this back into the bytes.Buffer without 267 | // doing a few allocations... avoiding those is the whole point here... 268 | // so from here on out just use it as a raw []byte 269 | data := buf.Bytes() 270 | 271 | if s.prefix != "" { 272 | data = append(data, s.prefix...) 273 | data = append(data, '.') 274 | } 275 | 276 | data = append(data, stat...) 277 | 278 | // infix tags, if present 279 | if !skiptags && s.tagFormat&AllInfix != 0 { 280 | data = s.tagFormat.WriteInfix(data, tags) 281 | // if we did infix already, no suffix also. 282 | skiptags = true 283 | } 284 | 285 | data = append(data, ':') 286 | 287 | if vprefix != "" { 288 | data = append(data, vprefix...) 289 | } 290 | 291 | switch v := value.(type) { 292 | case string: 293 | data = append(data, v...) 294 | case int64: 295 | data = strconv.AppendInt(data, v, 10) 296 | case float64: 297 | data = strconv.AppendFloat(data, v, 'f', -1, 64) 298 | default: 299 | return fmt.Errorf("No matching type format") 300 | } 301 | 302 | if suffix != "" { 303 | data = append(data, suffix...) 304 | } 305 | 306 | if rate < 1 { 307 | data = append(data, "|@"...) 308 | data = strconv.AppendFloat(data, float64(rate), 'f', 6, 32) 309 | } 310 | 311 | // suffix tags if present 312 | if !skiptags && s.tagFormat&AllSuffix != 0 { 313 | data = s.tagFormat.WriteSuffix(data, tags) 314 | } 315 | 316 | _, err := s.sender.Send(data) 317 | return err 318 | } 319 | 320 | // check for nil client, and perform sampling calculation 321 | func (s *Client) includeStat(rate float32) bool { 322 | if s == nil { 323 | return false 324 | } 325 | 326 | // test for nil in case someone builds their own 327 | // client without calling new (result is nil sampler) 328 | if s.sampler != nil { 329 | return s.sampler(rate) 330 | } 331 | return DefaultSampler(rate) 332 | } 333 | 334 | // SetPrefix sets/updates the statsd client prefix. 335 | // Note: Does not change the prefix of any SubStatters. 336 | func (s *Client) SetPrefix(prefix string) { 337 | if s == nil { 338 | return 339 | } 340 | 341 | s.prefix = prefix 342 | } 343 | 344 | // NewSubStatter returns a SubStatter with appended prefix 345 | func (s *Client) NewSubStatter(prefix string) SubStatter { 346 | var c *Client 347 | if s != nil { 348 | c = &Client{ 349 | prefix: joinPathComp(s.prefix, prefix), 350 | sender: s.sender, 351 | sampler: s.sampler, 352 | tagFormat: s.tagFormat, 353 | } 354 | } 355 | return c 356 | } 357 | 358 | // joinPathComp is a helper that ensures we combine path components with a dot 359 | // when it's appropriate to do so; prefix is the existing prefix and suffix is 360 | // the new component being added. 361 | // 362 | // It returns the joined prefix. 363 | func joinPathComp(prefix, suffix string) string { 364 | suffix = strings.TrimLeft(suffix, ".") 365 | if prefix != "" && suffix != "" { 366 | return prefix + "." + suffix 367 | } 368 | return prefix + suffix 369 | } 370 | -------------------------------------------------------------------------------- /statsd/client_buffered_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2016 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package statsd 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "log" 11 | "reflect" 12 | "strings" 13 | "testing" 14 | "time" 15 | ) 16 | 17 | func TestBufferedClientFlushSize(t *testing.T) { 18 | l, err := newUDPListener("127.0.0.1:0") 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | defer l.Close() 23 | 24 | for _, tt := range statsdPacketTests { 25 | // set flush length to the size of the expected output packet 26 | // so we can ensure a flush happens right away. 27 | // set flush time sufficiently high so that it never matters for this 28 | // test 29 | c, err := NewBufferedClient(l.LocalAddr().String(), tt.Prefix, 10*time.Second, len(tt.Expected)+1) 30 | if err != nil { 31 | c.Close() 32 | t.Fatal(err) 33 | } 34 | method := reflect.ValueOf(c).MethodByName(tt.Method) 35 | e := method.Call([]reflect.Value{ 36 | reflect.ValueOf(tt.Stat), 37 | reflect.ValueOf(tt.Value), 38 | reflect.ValueOf(tt.Rate)})[0] 39 | errInter := e.Interface() 40 | if errInter != nil { 41 | c.Close() 42 | t.Fatal(errInter.(error)) 43 | } 44 | 45 | data := make([]byte, len(tt.Expected)+16) 46 | _, _, err = l.ReadFrom(data) 47 | if err != nil { 48 | c.Close() 49 | t.Fatal(err) 50 | } 51 | 52 | data = bytes.TrimRight(data, "\x00\n") 53 | if !bytes.Equal(data, []byte(tt.Expected)) { 54 | t.Fatalf("%s got '%s' expected '%s'", tt.Method, data, tt.Expected) 55 | } 56 | c.Close() 57 | } 58 | } 59 | 60 | func TestBufferedClientFlushTime(t *testing.T) { 61 | l, err := newUDPListener("127.0.0.1:0") 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | defer l.Close() 66 | 67 | for _, tt := range statsdPacketTests { 68 | // set flush length to the size of the expected output packet 69 | // so we can ensure a flush happens right away. 70 | // set flush time sufficiently high so that it never matters for this 71 | // test 72 | c, err := NewBufferedClient(l.LocalAddr().String(), tt.Prefix, 1*time.Microsecond, 1024) 73 | if err != nil { 74 | c.Close() 75 | t.Fatal(err) 76 | } 77 | method := reflect.ValueOf(c).MethodByName(tt.Method) 78 | e := method.Call([]reflect.Value{ 79 | reflect.ValueOf(tt.Stat), 80 | reflect.ValueOf(tt.Value), 81 | reflect.ValueOf(tt.Rate)})[0] 82 | errInter := e.Interface() 83 | if errInter != nil { 84 | c.Close() 85 | t.Fatal(errInter.(error)) 86 | } 87 | 88 | time.Sleep(1 * time.Millisecond) 89 | 90 | data := make([]byte, len(tt.Expected)+16) 91 | _, _, err = l.ReadFrom(data) 92 | if err != nil { 93 | c.Close() 94 | t.Fatal(err) 95 | } 96 | 97 | data = bytes.TrimRight(data, "\x00\n") 98 | if !bytes.Equal(data, []byte(tt.Expected)) { 99 | t.Fatalf("%s got '%s' expected '%s'", tt.Method, data, tt.Expected) 100 | } 101 | c.Close() 102 | } 103 | } 104 | 105 | func TestBufferedClientBigPacket(t *testing.T) { 106 | l, err := newUDPListener("127.0.0.1:0") 107 | if err != nil { 108 | t.Fatal(err) 109 | } 110 | defer l.Close() 111 | 112 | c, err := NewBufferedClient(l.LocalAddr().String(), "test", 10*time.Millisecond, 1024) 113 | if err != nil { 114 | t.Fatal(err) 115 | } 116 | defer c.Close() 117 | 118 | for _, tt := range statsdPacketTests { 119 | if tt.Prefix != "test" { 120 | continue 121 | } 122 | method := reflect.ValueOf(c).MethodByName(tt.Method) 123 | e := method.Call([]reflect.Value{ 124 | reflect.ValueOf(tt.Stat), 125 | reflect.ValueOf(tt.Value), 126 | reflect.ValueOf(tt.Rate)})[0] 127 | errInter := e.Interface() 128 | if errInter != nil { 129 | t.Fatal(errInter.(error)) 130 | } 131 | } 132 | 133 | expected := "" 134 | for _, tt := range statsdPacketTests { 135 | if tt.Prefix != "test" { 136 | continue 137 | } 138 | expected = expected + tt.Expected + "\n" 139 | } 140 | 141 | expected = strings.TrimSuffix(expected, "\n") 142 | 143 | time.Sleep(12 * time.Millisecond) 144 | data := make([]byte, 1024) 145 | _, _, err = l.ReadFrom(data) 146 | if err != nil { 147 | t.Fatal(err) 148 | } 149 | 150 | data = bytes.TrimRight(data, "\x00") 151 | if !bytes.Equal(data, []byte(expected)) { 152 | t.Fatalf("got '%s' expected '%s'", data, expected) 153 | } 154 | } 155 | 156 | func TestFlushOnClose(t *testing.T) { 157 | l, err := newUDPListener("127.0.0.1:0") 158 | if err != nil { 159 | t.Fatal(err) 160 | } 161 | defer l.Close() 162 | 163 | c, err := NewBufferedClient(l.LocalAddr().String(), "test", 1*time.Second, 1024) 164 | if err != nil { 165 | t.Fatal(err) 166 | } 167 | 168 | c.Inc("count", int64(1), 1.0) 169 | c.Close() 170 | 171 | expected := "test.count:1|c" 172 | 173 | data := make([]byte, 1024) 174 | _, _, err = l.ReadFrom(data) 175 | if err != nil { 176 | t.Fatal(err) 177 | } 178 | 179 | data = bytes.TrimRight(data, "\x00") 180 | if !bytes.Equal(data, []byte(expected)) { 181 | fmt.Println(data) 182 | fmt.Println([]byte(expected)) 183 | t.Fatalf("got '%s' expected '%s'", data, expected) 184 | } 185 | } 186 | 187 | func ExampleClient_buffered() { 188 | // This one is for a buffered client, which sends multiple stats in one 189 | // packet, is recommended when your server supports it (better performance). 190 | config := &ClientConfig{ 191 | Address: "127.0.0.1:8125", 192 | Prefix: "test-client", 193 | UseBuffered: true, 194 | // interval to force flush buffer. full buffers will flush on their own, 195 | // but for data not frequently sent, a max threshold is useful 196 | FlushInterval: 300 * time.Millisecond, 197 | } 198 | 199 | // Now create the client 200 | client, err := NewClientWithConfig(config) 201 | 202 | // and handle any initialization errors 203 | if err != nil { 204 | log.Fatal(err) 205 | } 206 | 207 | // make sure to close to clean up when done, to avoid leaks. 208 | defer client.Close() 209 | 210 | // Send a stat 211 | err = client.Inc("stat1", 42, 1.0) 212 | // handle any errors 213 | if err != nil { 214 | log.Printf("Error sending metric: %+v", err) 215 | } 216 | } 217 | 218 | func ExampleClient_legacyBuffered() { 219 | // first create a client 220 | client, err := NewBufferedClient("127.0.0.1:8125", "test-client", 10*time.Millisecond, 0) 221 | // handle any errors 222 | if err != nil { 223 | log.Fatal(err) 224 | } 225 | // make sure to close to clean up when done, to avoid leaks. 226 | defer client.Close() 227 | 228 | // Send a stat 229 | err = client.Inc("stat1", 42, 1.0) 230 | // handle any errors 231 | if err != nil { 232 | log.Printf("Error sending metric: %+v", err) 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /statsd/client_config.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2016 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package statsd 6 | 7 | import ( 8 | "fmt" 9 | "time" 10 | ) 11 | 12 | type ClientConfig struct { 13 | // addr is a string of the format "hostname:port", and must be something 14 | // validly parsable by net.ResolveUDPAddr. 15 | Address string 16 | 17 | // prefix is the statsd client prefix. Can be "" if no prefix is desired. 18 | Prefix string 19 | 20 | // ResInterval is the interval over which the addr is re-resolved. 21 | // Do note that this /does/ add overhead! 22 | // If you need higher performance, leave unset (or set to 0), 23 | // in which case the address will not be re-resolved. 24 | // 25 | // Note that if Address is an {ip}:{port} and not a {hostname}:{port}, then 26 | // ResInterval will be ignored. 27 | ResInterval time.Duration 28 | 29 | // UseBuffered determines whether a buffered sender is used or not. 30 | // If a buffered sender is /not/ used, FlushInterval and FlushBytes values are 31 | // ignored. Default is false. 32 | UseBuffered bool 33 | 34 | // FlushInterval is a time.Duration, and specifies the maximum interval for 35 | // packet sending. Note that if you send lots of metrics, you will send more 36 | // often. This is just a maximal threshold. 37 | // If FlushInterval is 0, defaults to 300ms. 38 | FlushInterval time.Duration 39 | 40 | // If flushBytes is 0, defaults to 1432 bytes, which is considered safe 41 | // for local traffic. If sending over the public internet, 512 bytes is 42 | // the recommended value. 43 | FlushBytes int 44 | 45 | // The desired tag format to use for tags (note: statsd tag support varies) 46 | // Supported formats are one of: statsd.DataDog, statsd.Grahpite, statsd.Influx 47 | TagFormat TagFormat 48 | } 49 | 50 | // NewClientWithConfig returns a new BufferedClient 51 | // 52 | // config is a ClientConfig, which holds various configuration values. 53 | func NewClientWithConfig(config *ClientConfig) (Statter, error) { 54 | var sender Sender 55 | var err error 56 | 57 | // guard against nil config 58 | if config == nil { 59 | return nil, fmt.Errorf("config cannot be nil") 60 | } 61 | 62 | // Use a re-resolving simple sender iff: 63 | // * The time duration greater than 0 64 | // * The Address is not an ip (eg. {ip}:{port}). 65 | // Otherwise, re-resolution is not required. 66 | if config.ResInterval > 0 && !mustBeIP(config.Address) { 67 | sender, err = NewResolvingSimpleSender(config.Address, config.ResInterval) 68 | } else { 69 | sender, err = NewSimpleSender(config.Address) 70 | } 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | if config.UseBuffered { 76 | return newBufferedC(sender, config) 77 | } else { 78 | return NewClientWithSender(sender, config.Prefix, config.TagFormat) 79 | } 80 | } 81 | 82 | func newBufferedC(baseSender Sender, config *ClientConfig) (Statter, error) { 83 | 84 | flushBytes := config.FlushBytes 85 | if flushBytes <= 0 { 86 | // ref: 87 | // github.com/etsy/statsd/blob/master/docs/metric_types.md#multi-metric-packets 88 | flushBytes = 1432 89 | } 90 | 91 | flushInterval := config.FlushInterval 92 | if flushInterval <= time.Duration(0) { 93 | flushInterval = 300 * time.Millisecond 94 | } 95 | 96 | bufsender, err := NewBufferedSenderWithSender(baseSender, flushInterval, flushBytes) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | return NewClientWithSender(bufsender, config.Prefix, config.TagFormat) 102 | } 103 | 104 | // NewClientWithSender returns a pointer to a new Client and an error. 105 | // 106 | // sender is an instance of a statsd.Sender interface and may not be nil 107 | // 108 | // prefix is the stastd client prefix. Can be "" if no prefix is desired. 109 | // 110 | // tagFormat is the desired tag format, if any. If you don't plan on using 111 | // tags, use 0 to use the default. 112 | func NewClientWithSender(sender Sender, prefix string, tagFormat TagFormat) (Statter, error) { 113 | if sender == nil { 114 | return nil, fmt.Errorf("Client sender may not be nil") 115 | } 116 | 117 | // if zero value is supplied, pick something as a default 118 | if tagFormat == 0 { 119 | tagFormat = SuffixOctothorpe 120 | } 121 | 122 | if tagFormat&(AllInfix|AllSuffix) == 0 { 123 | return nil, fmt.Errorf("Invalid tagFormat section") 124 | } 125 | 126 | client := &Client{ 127 | prefix: prefix, 128 | sender: sender, 129 | tagFormat: tagFormat, 130 | } 131 | return client, nil 132 | } 133 | -------------------------------------------------------------------------------- /statsd/client_legacy.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2016 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package statsd 6 | 7 | import "time" 8 | 9 | // Deprecated stuff here... 10 | 11 | // NewBufferedClient returns a new BufferedClient 12 | // 13 | // addr is a string of the format "hostname:port", and must be parsable by 14 | // net.ResolveUDPAddr. 15 | // 16 | // prefix is the statsd client prefix. Can be "" if no prefix is desired. 17 | // 18 | // flushInterval is a time.Duration, and specifies the maximum interval for 19 | // packet sending. Note that if you send lots of metrics, you will send more 20 | // often. This is just a maximal threshold. 21 | // 22 | // If flushInterval is 0ms, defaults to 300ms. 23 | // 24 | // flushBytes specifies the maximum udp packet size you wish to send. If adding 25 | // a metric would result in a larger packet than flushBytes, the packet will 26 | // first be send, then the new data will be added to the next packet. 27 | // 28 | // If flushBytes is 0, defaults to 1432 bytes, which is considered safe 29 | // for local traffic. If sending over the public internet, 512 bytes is 30 | // the recommended value. 31 | // 32 | // Deprecated: This interface is "legacy", and it is recommented to migrate to 33 | // using NewClientWithConfig in the future. 34 | func NewBufferedClient(addr, prefix string, flushInterval time.Duration, flushBytes int) (Statter, error) { 35 | config := &ClientConfig{ 36 | Address: addr, 37 | Prefix: prefix, 38 | UseBuffered: true, 39 | FlushInterval: flushInterval, 40 | FlushBytes: flushBytes, 41 | } 42 | return NewClientWithConfig(config) 43 | } 44 | 45 | // NewClient returns a pointer to a new Client, and an error. 46 | // 47 | // addr is a string of the format "hostname:port", and must be parsable by 48 | // net.ResolveUDPAddr. 49 | // 50 | // prefix is the statsd client prefix. Can be "" if no prefix is desired. 51 | // 52 | // Deprecated: This interface is "legacy", and it is recommented to migrate to 53 | // using NewClientWithConfig in the future. 54 | func NewClient(addr, prefix string) (Statter, error) { 55 | config := &ClientConfig{ 56 | Address: addr, 57 | Prefix: prefix, 58 | UseBuffered: false, 59 | } 60 | return NewClientWithConfig(config) 61 | } 62 | 63 | // Dial is a compatibility alias for NewClient 64 | // 65 | // Deprecated: This interface is "legacy", and it is recommented to migrate to 66 | // using NewClientWithConfig in the future. 67 | var Dial = NewClient 68 | 69 | // New is a compatibility alias for NewClient 70 | // 71 | // Deprecated: This interface is "legacy", and it is recommented to migrate to 72 | // using NewClientWithConfig in the future. 73 | var New = NewClient 74 | -------------------------------------------------------------------------------- /statsd/client_substatter_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2016 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package statsd 6 | 7 | import ( 8 | "bytes" 9 | "log" 10 | "reflect" 11 | "strings" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | var statsdSubStatterPacketTests = []struct { 17 | Prefix string 18 | SubPrefix string 19 | Method string 20 | Stat string 21 | Value interface{} 22 | Rate float32 23 | Expected string 24 | }{ 25 | {"test", "sub", "Gauge", "gauge", int64(1), 1.0, "test.sub.gauge:1|g"}, 26 | {"test", "sub", "Inc", "count", int64(1), 0.999999, "test.sub.count:1|c|@0.999999"}, 27 | {"test", "sub", "Inc", "count", int64(1), 1.0, "test.sub.count:1|c"}, 28 | {"test", "sub", "Dec", "count", int64(1), 1.0, "test.sub.count:-1|c"}, 29 | {"test", "sub", "Timing", "timing", int64(1), 1.0, "test.sub.timing:1|ms"}, 30 | {"test", "sub", "TimingDuration", "timing", 1500 * time.Microsecond, 1.0, "test.sub.timing:1.5|ms"}, 31 | {"test", "sub", "TimingDuration", "timing", 3 * time.Microsecond, 1.0, "test.sub.timing:0.003|ms"}, 32 | {"test", "sub", "Set", "strset", "pickle", 1.0, "test.sub.strset:pickle|s"}, 33 | {"test", "sub", "SetInt", "intset", int64(1), 1.0, "test.sub.intset:1|s"}, 34 | {"test", "sub", "GaugeDelta", "gauge", int64(1), 1.0, "test.sub.gauge:+1|g"}, 35 | {"test", "sub", "GaugeDelta", "gauge", int64(-1), 1.0, "test.sub.gauge:-1|g"}, 36 | // empty sub prefix -- note: not used in subsub tests 37 | {"test", "", "Inc", "count", int64(1), 1.0, "test.count:1|c"}, 38 | // empty base prefix 39 | {"", "sub", "Inc", "count", int64(1), 1.0, "sub.count:1|c"}, 40 | } 41 | 42 | func TestSubStatterClient(t *testing.T) { 43 | l, err := newUDPListener("127.0.0.1:0") 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | defer l.Close() 48 | for _, tt := range statsdSubStatterPacketTests { 49 | c, err := NewClient(l.LocalAddr().String(), tt.Prefix) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | s := c.NewSubStatter(tt.SubPrefix) 54 | method := reflect.ValueOf(s).MethodByName(tt.Method) 55 | e := method.Call([]reflect.Value{ 56 | reflect.ValueOf(tt.Stat), 57 | reflect.ValueOf(tt.Value), 58 | reflect.ValueOf(tt.Rate)})[0] 59 | errInter := e.Interface() 60 | if errInter != nil { 61 | t.Fatal(errInter.(error)) 62 | } 63 | 64 | data := make([]byte, 128) 65 | _, _, err = l.ReadFrom(data) 66 | if err != nil { 67 | c.Close() 68 | t.Fatal(err) 69 | } 70 | 71 | data = bytes.TrimRight(data, "\x00") 72 | if !bytes.Equal(data, []byte(tt.Expected)) { 73 | c.Close() 74 | t.Fatalf("%s got '%s' expected '%s'", tt.Method, data, tt.Expected) 75 | } 76 | c.Close() 77 | } 78 | } 79 | 80 | func TestMultSubStatterClient(t *testing.T) { 81 | l, err := newUDPListener("127.0.0.1:0") 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | defer l.Close() 86 | for _, tt := range statsdSubStatterPacketTests { 87 | // ignore empty sub test for this, as there is nothing to regex sub 88 | if tt.SubPrefix == "" { 89 | continue 90 | } 91 | c, err := NewClient(l.LocalAddr().String(), tt.Prefix) 92 | if err != nil { 93 | t.Fatal(err) 94 | } 95 | s1 := c.NewSubStatter("sub1") 96 | s2 := c.NewSubStatter("sub2") 97 | 98 | responses := [][]byte{} 99 | for _, s := range []SubStatter{s1, s2} { 100 | method := reflect.ValueOf(s).MethodByName(tt.Method) 101 | e := method.Call([]reflect.Value{ 102 | reflect.ValueOf(tt.Stat), 103 | reflect.ValueOf(tt.Value), 104 | reflect.ValueOf(tt.Rate)})[0] 105 | errInter := e.Interface() 106 | if errInter != nil { 107 | t.Fatal(errInter.(error)) 108 | } 109 | 110 | data := make([]byte, 128) 111 | _, _, err = l.ReadFrom(data) 112 | if err != nil { 113 | c.Close() 114 | t.Fatal(err) 115 | } 116 | 117 | data = bytes.TrimRight(data, "\x00") 118 | responses = append(responses, data) 119 | } 120 | 121 | expected := strings.Replace(tt.Expected, "sub.", "sub1.", -1) 122 | if !bytes.Equal(responses[0], []byte(expected)) { 123 | c.Close() 124 | t.Fatalf("%s got '%s' expected '%s'", 125 | tt.Method, responses[0], tt.Expected) 126 | } 127 | 128 | expected = strings.Replace(tt.Expected, "sub.", "sub2.", -1) 129 | if !bytes.Equal(responses[1], []byte(expected)) { 130 | c.Close() 131 | t.Fatalf("%s got '%s' expected '%s'", 132 | tt.Method, responses[1], tt.Expected) 133 | } 134 | c.Close() 135 | } 136 | } 137 | 138 | func TestSubSubStatterClient(t *testing.T) { 139 | l, err := newUDPListener("127.0.0.1:0") 140 | if err != nil { 141 | t.Fatal(err) 142 | } 143 | defer l.Close() 144 | for _, tt := range statsdSubStatterPacketTests { 145 | // ignore empty sub test for this, as there is nothing to regex sub 146 | if tt.SubPrefix == "" { 147 | continue 148 | } 149 | c, err := NewClient(l.LocalAddr().String(), tt.Prefix) 150 | if err != nil { 151 | t.Fatal(err) 152 | } 153 | s := c.NewSubStatter(tt.SubPrefix).NewSubStatter("sub2") 154 | 155 | method := reflect.ValueOf(s).MethodByName(tt.Method) 156 | e := method.Call([]reflect.Value{ 157 | reflect.ValueOf(tt.Stat), 158 | reflect.ValueOf(tt.Value), 159 | reflect.ValueOf(tt.Rate)})[0] 160 | errInter := e.Interface() 161 | if errInter != nil { 162 | t.Fatal(errInter.(error)) 163 | } 164 | 165 | data := make([]byte, 128) 166 | _, _, err = l.ReadFrom(data) 167 | if err != nil { 168 | c.Close() 169 | t.Fatal(err) 170 | } 171 | 172 | data = bytes.TrimRight(data, "\x00") 173 | expected := strings.Replace(tt.Expected, "sub.", "sub.sub2.", -1) 174 | if !bytes.Equal(data, []byte(expected)) { 175 | c.Close() 176 | t.Fatalf("%s got '%s' expected '%s'", tt.Method, data, tt.Expected) 177 | } 178 | c.Close() 179 | } 180 | } 181 | 182 | func TestSubStatterClosedClient(t *testing.T) { 183 | l, err := newUDPListener("127.0.0.1:0") 184 | if err != nil { 185 | t.Fatal(err) 186 | } 187 | defer l.Close() 188 | for _, tt := range statsdSubStatterPacketTests { 189 | c, err := NewClient(l.LocalAddr().String(), tt.Prefix) 190 | if err != nil { 191 | t.Fatal(err) 192 | } 193 | c.Close() 194 | s := c.NewSubStatter(tt.SubPrefix) 195 | method := reflect.ValueOf(s).MethodByName(tt.Method) 196 | e := method.Call([]reflect.Value{ 197 | reflect.ValueOf(tt.Stat), 198 | reflect.ValueOf(tt.Value), 199 | reflect.ValueOf(tt.Rate)})[0] 200 | errInter := e.Interface() 201 | if errInter == nil { 202 | t.Fatal("Expected error but got none") 203 | } 204 | } 205 | } 206 | 207 | func TestNilSubStatterClient(t *testing.T) { 208 | l, err := newUDPListener("127.0.0.1:0") 209 | if err != nil { 210 | t.Fatal(err) 211 | } 212 | defer l.Close() 213 | 214 | for _, tt := range statsdSubStatterPacketTests { 215 | var c *Client 216 | s := c.NewSubStatter(tt.SubPrefix) 217 | 218 | method := reflect.ValueOf(s).MethodByName(tt.Method) 219 | e := method.Call([]reflect.Value{ 220 | reflect.ValueOf(tt.Stat), 221 | reflect.ValueOf(tt.Value), 222 | reflect.ValueOf(tt.Rate)})[0] 223 | errInter := e.Interface() 224 | if errInter != nil { 225 | t.Fatal(errInter.(error)) 226 | } 227 | 228 | data := make([]byte, 128) 229 | n, _, err := l.ReadFrom(data) 230 | // this is expected to error, since there should 231 | // be no udp data sent, so the read will time out 232 | if err == nil || n != 0 { 233 | c.Close() 234 | t.Fatal(err) 235 | } 236 | c.Close() 237 | } 238 | } 239 | 240 | func ExampleClient_substatter() { 241 | // First create a client config. Here is a simple config that sends one 242 | // stat per packet (for compatibility). 243 | config := &ClientConfig{ 244 | Address: "127.0.0.1:8125", 245 | Prefix: "test-client", 246 | } 247 | 248 | // Now create the client 249 | client, err := NewClientWithConfig(config) 250 | // handle any errors 251 | if err != nil { 252 | log.Fatal(err) 253 | } 254 | // make sure to close to clean up when done, to avoid leaks. 255 | defer client.Close() 256 | 257 | // create a substatter 258 | subclient := client.NewSubStatter("sub") 259 | // send a stat 260 | err = subclient.Inc("stat1", 42, 1.0) 261 | // handle any errors 262 | if err != nil { 263 | log.Printf("Error sending metric: %+v", err) 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /statsd/client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2016 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package statsd 6 | 7 | import ( 8 | "bytes" 9 | "log" 10 | "net" 11 | "reflect" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | var statsdPacketTests = []struct { 17 | Prefix string 18 | Method string 19 | Stat string 20 | Value interface{} 21 | Rate float32 22 | Expected string 23 | }{ 24 | {"test", "Gauge", "gauge", int64(1), 1.0, "test.gauge:1|g"}, 25 | {"test", "Inc", "count", int64(1), 0.999999, "test.count:1|c|@0.999999"}, 26 | {"test", "Inc", "count", int64(1), 1.0, "test.count:1|c"}, 27 | {"test", "Dec", "count", int64(1), 1.0, "test.count:-1|c"}, 28 | {"test", "Timing", "timing", int64(1), 1.0, "test.timing:1|ms"}, 29 | {"test", "TimingDuration", "timing", 1500 * time.Microsecond, 1.0, "test.timing:1.5|ms"}, 30 | {"test", "TimingDuration", "timing", 3 * time.Microsecond, 1.0, "test.timing:0.003|ms"}, 31 | {"test", "Set", "strset", "pickle", 1.0, "test.strset:pickle|s"}, 32 | {"test", "SetInt", "intset", int64(1), 1.0, "test.intset:1|s"}, 33 | {"test", "SetInt", "intset", int64(-1), 1.0, "test.intset:-1|s"}, 34 | {"test", "GaugeDelta", "gauge", int64(1), 1.0, "test.gauge:+1|g"}, 35 | {"test", "GaugeDelta", "gauge", int64(-1), 1.0, "test.gauge:-1|g"}, 36 | {"test", "GaugeFloatDelta", "gauge", float64(1.1), 1.0, "test.gauge:+1.1|g"}, 37 | {"test", "GaugeFloatDelta", "gauge", float64(-1.1), 1.0, "test.gauge:-1.1|g"}, 38 | {"test", "SetFloat", "floatset", float64(1.1), 1.0, "test.floatset:1.1|s"}, 39 | {"test", "SetFloat", "floatset", float64(-1.1), 1.0, "test.floatset:-1.1|s"}, 40 | 41 | {"", "Gauge", "gauge", int64(1), 1.0, "gauge:1|g"}, 42 | {"", "Inc", "count", int64(1), 0.999999, "count:1|c|@0.999999"}, 43 | {"", "Inc", "count", int64(1), 1.0, "count:1|c"}, 44 | {"", "Dec", "count", int64(1), 1.0, "count:-1|c"}, 45 | {"", "Timing", "timing", int64(1), 1.0, "timing:1|ms"}, 46 | {"", "TimingDuration", "timing", 1500 * time.Microsecond, 1.0, "timing:1.5|ms"}, 47 | {"", "Set", "strset", "pickle", 1.0, "strset:pickle|s"}, 48 | {"", "SetInt", "intset", int64(1), 1.0, "intset:1|s"}, 49 | {"", "SetInt", "intset", int64(-1), 1.0, "intset:-1|s"}, 50 | {"", "GaugeDelta", "gauge", int64(1), 1.0, "gauge:+1|g"}, 51 | {"", "GaugeDelta", "gauge", int64(-1), 1.0, "gauge:-1|g"}, 52 | {"", "GaugeFloatDelta", "gauge", float64(1.1), 1.0, "gauge:+1.1|g"}, 53 | {"", "GaugeFloatDelta", "gauge", float64(-1.1), 1.0, "gauge:-1.1|g"}, 54 | {"", "SetFloat", "floatset", float64(1.1), 1.0, "floatset:1.1|s"}, 55 | {"", "SetFloat", "floatset", float64(-1.1), 1.0, "floatset:-1.1|s"}, 56 | } 57 | 58 | func TestClient(t *testing.T) { 59 | l, err := newUDPListener("127.0.0.1:0") 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | defer l.Close() 64 | for _, tt := range statsdPacketTests { 65 | c, err := NewClient(l.LocalAddr().String(), tt.Prefix) 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | method := reflect.ValueOf(c).MethodByName(tt.Method) 70 | values := []reflect.Value{ 71 | reflect.ValueOf(tt.Stat), 72 | reflect.ValueOf(tt.Value), 73 | reflect.ValueOf(tt.Rate), 74 | } 75 | e := method.Call(values)[0] 76 | errInter := e.Interface() 77 | if errInter != nil { 78 | t.Fatal(errInter.(error)) 79 | } 80 | 81 | data := make([]byte, 128) 82 | _, _, err = l.ReadFrom(data) 83 | if err != nil { 84 | c.Close() 85 | t.Fatal(err) 86 | } 87 | 88 | data = bytes.TrimRight(data, "\x00") 89 | if !bytes.Equal(data, []byte(tt.Expected)) { 90 | c.Close() 91 | t.Fatalf("%s got '%s' expected '%s'", tt.Method, data, tt.Expected) 92 | } 93 | c.Close() 94 | } 95 | } 96 | 97 | func TestClientTags(t *testing.T) { 98 | statsdTaggedPacketTests := []struct { 99 | TagFormat TagFormat 100 | Prefix string 101 | Method string 102 | Stat string 103 | Value interface{} 104 | Rate float32 105 | Tags []Tag 106 | Expected string 107 | }{ 108 | { 109 | SuffixOctothorpe, 110 | "test", "Inc", "count", int64(1), 0.999999, 111 | []Tag{{"tag1", "val1"}}, 112 | "test.count:1|c|@0.999999|#tag1:val1", 113 | }, 114 | { 115 | SuffixOctothorpe, 116 | "test", "Inc", "count", int64(1), 1.0, 117 | []Tag{{"tag1", "val1"}, {"tag2", "val2"}}, 118 | "test.count:1|c|#tag1:val1,tag2:val2", 119 | }, 120 | { 121 | InfixComma, 122 | "test", "Inc", "count", int64(1), 1.0, 123 | []Tag{{"tag1", "val1"}}, 124 | "test.count,tag1=val1:1|c", 125 | }, 126 | { 127 | InfixComma, 128 | "test", "Inc", "count", int64(1), 1.0, 129 | []Tag{{"tag1", "val1"}, {"tag2", "val2"}}, 130 | "test.count,tag1=val1,tag2=val2:1|c", 131 | }, 132 | { 133 | InfixSemicolon, 134 | "test", "Inc", "count", int64(1), 1.0, 135 | []Tag{{"tag1", "val1"}}, 136 | "test.count;tag1=val1:1|c", 137 | }, 138 | { 139 | InfixSemicolon, 140 | "test", "Inc", "count", int64(1), 1.0, 141 | []Tag{{"tag1", "val1"}, {"tag2", "val2"}}, 142 | "test.count;tag1=val1;tag2=val2:1|c", 143 | }, 144 | } 145 | 146 | l, err := newUDPListener("127.0.0.1:0") 147 | if err != nil { 148 | t.Fatal(err) 149 | } 150 | defer l.Close() 151 | for _, tt := range statsdTaggedPacketTests { 152 | config := &ClientConfig{ 153 | Address: l.LocalAddr().String(), 154 | Prefix: tt.Prefix, 155 | TagFormat: tt.TagFormat, 156 | } 157 | 158 | c, err := NewClientWithConfig(config) 159 | if err != nil { 160 | t.Fatal(err) 161 | } 162 | c.(*Client).tagFormat = tt.TagFormat 163 | method := reflect.ValueOf(c).MethodByName(tt.Method) 164 | values := []reflect.Value{ 165 | reflect.ValueOf(tt.Stat), 166 | reflect.ValueOf(tt.Value), 167 | reflect.ValueOf(tt.Rate)} 168 | for _, tag := range tt.Tags { 169 | values = append(values, reflect.ValueOf(tag)) 170 | } 171 | e := method.Call(values)[0] 172 | errInter := e.Interface() 173 | if errInter != nil { 174 | t.Fatal(errInter.(error)) 175 | } 176 | 177 | data := make([]byte, 128) 178 | _, _, err = l.ReadFrom(data) 179 | if err != nil { 180 | c.Close() 181 | t.Fatal(err) 182 | } 183 | 184 | data = bytes.TrimRight(data, "\x00") 185 | if !bytes.Equal(data, []byte(tt.Expected)) { 186 | c.Close() 187 | t.Fatalf("%s got '%s' expected '%s'", tt.Method, data, tt.Expected) 188 | } 189 | c.Close() 190 | } 191 | } 192 | 193 | func TestNilClient(t *testing.T) { 194 | l, err := newUDPListener("127.0.0.1:0") 195 | if err != nil { 196 | t.Fatal(err) 197 | } 198 | defer l.Close() 199 | for _, tt := range statsdPacketTests { 200 | var c *Client 201 | 202 | method := reflect.ValueOf(c).MethodByName(tt.Method) 203 | e := method.Call([]reflect.Value{ 204 | reflect.ValueOf(tt.Stat), 205 | reflect.ValueOf(tt.Value), 206 | reflect.ValueOf(tt.Rate)})[0] 207 | errInter := e.Interface() 208 | if errInter != nil { 209 | t.Fatal(errInter.(error)) 210 | } 211 | 212 | data := make([]byte, 128) 213 | n, _, err := l.ReadFrom(data) 214 | // this is expected to error, since there should 215 | // be no udp data sent, so the read will time out 216 | if err == nil || n != 0 { 217 | c.Close() 218 | t.Fatal(err) 219 | } 220 | c.Close() 221 | } 222 | } 223 | 224 | func newUDPListener(addr string) (*net.UDPConn, error) { 225 | l, err := net.ListenPacket("udp", addr) 226 | if err != nil { 227 | return nil, err 228 | } 229 | l.SetDeadline(time.Now().Add(100 * time.Millisecond)) 230 | l.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) 231 | l.SetWriteDeadline(time.Now().Add(100 * time.Millisecond)) 232 | return l.(*net.UDPConn), nil 233 | } 234 | 235 | func ExampleClient() { 236 | // First create a client config. Here is a simple config that sends one 237 | // stat per packet (for compatibility). 238 | config := &ClientConfig{ 239 | Address: "127.0.0.1:8125", 240 | Prefix: "test-client", 241 | } 242 | 243 | // Now create the client 244 | client, err := NewClientWithConfig(config) 245 | 246 | // and handle any initialization errors 247 | if err != nil { 248 | log.Fatal(err) 249 | } 250 | 251 | // make sure to clean up 252 | defer client.Close() 253 | 254 | // Send a stat 255 | err = client.Inc("stat1", 42, 1.0) 256 | // handle any errors 257 | if err != nil { 258 | log.Printf("Error sending metric: %+v", err) 259 | } 260 | } 261 | 262 | func ExampleClient_legacySimple() { 263 | // first create a client 264 | client, err := NewClient("127.0.0.1:8125", "test-client") 265 | // handle any errors 266 | if err != nil { 267 | log.Fatal(err) 268 | } 269 | // make sure to close to clean up when done, to avoid leaks. 270 | defer client.Close() 271 | 272 | // Send a stat 273 | err = client.Inc("stat1", 42, 1.0) 274 | // handle any errors 275 | if err != nil { 276 | log.Printf("Error sending metric: %+v", err) 277 | } 278 | } 279 | 280 | func ExampleClient_nil() { 281 | // use interface so we can sub nil client if needed 282 | var client Statter 283 | var err error 284 | 285 | // First create a client config. Here is a simple config that sends one 286 | // stat per packet (for compatibility). 287 | config := &ClientConfig{ 288 | Address: "not-resolvable:8125", 289 | Prefix: "test-client", 290 | } 291 | 292 | // Now create the client 293 | client, err = NewClientWithConfig(config) 294 | 295 | // Let us say real client creation fails, but you don't care enough about 296 | // stats that you don't want your program to run. Just log an error and 297 | // make a nil instead 298 | if err != nil { 299 | log.Println("Remote endpoint did not resolve. Disabling stats", err) 300 | } 301 | // at this point, client is a nil *Client. This will work like a noop client. 302 | // It is ok to call close when client is nil. It will be a noop too. 303 | defer client.Close() 304 | 305 | // Since client is nil, this is a noop. 306 | client.Inc("stat1", 42, 1.0) 307 | } 308 | -------------------------------------------------------------------------------- /statsd/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2016 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package statsd provides a StatsD client implementation that is safe for 7 | concurrent use by multiple goroutines and for efficiency can be created and 8 | reused. 9 | 10 | Example usage: 11 | 12 | // First create a client config. Here is a simple config that sends one 13 | // stat per packet (for compatibility). 14 | config := &statsd.ClientConfig{ 15 | Address: "127.0.0.1:8125", 16 | Prefix: "test-client", 17 | } 18 | 19 | // Now create the client 20 | client, err := statsd.NewClientWithConfig(config) 21 | 22 | // and handle any initialization errors 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | 27 | // make sure to clean up 28 | defer client.Close() 29 | 30 | // Send a stat 31 | err = client.Inc("stat1", 42, 1.0) 32 | // handle any errors 33 | if err != nil { 34 | log.Printf("Error sending metric: %+v", err) 35 | } 36 | 37 | */ 38 | package statsd 39 | -------------------------------------------------------------------------------- /statsd/sender.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2016 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package statsd 6 | 7 | import ( 8 | "errors" 9 | "net" 10 | ) 11 | 12 | // The Sender interface wraps a Send and Close 13 | type Sender interface { 14 | Send(data []byte) (int, error) 15 | Close() error 16 | } 17 | 18 | // SimpleSender provides a socket send interface. 19 | type SimpleSender struct { 20 | // underlying connection 21 | c net.PacketConn 22 | // resolved udp address 23 | ra *net.UDPAddr 24 | } 25 | 26 | // Send sends the data to the server endpoint. 27 | func (s *SimpleSender) Send(data []byte) (int, error) { 28 | // no need for locking here, as the underlying fdNet 29 | // already serialized writes 30 | n, err := s.c.(*net.UDPConn).WriteToUDP(data, s.ra) 31 | if err != nil { 32 | return 0, err 33 | } 34 | if n == 0 { 35 | return n, errors.New("Wrote no bytes") 36 | } 37 | return n, nil 38 | } 39 | 40 | // Close closes the SimpleSender and cleans up. 41 | func (s *SimpleSender) Close() error { 42 | err := s.c.Close() 43 | return err 44 | } 45 | 46 | // NewSimpleSender returns a new SimpleSender for sending to the supplied 47 | // addresss. 48 | // 49 | // addr is a string of the format "hostname:port", and must be parsable by 50 | // net.ResolveUDPAddr. 51 | func NewSimpleSender(addr string) (Sender, error) { 52 | c, err := net.ListenPacket("udp", ":0") 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | ra, err := net.ResolveUDPAddr("udp", addr) 58 | if err != nil { 59 | c.Close() 60 | return nil, err 61 | } 62 | 63 | sender := &SimpleSender{ 64 | c: c, 65 | ra: ra, 66 | } 67 | 68 | return sender, nil 69 | } 70 | -------------------------------------------------------------------------------- /statsd/sender_buffered.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2016 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package statsd 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | var senderPool = newBufferPool() 15 | 16 | // BufferedSender provides a buffered statsd udp, sending multiple 17 | // metrics, where possible. 18 | type BufferedSender struct { 19 | sender Sender 20 | flushBytes int 21 | flushInterval time.Duration 22 | // buffers 23 | bufmx sync.Mutex 24 | buffer *bytes.Buffer 25 | bufs chan *bytes.Buffer 26 | // lifecycle 27 | runmx sync.RWMutex 28 | shutdown chan chan error 29 | running bool 30 | } 31 | 32 | // Send bytes. 33 | func (s *BufferedSender) Send(data []byte) (int, error) { 34 | // Note: use manual unlocking instead of defer unlocking, 35 | // due to the overhead of defers in this hot code path. 36 | // https://go-review.googlesource.com/c/go/+/190098 37 | // removes a lot of the overhead (in go versions >= 1.14), 38 | // but it is still faster to not use it in some cases 39 | // (like this one). 40 | 41 | s.runmx.RLock() 42 | if !s.running { 43 | s.runmx.RUnlock() 44 | return 0, fmt.Errorf("BufferedSender is not running") 45 | } 46 | 47 | s.withBufferLock(func() { 48 | blen := s.buffer.Len() 49 | if blen > 0 && blen+len(data)+1 >= s.flushBytes { 50 | s.swapnqueue() 51 | } 52 | 53 | s.buffer.Write(data) 54 | s.buffer.WriteByte('\n') 55 | 56 | if s.buffer.Len() >= s.flushBytes { 57 | s.swapnqueue() 58 | } 59 | }) 60 | s.runmx.RUnlock() 61 | return len(data), nil 62 | } 63 | 64 | // Close closes the Buffered Sender and cleans up. 65 | func (s *BufferedSender) Close() error { 66 | // since we are running, write lock during cleanup 67 | s.runmx.Lock() 68 | defer s.runmx.Unlock() 69 | if !s.running { 70 | return nil 71 | } 72 | 73 | errChan := make(chan error) 74 | s.running = false 75 | s.shutdown <- errChan 76 | return <-errChan 77 | } 78 | 79 | // Start Buffered Sender 80 | // Begins ticker and read loop 81 | func (s *BufferedSender) Start() { 82 | // write lock to start running 83 | s.runmx.Lock() 84 | defer s.runmx.Unlock() 85 | if s.running { 86 | return 87 | } 88 | 89 | s.running = true 90 | s.bufs = make(chan *bytes.Buffer, 32) 91 | go s.run() 92 | } 93 | 94 | func (s *BufferedSender) withBufferLock(fn func()) { 95 | // Note: use manual unlocking instead of defer unlocking, 96 | // due to the overhead of defers in this hot code path. 97 | // https://go-review.googlesource.com/c/go/+/190098 98 | // removes a lot of the overhead (in go versions >= 1.14), 99 | // but it is still faster to not use it in some cases 100 | // (like this one). 101 | 102 | s.bufmx.Lock() 103 | fn() 104 | s.bufmx.Unlock() 105 | } 106 | 107 | func (s *BufferedSender) swapnqueue() { 108 | if s.buffer.Len() == 0 { 109 | return 110 | } 111 | ob := s.buffer 112 | nb := senderPool.Get() 113 | s.buffer = nb 114 | s.bufs <- ob 115 | } 116 | 117 | func (s *BufferedSender) run() { 118 | ticker := time.NewTicker(s.flushInterval) 119 | defer ticker.Stop() 120 | 121 | doneChan := make(chan bool) 122 | go func() { 123 | for buf := range s.bufs { 124 | s.flush(buf) 125 | senderPool.Put(buf) 126 | } 127 | doneChan <- true 128 | }() 129 | 130 | for { 131 | select { 132 | case <-ticker.C: 133 | s.withBufferLock(func() { 134 | s.swapnqueue() 135 | }) 136 | case errChan := <-s.shutdown: 137 | s.withBufferLock(func() { 138 | s.swapnqueue() 139 | }) 140 | close(s.bufs) 141 | <-doneChan 142 | errChan <- s.sender.Close() 143 | return 144 | } 145 | } 146 | } 147 | 148 | // send to remove endpoint and truncate buffer 149 | func (s *BufferedSender) flush(b *bytes.Buffer) (int, error) { 150 | bb := b.Bytes() 151 | bbl := len(bb) 152 | if bb[bbl-1] == '\n' { 153 | bb = bb[:bbl-1] 154 | } 155 | //n, err := s.sender.Send(bytes.TrimSuffix(b.Bytes(), []byte("\n"))) 156 | n, err := s.sender.Send(bb) 157 | b.Truncate(0) // clear the buffer 158 | return n, err 159 | } 160 | 161 | // NewBufferedSender returns a new BufferedSender 162 | // 163 | // addr is a string of the format "hostname:port", and must be parsable by 164 | // net.ResolveUDPAddr. 165 | // 166 | // flushInterval is a time.Duration, and specifies the maximum interval for 167 | // packet sending. Note that if you send lots of metrics, you will send more 168 | // often. This is just a maximal threshold. 169 | // 170 | // flushBytes specifies the maximum udp packet size you wish to send. If adding 171 | // a metric would result in a larger packet than flushBytes, the packet will 172 | // first be send, then the new data will be added to the next packet. 173 | func NewBufferedSender(addr string, flushInterval time.Duration, flushBytes int) (Sender, error) { 174 | simpleSender, err := NewSimpleSender(addr) 175 | if err != nil { 176 | return nil, err 177 | } 178 | return NewBufferedSenderWithSender(simpleSender, flushInterval, flushBytes) 179 | } 180 | 181 | // NewBufferedSenderWithSender returns a new BufferedSender, wrapping the 182 | // provided sender. 183 | // 184 | // sender is an instance of a statsd.Sender interface. Sender is required. 185 | // 186 | // flushInterval is a time.Duration, and specifies the maximum interval for 187 | // packet sending. Note that if you send lots of metrics, you will send more 188 | // often. This is just a maximal threshold. 189 | // 190 | // flushBytes specifies the maximum udp packet size you wish to send. If adding 191 | // a metric would result in a larger packet than flushBytes, the packet will 192 | // first be send, then the new data will be added to the next packet. 193 | func NewBufferedSenderWithSender(sender Sender, flushInterval time.Duration, flushBytes int) (Sender, error) { 194 | if sender == nil { 195 | return nil, fmt.Errorf("sender may not be nil") 196 | } 197 | 198 | bufSender := &BufferedSender{ 199 | flushBytes: flushBytes, 200 | flushInterval: flushInterval, 201 | sender: sender, 202 | buffer: senderPool.Get(), 203 | shutdown: make(chan chan error), 204 | } 205 | 206 | bufSender.Start() 207 | return bufSender, nil 208 | } 209 | -------------------------------------------------------------------------------- /statsd/sender_buffered_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2016 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package statsd 6 | 7 | import ( 8 | "bytes" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | type mockSender struct { 14 | closeCallCount int 15 | } 16 | 17 | func (m *mockSender) Send(data []byte) (int, error) { 18 | return 0, nil 19 | } 20 | 21 | func (m *mockSender) Close() error { 22 | m.closeCallCount++ 23 | return nil 24 | } 25 | 26 | func TestClose(t *testing.T) { 27 | mockSender := &mockSender{} 28 | sender := &BufferedSender{ 29 | flushBytes: 512, 30 | flushInterval: 1 * time.Second, 31 | sender: mockSender, 32 | buffer: bytes.NewBuffer(make([]byte, 0, 512)), 33 | shutdown: make(chan chan error), 34 | } 35 | 36 | sender.Close() 37 | if mockSender.closeCallCount != 0 { 38 | t.Fatalf("expected close to have been called zero times, but got %d", mockSender.closeCallCount) 39 | } 40 | 41 | sender.Start() 42 | if !sender.running { 43 | t.Fatal("sender failed to start") 44 | } 45 | 46 | sender.Close() 47 | if mockSender.closeCallCount != 1 { 48 | t.Fatalf("expected close to have been called once, but got %d", mockSender.closeCallCount) 49 | } 50 | } 51 | 52 | func TestCloseConcurrent(t *testing.T) { 53 | mockSender := &mockSender{} 54 | sender := &BufferedSender{ 55 | flushBytes: 512, 56 | flushInterval: 1 * time.Second, 57 | sender: mockSender, 58 | buffer: bytes.NewBuffer(make([]byte, 0, 512)), 59 | shutdown: make(chan chan error), 60 | } 61 | sender.Start() 62 | 63 | const N = 10 64 | c := make(chan struct{}, N) 65 | for i := 0; i < N; i++ { 66 | go func() { 67 | sender.Close() 68 | c <- struct{}{} 69 | }() 70 | } 71 | 72 | for i := 0; i < N; i++ { 73 | <-c 74 | } 75 | 76 | if mockSender.closeCallCount != 1 { 77 | t.Errorf("expected close to have been called once, but got %d", mockSender.closeCallCount) 78 | } 79 | } 80 | 81 | func TestCloseDuringSendConcurrent(t *testing.T) { 82 | mockSender := &mockSender{} 83 | sender := &BufferedSender{ 84 | flushBytes: 512, 85 | flushInterval: 1 * time.Second, 86 | sender: mockSender, 87 | buffer: bytes.NewBuffer(make([]byte, 0, 512)), 88 | shutdown: make(chan chan error), 89 | } 90 | sender.Start() 91 | 92 | const N = 10 93 | c := make(chan struct{}, N) 94 | for i := 0; i < N; i++ { 95 | go func() { 96 | for { 97 | _, err := sender.Send([]byte("stat:1|c")) 98 | if err != nil { 99 | c <- struct{}{} 100 | return 101 | } 102 | } 103 | }() 104 | } 105 | 106 | // senders should error out now 107 | // we should not receive any panics 108 | sender.Close() 109 | for i := 0; i < N; i++ { 110 | <-c 111 | } 112 | 113 | if mockSender.closeCallCount != 1 { 114 | t.Errorf("expected close to have been called once, but got %d", mockSender.closeCallCount) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /statsd/sender_resolving.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2016 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package statsd 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "net" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | // ResolvingSimpleSender provides a socket send interface that re-resolves and 16 | // reconnects. 17 | type ResolvingSimpleSender struct { 18 | // underlying connection 19 | conn net.PacketConn 20 | // resolved udp address 21 | addrResolved *net.UDPAddr 22 | // unresolved addr 23 | addrUnresolved string 24 | // interval time 25 | reresolveInterval time.Duration 26 | // lifecycle 27 | mx sync.RWMutex 28 | doneChan chan struct{} 29 | running bool 30 | } 31 | 32 | // Send sends the data to the server endpoint. 33 | func (s *ResolvingSimpleSender) Send(data []byte) (int, error) { 34 | // Note: use manual unlocking instead of defer unlocking, 35 | // due to the overhead of defers in this hot code path. 36 | // https://go-review.googlesource.com/c/go/+/190098 37 | // removes a lot of the overhead (in go versions >= 1.14), 38 | // but it is still faster to not use it in some cases 39 | // (like this one). 40 | 41 | s.mx.RLock() 42 | if !s.running { 43 | s.mx.RUnlock() 44 | return 0, fmt.Errorf("ResolvingSimpleSender is not running") 45 | } 46 | 47 | // no need for locking here, as the underlying fdNet 48 | // already serialized writes 49 | n, err := s.conn.(*net.UDPConn).WriteToUDP(data, s.addrResolved) 50 | 51 | s.mx.RUnlock() 52 | 53 | if err != nil { 54 | return 0, err 55 | } 56 | if n == 0 { 57 | return n, errors.New("Wrote no bytes") 58 | } 59 | return n, nil 60 | } 61 | 62 | // Close closes the ResolvingSender and cleans up 63 | func (s *ResolvingSimpleSender) Close() error { 64 | // lock to guard against ra reconnection modification 65 | s.mx.Lock() 66 | defer s.mx.Unlock() 67 | 68 | if !s.running { 69 | return nil 70 | } 71 | 72 | s.running = false 73 | close(s.doneChan) 74 | 75 | err := s.conn.Close() 76 | return err 77 | } 78 | 79 | func (s *ResolvingSimpleSender) Reconnect() { 80 | // Note: use manual unlocking instead of defer unlocking. 81 | // This is done here because we use a read lock first, 82 | // read a value safely, then perform an action that doesn't require 83 | // locking, then acquire a write lock for safe updating. 84 | 85 | // lock to guard against s.running mutation 86 | s.mx.RLock() 87 | 88 | if !s.running { 89 | s.mx.RUnlock() 90 | return 91 | } 92 | 93 | // get old addr for comparison, then release lock (asap) 94 | oldAddr := s.addrResolved.String() 95 | 96 | // done with rlock for now 97 | s.mx.RUnlock() 98 | 99 | // s.addrUnresolved doesn't change, so no do this under read lock 100 | addrResolved, err := net.ResolveUDPAddr("udp", s.addrUnresolved) 101 | 102 | if err != nil { 103 | // no good new address.. so continue with old address 104 | return 105 | } 106 | 107 | if oldAddr == addrResolved.String() { 108 | // got same address.. so continue with old address 109 | return 110 | } 111 | 112 | // acquire write lock to both guard against s.running having been mutated in the 113 | // meantime, as well as for safely setting s.ra 114 | s.mx.Lock() 115 | 116 | // check running again, just to be sure nothing was terminated in the meantime... 117 | if s.running { 118 | s.addrResolved = addrResolved 119 | } 120 | s.mx.Unlock() 121 | } 122 | 123 | // Start Resolving Simple Sender 124 | // Begins ticker and read loop 125 | func (s *ResolvingSimpleSender) Start() { 126 | // write lock to start running 127 | s.mx.Lock() 128 | defer s.mx.Unlock() 129 | 130 | if s.running { 131 | return 132 | } 133 | 134 | s.running = true 135 | go s.run() 136 | } 137 | 138 | func (s *ResolvingSimpleSender) run() { 139 | ticker := time.NewTicker(s.reresolveInterval) 140 | defer ticker.Stop() 141 | 142 | for { 143 | select { 144 | case <-s.doneChan: 145 | return 146 | case <-ticker.C: 147 | // reconnect locks/checks running, so no need to do it here 148 | s.Reconnect() 149 | } 150 | } 151 | } 152 | 153 | // NewResolvingSimpleSender returns a new ResolvingSimpleSender for 154 | // sending to the supplied addresss. 155 | // 156 | // addr is a string of the format "hostname:port", and must be parsable by 157 | // net.ResolveUDPAddr. 158 | func NewResolvingSimpleSender(addr string, interval time.Duration) (Sender, error) { 159 | conn, err := net.ListenPacket("udp", ":0") 160 | if err != nil { 161 | return nil, err 162 | } 163 | 164 | addrResolved, err := net.ResolveUDPAddr("udp", addr) 165 | if err != nil { 166 | conn.Close() 167 | return nil, err 168 | } 169 | 170 | sender := &ResolvingSimpleSender{ 171 | conn: conn, 172 | addrResolved: addrResolved, 173 | addrUnresolved: addr, 174 | reresolveInterval: interval, 175 | doneChan: make(chan struct{}), 176 | running: false, 177 | } 178 | 179 | sender.Start() 180 | return sender, nil 181 | } 182 | -------------------------------------------------------------------------------- /statsd/statsdtest/recorder.go: -------------------------------------------------------------------------------- 1 | package statsdtest 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | ) 7 | 8 | // RecordingSender implements statsd.Sender but parses individual Stats into a 9 | // buffer that can be later inspected instead of sending to some server. It 10 | // should constructed with NewRecordingSender(). 11 | type RecordingSender struct { 12 | m sync.Mutex 13 | buffer Stats 14 | closed bool 15 | } 16 | 17 | // NewRecordingSender creates a new RecordingSender for use by a statsd.Client. 18 | func NewRecordingSender() *RecordingSender { 19 | rs := &RecordingSender{} 20 | rs.buffer = make(Stats, 0) 21 | return rs 22 | } 23 | 24 | // GetSent returns the stats that have been sent. Locks and copies the current 25 | // state of the sent Stats. 26 | // 27 | // The entire buffer of Stat objects (including Stat.Raw is copied). 28 | func (rs *RecordingSender) GetSent() Stats { 29 | rs.m.Lock() 30 | defer rs.m.Unlock() 31 | 32 | results := make(Stats, len(rs.buffer)) 33 | for i, e := range rs.buffer { 34 | results[i] = e 35 | results[i].Raw = make([]byte, len(e.Raw)) 36 | copy(results[i].Raw, e.Raw) 37 | } 38 | 39 | return results 40 | } 41 | 42 | // ClearSent locks the sender and clears any Stats that have been recorded. 43 | func (rs *RecordingSender) ClearSent() { 44 | rs.m.Lock() 45 | defer rs.m.Unlock() 46 | 47 | rs.buffer = rs.buffer[:0] 48 | } 49 | 50 | // Send parses the provided []byte into stat objects and then appends these to 51 | // the buffer of sent stats. Buffer operations are synchronized so it is safe 52 | // to call this from multiple goroutines (though contenion will impact 53 | // performance so don't use this during a benchmark). Send treats '\n' as a 54 | // delimiter between multiple sats in the same []byte. 55 | // 56 | // Calling after the Sender has been closed will return an error (and not 57 | // mutate the buffer). 58 | func (rs *RecordingSender) Send(data []byte) (int, error) { 59 | sent := ParseStats(data) 60 | 61 | rs.m.Lock() 62 | defer rs.m.Unlock() 63 | if rs.closed { 64 | return 0, errors.New("writing to a closed sender") 65 | } 66 | 67 | rs.buffer = append(rs.buffer, sent...) 68 | return len(data), nil 69 | } 70 | 71 | // Close marks this sender as closed. Subsequent attempts to Send stats will 72 | // result in an error. 73 | func (rs *RecordingSender) Close() error { 74 | rs.m.Lock() 75 | defer rs.m.Unlock() 76 | 77 | rs.closed = true 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /statsd/statsdtest/recorder_test.go: -------------------------------------------------------------------------------- 1 | package statsdtest 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strconv" 7 | "testing" 8 | "time" 9 | 10 | "github.com/cactus/go-statsd-client/v5/statsd" 11 | ) 12 | 13 | func TestRecordingSenderIsSender(t *testing.T) { 14 | // This ensures that if the Sender interface changes in the future we'll get 15 | // compile time failures should the RecordingSender not be updated to meet 16 | // the new definition. This keeps changes from inadvertently breaking tests 17 | // of folks that use go-statsd-client. 18 | var _ statsd.Sender = NewRecordingSender() 19 | } 20 | 21 | func TestRecordingSender(t *testing.T) { 22 | start := time.Now() 23 | rs := new(RecordingSender) 24 | statter, err := statsd.NewClientWithSender(rs, "test", 0) 25 | if err != nil { 26 | t.Errorf("failed to construct client") 27 | return 28 | } 29 | 30 | statter.Inc("stat", 4444, 1.0) 31 | statter.Dec("stat", 5555, 1.0) 32 | statter.Set("set-stat", "some string", 1.0) 33 | 34 | d := time.Since(start) 35 | statter.TimingDuration("timing", d, 1.0) 36 | 37 | sent := rs.GetSent() 38 | if len(sent) != 4 { 39 | // just dive out because everything else relies on ordering 40 | t.Fatalf("Did not capture all stats sent; got: %s", sent) 41 | } 42 | 43 | ms := float64(d) / float64(time.Millisecond) 44 | // somewhat fragile in that it assums float rendering within client *shrug* 45 | msStr := string(strconv.AppendFloat([]byte(""), ms, 'f', -1, 64)) 46 | 47 | expected := Stats{ 48 | {[]byte("test.stat:4444|c"), "test.stat", "4444", "c", "", true}, 49 | {[]byte("test.stat:-5555|c"), "test.stat", "-5555", "c", "", true}, 50 | {[]byte("test.set-stat:some string|s"), "test.set-stat", "some string", "s", "", true}, 51 | {[]byte(fmt.Sprintf("test.timing:%s|ms", msStr)), "test.timing", msStr, "ms", "", true}, 52 | } 53 | 54 | if !reflect.DeepEqual(sent, expected) { 55 | t.Errorf("got: %s, want: %s", sent, expected) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /statsd/statsdtest/stat.go: -------------------------------------------------------------------------------- 1 | package statsdtest 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // Stat contains the raw and extracted stat information from a stat that was 10 | // sent by the RecordingSender. Raw will always have the content that was 11 | // consumed for this specific stat and Parsed will be set if no errors were hit 12 | // pulling information out of it. 13 | type Stat struct { 14 | Raw []byte 15 | Stat string 16 | Value string 17 | Tag string 18 | Rate string 19 | Parsed bool 20 | } 21 | 22 | // String fulfils the stringer interface 23 | func (s *Stat) String() string { 24 | return fmt.Sprintf("%s %s %s", s.Stat, s.Value, s.Rate) 25 | } 26 | 27 | // ParseStats takes a sequence of bytes destined for a Statsd server and parses 28 | // it out into one or more Stat structs. Each struct includes both the raw 29 | // bytes (copied, so the src []byte may be reused if desired) as well as each 30 | // component it was able to parse out. If parsing was incomplete Stat.Parsed 31 | // will be set to false but no error is returned / kept. 32 | func ParseStats(src []byte) Stats { 33 | d := make([]byte, len(src)) 34 | copy(d, src) 35 | 36 | // standard protocol indicates one stat per line 37 | entries := bytes.Split(d, []byte{'\n'}) 38 | 39 | result := make(Stats, len(entries)) 40 | 41 | for i, e := range entries { 42 | result[i] = Stat{Raw: e} 43 | ss := &result[i] 44 | 45 | // : deliniates the stat name from the stat data 46 | marker := bytes.IndexByte(e, ':') 47 | if marker == -1 { 48 | continue 49 | } 50 | ss.Stat = string(e[0:marker]) 51 | 52 | // stat data folows ':' with the form {value}|{type tag}[|@{sample rate}] 53 | e = e[marker+1:] 54 | marker = bytes.IndexByte(e, '|') 55 | if marker == -1 { 56 | continue 57 | } 58 | 59 | ss.Value = string(e[:marker]) 60 | 61 | e = e[marker+1:] 62 | marker = bytes.IndexByte(e, '|') 63 | if marker == -1 { 64 | // no sample rate 65 | ss.Tag = string(e) 66 | } else { 67 | ss.Tag = string(e[:marker]) 68 | e = e[marker+1:] 69 | if len(e) == 0 || e[0] != '@' { 70 | // sample rate should be prefixed with '@'; bail otherwise 71 | continue 72 | } 73 | ss.Rate = string(e[1:]) 74 | } 75 | 76 | ss.Parsed = true 77 | } 78 | 79 | return result 80 | } 81 | 82 | // Stats is a slice of Stat 83 | type Stats []Stat 84 | 85 | // Unparsed returns any stats that were unable to be completely parsed. 86 | func (s Stats) Unparsed() Stats { 87 | var r Stats 88 | for _, e := range s { 89 | if !e.Parsed { 90 | r = append(r, e) 91 | } 92 | } 93 | 94 | return r 95 | } 96 | 97 | // CollectNamed returns all data sent for a given stat name. 98 | func (s Stats) CollectNamed(statName string) Stats { 99 | return s.Collect(func(e Stat) bool { 100 | return e.Stat == statName 101 | }) 102 | } 103 | 104 | // Collect gathers all stats that make some predicate true. 105 | func (s Stats) Collect(pred func(Stat) bool) Stats { 106 | var r Stats 107 | for _, e := range s { 108 | if pred(e) { 109 | r = append(r, e) 110 | } 111 | } 112 | return r 113 | } 114 | 115 | // Values returns the values associated with this Stats object. 116 | func (s Stats) Values() []string { 117 | if len(s) == 0 { 118 | return nil 119 | } 120 | 121 | r := make([]string, len(s)) 122 | for i, e := range s { 123 | r[i] = e.Value 124 | } 125 | return r 126 | } 127 | 128 | // String fulfils the stringer interface 129 | func (s Stats) String() string { 130 | if len(s) == 0 { 131 | return "" 132 | } 133 | 134 | r := make([]string, len(s)) 135 | for i, e := range s { 136 | r[i] = e.String() 137 | } 138 | return strings.Join(r, "\n") 139 | } 140 | -------------------------------------------------------------------------------- /statsd/statsdtest/stat_test.go: -------------------------------------------------------------------------------- 1 | package statsdtest 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | type parsingTestCase struct { 10 | name string 11 | sent [][]byte 12 | expected Stats 13 | } 14 | 15 | var ( 16 | badStatNameOnly = []byte("foo.bar.baz:") 17 | bsnoStat = Stat{ 18 | Raw: badStatNameOnly, 19 | Stat: "foo.bar.baz", 20 | Parsed: false, 21 | } 22 | 23 | gaugeWithoutRate = []byte("foo.bar.baz:1.000|g") 24 | gworStat = Stat{ 25 | Raw: gaugeWithoutRate, 26 | Stat: "foo.bar.baz", 27 | Value: "1.000", 28 | Tag: "g", 29 | Parsed: true, 30 | } 31 | 32 | counterWithRate = []byte("foo.bar.baz:1.000|c|@0.75") 33 | cwrStat = Stat{ 34 | Raw: counterWithRate, 35 | Stat: "foo.bar.baz", 36 | Value: "1.000", 37 | Tag: "c", 38 | Rate: "0.75", 39 | Parsed: true, 40 | } 41 | 42 | stringStat = []byte(":some string value|s") 43 | sStat = Stat{ 44 | Raw: stringStat, 45 | Stat: "", 46 | Value: "some string value", 47 | Tag: "s", 48 | Parsed: true, 49 | } 50 | 51 | badValue = []byte("asoentuh") 52 | bvStat = Stat{Raw: badValue} 53 | 54 | testCases = []parsingTestCase{ 55 | {name: "no stat data", 56 | sent: [][]byte{badStatNameOnly}, 57 | expected: Stats{bsnoStat}}, 58 | {name: "trivial case", 59 | sent: [][]byte{gaugeWithoutRate}, 60 | expected: Stats{gworStat}}, 61 | {name: "multiple simple", 62 | sent: [][]byte{gaugeWithoutRate, counterWithRate}, 63 | expected: Stats{gworStat, cwrStat}}, 64 | {name: "mixed good and bad", 65 | sent: [][]byte{badValue, badValue, stringStat, badValue, counterWithRate, badValue}, 66 | expected: Stats{bvStat, bvStat, sStat, bvStat, cwrStat, bvStat}}, 67 | } 68 | ) 69 | 70 | func TestParseBytes(t *testing.T) { 71 | for _, tc := range testCases { 72 | got := ParseStats(bytes.Join(tc.sent, []byte("\n"))) 73 | want := tc.expected 74 | if !reflect.DeepEqual(got, want) { 75 | t.Errorf("%s: got: %+v, want: %+v", tc.name, got, want) 76 | } 77 | } 78 | } 79 | 80 | func TestStatsUnparsed(t *testing.T) { 81 | start := Stats{bsnoStat, gworStat, bsnoStat, bsnoStat, cwrStat} 82 | got := start.Unparsed() 83 | want := Stats{bsnoStat, bsnoStat, bsnoStat} 84 | if !reflect.DeepEqual(got, want) { 85 | t.Errorf("got: %+v, want: %+v", got, want) 86 | } 87 | } 88 | 89 | func TestStatsCollectNamed(t *testing.T) { 90 | type test struct { 91 | name string 92 | start Stats 93 | want Stats 94 | matchOn string 95 | } 96 | 97 | cases := []test{ 98 | {"No matches", 99 | Stats{bsnoStat, cwrStat}, 100 | nil, 101 | "foo"}, 102 | {"One match", 103 | Stats{bsnoStat, Stat{Stat: "foo"}, cwrStat}, 104 | Stats{Stat{Stat: "foo"}}, 105 | "foo"}, 106 | {"Two matches", 107 | Stats{bsnoStat, Stat{Stat: "foo"}, cwrStat}, 108 | Stats{bsnoStat, cwrStat}, 109 | "foo.bar.baz"}, 110 | } 111 | 112 | for _, c := range cases { 113 | got := c.start.CollectNamed(c.matchOn) 114 | if !reflect.DeepEqual(got, c.want) { 115 | t.Errorf("%s: got: %+v, want: %+v", c.name, got, c.want) 116 | } 117 | } 118 | } 119 | 120 | func TestStatsCollect(t *testing.T) { 121 | type test struct { 122 | name string 123 | start Stats 124 | want Stats 125 | pred func(Stat) bool 126 | } 127 | 128 | cases := []test{ 129 | {"Not called", 130 | Stats{}, 131 | nil, 132 | func(_ Stat) bool { t.Errorf("should not be called"); return true }}, 133 | {"Matches value = 1.000", 134 | Stats{bsnoStat, gworStat, cwrStat, sStat, bsnoStat}, 135 | Stats{gworStat, cwrStat}, 136 | func(s Stat) bool { return s.Value == "1.000" }}, 137 | } 138 | 139 | for _, c := range cases { 140 | got := c.start.Collect(c.pred) 141 | if !reflect.DeepEqual(got, c.want) { 142 | t.Errorf("%s: got: %+v, want: %+v", c.name, got, c.want) 143 | } 144 | } 145 | } 146 | 147 | func TestStatsValues(t *testing.T) { 148 | start := Stats{bsnoStat, sStat, gworStat} 149 | got := start.Values() 150 | want := []string{bsnoStat.Value, sStat.Value, gworStat.Value} 151 | if !reflect.DeepEqual(got, want) { 152 | t.Errorf("got: %+v, want: %+v", got, want) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /statsd/tags.go: -------------------------------------------------------------------------------- 1 | package statsd 2 | 3 | type Tag [2]string 4 | type TagFormat uint8 5 | 6 | func (tf TagFormat) WriteInfix(data []byte, tags []Tag) []byte { 7 | switch { 8 | case tf&InfixComma != 0: 9 | for _, v := range tags { 10 | data = append(data, ',') 11 | data = append(data, v[0]...) 12 | data = append(data, '=') 13 | data = append(data, v[1]...) 14 | } 15 | return data 16 | case tf&InfixSemicolon != 0: 17 | for _, v := range tags { 18 | data = append(data, ';') 19 | data = append(data, v[0]...) 20 | data = append(data, '=') 21 | data = append(data, v[1]...) 22 | } 23 | } 24 | 25 | return data 26 | } 27 | 28 | func (tf TagFormat) WriteSuffix(data []byte, tags []Tag) []byte { 29 | switch { 30 | // make the zero value useful 31 | case tf == 0, tf&SuffixOctothorpe != 0: 32 | data = append(data, "|#"...) 33 | tlen := len(tags) 34 | for i, v := range tags { 35 | data = append(data, v[0]...) 36 | data = append(data, ':') 37 | data = append(data, v[1]...) 38 | if tlen > 1 && i < tlen-1 { 39 | data = append(data, ',') 40 | } 41 | } 42 | } 43 | 44 | return data 45 | } 46 | 47 | const ( 48 | SuffixOctothorpe TagFormat = 1 << iota 49 | InfixSemicolon 50 | InfixComma 51 | 52 | AllInfix = InfixSemicolon | InfixComma 53 | AllSuffix = SuffixOctothorpe 54 | ) 55 | -------------------------------------------------------------------------------- /statsd/validator.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2016 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package statsd 6 | 7 | import ( 8 | "fmt" 9 | "net" 10 | "regexp" 11 | ) 12 | 13 | // The ValidatorFunc type defines a function that can serve 14 | // as a stat name validation function. 15 | type ValidatorFunc func(string) error 16 | 17 | var safeName = regexp.MustCompile(`^[a-zA-Z0-9\-_.]+$`) 18 | 19 | // CheckName may be used to validate whether a stat name contains invalid 20 | // characters. If invalid characters are found, the function will return an 21 | // error. 22 | func CheckName(stat string) error { 23 | if !safeName.MatchString(stat) { 24 | return fmt.Errorf("invalid stat name: %s", stat) 25 | } 26 | return nil 27 | } 28 | 29 | func mustBeIP(hostport string) bool { 30 | host, _, err := net.SplitHostPort(hostport) 31 | if err != nil { 32 | return false 33 | } 34 | 35 | ip := net.ParseIP(host) 36 | return ip != nil 37 | } 38 | -------------------------------------------------------------------------------- /statsd/validator_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2016 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package statsd 6 | 7 | import "testing" 8 | 9 | var validatorTests = []struct { 10 | Stat string 11 | Valid bool 12 | }{ 13 | {"test.one", true}, 14 | {"test#two", false}, 15 | {"test|three", false}, 16 | {"test@four", false}, 17 | } 18 | 19 | func TestValidator(t *testing.T) { 20 | var err error 21 | for _, tt := range validatorTests { 22 | err = CheckName(tt.Stat) 23 | switch { 24 | case err != nil && tt.Valid: 25 | t.Fatal(err) 26 | case err == nil && !tt.Valid: 27 | t.Fatalf("validation should have failed for %s", tt.Stat) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test-client/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2016 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | "log" 10 | "os" 11 | "time" 12 | 13 | "github.com/cactus/go-statsd-client/v5/statsd" 14 | flags "github.com/jessevdk/go-flags" 15 | ) 16 | 17 | func main() { 18 | 19 | // command line flags 20 | var opts struct { 21 | HostPort string `long:"host" default:"127.0.0.1:8125" description:"host:port of statsd server"` 22 | Prefix string `long:"prefix" default:"test-client" description:"Statsd prefix"` 23 | StatType string `long:"type" default:"count" description:"stat type to send. Can be one of: timing, count, gauge"` 24 | StatValue int64 `long:"value" default:"1" description:"Value to send"` 25 | Name string `short:"n" long:"name" default:"counter" description:"stat name"` 26 | Rate float32 `short:"r" long:"rate" default:"1.0" description:"sample rate"` 27 | Volume int `short:"c" long:"count" default:"1000" description:"Number of stats to send. Volume."` 28 | Nil bool `long:"nil" description:"Use nil client"` 29 | Buffered bool `long:"buffered" description:"Use a buffered client"` 30 | 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."` 31 | } 32 | 33 | // parse said flags 34 | _, err := flags.Parse(&opts) 35 | if err != nil { 36 | if e, ok := err.(*flags.Error); ok { 37 | if e.Type == flags.ErrHelp { 38 | os.Exit(0) 39 | } 40 | } 41 | fmt.Printf("Error: %+v\n", err) 42 | os.Exit(1) 43 | } 44 | 45 | if opts.Nil && opts.Buffered { 46 | fmt.Printf("Specifying both nil and buffered together is invalid\n") 47 | os.Exit(1) 48 | } 49 | 50 | if opts.Name == "" || statsd.CheckName(opts.Name) != nil { 51 | fmt.Printf("Stat name contains invalid characters\n") 52 | os.Exit(1) 53 | } 54 | 55 | if statsd.CheckName(opts.Prefix) != nil { 56 | fmt.Printf("Stat prefix contains invalid characters\n") 57 | os.Exit(1) 58 | } 59 | 60 | config := &statsd.ClientConfig{ 61 | Address: opts.HostPort, 62 | Prefix: opts.Prefix, 63 | ResInterval: time.Duration(0), 64 | } 65 | 66 | var client statsd.Statter = (*statsd.Client)(nil) 67 | if !opts.Nil { 68 | if opts.Buffered { 69 | config.UseBuffered = true 70 | config.FlushInterval = opts.Duration / time.Duration(4) 71 | config.FlushBytes = 0 72 | } 73 | 74 | client, err = statsd.NewClientWithConfig(config) 75 | if err != nil { 76 | log.Fatal(err) 77 | } 78 | defer client.Close() 79 | } 80 | 81 | var stat func(stat string, value int64, rate float32) error 82 | switch opts.StatType { 83 | case "count": 84 | stat = func(stat string, value int64, rate float32) error { 85 | return client.Inc(stat, value, rate) 86 | } 87 | case "gauge": 88 | stat = func(stat string, value int64, rate float32) error { 89 | return client.Gauge(stat, value, rate) 90 | } 91 | case "timing": 92 | stat = func(stat string, value int64, rate float32) error { 93 | return client.Timing(stat, value, rate) 94 | } 95 | default: 96 | log.Fatal("Unsupported state type") 97 | } 98 | 99 | pertick := opts.Volume / int(opts.Duration.Seconds()) / 10 100 | // add some extra time, because the first tick takes a while 101 | ender := time.After(opts.Duration + 100*time.Millisecond) 102 | c := time.Tick(time.Second / 10) 103 | count := 0 104 | for { 105 | select { 106 | case <-c: 107 | for x := 0; x < pertick; x++ { 108 | err := stat(opts.Name, opts.StatValue, opts.Rate) 109 | if err != nil { 110 | log.Printf("Got Error: %+v\n", err) 111 | break 112 | } 113 | count++ 114 | } 115 | case <-ender: 116 | log.Printf("%d events called\n", count) 117 | return 118 | } 119 | } 120 | } 121 | --------------------------------------------------------------------------------