-
Notifications
You must be signed in to change notification settings - Fork 329
/
resource_metrics.go
350 lines (311 loc) · 10.2 KB
/
resource_metrics.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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
/*
Copyright 2020 The Knative Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package metricstest simplifies some of the common boilerplate around testing
// metrics exports. It should work with or without the code in metrics, but this
// code particularly knows how to deal with metrics which are exported for
// multiple Resources in the same process.
package metricstest
import (
"fmt"
"sort"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"go.opencensus.io/metric/metricdata"
"go.opencensus.io/metric/metricproducer"
"go.opencensus.io/resource"
"go.opencensus.io/stats/view"
)
// Value provides a simplified implementation of a metric Value suitable for
// easy testing.
type Value struct {
Tags map[string]string
// union interface, only one of these will be set
Int64 *int64
Float64 *float64
Distribution *metricdata.Distribution
// VerifyDistributionCountOnly makes Equal compare the Distribution with the
// field Count only, and ignore all other fields of Distribution.
// This is ignored when the value is not a Distribution.
VerifyDistributionCountOnly bool
}
// Metric provides a simplified (for testing) implementation of a metric report
// for a given metric name in a given Resource.
type Metric struct {
// Name is the exported name of the metric, probably from the View's name.
Name string
// Unit is the units of measure of the metric. This is only checked for
// equality if Unit is non-empty or VerifyMetadata is true on both Metrics.
Unit metricdata.Unit
// Type is the type of measurement represented by the metric. This is only
// checked for equality if VerifyMetadata is true on both Metrics.
Type metricdata.Type
// Resource is the reported Resource (if any) for this metric. This is only
// checked for equality if Resource is non-nil or VerifyResource is true on
// both Metrics.
Resource *resource.Resource
// Values contains the values recorded for different Key=Value Tag
// combinations. Value is checked for equality if present.
Values []Value
// Equality testing/validation settings on the Metric. These are used to
// allow simple construction and usage with github.com/google/go-cmp/cmp
// VerifyMetadata makes Equal compare Unit and Type if it is true on both
// Metrics.
VerifyMetadata bool
// VerifyResource makes Equal compare Resource if it is true on Metrics with
// nil Resource. Metrics with non-nil Resource are always compared.
VerifyResource bool
}
// NewMetric creates a Metric from a metricdata.Metric, which is designed for
// compact wire representation.
func NewMetric(metric *metricdata.Metric) Metric {
value := Metric{
Name: metric.Descriptor.Name,
Unit: metric.Descriptor.Unit,
Type: metric.Descriptor.Type,
Resource: metric.Resource,
VerifyMetadata: true,
VerifyResource: true,
Values: make([]Value, 0, len(metric.TimeSeries)),
}
for _, ts := range metric.TimeSeries {
tags := make(map[string]string, len(metric.Descriptor.LabelKeys))
for i, k := range metric.Descriptor.LabelKeys {
if ts.LabelValues[i].Present {
tags[k.Key] = ts.LabelValues[i].Value
}
}
v := Value{Tags: tags}
ts.Points[0].ReadValue(&v)
value.Values = append(value.Values, v)
}
return value
}
// EnsureRecorded makes sure that all stats metrics are actually flushed and recorded.
func EnsureRecorded() {
// stats.Record queues the actual record to a channel to be accounted for by
// a background goroutine (nonblocking). Call a method which does a
// round-trip to that goroutine to ensure that records have been flushed.
for _, producer := range metricproducer.GlobalManager().GetAll() {
if meter, ok := producer.(view.Meter); ok {
meter.Find("nonexistent")
}
}
}
// GetMetric returns all values for the named metric.
func GetMetric(name string) []Metric {
producers := metricproducer.GlobalManager().GetAll()
retval := make([]Metric, 0, len(producers))
for _, p := range producers {
for _, m := range p.Read() {
if m.Descriptor.Name == name && len(m.TimeSeries) > 0 {
retval = append(retval, NewMetric(m))
}
}
}
return retval
}
// GetOneMetric is like GetMetric, but it panics if more than a single Metric is
// found.
func GetOneMetric(name string) Metric {
m := GetMetric(name)
if len(m) != 1 {
panic(fmt.Sprint("Got wrong number of metrics:", m))
}
return m[0]
}
// IntMetric creates an Int64 metric.
func IntMetric(name string, value int64, tags map[string]string) Metric {
return Metric{
Name: name,
Values: []Value{{Int64: &value, Tags: tags}},
}
}
// FloatMetric creates a Float64 metric
func FloatMetric(name string, value float64, tags map[string]string) Metric {
return Metric{
Name: name,
Values: []Value{{Float64: &value, Tags: tags}},
}
}
// DistributionCountOnlyMetric creates a distribution metric for test, and verifying only the count.
func DistributionCountOnlyMetric(name string, count int64, tags map[string]string) Metric {
return Metric{
Name: name,
Values: []Value{{
Distribution: &metricdata.Distribution{Count: count},
Tags: tags,
VerifyDistributionCountOnly: true}},
}
}
// WithResource sets the resource of the metric.
func (m Metric) WithResource(r *resource.Resource) Metric {
m.Resource = r
return m
}
// AssertMetric verifies that the metrics have the specified values. Note that
// this method will spuriously fail if there are multiple metrics with the same
// name on different Meters. Calls EnsureRecorded internally before fetching the
// batch of metrics.
func AssertMetric(t *testing.T, values ...Metric) {
t.Helper()
EnsureRecorded()
for _, v := range values {
if diff := cmp.Diff(v, GetOneMetric(v.Name)); diff != "" {
t.Error("Wrong metric (-want +got):", diff)
}
}
}
// AssertMetricExists verifies that at least one metric values has been reported for
// each of metric names.
// Calls EnsureRecorded internally before fetching the batch of metrics.
func AssertMetricExists(t *testing.T, names ...string) {
metrics := make([]Metric, 0, len(names))
for _, n := range names {
metrics = append(metrics, Metric{Name: n})
}
AssertMetric(t, metrics...)
}
// AssertNoMetric verifies that no metrics have been reported for any of the
// metric names.
// Calls EnsureRecorded internally before fetching the batch of metrics.
func AssertNoMetric(t *testing.T, names ...string) {
t.Helper()
EnsureRecorded()
for _, name := range names {
if m := GetMetric(name); len(m) != 0 {
t.Error("Found unexpected data for:", m)
}
}
}
// VisitFloat64Value implements metricdata.ValueVisitor.
func (v *Value) VisitFloat64Value(f float64) {
v.Float64 = &f
v.Int64 = nil
v.Distribution = nil
}
// VisitInt64Value implements metricdata.ValueVisitor.
func (v *Value) VisitInt64Value(i int64) {
v.Int64 = &i
v.Float64 = nil
v.Distribution = nil
}
// VisitDistributionValue implements metricdata.ValueVisitor.
func (v *Value) VisitDistributionValue(d *metricdata.Distribution) {
v.Distribution = d
v.Int64 = nil
v.Float64 = nil
}
// VisitSummaryValue implements metricdata.ValueVisitor.
func (v *Value) VisitSummaryValue(*metricdata.Summary) {
panic("Attempted to fetch summary value, which we never use!")
}
// Equal provides a contract for use with github.com/google/go-cmp/cmp. Due to
// the reflection in cmp, it only works if the type of the two arguments to cmp
// are the same.
func (m Metric) Equal(other Metric) bool {
if m.Name != other.Name {
return false
}
if (m.Unit != "" || m.VerifyMetadata) && (other.Unit != "" || other.VerifyMetadata) {
if m.Unit != other.Unit {
return false
}
}
if m.VerifyMetadata && other.VerifyMetadata {
if m.Type != other.Type {
return false
}
}
if (m.Resource != nil || m.VerifyResource) && (other.Resource != nil || other.VerifyResource) {
if !cmp.Equal(m.Resource, other.Resource) {
return false
}
}
if len(m.Values) > 0 && len(other.Values) > 0 {
if len(m.Values) != len(other.Values) {
return false
}
myValues := make(map[string]Value, len(m.Values))
for _, v := range m.Values {
myValues[tagsToString(v.Tags)] = v
}
for _, v := range other.Values {
myV, ok := myValues[tagsToString(v.Tags)]
if !ok || !myV.Equal(v) {
return false
}
}
}
return true
}
// Equal provides a contract for github.com/google/go-cmp/cmp. It compares two
// values, including deep comparison of Distributions. (Exemplars are
// intentional not included in the comparison, but other fields are considered).
func (v Value) Equal(other Value) bool {
if len(v.Tags) != len(other.Tags) {
return false
}
for k, v := range v.Tags {
if v != other.Tags[k] {
return false
}
}
if v.Int64 != nil {
return other.Int64 != nil && *v.Int64 == *other.Int64
}
if v.Float64 != nil {
return other.Float64 != nil && *v.Float64 == *other.Float64
}
if v.Distribution != nil {
if other.Distribution == nil {
return false
}
if v.Distribution.Count != other.Distribution.Count {
return false
}
if v.VerifyDistributionCountOnly || other.VerifyDistributionCountOnly {
return true
}
if v.Distribution.Sum != other.Distribution.Sum {
return false
}
if v.Distribution.SumOfSquaredDeviation != other.Distribution.SumOfSquaredDeviation {
return false
}
if v.Distribution.BucketOptions != nil {
if other.Distribution.BucketOptions == nil {
return false
}
for i, bo := range v.Distribution.BucketOptions.Bounds {
if bo != other.Distribution.BucketOptions.Bounds[i] {
return false
}
}
}
for i, b := range v.Distribution.Buckets {
if b.Count != other.Distribution.Buckets[i].Count {
return false
}
}
}
return true
}
func tagsToString(tags map[string]string) string {
pairs := make([]string, 0, len(tags))
for k, v := range tags {
pairs = append(pairs, fmt.Sprintf("%s=%s", k, v))
}
sort.Strings(pairs)
return strings.Join(pairs, ",")
}