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

Commit

Permalink
Added test exporter for use in unit tests. (#1185)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
jkohen authored and rghetia committed Dec 4, 2019
1 parent aad2c52 commit 643eada
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
golang.org/x/net v0.0.0-20190620200207-3b0461eec859
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd // indirect
golang.org/x/text v0.3.2 // indirect
google.golang.org/appengine v1.4.0 // indirect
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb // indirect
google.golang.org/grpc v1.20.1
)
Expand Down
17 changes: 17 additions & 0 deletions metric/test/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2019, OpenCensus 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 test for testing code instrumented with the metric and stats packages.
package test
104 changes: 104 additions & 0 deletions metric/test/exporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package test

import (
"context"
"fmt"
"sort"
"strings"
"time"

"go.opencensus.io/metric/metricdata"
"go.opencensus.io/metric/metricexport"
"go.opencensus.io/stats/view"
)

// Exporter keeps exported metric data in memory to aid in testing the instrumentation.
//
// Metrics can be retrieved with `GetPoint()`. In order to deterministically retrieve the most recent values, you must first invoke `ReadAndExport()`.
type Exporter struct {
// points is a map from a label signature to the latest value for the time series represented by the signature.
// Use function `labelSignature` to get a signature from a `metricdata.Metric`.
points map[string]metricdata.Point
metricReader *metricexport.Reader
}

var _ metricexport.Exporter = &Exporter{}

// NewExporter returns a new exporter.
func NewExporter(metricReader *metricexport.Reader) *Exporter {
return &Exporter{points: make(map[string]metricdata.Point), metricReader: metricReader}
}

// ExportMetrics records the view data.
func (e *Exporter) ExportMetrics(ctx context.Context, data []*metricdata.Metric) error {
for _, metric := range data {
for _, ts := range metric.TimeSeries {
signature := labelSignature(metric.Descriptor.Name, labelObjectsToKeyValue(metric.Descriptor.LabelKeys, ts.LabelValues))
e.points[signature] = ts.Points[len(ts.Points)-1]
}
}
return nil
}

// GetPoint returns the latest point for the time series identified by the given labels.
func (e *Exporter) GetPoint(metricName string, labels map[string]string) (metricdata.Point, bool) {
v, ok := e.points[labelSignature(metricName, labelMapToKeyValue(labels))]
return v, ok
}

// ReadAndExport reads the current values for all metrics and makes them available to this exporter.
func (e *Exporter) ReadAndExport() {
// The next line forces the view worker to process all stats.Record* calls that
// happened within Store() before the call to ReadAndExport below. This abuses the
// worker implementation to work around lack of synchronization.
// TODO(jkohen,rghetia): figure out a clean way to make this deterministic.
view.SetReportingPeriod(time.Minute)
e.metricReader.ReadAndExport(e)
}

// String defines the ``native'' format for the exporter.
func (e *Exporter) String() string {
return fmt.Sprintf("points{%v}", e.points)
}

type keyValue struct {
Key string
Value string
}

func sortKeyValue(kv []keyValue) {
sort.Slice(kv, func(i, j int) bool { return kv[i].Key < kv[j].Key })
}

func labelMapToKeyValue(labels map[string]string) []keyValue {
kv := make([]keyValue, 0, len(labels))
for k, v := range labels {
kv = append(kv, keyValue{Key: k, Value: v})
}
sortKeyValue(kv)
return kv
}

func labelObjectsToKeyValue(keys []metricdata.LabelKey, values []metricdata.LabelValue) []keyValue {
if len(keys) != len(values) {
panic("keys and values must have the same length")
}
kv := make([]keyValue, 0, len(values))
for i := range keys {
if values[i].Present {
kv = append(kv, keyValue{Key: keys[i].Key, Value: values[i].Value})
}
}
sortKeyValue(kv)
return kv
}

// labelSignature returns a string that uniquely identifies the list of labels given in the input.
func labelSignature(metricName string, kv []keyValue) string {
var builder strings.Builder
for _, x := range kv {
builder.WriteString(x.Key)
builder.WriteString(x.Value)
}
return fmt.Sprintf("%s{%s}", metricName, builder.String())
}
96 changes: 96 additions & 0 deletions metric/test/exporter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package test

import (
"context"
"fmt"

"go.opencensus.io/metric"
"go.opencensus.io/metric/metricdata"
"go.opencensus.io/metric/metricexport"
"go.opencensus.io/stats"
"go.opencensus.io/stats/view"
"go.opencensus.io/tag"
)

var (
myTag = tag.MustNewKey("my_label")
myMetric = stats.Int64("my_metric", "description", stats.UnitDimensionless)
)

func init() {
if err := view.Register(
&view.View{
Measure: myMetric,
TagKeys: []tag.Key{myTag},
Aggregation: view.Sum(),
},
); err != nil {
panic(err)
}
}

func ExampleExporter_stats() {
metricReader := metricexport.NewReader()
metrics := NewExporter(metricReader)
metrics.ReadAndExport()
metricBase := getCounter(metrics, myMetric.Name(), newMetricKey("label1"))

for i := 1; i <= 3; i++ {
// The code under test begins here.
stats.RecordWithTags(context.Background(),
[]tag.Mutator{tag.Upsert(myTag, "label1")},
myMetric.M(int64(i)))
// The code under test ends here.

metrics.ReadAndExport()
metricValue := getCounter(metrics, myMetric.Name(), newMetricKey("label1"))
fmt.Printf("increased by %d\n", metricValue-metricBase)
}
// Output:
// increased by 1
// increased by 3
// increased by 6
}

type derivedMetric struct {
i int64
}

func (m *derivedMetric) ToInt64() int64 {
return m.i
}

func ExampleExporter_metric() {
metricReader := metricexport.NewReader()
metrics := NewExporter(metricReader)
m := derivedMetric{}
r := metric.NewRegistry()
g, _ := r.AddInt64DerivedCumulative("derived", metric.WithLabelKeys(myTag.Name()))
g.UpsertEntry(m.ToInt64, metricdata.NewLabelValue("l1"))
for i := 1; i <= 3; i++ {
// The code under test begins here.
m.i = int64(i)
// The code under test ends here.

metrics.ExportMetrics(context.Background(), r.Read())
metricValue := getCounter(metrics, "derived", newMetricKey("l1"))
fmt.Println(metricValue)
}
// Output:
// 1
// 2
// 3
}

func newMetricKey(v string) map[string]string {
return map[string]string{myTag.Name(): v}
}

func getCounter(metrics *Exporter, metricName string, metricKey map[string]string) int64 {
p, ok := metrics.GetPoint(metricName, metricKey)
if !ok {
// This is expected before the metric is recorded the first time.
return 0
}
return p.Value.(int64)
}

0 comments on commit 643eada

Please sign in to comment.