Skip to content
This repository was archived by the owner on Jul 31, 2023. It is now read-only.

Commit 643eada

Browse files
jkohenrghetia
authored andcommitted
Added test exporter for use in unit tests. (#1185)
* Added test exporter for use in unit tests. With this exporter one can write unit tests to verify that the instrumentation is working. See the included code example. * Clarified comment. * Fixed copyright date. * Added type assertion. * Checke key vs value length. * Added example for the metric package. * Improved API usage for derived metrics.
1 parent aad2c52 commit 643eada

File tree

4 files changed

+218
-0
lines changed

4 files changed

+218
-0
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
golang.org/x/net v0.0.0-20190620200207-3b0461eec859
99
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd // indirect
1010
golang.org/x/text v0.3.2 // indirect
11+
google.golang.org/appengine v1.4.0 // indirect
1112
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb // indirect
1213
google.golang.org/grpc v1.20.1
1314
)

metric/test/doc.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright 2019, OpenCensus Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
16+
// Package test for testing code instrumented with the metric and stats packages.
17+
package test

metric/test/exporter.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package test
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"sort"
7+
"strings"
8+
"time"
9+
10+
"go.opencensus.io/metric/metricdata"
11+
"go.opencensus.io/metric/metricexport"
12+
"go.opencensus.io/stats/view"
13+
)
14+
15+
// Exporter keeps exported metric data in memory to aid in testing the instrumentation.
16+
//
17+
// Metrics can be retrieved with `GetPoint()`. In order to deterministically retrieve the most recent values, you must first invoke `ReadAndExport()`.
18+
type Exporter struct {
19+
// points is a map from a label signature to the latest value for the time series represented by the signature.
20+
// Use function `labelSignature` to get a signature from a `metricdata.Metric`.
21+
points map[string]metricdata.Point
22+
metricReader *metricexport.Reader
23+
}
24+
25+
var _ metricexport.Exporter = &Exporter{}
26+
27+
// NewExporter returns a new exporter.
28+
func NewExporter(metricReader *metricexport.Reader) *Exporter {
29+
return &Exporter{points: make(map[string]metricdata.Point), metricReader: metricReader}
30+
}
31+
32+
// ExportMetrics records the view data.
33+
func (e *Exporter) ExportMetrics(ctx context.Context, data []*metricdata.Metric) error {
34+
for _, metric := range data {
35+
for _, ts := range metric.TimeSeries {
36+
signature := labelSignature(metric.Descriptor.Name, labelObjectsToKeyValue(metric.Descriptor.LabelKeys, ts.LabelValues))
37+
e.points[signature] = ts.Points[len(ts.Points)-1]
38+
}
39+
}
40+
return nil
41+
}
42+
43+
// GetPoint returns the latest point for the time series identified by the given labels.
44+
func (e *Exporter) GetPoint(metricName string, labels map[string]string) (metricdata.Point, bool) {
45+
v, ok := e.points[labelSignature(metricName, labelMapToKeyValue(labels))]
46+
return v, ok
47+
}
48+
49+
// ReadAndExport reads the current values for all metrics and makes them available to this exporter.
50+
func (e *Exporter) ReadAndExport() {
51+
// The next line forces the view worker to process all stats.Record* calls that
52+
// happened within Store() before the call to ReadAndExport below. This abuses the
53+
// worker implementation to work around lack of synchronization.
54+
// TODO(jkohen,rghetia): figure out a clean way to make this deterministic.
55+
view.SetReportingPeriod(time.Minute)
56+
e.metricReader.ReadAndExport(e)
57+
}
58+
59+
// String defines the ``native'' format for the exporter.
60+
func (e *Exporter) String() string {
61+
return fmt.Sprintf("points{%v}", e.points)
62+
}
63+
64+
type keyValue struct {
65+
Key string
66+
Value string
67+
}
68+
69+
func sortKeyValue(kv []keyValue) {
70+
sort.Slice(kv, func(i, j int) bool { return kv[i].Key < kv[j].Key })
71+
}
72+
73+
func labelMapToKeyValue(labels map[string]string) []keyValue {
74+
kv := make([]keyValue, 0, len(labels))
75+
for k, v := range labels {
76+
kv = append(kv, keyValue{Key: k, Value: v})
77+
}
78+
sortKeyValue(kv)
79+
return kv
80+
}
81+
82+
func labelObjectsToKeyValue(keys []metricdata.LabelKey, values []metricdata.LabelValue) []keyValue {
83+
if len(keys) != len(values) {
84+
panic("keys and values must have the same length")
85+
}
86+
kv := make([]keyValue, 0, len(values))
87+
for i := range keys {
88+
if values[i].Present {
89+
kv = append(kv, keyValue{Key: keys[i].Key, Value: values[i].Value})
90+
}
91+
}
92+
sortKeyValue(kv)
93+
return kv
94+
}
95+
96+
// labelSignature returns a string that uniquely identifies the list of labels given in the input.
97+
func labelSignature(metricName string, kv []keyValue) string {
98+
var builder strings.Builder
99+
for _, x := range kv {
100+
builder.WriteString(x.Key)
101+
builder.WriteString(x.Value)
102+
}
103+
return fmt.Sprintf("%s{%s}", metricName, builder.String())
104+
}

