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
|
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())
}
|