├── .gitignore ├── .travis.yml ├── README.md ├── average.go ├── counter.go ├── current.go ├── reporters ├── http.go └── influxdb.go ├── telemetry.go ├── telemetry_test.go └── total.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.4 4 | - tip 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # telemetry 2 | 3 | [![build-status](https://travis-ci.org/arussellsaw/telemetry.svg?branch=master)](https://travis-ci.org/arussellsaw/telemetry) [![code-coverage](http://gocover.io/_badge/github.com/arussellsaw/telemetry)](http://gocover.io/github.com/arussellsaw/telemetry) 4 | [![go-doc](https://godoc.org/github.com/arussellsaw/telemetry?status.svg)](https://godoc.org/github.com/arussellsaw/telemetry) 5 | 6 | metric reporting for Go applications 7 | 8 | sample usage: 9 | 10 | ```go 11 | package main 12 | 13 | import( 14 | "github.com/arussellsaw/telemetry" 15 | "github.com/arussellsaw/telemetry/reporters" 16 | "time" 17 | "net/http" 18 | ) 19 | 20 | func main() { 21 | //New telemetry object (prefix, maintainance schedule) 22 | tel := telemetry.New("test", 5 * time.Second) 23 | avg := telemetry.NewAverage(tel, "average", 60 * time.Second) 24 | 25 | //Register influxdb reporter 26 | influx := reporters.InfluxReporter{ 27 | Host: "192.168.1.100:8086", 28 | Interval: 60 * time.Second, 29 | Tel: tel, 30 | Database: "telemetry" 31 | } 32 | influx.Report() //trigger reporting loop 33 | 34 | //Create http handler for json metrics 35 | telemetryHandler := reporters.TelemetryHandler{ 36 | Tel: tel, 37 | } 38 | http.HandleFunc("/metrics", telemetryHandler.ServeHTTP) 39 | http.ListenAndServe(":8080", nil) 40 | 41 | start = time.Now() 42 | somethingYouWantToTime() 43 | avg.Add(tel, float64(time.Since(start).Nanoseconds())) 44 | } 45 | 46 | ``` 47 | -------------------------------------------------------------------------------- /average.go: -------------------------------------------------------------------------------- 1 | package telemetry 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | //Average - A running average of values provided to it, over a time period 9 | type Average struct { 10 | Name string 11 | value float64 12 | points map[time.Time]float64 13 | duration time.Duration 14 | lock sync.Mutex 15 | } 16 | 17 | //NewAverage - Create new Average metric with a duration for averaging points over 18 | func NewAverage(tel *Telemetry, name string, duration time.Duration) *Average { 19 | avg := Average{ 20 | Name: name, 21 | value: 0, 22 | points: make(map[time.Time]float64), 23 | duration: duration, 24 | } 25 | tel.lock.Lock() 26 | defer tel.lock.Unlock() 27 | tel.registry[name] = &avg 28 | return &avg 29 | } 30 | 31 | //Add - Add a value to the metric 32 | func (a *Average) Add(tel *Telemetry, value float64) error { 33 | tel.lock.Lock() 34 | a.lock.Lock() 35 | tel.registry[a.Name].(*Average).points[time.Now()] = value 36 | tel.lock.Unlock() 37 | a.lock.Unlock() 38 | a.Maintain() 39 | return nil 40 | } 41 | 42 | //Get - Fetch the metric, performing the average (mean) 43 | func (a *Average) Get(tel *Telemetry) float64 { 44 | a.Maintain() 45 | return tel.registry[a.Name].(*Average).value 46 | } 47 | 48 | //GetName - get the name of the metric 49 | func (a *Average) GetName() string { 50 | return a.Name 51 | } 52 | 53 | //Maintain - maintain metric value 54 | func (a *Average) Maintain() { 55 | a.lock.Lock() 56 | defer a.lock.Unlock() 57 | points := make(map[time.Time]float64) 58 | for pointTime, point := range a.points { 59 | if time.Since(pointTime) < a.duration { 60 | points[pointTime] = point 61 | } 62 | } 63 | a.points = points 64 | if len(a.points) > 0 { 65 | var avg float64 66 | for _, point := range a.points { 67 | avg = avg + point 68 | } 69 | avg = avg / float64(len(a.points)) 70 | a.value = avg 71 | } else { 72 | a.value = 0 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /counter.go: -------------------------------------------------------------------------------- 1 | package telemetry 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | //Counter - A running total of values provided to it, over a time period 9 | type Counter struct { 10 | Name string 11 | value float64 12 | points map[time.Time]float64 13 | duration time.Duration 14 | lock sync.Mutex 15 | } 16 | 17 | //NewCounter - Create new Counter metric with a duration for keeping points 18 | func NewCounter(tel *Telemetry, name string, duration time.Duration) *Counter { 19 | count := Counter{ 20 | Name: name, 21 | value: 0, 22 | points: make(map[time.Time]float64), 23 | duration: duration, 24 | } 25 | tel.lock.Lock() 26 | defer tel.lock.Unlock() 27 | tel.registry[name] = &count 28 | return &count 29 | } 30 | 31 | //Add - Add a value to the metric 32 | func (c *Counter) Add(tel *Telemetry, value float64) error { 33 | tel.lock.Lock() 34 | c.lock.Lock() 35 | tel.registry[c.Name].(*Counter).points[time.Now()] = value 36 | tel.lock.Unlock() 37 | c.lock.Unlock() 38 | c.Maintain() 39 | return nil 40 | } 41 | 42 | //Get - Fetch the metric value 43 | func (c *Counter) Get(tel *Telemetry) float64 { 44 | c.Maintain() 45 | return tel.registry[c.Name].(*Counter).value 46 | } 47 | 48 | //GetName - get the name of the metric 49 | func (c *Counter) GetName() string { 50 | return c.Name 51 | } 52 | 53 | //Maintain - maintain metric value 54 | func (c *Counter) Maintain() { 55 | c.lock.Lock() 56 | defer c.lock.Unlock() 57 | points := make(map[time.Time]float64) 58 | for pointTime, point := range c.points { 59 | if time.Since(pointTime) < c.duration { 60 | points[pointTime] = point 61 | } 62 | } 63 | c.points = points 64 | var count float64 65 | for _, point := range c.points { 66 | count = count + point 67 | } 68 | c.value = count 69 | } 70 | -------------------------------------------------------------------------------- /current.go: -------------------------------------------------------------------------------- 1 | package telemetry 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | //Current - a metric containing the value most recently passed to it 9 | type Current struct { 10 | Name string 11 | value float64 12 | lock sync.Mutex 13 | } 14 | 15 | //NewCurrent - Create a new current metric and add it to the telemetry register 16 | func NewCurrent(tel *Telemetry, name string, _ time.Duration) *Current { 17 | current := Current{ 18 | Name: name, 19 | value: float64(0), 20 | } 21 | tel.lock.Lock() 22 | defer tel.lock.Unlock() 23 | tel.registry[name] = ¤t 24 | return ¤t 25 | } 26 | 27 | //Add - set the value of the Current metric 28 | func (c *Current) Add(tel *Telemetry, value float64) error { 29 | c.lock.Lock() 30 | defer c.lock.Unlock() 31 | tel.lock.Lock() 32 | defer tel.lock.Unlock() 33 | tel.registry[c.Name].(*Current).value = value 34 | return nil 35 | } 36 | 37 | //Get - return the value of the metric 38 | func (c *Current) Get(tel *Telemetry) float64 { 39 | return tel.registry[c.Name].(*Current).value 40 | } 41 | 42 | //GetName - return the human readable name of the metric 43 | func (c *Current) GetName() string { 44 | return c.Name 45 | } 46 | 47 | //Maintain - stub method for interface, metric is so simple that it isn't needed 48 | func (c *Current) Maintain() { 49 | return 50 | } 51 | -------------------------------------------------------------------------------- /reporters/http.go: -------------------------------------------------------------------------------- 1 | package reporters 2 | 3 | import ( 4 | "github.com/arussellsaw/telemetry" 5 | 6 | "encoding/json" 7 | "net/http" 8 | ) 9 | 10 | //TelemetryHandler - http handler for telemetry 11 | type TelemetryHandler struct { 12 | Tel *telemetry.Telemetry 13 | } 14 | 15 | func (t TelemetryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 16 | 17 | if len(t.Tel.GetAll()) == 0 { 18 | w.WriteHeader(204) 19 | return 20 | } 21 | 22 | output, err := json.MarshalIndent(t.Tel.GetAll(), "", " ") 23 | if err != nil { 24 | w.WriteHeader(500) 25 | w.Write([]byte("{\"error\": \"" + err.Error() + "\"}")) 26 | } 27 | 28 | w.Write(output) 29 | } 30 | -------------------------------------------------------------------------------- /reporters/influxdb.go: -------------------------------------------------------------------------------- 1 | package reporters 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "time" 7 | 8 | "github.com/arussellsaw/telemetry" 9 | "github.com/influxdb/influxdb/client" 10 | ) 11 | 12 | //InfluxReporter influxdb reporter 13 | type InfluxReporter struct { 14 | Host string 15 | Interval time.Duration 16 | Tel *telemetry.Telemetry 17 | Database string 18 | Connection *client.Client 19 | } 20 | 21 | //Report - report metrics to influxdb 22 | func (r *InfluxReporter) Report() error { 23 | url, err := url.Parse(r.Host) 24 | if err != nil { 25 | return err 26 | } 27 | clientConf := client.Config{ 28 | URL: *url, 29 | } 30 | conn, err := client.NewClient(clientConf) 31 | if err != nil { 32 | return err 33 | } 34 | r.Connection = conn 35 | 36 | go r.submitMetrics() 37 | return nil 38 | } 39 | 40 | func (r *InfluxReporter) submitMetrics() { 41 | for { 42 | i := 0 43 | points := make([]client.Point, len(r.Tel.GetAll())) 44 | for name, metric := range r.Tel.GetAll() { 45 | point := client.Point{ 46 | Measurement: name, 47 | Time: time.Now(), 48 | Fields: map[string]interface{}{ 49 | "value": metric, 50 | }, 51 | Precision: "s", 52 | } 53 | points[i] = point 54 | i++ 55 | } 56 | 57 | batch := client.BatchPoints{ 58 | Points: points, 59 | Database: r.Database, 60 | } 61 | _, err := r.Connection.Write(batch) 62 | if err != nil { 63 | fmt.Println("failed write: ", err) 64 | } 65 | time.Sleep(r.Interval) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /telemetry.go: -------------------------------------------------------------------------------- 1 | package telemetry 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | //Telemetry container struct 10 | type Telemetry struct { 11 | registry map[string]Metric 12 | prefix string 13 | lock sync.Mutex 14 | duration time.Duration 15 | } 16 | 17 | type Metric interface { 18 | GetName() string 19 | Add(*Telemetry, float64) error 20 | Get(*Telemetry) float64 21 | Maintain() 22 | } 23 | 24 | //New create new telemetry struct 25 | func New(prefix string, duration time.Duration) *Telemetry { 26 | tel := Telemetry{ 27 | registry: make(map[string]Metric), 28 | prefix: prefix, 29 | duration: duration, 30 | } 31 | go tel.maintainance() 32 | return &tel 33 | } 34 | 35 | //GetAll get all metrics registered to Telemetry 36 | func (t *Telemetry) GetAll() map[string]float64 { 37 | metrics := make(map[string]float64) 38 | for _, metric := range t.registry { 39 | metrics[fmt.Sprintf("%s%s", t.prefix, metric.GetName())] = metric.Get(t) 40 | } 41 | return metrics 42 | } 43 | 44 | //maintainance run management on existing metrics 45 | func (t *Telemetry) maintainance() { 46 | for { 47 | for _, metric := range t.registry { 48 | metric.Maintain() 49 | } 50 | time.Sleep(t.duration) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /telemetry_test.go: -------------------------------------------------------------------------------- 1 | package telemetry_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/arussellsaw/telemetry" 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | //Counters 12 | 13 | func TestCounter(t *testing.T) { 14 | Convey("Test Counters", t, func() { 15 | Convey("New Counters should exist and be zero value", func() { 16 | tel := telemetry.New("test", (100 * time.Millisecond)) 17 | counter := telemetry.NewCounter(tel, "test.counter", (60 * time.Second)) 18 | So(counter.Get(tel), ShouldEqual, 0) 19 | }) 20 | Convey("Counters should equal the total of values added", func() { 21 | tel := telemetry.New("test", (1 * time.Second)) 22 | counter := telemetry.NewCounter(tel, "test.counter", (60 * time.Second)) 23 | counter.Add(tel, float64(3)) 24 | time.Sleep(1 * time.Second) 25 | So(counter.Get(tel), ShouldEqual, 3) 26 | }) 27 | Convey("Values in counters should expire after their period has elapsed", func() { 28 | tel := telemetry.New("test", (100 * time.Millisecond)) 29 | counter := telemetry.NewCounter(tel, "test.counter", (200 * time.Millisecond)) 30 | counter.Add(tel, float64(3)) 31 | time.Sleep(100 * time.Millisecond) 32 | So(counter.Get(tel), ShouldEqual, 3) 33 | time.Sleep(200 * time.Millisecond) 34 | So(counter.Get(tel), ShouldEqual, 0) 35 | }) 36 | }) 37 | } 38 | 39 | //Averages 40 | 41 | func TestAverage(t *testing.T) { 42 | Convey("Test Averages", t, func() { 43 | Convey("New Averagess should exist and be zero value", func() { 44 | tel := telemetry.New("test", (100 * time.Millisecond)) 45 | avg := telemetry.NewAverage(tel, "test.avg", (60 * time.Second)) 46 | avg.Maintain() 47 | So(avg.Get(tel), ShouldEqual, 0) 48 | }) 49 | Convey("Averages should equal the total of values added", func() { 50 | tel := telemetry.New("test", (100 * time.Millisecond)) 51 | avg := telemetry.NewAverage(tel, "test.avg", (60 * time.Second)) 52 | avg.Add(tel, float64(3)) 53 | time.Sleep(1 * time.Second) 54 | So(avg.Get(tel), ShouldEqual, 3) 55 | }) 56 | Convey("Values in averages should expire after their period has elapsed", func() { 57 | tel := telemetry.New("test", (100 * time.Millisecond)) 58 | avg := telemetry.NewAverage(tel, "test.avg", (200 * time.Millisecond)) 59 | avg.Add(tel, float64(3)) 60 | time.Sleep(100 * time.Millisecond) 61 | So(avg.Get(tel), ShouldEqual, 3) 62 | time.Sleep(200 * time.Millisecond) 63 | So(avg.Get(tel), ShouldEqual, 0) 64 | }) 65 | }) 66 | } 67 | 68 | //Currents 69 | 70 | func TestCurrent(t *testing.T) { 71 | Convey("Test Currents", t, func() { 72 | Convey("New Currents should exist and be zero value", func() { 73 | tel := telemetry.New("test", (100 * time.Millisecond)) 74 | cur := telemetry.NewCurrent(tel, "test.cur", (60 * time.Second)) 75 | So(cur.Get(tel), ShouldEqual, 0) 76 | }) 77 | Convey("Current's value should be the latest one passed to it", func() { 78 | tel := telemetry.New("test", (100 * time.Millisecond)) 79 | cur := telemetry.NewCurrent(tel, "test.cur", (60 * time.Second)) 80 | cur.Add(tel, float64(10)) 81 | cur.Add(tel, float64(3)) 82 | So(cur.Get(tel), ShouldEqual, 3) 83 | }) 84 | }) 85 | } 86 | 87 | //Totals 88 | 89 | func TestTotal(t *testing.T) { 90 | Convey("Test Totals", t, func() { 91 | Convey("New Totals should exist and be zero value", func() { 92 | tel := telemetry.New("test", (100 * time.Millisecond)) 93 | tot := telemetry.NewTotal(tel, "test.total", (60 * time.Second)) 94 | So(tot.Get(tel), ShouldEqual, 0) 95 | }) 96 | Convey("Total's value should be the sum of all values passed to it", func() { 97 | tel := telemetry.New("test", (100 * time.Millisecond)) 98 | tot := telemetry.NewTotal(tel, "test.total", (60 * time.Second)) 99 | tot.Add(tel, float64(10)) 100 | tot.Add(tel, float64(3)) 101 | So(tot.Get(tel), ShouldEqual, 13) 102 | }) 103 | }) 104 | } 105 | -------------------------------------------------------------------------------- /total.go: -------------------------------------------------------------------------------- 1 | package telemetry 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | //Total a total sum of a metric over the lifetime of the process 9 | type Total struct { 10 | Name string 11 | value float64 12 | lock sync.Mutex 13 | } 14 | 15 | //NewTotal - create new total metric type, add it to telemetry register 16 | func NewTotal(tel *Telemetry, name string, duration time.Duration) *Total { 17 | total := Total{ 18 | Name: name, 19 | value: 0, 20 | } 21 | tel.lock.Lock() 22 | defer tel.lock.Unlock() 23 | tel.registry[name] = &total 24 | return &total 25 | } 26 | 27 | //Add - add value to total 28 | func (t *Total) Add(tel *Telemetry, value float64) error { 29 | t.lock.Lock() 30 | defer t.lock.Unlock() 31 | tel.lock.Lock() 32 | defer tel.lock.Unlock() 33 | tel.registry[t.Name].(*Total).value += value 34 | return nil 35 | } 36 | 37 | //Get - get current value 38 | func (t *Total) Get(tel *Telemetry) float64 { 39 | return tel.registry[t.Name].(*Total).value 40 | } 41 | 42 | //GetName - get metric name 43 | func (t *Total) GetName() string { 44 | return t.Name 45 | } 46 | 47 | //Maintain - stub method for interface, does nothing 48 | func (t *Total) Maintain() { 49 | return 50 | } 51 | --------------------------------------------------------------------------------