metric/test/exporter_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package test
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"go.opencensus.io/metric"
8+
"go.opencensus.io/metric/metricdata"
9+
"go.opencensus.io/metric/metricexport"
10+
"go.opencensus.io/stats"
11+
"go.opencensus.io/stats/view"
12+
"go.opencensus.io/tag"
13+
)
14+
15+
var (
16+
myTag = tag.MustNewKey("my_label")
17+
myMetric = stats.Int64("my_metric", "description", stats.UnitDimensionless)
18+
)
19+
20+
func init() {
21+
if err := view.Register(
22+
&view.View{
23+
Measure: myMetric,
24+
TagKeys: []tag.Key{myTag},
25+
Aggregation: view.Sum(),
26+
},
27+
); err != nil {
28+
panic(err)
29+
}
30+
}
31+
32+
func ExampleExporter_stats() {
33+
metricReader := metricexport.NewReader()
34+
metrics := NewExporter(metricReader)
35+
metrics.ReadAndExport()
36+
metricBase := getCounter(metrics, myMetric.Name(), newMetricKey("label1"))
37+
38+
for i := 1; i <= 3; i++ {
39+
// The code under test begins here.
40+
stats.RecordWithTags(context.Background(),
41+
[]tag.Mutator{tag.Upsert(myTag, "label1")},
42+
myMetric.M(int64(i)))
43+
// The code under test ends here.
44+
45+
metrics.ReadAndExport()
46+
metricValue := getCounter(metrics, myMetric.Name(), newMetricKey("label1"))
47+
fmt.Printf("increased by %d\n", metricValue-metricBase)
48+
}
49+
// Output:
50+
// increased by 1
51+
// increased by 3
52+
// increased by 6
53+
}
54+
55+
type derivedMetric struct {
56+
i int64
57+
}
58+
59+
func (m *derivedMetric) ToInt64() int64 {
60+
return m.i
61+
}
62+
63+
func ExampleExporter_metric() {
64+
metricReader := metricexport.NewReader()
65+
metrics := NewExporter(metricReader)
66+
m := derivedMetric{}
67+
r := metric.NewRegistry()
68+
g, _ := r.AddInt64DerivedCumulative("derived", metric.WithLabelKeys(myTag.Name()))
69+
g.UpsertEntry(m.ToInt64, metricdata.NewLabelValue("l1"))
70+
for i := 1; i <= 3; i++ {
71+
// The code under test begins here.
72+
m.i = int64(i)
73+
// The code under test ends here.
74+
75+
metrics.ExportMetrics(context.Background(), r.Read())
76+
metricValue := getCounter(metrics, "derived", newMetricKey("l1"))
77+
fmt.Println(metricValue)
78+
}
79+
// Output:
80+
// 1
81+
// 2
82+
// 3
83+
}
84+
85+
func newMetricKey(v string) map[string]string {
86+
return map[string]string{myTag.Name(): v}
87+
}
88+
89+
func getCounter(metrics *Exporter, metricName string, metricKey map[string]string) int64 {
90+
p, ok := metrics.GetPoint(metricName, metricKey)
91+
if !ok {
92+
// This is expected before the metric is recorded the first time.
93+
return 0
94+
}
95+
return p.Value.(int64)
96+
}

0 commit comments

Comments
 (0)