diff --git a/go.mod b/go.mod index c867df5f5..139157cd3 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/metric/test/doc.go b/metric/test/doc.go new file mode 100644 index 000000000..4ebb2b9d8 --- /dev/null +++ b/metric/test/doc.go @@ -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 diff --git a/metric/test/exporter.go b/metric/test/exporter.go new file mode 100644 index 000000000..7b579356f --- /dev/null +++ b/metric/test/exporter.go @@ -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()) +} diff --git a/metric/test/exporter_test.go b/metric/test/exporter_test.go new file mode 100644 index 000000000..70d3ecc67 --- /dev/null +++ b/metric/test/exporter_test.go @@ -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) +}