├── go.mod ├── .travis.yml ├── README.md ├── timer_metrics_test.go ├── LICENSE └── timer_metrics.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bitly/timer_metrics 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.x 4 | notifications: 5 | email: false 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # timer_metrics 2 | 3 | 4 | [![Build Status](https://secure.travis-ci.org/bitly/timer_metrics.png?branch=master)](http://travis-ci.org/bitly/timer_metrics) [![GoDoc](https://godoc.org/github.com/bitly/timer_metrics?status.svg)](https://godoc.org/github.com/bitly/timer_metrics) [![GitHub release](https://img.shields.io/github/release/bitly/timer_metrics.svg)](https://github.com/bitly/timer_metrics/releases/latest) 5 | 6 | A efficient way to capture timing information and periodically output metrics 7 | 8 | See -------------------------------------------------------------------------------- /timer_metrics_test.go: -------------------------------------------------------------------------------- 1 | package timer_metrics 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestMetrics(t *testing.T) { 10 | m := NewTimerMetrics(5, "prefix") 11 | for _, n := range []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11} { 12 | m.StatusDuration(time.Duration(n) * time.Second) 13 | stats := m.Stats() 14 | log.Printf("%s", stats) 15 | log.Printf("> %#v", m.timings) 16 | } 17 | stats := m.Stats() 18 | if stats.Avg != time.Duration(9)*time.Second { 19 | t.Errorf("avg is %s expected 9s", stats.Avg) 20 | } 21 | if stats.P99 != time.Duration(11)*time.Second { 22 | t.Errorf("99th is %s expected 11s", stats.P99) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy 2 | of this software and associated documentation files (the "Software"), to deal 3 | in the Software without restriction, including without limitation the rights 4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 5 | copies of the Software, and to permit persons to whom the Software is 6 | furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in 9 | all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 17 | THE SOFTWARE. 18 | -------------------------------------------------------------------------------- /timer_metrics.go: -------------------------------------------------------------------------------- 1 | package timer_metrics 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math" 7 | "sort" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | type TimerMetrics struct { 13 | sync.Mutex 14 | timings durations 15 | prefix string 16 | statusEvery int 17 | position int 18 | } 19 | 20 | // start a new TimerMetrics to print out metrics every n times 21 | func NewTimerMetrics(statusEvery int, prefix string) *TimerMetrics { 22 | s := &TimerMetrics{ 23 | statusEvery: statusEvery, 24 | prefix: prefix, 25 | } 26 | if statusEvery > 0 { 27 | if statusEvery < 100 { 28 | log.Printf("Warning: use more than 100 status values for accurate percentiles (configured with %d)", statusEvery) 29 | } 30 | s.timings = make(durations, 0, statusEvery) 31 | } 32 | return s 33 | } 34 | 35 | type durations []time.Duration 36 | 37 | func (s durations) Len() int { return len(s) } 38 | func (s durations) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 39 | func (s durations) Less(i, j int) bool { return s[i] < s[j] } 40 | 41 | func percentile(perc float64, arr durations) time.Duration { 42 | length := len(arr) 43 | if length == 0 { 44 | return 0 45 | } 46 | indexOfPerc := int(math.Ceil(((perc / 100.0) * float64(length)) + 0.5)) 47 | if indexOfPerc >= length { 48 | indexOfPerc = length - 1 49 | } 50 | return arr[indexOfPerc] 51 | } 52 | 53 | type Stats struct { 54 | Prefix string 55 | Count int 56 | Avg time.Duration 57 | P95 time.Duration 58 | P99 time.Duration 59 | } 60 | 61 | func (s *Stats) String() string { 62 | p95Ms := s.P95.Seconds() * 1000 63 | p99Ms := s.P99.Seconds() * 1000 64 | avgMs := s.Avg.Seconds() * 1000 65 | return fmt.Sprintf("%s finished %d - 99th: %.02fms - 95th: %.02fms - avg: %.02fms", 66 | s.Prefix, s.Count, p99Ms, p95Ms, avgMs) 67 | } 68 | 69 | func (m *TimerMetrics) getStats() *Stats { 70 | var total time.Duration 71 | for _, v := range m.timings { 72 | total += v 73 | } 74 | 75 | // make a copy of timings so we still rotate through in order 76 | timings := make(durations, len(m.timings)) 77 | copy(timings, m.timings) 78 | sort.Sort(timings) 79 | var avg time.Duration 80 | if len(timings) > 0 { 81 | avg = total / time.Duration(len(m.timings)) 82 | } 83 | return &Stats{ 84 | Prefix: m.prefix, 85 | Count: len(m.timings), 86 | Avg: avg, 87 | P95: percentile(95.0, timings), 88 | P99: percentile(99.0, timings), 89 | } 90 | } 91 | 92 | // get the current Stats 93 | func (m *TimerMetrics) Stats() *Stats { 94 | m.Lock() 95 | s := m.getStats() 96 | m.Unlock() 97 | return s 98 | } 99 | 100 | // record a delta from time.Now() 101 | func (m *TimerMetrics) Status(startTime time.Time) { 102 | if m.statusEvery == 0 { 103 | return 104 | } 105 | m.StatusDuration(time.Now().Sub(startTime)) 106 | } 107 | 108 | // Record a duration, printing out stats every statusEvery interval 109 | func (m *TimerMetrics) StatusDuration(duration time.Duration) { 110 | if m.statusEvery == 0 { 111 | return 112 | } 113 | 114 | m.Lock() 115 | m.position++ 116 | var looped bool 117 | if m.position > m.statusEvery { 118 | // loop back around 119 | looped = true 120 | m.position = 1 121 | } 122 | if m.position > len(m.timings) { 123 | m.timings = append(m.timings, duration) 124 | } else { 125 | m.timings[m.position-1] = duration 126 | } 127 | 128 | if !looped { 129 | m.Unlock() 130 | return 131 | } 132 | 133 | stats := m.getStats() 134 | m.Unlock() 135 | 136 | log.Printf("%s", stats) 137 | } 138 | --------------------------------------------------------------------------------