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