├── README.md ├── http.go ├── metrics.go └── metrics_test.go /README.md: -------------------------------------------------------------------------------- 1 | # measure 2 | 3 | ## Usage 4 | 5 | ### Measure 6 | 7 | Add measure to your code. 8 | 9 | ```go 10 | import "github.com/najeira/measure" 11 | 12 | func foo() { 13 | defer measure.Start("foo").Stop() 14 | 15 | // your code 16 | 17 | } 18 | ``` 19 | 20 | or 21 | 22 | ```go 23 | ... 24 | m := measure.Start("foo") 25 | // your code 26 | m.Stop() 27 | ... 28 | ``` 29 | 30 | ### Stats 31 | 32 | Get statistics. 33 | 34 | ```go 35 | stats := measure.GetStats() 36 | stats.SortDesc("sum") 37 | 38 | // print stats in CSV format 39 | for _, s := range stats { 40 | fmt.Fprintf(w, "%s,%d,%f,%f,%f,%f,%f,%f\n", 41 | s.Key, s.Count, s.Sum, s.Min, s.Max, s.Avg, s.Rate, s.P95) 42 | } 43 | ``` 44 | 45 | Reset statistics. 46 | 47 | ```go 48 | measure.Reset() 49 | ``` 50 | 51 | ### Metrics 52 | 53 | You can handle multiple metrics. 54 | 55 | ```go 56 | var metricsA = measure.NewMetrics() 57 | var metricsB = measure.NewMetrics() 58 | 59 | func foo() { 60 | defer metricsA.Start("foo").Stop() 61 | } 62 | 63 | func bar() { 64 | defer metricsB.Start("bar").Stop() 65 | } 66 | ``` 67 | 68 | ## License 69 | 70 | MIT 71 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package measure 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | func HandleStats(w http.ResponseWriter, r *http.Request) { 9 | q := r.URL.Query() 10 | key := q.Get("key") 11 | 12 | stats := GetStats() 13 | stats.SortDesc(key) 14 | 15 | w.WriteHeader(http.StatusOK) 16 | 17 | fmt.Fprint(w, "key,count,sum,min,max,avg,rate,p95\n") 18 | for _, s := range stats { 19 | fmt.Fprintf(w, "%s,%d,%f,%f,%f,%f,%f,%f\n", 20 | s.Key, s.Count, s.Sum, s.Min, s.Max, s.Avg, s.Rate, s.P95) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /metrics.go: -------------------------------------------------------------------------------- 1 | package measure 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | "sync" 7 | "time" 8 | 9 | mt "github.com/rcrowley/go-metrics" 10 | ) 11 | 12 | const ( 13 | Key = "key" 14 | Count = "count" 15 | Sum = "sum" 16 | Min = "min" 17 | Max = "max" 18 | Avg = "avg" 19 | Rate = "rate" 20 | P95 = "p95" 21 | ) 22 | 23 | var ( 24 | Disabled bool 25 | 26 | defaultMetrics *Metrics 27 | ) 28 | 29 | func init() { 30 | defaultMetrics = NewMetrics() 31 | } 32 | 33 | type Measure struct { 34 | key string 35 | start time.Time 36 | metrics *Metrics 37 | } 38 | 39 | func Start(key string) Measure { 40 | return defaultMetrics.Start(key) 41 | } 42 | 43 | func (m Measure) Stop() { 44 | if Disabled { 45 | return 46 | } 47 | m.metrics.Update(m.key, m.start) 48 | } 49 | 50 | type Metrics struct { 51 | mu sync.RWMutex 52 | metrics map[string]mt.Timer 53 | } 54 | 55 | func GetStats() StatsSlice { 56 | return defaultMetrics.GetStats() 57 | } 58 | 59 | func Reset() { 60 | defaultMetrics.Reset() 61 | } 62 | 63 | func NewMetrics() *Metrics { 64 | return &Metrics{metrics: make(map[string]mt.Timer)} 65 | } 66 | 67 | func (m *Metrics) Start(key string) Measure { 68 | if Disabled { 69 | return Measure{} 70 | } 71 | return Measure{key: key, start: time.Now(), metrics: m} 72 | } 73 | 74 | func (m *Metrics) Update(key string, start time.Time) { 75 | m.mu.RLock() 76 | t, ok := m.metrics[key] 77 | m.mu.RUnlock() 78 | 79 | if !ok { 80 | m.mu.Lock() 81 | t, ok = m.metrics[key] 82 | if !ok { 83 | t = mt.NewTimer() 84 | m.metrics[key] = t 85 | } 86 | m.mu.Unlock() 87 | } 88 | 89 | t.Update(time.Since(start)) 90 | } 91 | 92 | func (m *Metrics) GetStats() StatsSlice { 93 | m.mu.RLock() 94 | defer m.mu.RUnlock() 95 | result := make(StatsSlice, 0, len(m.metrics)) 96 | for key, t := range m.metrics { 97 | stats := Stats{ 98 | Key: key, 99 | Count: t.Count(), 100 | Sum: float64(t.Sum()) / float64(time.Millisecond), 101 | Min: float64(t.Min()) / float64(time.Millisecond), 102 | Max: float64(t.Max()) / float64(time.Millisecond), 103 | Avg: t.Mean() / float64(time.Millisecond), 104 | Rate: t.Rate1(), 105 | P95: t.Percentile(0.95) / float64(time.Millisecond), 106 | } 107 | result = append(result, stats) 108 | } 109 | return result 110 | } 111 | 112 | func (m *Metrics) Reset() { 113 | m.mu.Lock() 114 | m.metrics = make(map[string]mt.Timer) 115 | m.mu.Unlock() 116 | } 117 | 118 | type Stats struct { 119 | Key string `csv:"key" json:"key"` 120 | Count int64 `csv:"count" json:"count"` 121 | Sum float64 `csv:"sum" json:"sum"` 122 | Min float64 `csv:"min" json:"min"` 123 | Max float64 `csv:"max" json:"max"` 124 | Avg float64 `csv:"avg" json:"avg"` 125 | Rate float64 `csv:"rate" json:"rate"` 126 | P95 float64 `csv:"p95" json:"p95"` 127 | } 128 | 129 | type StatsSlice []Stats 130 | 131 | func (s StatsSlice) SortAsc(key string) { 132 | s.sortBy(key, true) 133 | } 134 | 135 | func (s StatsSlice) SortDesc(key string) { 136 | s.sortBy(key, false) 137 | } 138 | 139 | func (s StatsSlice) sortBy(key string, asc bool) { 140 | p := StatsSorter{stats: s, key: key} 141 | if asc { 142 | sort.Sort(p) 143 | } else { 144 | sort.Sort(sort.Reverse(p)) 145 | } 146 | } 147 | 148 | type StatsSorter struct { 149 | stats []Stats 150 | key string 151 | } 152 | 153 | func (p StatsSorter) Len() int { 154 | return len(p.stats) 155 | } 156 | 157 | func (p StatsSorter) Less(i, j int) bool { 158 | n, m := p.stats[i], p.stats[j] 159 | switch p.key { 160 | case Key: 161 | return strings.Compare(n.Key, m.Key) < 0 162 | case Count: 163 | return n.Count < m.Count 164 | case Min: 165 | return n.Min < m.Min 166 | case Max: 167 | return n.Max < m.Max 168 | case Avg: 169 | return n.Avg < m.Avg 170 | case Rate: 171 | return n.Rate < m.Rate 172 | case P95: 173 | return n.P95 < m.P95 174 | } 175 | return n.Sum < m.Sum 176 | } 177 | 178 | func (p StatsSorter) Swap(i, j int) { 179 | p.stats[i], p.stats[j] = p.stats[j], p.stats[i] 180 | } 181 | -------------------------------------------------------------------------------- /metrics_test.go: -------------------------------------------------------------------------------- 1 | package measure 2 | 3 | import ( 4 | "log" 5 | "strconv" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | var enablePrint = false 11 | 12 | func printf(format string, a ...interface{}) { 13 | if enablePrint { 14 | log.Printf(format, a...) 15 | } 16 | } 17 | 18 | func TestMeasure(t *testing.T) { 19 | const key = "test" 20 | 21 | Reset() 22 | m := Start(key) 23 | m.Stop() 24 | 25 | stats := GetStats() 26 | if stats == nil || len(stats) < 1 { 27 | t.Fatal("GetStats returns nil") 28 | } 29 | 30 | if stats[0].Count != 1 { 31 | t.Errorf("Stats.Count got %d expect %d", stats[0].Count, 1) 32 | } 33 | } 34 | 35 | func TestMeasureMulti(t *testing.T) { 36 | const key = "test_multi" 37 | const loop = 100 38 | 39 | Reset() 40 | for i := 0; i < loop; i++ { 41 | m := Start(key) 42 | time.Sleep(time.Microsecond) 43 | m.Stop() 44 | } 45 | 46 | stats := GetStats() 47 | if stats == nil || len(stats) < 1 { 48 | t.Fatal("GetStats returns nil") 49 | } 50 | 51 | if stats[0].Count != loop { 52 | t.Errorf("Stats.Count got %d expect %d", stats[0].Count, loop) 53 | } 54 | 55 | if stats[0].Min > stats[0].Max { 56 | t.Errorf("Stats.Min %f > Stats.Max %f", stats[0].Min, stats[0].Max) 57 | } 58 | if stats[0].Min > stats[0].Avg { 59 | t.Errorf("Stats.Min %f > Stats.Max %f", stats[0].Min, stats[0].Avg) 60 | } 61 | if stats[0].Max < stats[0].Avg { 62 | t.Errorf("Stats.Min %f < Stats.Max %f", stats[0].Max, stats[0].Avg) 63 | } 64 | if stats[0].Sum < stats[0].Max { 65 | t.Errorf("Stats.Sum %f < Stats.Max %f", stats[0].Sum, stats[0].Max) 66 | } 67 | } 68 | 69 | func TestMeasureSort(t *testing.T) { 70 | const loop = 100 71 | 72 | Reset() 73 | for i := 0; i < loop; i++ { 74 | m := Start(strconv.Itoa(i)) 75 | time.Sleep(time.Microsecond) 76 | m.Stop() 77 | } 78 | 79 | stats := GetStats() 80 | if stats == nil || len(stats) < 1 { 81 | t.Fatal("GetStats returns nil") 82 | } 83 | 84 | stats.SortAsc("sum") 85 | for i := 0; i < len(stats)-1; i++ { 86 | n, m := stats[i], stats[i+1] 87 | if n.Sum > m.Sum { 88 | t.Fatal("SortAsc fail") 89 | } 90 | printf("%f <= %f", n.Sum, m.Sum) 91 | } 92 | 93 | stats.SortDesc("sum") 94 | for i := 0; i < len(stats)-1; i++ { 95 | n, m := stats[i], stats[i+1] 96 | if n.Sum < m.Sum { 97 | t.Fatal("SortDesc fail") 98 | } 99 | printf("%f >= %f", n.Sum, m.Sum) 100 | } 101 | } 102 | 103 | func TestMeasureMetrics(t *testing.T) { 104 | m1 := NewMetrics() 105 | m2 := NewMetrics() 106 | 107 | s1 := m1.Start("test") 108 | s2 := m2.Start("test") 109 | 110 | s1.Stop() 111 | s2.Stop() 112 | 113 | if len(m1.metrics) != 1 { 114 | t.Error("invalid len") 115 | } else if _, ok := m1.metrics["test"]; !ok { 116 | t.Error("invalid map") 117 | } 118 | 119 | if len(m2.metrics) != 1 { 120 | t.Error("invalid len") 121 | } else if _, ok := m2.metrics["test"]; !ok { 122 | t.Error("invalid map") 123 | } 124 | 125 | log.Print(m1.GetStats()) 126 | log.Print(m2.GetStats()) 127 | } 128 | 129 | func BenchmarkMeasure(b *testing.B) { 130 | const key = "test" 131 | b.ReportAllocs() 132 | for i := 0; i < b.N; i++ { 133 | m := Start(key) 134 | m.Stop() 135 | } 136 | } 137 | 138 | func BenchmarkMeasureDisabled(b *testing.B) { 139 | const key = "test" 140 | Disabled = true 141 | b.ReportAllocs() 142 | for i := 0; i < b.N; i++ { 143 | m := Start(key) 144 | m.Stop() 145 | } 146 | Disabled = false 147 | } 148 | --------------------------------------------------------------------------------