This repository has been archived by the owner on Dec 3, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 23
/
testreporter.go
219 lines (188 loc) · 7.28 KB
/
testreporter.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
// Copyright (c) 2016 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package metrics
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
type (
// TestReporter is the reporter used to analyze metrics for unit tests
TestReporter struct {
tags map[string]string
}
testStopWatch struct {
metricName string
reporter *TestReporter
startTime time.Time
elapsed time.Duration
}
)
type HandlerFn func(metricName string, baseTags, tags map[string]string, value int64)
var handlers = make(map[string]map[string]HandlerFn) // Key1 - metricName; Key2 - "filterTag:filterVal"
var handlerMutex sync.RWMutex
// NewTestReporter create an instance of Reporter which can be used for driver to emit metric to console
func NewTestReporter(tags map[string]string) Reporter {
reporter := &TestReporter{
tags: mapCopy(tags),
}
return reporter
}
// InitMetrics is used to initialize the metrics map with the respective type
func (r *TestReporter) InitMetrics(metricMap map[MetricName]MetricType) {
// This is a no-op for test reporter as it is already have a static list of metric to work with
}
// GetChildReporter creates the child reporter for this parent reporter
func (r *TestReporter) GetChildReporter(tags map[string]string) Reporter {
if len(r.tags) != 0 {
panic(r.tags)
}
sr := &TestReporter{
tags: mapCopy(tags),
}
return sr
}
// GetTags returns the tags for this reporter object
func (r *TestReporter) GetTags() map[string]string {
return r.tags
}
// IncCounter reports Counter metric to M3
func (r *TestReporter) IncCounter(name string, tags map[string]string, delta int64) {
r.executeHandler(name, tags, delta)
}
// UpdateGauge reports Gauge type metric
func (r *TestReporter) UpdateGauge(name string, tags map[string]string, value int64) {
r.executeHandler(name, tags, value)
}
func (r *TestReporter) executeHandler(name string, tags map[string]string, value int64) {
handlerMutex.RLock()
_, ok0 := handlers[``]
_, ok1 := handlers[name]
if ok0 || ok1 {
if allHandler2, ok2 := handlers[``][``]; ok2 { // Global handler
allHandler2(name, r.tags, tags, value)
}
if allHandler3, ok3 := handlers[name][``]; ok3 { // Handler for all metrics named 'name'
allHandler3(name, r.tags, tags, value)
}
// TODO: technically, this is wrong, as we don't have the local tags overriding the struct tags, but this
// has no practical effect in our current use of metrics, since we never override
for _, q := range []map[string]string{r.tags, tags} {
for filterTag, filterTagVal := range q {
key2 := filterTag + `:` + filterTagVal
if handler4, ok4 := handlers[``][key2]; ok4 { // Handler for this tag, any name
handler4(name, r.tags, tags, value)
}
if handler5, ok5 := handlers[name][key2]; ok5 { // Handler for specifically this name and tag
handler5(name, r.tags, tags, value)
}
}
}
}
handlerMutex.RUnlock()
}
// Register a handler (closure) that receives updates for a particular guage or counter based on the metric name and
// the name/value of one of the metric's tags. If the filterTag/Val are both empty, all updates to that metric will
// trigger the handler. If metricName is empty, all metrics matching the tag filter will pass through your function.
// A nil handler unregisters the handler for the given filter parameters
//
// Dev notes:
// * It is advisible to defer a call to unregister your handler when your test ends
// * Your handler can be called concurrently. Capture your own sync.Mutex if you must serialize
// * Counters report the delta; you must maintain the cumulative value of your counter if it is important
// * Your handler executes synchronously with the metrics code; DO NOT BLOCK
func RegisterHandler(metricName, filterTag, filterTagVal string, handler HandlerFn) {
defer handlerMutex.Unlock()
handlerMutex.Lock()
if _, ok := handlers[metricName]; !ok {
handlers[metricName] = make(map[string]HandlerFn)
}
key2 := filterTag + `:` + filterTagVal
if key2 == `:` {
key2 = ``
}
if handler == nil {
delete(handlers[metricName], key2)
if len(handlers[metricName]) == 0 {
delete(handlers, metricName)
}
return
}
if hf, ok2 := handlers[metricName][key2]; ok2 {
panic(fmt.Sprintf("Metrics handler %v (for '%s'/'%s') should have been unregistered", hf, metricName, key2))
}
handlers[metricName][key2] = handler
}
func newTestStopWatch(metricName string, reporter *TestReporter) *testStopWatch {
watch := &testStopWatch{
metricName: metricName,
reporter: reporter,
}
return watch
}
func (w *testStopWatch) Start() {
w.startTime = time.Now()
}
func (w *testStopWatch) Stop() time.Duration {
w.elapsed = time.Since(w.startTime)
w.reporter.executeHandler(w.metricName, w.reporter.GetTags(), int64(w.elapsed))
return w.elapsed
}
// StartTimer returns a Stopwatch which when stopped will report the metric to M3
func (r *TestReporter) StartTimer(name string, tags map[string]string) Stopwatch {
w := newTestStopWatch(name, r)
w.Start()
return w
}
// RecordTimer should be used for measuring latency when you cannot start the stop watch.
func (r *TestReporter) RecordTimer(name string, tags map[string]string, d time.Duration) {
r.executeHandler(name, tags, int64(d))
}
func mapCopy(s map[string]string) (d map[string]string) {
d = make(map[string]string, len(s))
for a, b := range s {
d[a] = b
}
return
}
// SummingHandler returns a HandlerFn that atomically stores the sum and count of successive calls into the provided
// atomic variables
// NOTE: there are two race-free way to reset the counts together:
// 1) unregister the handler, then zero the counts, and re-register; may lose events
// 2) register a new handler with a second set of counting variables
func SummingHandler(atomicSum, atomicCount *int64) HandlerFn {
return func(metricName string, baseTags, tags map[string]string, value int64) {
atomic.AddInt64(atomicSum, value)
if atomicCount != nil {
atomic.AddInt64(atomicCount, 1)
}
}
}
// KeepLastValueHandler returns a HandlerFn that atomically stores the last value and count of successive calls into
// the provided atomic variables
func KeepLastValueHandler(atomicLastValue, atomicCount *int64) HandlerFn {
return func(metricName string, baseTags, tags map[string]string, value int64) {
atomic.StoreInt64(atomicLastValue, value)
if atomicCount != nil {
atomic.AddInt64(atomicCount, 1)
}
}
}