This repository has been archived by the owner on Jul 31, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 327
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
4 changed files
with
218 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |