1 package cloudwatch
2
3 import (
4 "errors"
5 "fmt"
6 "strconv"
7 "sync"
8 "testing"
9
10 "github.com/aws/aws-sdk-go/service/cloudwatch"
11 "github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface"
12
13 "github.com/go-kit/kit/metrics"
14 "github.com/go-kit/kit/metrics/teststat"
15 "github.com/go-kit/log"
16 )
17
18 const metricNameToGenerateError = "metric_name_used_to_throw_an_error"
19
20 var errTest = errors.New("test error")
21
22 type mockCloudWatch struct {
23 cloudwatchiface.CloudWatchAPI
24 mtx sync.RWMutex
25 valuesReceived map[string][]float64
26 dimensionsReceived map[string][]*cloudwatch.Dimension
27 }
28
29 func newMockCloudWatch() *mockCloudWatch {
30 return &mockCloudWatch{
31 valuesReceived: map[string][]float64{},
32 dimensionsReceived: map[string][]*cloudwatch.Dimension{},
33 }
34 }
35
36 func (mcw *mockCloudWatch) PutMetricData(input *cloudwatch.PutMetricDataInput) (*cloudwatch.PutMetricDataOutput, error) {
37 mcw.mtx.Lock()
38 defer mcw.mtx.Unlock()
39 for _, datum := range input.MetricData {
40 if *datum.MetricName == metricNameToGenerateError {
41 return nil, errTest
42 }
43
44 if len(datum.Values) > 0 {
45 for _, v := range datum.Values {
46 mcw.valuesReceived[*datum.MetricName] = append(mcw.valuesReceived[*datum.MetricName], *v)
47 }
48 } else {
49 mcw.valuesReceived[*datum.MetricName] = append(mcw.valuesReceived[*datum.MetricName], *datum.Value)
50 }
51 mcw.dimensionsReceived[*datum.MetricName] = datum.Dimensions
52 }
53 return nil, nil
54 }
55
56 func (mcw *mockCloudWatch) testDimensions(name string, labelValues ...string) error {
57 mcw.mtx.RLock()
58 _, hasValue := mcw.valuesReceived[name]
59 if !hasValue {
60 return nil
61 }
62 dimensions, ok := mcw.dimensionsReceived[name]
63 mcw.mtx.RUnlock()
64
65 if !ok {
66 if len(labelValues) > 0 {
67 return errors.New("Expected dimensions to be available, but none were")
68 }
69 }
70 LabelValues:
71 for i, j := 0, 0; i < len(labelValues); i, j = i+2, j+1 {
72 name, value := labelValues[i], labelValues[i+1]
73 for _, dimension := range dimensions {
74 if *dimension.Name == name {
75 if *dimension.Value == value {
76 break LabelValues
77 }
78 }
79 }
80 return fmt.Errorf("could not find dimension with name %s and value %s", name, value)
81 }
82
83 return nil
84 }
85
86 func TestCounter(t *testing.T) {
87 namespace, name := "abc", "def"
88 label, value := "label", "value"
89 svc := newMockCloudWatch()
90 cw := New(namespace, svc, WithLogger(log.NewNopLogger()))
91 counter := cw.NewCounter(name).With(label, value)
92 valuef := func() float64 {
93 if err := cw.Send(); err != nil {
94 t.Fatal(err)
95 }
96 svc.mtx.RLock()
97 defer svc.mtx.RUnlock()
98 value := svc.valuesReceived[name][len(svc.valuesReceived[name])-1]
99 delete(svc.valuesReceived, name)
100
101 return value
102 }
103 if err := teststat.TestCounter(counter, valuef); err != nil {
104 t.Fatal(err)
105 }
106 if err := teststat.TestCounter(counter, valuef); err != nil {
107 t.Fatal("Fill and flush counter 2nd time: ", err)
108 }
109 if err := svc.testDimensions(name, label, value); err != nil {
110 t.Fatal(err)
111 }
112 }
113
114 func TestCounterLowSendConcurrency(t *testing.T) {
115 namespace := "abc"
116 var names, labels, values []string
117 for i := 1; i <= 45; i++ {
118 num := strconv.Itoa(i)
119 names = append(names, "name"+num)
120 labels = append(labels, "label"+num)
121 values = append(values, "value"+num)
122 }
123 svc := newMockCloudWatch()
124 cw := New(namespace, svc,
125 WithLogger(log.NewNopLogger()),
126 WithConcurrentRequests(2),
127 )
128
129 counters := make(map[string]metrics.Counter)
130 var wants []float64
131 for i, name := range names {
132 counters[name] = cw.NewCounter(name).With(labels[i], values[i])
133 wants = append(wants, teststat.FillCounter(counters[name]))
134 }
135
136 if err := cw.Send(); err != nil {
137 t.Fatal(err)
138 }
139
140 for i, name := range names {
141 if l := len(svc.valuesReceived[name]); l == 0 && wants[i] == 0 {
142 continue
143 } else if l != 1 {
144 t.Fatalf("one value expected, got %d", l)
145 }
146
147 if svc.valuesReceived[name][0] != wants[i] {
148 t.Fatalf("want %f, have %f", wants[i], svc.valuesReceived[name])
149 }
150 if err := svc.testDimensions(name, labels[i], values[i]); err != nil {
151 t.Fatal(err)
152 }
153 }
154 }
155
156 func TestGauge(t *testing.T) {
157 namespace, name := "abc", "def"
158 label, value := "label", "value"
159 svc := newMockCloudWatch()
160 cw := New(namespace, svc, WithLogger(log.NewNopLogger()))
161 gauge := cw.NewGauge(name).With(label, value)
162 valuef := func() []float64 {
163 if err := cw.Send(); err != nil {
164 t.Fatal(err)
165 }
166 svc.mtx.RLock()
167 defer svc.mtx.RUnlock()
168 res := svc.valuesReceived[name]
169 delete(svc.valuesReceived, name)
170 return res
171 }
172
173 if err := teststat.TestGauge(gauge, valuef); err != nil {
174 t.Fatal(err)
175 }
176 if err := svc.testDimensions(name, label, value); err != nil {
177 t.Fatal(err)
178 }
179 }
180
181 func TestHistogram(t *testing.T) {
182 namespace, name := "abc", "def"
183 label, value := "label", "value"
184 svc := newMockCloudWatch()
185 cw := New(namespace, svc, WithLogger(log.NewNopLogger()))
186 histogram := cw.NewHistogram(name).With(label, value)
187 n50 := fmt.Sprintf("%s_50", name)
188 n90 := fmt.Sprintf("%s_90", name)
189 n95 := fmt.Sprintf("%s_95", name)
190 n99 := fmt.Sprintf("%s_99", name)
191 quantiles := func() (p50, p90, p95, p99 float64) {
192 err := cw.Send()
193 if err != nil {
194 t.Fatal(err)
195 }
196
197 svc.mtx.RLock()
198 defer svc.mtx.RUnlock()
199 if len(svc.valuesReceived[n50]) > 0 {
200 p50 = svc.valuesReceived[n50][0]
201 delete(svc.valuesReceived, n50)
202 }
203
204 if len(svc.valuesReceived[n90]) > 0 {
205 p90 = svc.valuesReceived[n90][0]
206 delete(svc.valuesReceived, n90)
207 }
208
209 if len(svc.valuesReceived[n95]) > 0 {
210 p95 = svc.valuesReceived[n95][0]
211 delete(svc.valuesReceived, n95)
212 }
213
214 if len(svc.valuesReceived[n99]) > 0 {
215 p99 = svc.valuesReceived[n99][0]
216 delete(svc.valuesReceived, n99)
217 }
218 return
219 }
220 if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
221 t.Fatal(err)
222 }
223 if err := svc.testDimensions(n50, label, value); err != nil {
224 t.Fatal(err)
225 }
226 if err := svc.testDimensions(n90, label, value); err != nil {
227 t.Fatal(err)
228 }
229 if err := svc.testDimensions(n95, label, value); err != nil {
230 t.Fatal(err)
231 }
232 if err := svc.testDimensions(n99, label, value); err != nil {
233 t.Fatal(err)
234 }
235
236
237
238 svc = newMockCloudWatch()
239 cw = New(namespace, svc, WithLogger(log.NewNopLogger()), WithPercentiles(0.50, 0.90))
240 histogram = cw.NewHistogram(name).With(label, value)
241
242 customQuantiles := func() (p50, p90, p95, p99 float64) {
243 err := cw.Send()
244 if err != nil {
245 t.Fatal(err)
246 }
247 svc.mtx.RLock()
248 defer svc.mtx.RUnlock()
249 if len(svc.valuesReceived[n50]) > 0 {
250 p50 = svc.valuesReceived[n50][0]
251 delete(svc.valuesReceived, n50)
252 }
253 if len(svc.valuesReceived[n90]) > 0 {
254 p90 = svc.valuesReceived[n90][0]
255 delete(svc.valuesReceived, n90)
256 }
257
258
259
260
261 p95 = 541.121341
262 p99 = 558.158697
263
264
265
266 if _, isSet := svc.valuesReceived[n95]; isSet {
267 t.Fatal("p95 should not be set")
268 }
269 if _, isSet := svc.valuesReceived[n99]; isSet {
270 t.Fatal("p99 should not be set")
271 }
272 return
273 }
274 if err := teststat.TestHistogram(histogram, customQuantiles, 0.01); err != nil {
275 t.Fatal(err)
276 }
277 if err := svc.testDimensions(n50, label, value); err != nil {
278 t.Fatal(err)
279 }
280 if err := svc.testDimensions(n90, label, value); err != nil {
281 t.Fatal(err)
282 }
283 if err := svc.testDimensions(n95, label, value); err != nil {
284 t.Fatal(err)
285 }
286 if err := svc.testDimensions(n99, label, value); err != nil {
287 t.Fatal(err)
288 }
289 }
290
291 func TestErrorLog(t *testing.T) {
292 namespace := "abc"
293 svc := newMockCloudWatch()
294 cw := New(namespace, svc, WithLogger(log.NewNopLogger()))
295 cw.NewGauge(metricNameToGenerateError).Set(123)
296 if err := cw.Send(); err != errTest {
297 t.Fatal("Expected error, but didn't get one")
298 }
299 }
300
View as plain text