├── .gitignore ├── .travis.yml ├── AUTHORS ├── README.md ├── LICENSE ├── graphite_test.go └── graphite.go /.gitignore: -------------------------------------------------------------------------------- 1 | .tags 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | script: go vet ./... && go test -cover ./... 4 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | These people have provided bug fixes, new features or improved the documentation. 2 | 3 | * Daniel Garcia 4 | * Peter Teichman 5 | * Phillip Kovalev 6 | * Richard Crowley 7 | * Timothée Peignier 8 | * Tomás Senart 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a reporter for the [go-metrics](https://github.com/rcrowley/go-metrics) 2 | library which will post the metrics to Graphite. It was originally part of the 3 | `go-metrics` library itself, but has been split off to make maintenance of 4 | both the core library and the client easier. 5 | 6 | ### Usage 7 | 8 | ```go 9 | import "github.com/cyberdelia/go-metrics-graphite" 10 | 11 | 12 | go graphite.Graphite(metrics.DefaultRegistry, 13 | 1*time.Second, "some.prefix", addr) 14 | ``` 15 | 16 | ### Migrating from `rcrowley/go-metrics` implementation 17 | 18 | Simply modify the import from `"github.com/rcrowley/go-metrics/graphite"` to 19 | `"github.com/cyberdelia/go-metrics-graphite"` and it should Just Work. 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Timothée Peignier. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 20 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /graphite_test.go: -------------------------------------------------------------------------------- 1 | package graphite 2 | 3 | import ( 4 | "bufio" 5 | "net" 6 | "strconv" 7 | "strings" 8 | "sync" 9 | "testing" 10 | "time" 11 | 12 | "github.com/rcrowley/go-metrics" 13 | ) 14 | 15 | func floatEquals(a, b float64) bool { 16 | return (a-b) < 0.000001 && (b-a) < 0.000001 17 | } 18 | 19 | func ExampleGraphite() { 20 | addr, _ := net.ResolveTCPAddr("net", ":2003") 21 | go Graphite(metrics.DefaultRegistry, 1*time.Second, "some.prefix", addr) 22 | } 23 | 24 | func ExampleWithConfig() { 25 | addr, _ := net.ResolveTCPAddr("net", ":2003") 26 | go WithConfig(Config{ 27 | Addr: addr, 28 | Registry: metrics.DefaultRegistry, 29 | FlushInterval: 1 * time.Second, 30 | DurationUnit: time.Millisecond, 31 | Percentiles: []float64{0.5, 0.75, 0.99, 0.999}, 32 | }) 33 | } 34 | 35 | func NewTestServer(t *testing.T, prefix string) (map[string]float64, net.Listener, metrics.Registry, Config, *sync.WaitGroup) { 36 | res := make(map[string]float64) 37 | 38 | ln, err := net.Listen("tcp", "127.0.0.1:0") 39 | if err != nil { 40 | t.Fatal("could not start dummy server:", err) 41 | } 42 | 43 | var wg sync.WaitGroup 44 | go func() { 45 | for { 46 | conn, err := ln.Accept() 47 | if err != nil { 48 | t.Fatal("dummy server error:", err) 49 | } 50 | r := bufio.NewReader(conn) 51 | line, err := r.ReadString('\n') 52 | for err == nil { 53 | parts := strings.Split(line, " ") 54 | i, _ := strconv.ParseFloat(parts[1], 0) 55 | if testing.Verbose() { 56 | t.Log("recv", parts[0], i) 57 | } 58 | res[parts[0]] = res[parts[0]] + i 59 | line, err = r.ReadString('\n') 60 | } 61 | wg.Done() 62 | conn.Close() 63 | } 64 | }() 65 | 66 | r := metrics.NewRegistry() 67 | 68 | c := Config{ 69 | Addr: ln.Addr().(*net.TCPAddr), 70 | Registry: r, 71 | FlushInterval: 10 * time.Millisecond, 72 | DurationUnit: time.Millisecond, 73 | Percentiles: []float64{0.5, 0.75, 0.99, 0.999}, 74 | Prefix: prefix, 75 | } 76 | 77 | return res, ln, r, c, &wg 78 | } 79 | 80 | func TestWrites(t *testing.T) { 81 | res, l, r, c, wg := NewTestServer(t, "foobar") 82 | defer l.Close() 83 | 84 | metrics.GetOrRegisterCounter("foo", r).Inc(2) 85 | 86 | // TODO: Use a mock meter rather than wasting 10s to get a QPS. 87 | for i := 0; i < 10*4; i++ { 88 | metrics.GetOrRegisterMeter("bar", r).Mark(1) 89 | time.Sleep(200 * time.Millisecond) 90 | } 91 | 92 | metrics.GetOrRegisterTimer("baz", r).Update(time.Second * 5) 93 | metrics.GetOrRegisterTimer("baz", r).Update(time.Second * 4) 94 | metrics.GetOrRegisterTimer("baz", r).Update(time.Second * 3) 95 | metrics.GetOrRegisterTimer("baz", r).Update(time.Second * 2) 96 | metrics.GetOrRegisterTimer("baz", r).Update(time.Second * 1) 97 | 98 | wg.Add(1) 99 | Once(c) 100 | wg.Wait() 101 | 102 | if expected, found := 2.0, res["foobar.foo.count"]; !floatEquals(found, expected) { 103 | t.Fatal("bad value:", expected, found) 104 | } 105 | 106 | if expected, found := 40.0, res["foobar.bar.count"]; !floatEquals(found, expected) { 107 | t.Fatal("bad value:", expected, found) 108 | } 109 | 110 | if expected, found := 5.0, res["foobar.bar.one-minute"]; !floatEquals(found, expected) { 111 | t.Fatal("bad value:", expected, found) 112 | } 113 | 114 | if expected, found := 5.0, res["foobar.baz.count"]; !floatEquals(found, expected) { 115 | t.Fatal("bad value:", expected, found) 116 | } 117 | 118 | if expected, found := 5000.0, res["foobar.baz.99-percentile"]; !floatEquals(found, expected) { 119 | t.Fatal("bad value:", expected, found) 120 | } 121 | 122 | if expected, found := 3000.0, res["foobar.baz.50-percentile"]; !floatEquals(found, expected) { 123 | t.Fatal("bad value:", expected, found) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /graphite.go: -------------------------------------------------------------------------------- 1 | package graphite 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "log" 7 | "net" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "github.com/rcrowley/go-metrics" 13 | ) 14 | 15 | // Config provides a container with configuration parameters for 16 | // the Graphite exporter 17 | type Config struct { 18 | Addr *net.TCPAddr // Network address to connect to 19 | Registry metrics.Registry // Registry to be exported 20 | FlushInterval time.Duration // Flush interval 21 | DurationUnit time.Duration // Time conversion unit for durations 22 | Prefix string // Prefix to be prepended to metric names 23 | Percentiles []float64 // Percentiles to export from timers and histograms 24 | } 25 | 26 | // Graphite is a blocking exporter function which reports metrics in r 27 | // to a graphite server located at addr, flushing them every d duration 28 | // and prepending metric names with prefix. 29 | func Graphite(r metrics.Registry, d time.Duration, prefix string, addr *net.TCPAddr) { 30 | WithConfig(Config{ 31 | Addr: addr, 32 | Registry: r, 33 | FlushInterval: d, 34 | DurationUnit: time.Nanosecond, 35 | Prefix: prefix, 36 | Percentiles: []float64{0.5, 0.75, 0.95, 0.99, 0.999}, 37 | }) 38 | } 39 | 40 | // WithConfig is a blocking exporter function just like Graphite, 41 | // but it takes a GraphiteConfig instead. 42 | func WithConfig(c Config) { 43 | for _ = range time.Tick(c.FlushInterval) { 44 | if err := graphite(&c); nil != err { 45 | log.Println(err) 46 | } 47 | } 48 | } 49 | 50 | // Once performs a single submission to Graphite, returning a 51 | // non-nil error on failed connections. This can be used in a loop 52 | // similar to GraphiteWithConfig for custom error handling. 53 | func Once(c Config) error { 54 | return graphite(&c) 55 | } 56 | 57 | func graphite(c *Config) error { 58 | now := time.Now().Unix() 59 | du := float64(c.DurationUnit) 60 | flushSeconds := float64(c.FlushInterval) / float64(time.Second) 61 | conn, err := net.DialTCP("tcp", nil, c.Addr) 62 | if nil != err { 63 | return err 64 | } 65 | defer conn.Close() 66 | w := bufio.NewWriter(conn) 67 | c.Registry.Each(func(name string, i interface{}) { 68 | switch metric := i.(type) { 69 | case metrics.Counter: 70 | count := metric.Count() 71 | fmt.Fprintf(w, "%s.%s.count %d %d\n", c.Prefix, name, count, now) 72 | fmt.Fprintf(w, "%s.%s.count_ps %.2f %d\n", c.Prefix, name, float64(count)/flushSeconds, now) 73 | case metrics.Gauge: 74 | fmt.Fprintf(w, "%s.%s.value %d %d\n", c.Prefix, name, metric.Value(), now) 75 | case metrics.GaugeFloat64: 76 | fmt.Fprintf(w, "%s.%s.value %f %d\n", c.Prefix, name, metric.Value(), now) 77 | case metrics.Histogram: 78 | h := metric.Snapshot() 79 | ps := h.Percentiles(c.Percentiles) 80 | fmt.Fprintf(w, "%s.%s.count %d %d\n", c.Prefix, name, h.Count(), now) 81 | fmt.Fprintf(w, "%s.%s.min %d %d\n", c.Prefix, name, h.Min(), now) 82 | fmt.Fprintf(w, "%s.%s.max %d %d\n", c.Prefix, name, h.Max(), now) 83 | fmt.Fprintf(w, "%s.%s.mean %.2f %d\n", c.Prefix, name, h.Mean(), now) 84 | fmt.Fprintf(w, "%s.%s.std-dev %.2f %d\n", c.Prefix, name, h.StdDev(), now) 85 | for psIdx, psKey := range c.Percentiles { 86 | key := strings.Replace(strconv.FormatFloat(psKey*100.0, 'f', -1, 64), ".", "", 1) 87 | fmt.Fprintf(w, "%s.%s.%s-percentile %.2f %d\n", c.Prefix, name, key, ps[psIdx], now) 88 | } 89 | case metrics.Meter: 90 | m := metric.Snapshot() 91 | fmt.Fprintf(w, "%s.%s.count %d %d\n", c.Prefix, name, m.Count(), now) 92 | fmt.Fprintf(w, "%s.%s.one-minute %.2f %d\n", c.Prefix, name, m.Rate1(), now) 93 | fmt.Fprintf(w, "%s.%s.five-minute %.2f %d\n", c.Prefix, name, m.Rate5(), now) 94 | fmt.Fprintf(w, "%s.%s.fifteen-minute %.2f %d\n", c.Prefix, name, m.Rate15(), now) 95 | fmt.Fprintf(w, "%s.%s.mean %.2f %d\n", c.Prefix, name, m.RateMean(), now) 96 | case metrics.Timer: 97 | t := metric.Snapshot() 98 | ps := t.Percentiles(c.Percentiles) 99 | count := t.Count() 100 | fmt.Fprintf(w, "%s.%s.count %d %d\n", c.Prefix, name, count, now) 101 | fmt.Fprintf(w, "%s.%s.count_ps %.2f %d\n", c.Prefix, name, float64(count)/flushSeconds, now) 102 | fmt.Fprintf(w, "%s.%s.min %d %d\n", c.Prefix, name, t.Min()/int64(du), now) 103 | fmt.Fprintf(w, "%s.%s.max %d %d\n", c.Prefix, name, t.Max()/int64(du), now) 104 | fmt.Fprintf(w, "%s.%s.mean %.2f %d\n", c.Prefix, name, t.Mean()/du, now) 105 | fmt.Fprintf(w, "%s.%s.std-dev %.2f %d\n", c.Prefix, name, t.StdDev()/du, now) 106 | for psIdx, psKey := range c.Percentiles { 107 | key := strings.Replace(strconv.FormatFloat(psKey*100.0, 'f', -1, 64), ".", "", 1) 108 | fmt.Fprintf(w, "%s.%s.%s-percentile %.2f %d\n", c.Prefix, name, key, ps[psIdx]/du, now) 109 | } 110 | fmt.Fprintf(w, "%s.%s.one-minute %.2f %d\n", c.Prefix, name, t.Rate1(), now) 111 | fmt.Fprintf(w, "%s.%s.five-minute %.2f %d\n", c.Prefix, name, t.Rate5(), now) 112 | fmt.Fprintf(w, "%s.%s.fifteen-minute %.2f %d\n", c.Prefix, name, t.Rate15(), now) 113 | fmt.Fprintf(w, "%s.%s.mean-rate %.2f %d\n", c.Prefix, name, t.RateMean(), now) 114 | default: 115 | log.Printf("unable to record metric of type %T\n", i) 116 | } 117 | w.Flush() 118 | }) 119 | return nil 120 | } 121 | --------------------------------------------------------------------------------