Repository: census-instrumentation/opencensus-go
Pull request #242 (2017-12-13 06:55:25)

Add an exporter which sends trace spans to a Zipkin server

bogdandrutu: /cc @adriancole 

Patch:
diff --git a/README.md b/README.md
index ea679d6..0f12fba 100644
--- a/README.md
+++ b/README.md
@@ -204,7 +204,7 @@ Unregister view:
 
 [embedmd]:# (stats.go unregisterView)
 ```go
-if v.Unregister(); err != nil {
+if stats.UnregisterView(v); err != nil {
 	log.Fatal(err)
 }
 ```
diff --git a/exporter/stats/prometheus/prometheus.go b/exporter/stats/prometheus/prometheus.go
index ffab6c2..d1fc2a2 100644
--- a/exporter/stats/prometheus/prometheus.go
+++ b/exporter/stats/prometheus/prometheus.go
@@ -219,11 +219,11 @@ func (c *collector) toMetric(desc *prometheus.Desc, view *stats.View, row *stats
 
 	case stats.MeanAggregation:
 		data := row.Data.(*stats.MeanData)
-		return prometheus.NewConstMetric(desc, prometheus.GaugeValue, data.Mean, tagValues(row.Tags)...)
+		return prometheus.NewConstMetric(desc, prometheus.UntypedValue, data.Mean, tagValues(row.Tags)...)
 
 	case stats.SumAggregation:
 		data := row.Data.(*stats.SumData)
-		return prometheus.NewConstMetric(desc, prometheus.GaugeValue, float64(*data), tagValues(row.Tags)...)
+		return prometheus.NewConstMetric(desc, prometheus.UntypedValue, float64(*data), tagValues(row.Tags)...)
 
 	default:
 		return nil, fmt.Errorf("aggregation %T is not yet supported", view.Aggregation())
diff --git a/exporter/stats/stackdriver/stackdriver.go b/exporter/stats/stackdriver/stackdriver.go
index 0dd4709..f2d2c7e 100644
--- a/exporter/stats/stackdriver/stackdriver.go
+++ b/exporter/stats/stackdriver/stackdriver.go
@@ -24,8 +24,10 @@ import (
 	"fmt"
 	"log"
 	"net/url"
+	"os"
 	"path"
 	"reflect"
+	"strconv"
 	"sync"
 	"time"
 
@@ -48,6 +50,8 @@ import (
 )
 
 const maxTimeSeriesPerUpload = 200
+const opencensusTaskKey = "opencensus_task"
+const opencensusTaskDescription = "Opencensus task identifier"
 
 // Exporter exports stats to the Stackdriver Monitoring.
 type Exporter struct {
@@ -57,7 +61,8 @@ type Exporter struct {
 	createdViewsMu sync.Mutex
 	createdViews   map[string]*metricpb.MetricDescriptor // Views already created remotely
 
-	c *monitoring.MetricClient
+	c         *monitoring.MetricClient
+	taskValue string
 }
 
 // Options contains options for configuring the exporter.
@@ -87,9 +92,17 @@ type Options struct {
 	// can be buffered before batch uploading them to the backend.
 	// Optional.
 	BundleCountThreshold int
+
+	// Resource is an optional field that represents the Stackdriver
+	// MonitoredResource, a resource that can be used for monitoring.
+	// If no custom ResourceDescriptor is set, a default MonitoredResource
+	// with type global and no resource labels will be used.
+	// Optional.
+	Resource *monitoredrespb.MonitoredResource
 }
 
 // NewExporter returns an exporter that uploads stats data to Stackdriver Monitoring.
+// Only one Stackdriver exporter should be created per process.
 func NewExporter(o Options) (*Exporter, error) {
 	opts := append(o.ClientOptions, option.WithUserAgent(internal.UserAgent))
 	client, err := monitoring.NewMetricClient(context.Background(), opts...)
@@ -100,12 +113,11 @@ func NewExporter(o Options) (*Exporter, error) {
 		c:            client,
 		o:            o,
 		createdViews: make(map[string]*metricpb.MetricDescriptor),
+		taskValue:    getTaskValue(),
 	}
 	e.bundler = bundler.NewBundler((*stats.ViewData)(nil), func(bundle interface{}) {
 		vds := bundle.([]*stats.ViewData)
-		if err := e.upload(vds); err != nil {
-			e.onError(err)
-		}
+		e.handleUpload(vds...)
 	})
 	e.bundler.DelayThreshold = e.o.BundleDelayThreshold
 	e.bundler.BundleCountThreshold = e.o.BundleCountThreshold
@@ -118,7 +130,35 @@ func (e *Exporter) Export(vd *stats.ViewData) {
 	if len(vd.Rows) == 0 {
 		return
 	}
-	e.bundler.Add(vd, 1)
+	err := e.bundler.Add(vd, 1)
+	switch err {
+	case nil:
+		return
+	case bundler.ErrOversizedItem:
+		go e.handleUpload(vd)
+	case bundler.ErrOverflow:
+		e.onError(errors.New("failed to upload: buffer full"))
+	default:
+		e.onError(err)
+	}
+}
+
+// getTaskValue returns a task label value in the format of
+// "go-<pid>@<hostname>".
+func getTaskValue() string {
+	hostname, err := os.Hostname()
+	if err != nil {
+		hostname = "localhost"
+	}
+	return "go-" + strconv.Itoa(os.Getpid()) + "@" + hostname
+}
+
+// handleUpload handles uploading a slice
+// of ViewData, as well as error handling.
+func (e *Exporter) handleUpload(vds ...*stats.ViewData) {
+	if err := e.upload(vds); err != nil {
+		e.onError(err)
+	}
 }
 
 // Flush waits for exported view data to be uploaded.
@@ -163,6 +203,13 @@ func (e *Exporter) makeReq(vds []*stats.ViewData, limit int) []*monitoringpb.Cre
 	var reqs []*monitoringpb.CreateTimeSeriesRequest
 	var timeSeries []*monitoringpb.TimeSeries
 
+	resource := e.o.Resource
+	if resource == nil {
+		resource = &monitoredrespb.MonitoredResource{
+			Type: "global",
+		}
+	}
+
 	for _, vd := range vds {
 		if _, ok := vd.View.Window().(stats.Cumulative); !ok {
 			// TODO(jbd): Only Cumulative window will be exported to Stackdriver in this version.
@@ -173,12 +220,10 @@ func (e *Exporter) makeReq(vds []*stats.ViewData, limit int) []*monitoringpb.Cre
 			ts := &monitoringpb.TimeSeries{
 				Metric: &metricpb.Metric{
 					Type:   namespacedViewName(vd.View.Name(), false),
-					Labels: newLabels(row.Tags),
+					Labels: newLabels(row.Tags, e.taskValue),
 				},
-				Resource: &monitoredrespb.MonitoredResource{
-					Type: "global",
-				},
-				Points: []*monitoringpb.Point{newPoint(vd.View, row, vd.Start, vd.End)},
+				Resource: resource,
+				Points:   []*monitoringpb.Point{newPoint(vd.View, row, vd.Start, vd.End)},
 			}
 			timeSeries = append(timeSeries, ts)
 			if len(timeSeries) == limit {
@@ -356,22 +401,29 @@ func namespacedViewName(v string, escaped bool) string {
 	return path.Join("custom.googleapis.com", p)
 }
 
-func newLabels(tags []tag.Tag) map[string]string {
+func newLabels(tags []tag.Tag, taskValue string) map[string]string {
 	labels := make(map[string]string)
 	for _, tag := range tags {
 		labels[internal.Sanitize(tag.Key.Name())] = tag.Value
 	}
+	labels[opencensusTaskKey] = taskValue
 	return labels
 }
 
 func newLabelDescriptors(keys []tag.Key) []*labelpb.LabelDescriptor {
-	labelDescriptors := make([]*labelpb.LabelDescriptor, len(keys))
+	labelDescriptors := make([]*labelpb.LabelDescriptor, len(keys)+1)
 	for i, key := range keys {
 		labelDescriptors[i] = &labelpb.LabelDescriptor{
 			Key:       internal.Sanitize(key.Name()),
 			ValueType: labelpb.LabelDescriptor_STRING, // We only use string tags
 		}
 	}
+	// Add a specific open census task id label.
+	labelDescriptors[len(keys)] = &labelpb.LabelDescriptor{
+		Key:         opencensusTaskKey,
+		ValueType:   labelpb.LabelDescriptor_STRING,
+		Description: opencensusTaskDescription,
+	}
 	return labelDescriptors
 }
 
@@ -411,14 +463,15 @@ func equalAggWindowTagKeys(md *metricpb.MetricDescriptor, agg stats.Aggregation,
 		return fmt.Errorf("stackdriver metric descriptor was not created with window type %T", w)
 	}
 
-	if len(md.Labels) != len(keys) {
+	if len(md.Labels) != len(keys)+1 {
 		return errors.New("stackdriver metric descriptor was not created with the view labels")
 	}
 
-	labels := make(map[string]struct{}, len(keys))
+	labels := make(map[string]struct{}, len(keys)+1)
 	for _, k := range keys {
 		labels[internal.Sanitize(k.Name())] = struct{}{}
 	}
+	labels[opencensusTaskKey] = struct{}{}
 
 	for _, k := range md.Labels {
 		if _, ok := labels[k.Key]; !ok {
diff --git a/exporter/trace/zipkin/zipkin.go b/exporter/trace/zipkin/zipkin.go
new file mode 100644
index 0000000..07d9543
--- /dev/null
+++ b/exporter/trace/zipkin/zipkin.go
@@ -0,0 +1,222 @@
+// Copyright 2017, 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 zipkin contains an exporter for Zipkin.
+//
+// Example:
+//
+// 	import (
+// 		openzipkin "github.com/openzipkin/zipkin-go"
+// 		"github.com/openzipkin/zipkin-go/reporter/http"
+// 		"go.opencensus.io/trace/adaptor/zipkin"
+// 	)
+//	...
+//		localEndpoint, err := openzipkin.NewEndpoint("server", "server:5454")
+// 		if err != nil {
+// 			log.Print(err)
+// 		}
+// 		reporter := http.NewReporter("http://localhost:9411/api/v2/spans")
+// 		exporter := zipkin.NewExporter(reporter, localEndpoint)
+// 		trace.RegisterExporter(exporter)
+package zipkin
+
+import (
+	"encoding/binary"
+	"strconv"
+	"strings"
+
+	"github.com/openzipkin/zipkin-go/model"
+	"github.com/openzipkin/zipkin-go/reporter"
+	"go.opencensus.io/trace"
+)
+
+// Exporter is an implementation of trace.Exporter that uploads spans to a
+// Zipkin server.
+type Exporter struct {
+	reporter      reporter.Reporter
+	localEndpoint *model.Endpoint
+}
+
+// NewExporter returns an implementation of trace.Exporter that uploads spans
+// to a Zipkin server.
+//
+// reporter is a Zipkin Reporter which will be used to send the spans.  These
+// can be created with the openzipkin library, using one of the packages under
+// github.com/openzipkin/zipkin-go/reporter.
+//
+// localEndpoint sets the local endpoint of exported spans.  It can be
+// constructed with github.com/openzipkin/zipkin-go.NewEndpoint, e.g.:
+// 	localEndpoint, err := NewEndpoint("my server", listener.Addr().String())
+// localEndpoint can be nil.
+func NewExporter(reporter reporter.Reporter, localEndpoint *model.Endpoint) *Exporter {
+	return &Exporter{
+		reporter:      reporter,
+		localEndpoint: localEndpoint,
+	}
+}
+
+// Export exports a span to a Zipkin server.
+func (e *Exporter) Export(s *trace.SpanData) {
+	e.reporter.Send(zipkinSpan(s, e.localEndpoint))
+}
+
+const (
+	statusCodeTagKey        = "error"
+	statusDescriptionTagKey = "opencensus.status_description"
+)
+
+var (
+	sampledTrue    = true
+	canonicalCodes = [...]string{
+		"OK",
+		"CANCELLED",
+		"UNKNOWN",
+		"INVALID_ARGUMENT",
+		"DEADLINE_EXCEEDED",
+		"NOT_FOUND",
+		"ALREADY_EXISTS",
+		"PERMISSION_DENIED",
+		"RESOURCE_EXHAUSTED",
+		"FAILED_PRECONDITION",
+		"ABORTED",
+		"OUT_OF_RANGE",
+		"UNIMPLEMENTED",
+		"INTERNAL",
+		"UNAVAILABLE",
+		"DATA_LOSS",
+		"UNAUTHENTICATED",
+	}
+)
+
+func canonicalCodeString(code int32) string {
+	if code < 0 || int(code) >= len(canonicalCodes) {
+		return "error code " + strconv.FormatInt(int64(code), 10)
+	}
+	return canonicalCodes[code]
+}
+
+func convertTraceID(t trace.TraceID) model.TraceID {
+	return model.TraceID{
+		High: binary.BigEndian.Uint64(t[:8]),
+		Low:  binary.BigEndian.Uint64(t[8:]),
+	}
+}
+
+func convertSpanID(s trace.SpanID) model.ID {
+	return model.ID(binary.BigEndian.Uint64(s[:]))
+}
+
+func spanKind(s *trace.SpanData) model.Kind {
+	if s.HasRemoteParent {
+		return model.Server
+	}
+	if strings.HasPrefix(s.Name, "Sent.") {
+		return model.Client
+	}
+	if strings.HasPrefix(s.Name, "Recv.") {
+		return model.Server
+	}
+	if len(s.MessageEvents) > 0 {
+		switch s.MessageEvents[0].EventType {
+		case trace.MessageEventTypeSent:
+			return model.Client
+		case trace.MessageEventTypeRecv:
+			return model.Server
+		}
+	}
+	return model.Undetermined
+}
+
+func zipkinSpan(s *trace.SpanData, localEndpoint *model.Endpoint) model.SpanModel {
+	sc := s.SpanContext
+	z := model.SpanModel{
+		SpanContext: model.SpanContext{
+			TraceID: convertTraceID(sc.TraceID),
+			ID:      convertSpanID(sc.SpanID),
+			Sampled: &sampledTrue,
+		},
+		Kind:          spanKind(s),
+		Name:          s.Name,
+		Timestamp:     s.StartTime,
+		Shared:        false,
+		LocalEndpoint: localEndpoint,
+	}
+
+	if s.ParentSpanID != (trace.SpanID{}) {
+		id := convertSpanID(s.ParentSpanID)
+		z.ParentID = &id
+	}
+
+	if s, e := s.StartTime, s.EndTime; !s.IsZero() && !e.IsZero() {
+		z.Duration = e.Sub(s)
+	}
+
+	// construct Tags from s.Attributes and s.Status.
+	if len(s.Attributes) != 0 {
+		m := make(map[string]string, len(s.Attributes)+2)
+		for key, value := range s.Attributes {
+			switch v := value.(type) {
+			case string:
+				m[key] = v
+			case bool:
+				if v {
+					m[key] = "true"
+				} else {
+					m[key] = "false"
+				}
+			case int64:
+				m[key] = strconv.FormatInt(v, 10)
+			}
+		}
+		z.Tags = m
+	}
+	if s.Status.Code != 0 || s.Status.Message != "" {
+		if z.Tags == nil {
+			z.Tags = make(map[string]string, 2)
+		}
+		if s.Status.Code != 0 {
+			z.Tags[statusCodeTagKey] = canonicalCodeString(s.Status.Code)
+		}
+		if s.Status.Message != "" {
+			z.Tags[statusDescriptionTagKey] = s.Status.Message
+		}
+	}
+
+	// construct Annotations from s.Annotations and s.MessageEvents.
+	if len(s.Annotations) != 0 || len(s.MessageEvents) != 0 {
+		z.Annotations = make([]model.Annotation, 0, len(s.Annotations)+len(s.MessageEvents))
+		for _, a := range s.Annotations {
+			z.Annotations = append(z.Annotations, model.Annotation{
+				Timestamp: a.Time,
+				Value:     a.Message,
+			})
+		}
+		for _, m := range s.MessageEvents {
+			a := model.Annotation{
+				Timestamp: m.Time,
+			}
+			switch m.EventType {
+			case trace.MessageEventTypeSent:
+				a.Value = "SENT"
+			case trace.MessageEventTypeRecv:
+				a.Value = "RECV"
+			default:
+				a.Value = "<?>"
+			}
+			z.Annotations = append(z.Annotations, a)
+		}
+	}
+
+	return z
+}
diff --git a/internal/readme/stats.go b/internal/readme/stats.go
index 631716f..a3d7e9a 100644
--- a/internal/readme/stats.go
+++ b/internal/readme/stats.go
@@ -99,7 +99,7 @@ func statsExamples() {
 	_ = v
 
 	// START unregisterView
-	if v.Unregister(); err != nil {
+	if stats.UnregisterView(v); err != nil {
 		log.Fatal(err)
 	}
 	// END unregisterView
diff --git a/plugin/grpc/grpcstats/client_handler.go b/plugin/grpc/grpcstats/client_handler.go
index 09b009f..9e11b8b 100644
--- a/plugin/grpc/grpcstats/client_handler.go
+++ b/plugin/grpc/grpcstats/client_handler.go
@@ -16,7 +16,6 @@
 package grpcstats
 
 import (
-	"strings"
 	"sync/atomic"
 	"time"
 
@@ -25,6 +24,7 @@ import (
 	"golang.org/x/net/context"
 	"google.golang.org/grpc/grpclog"
 	"google.golang.org/grpc/stats"
+	"google.golang.org/grpc/status"
 )
 
 // ClientStatsHandler is a stats.Handler implementation
@@ -66,27 +66,13 @@ func (h *ClientStatsHandler) TagRPC(ctx context.Context, info *stats.RPCTagInfo)
 		}
 		return ctx
 	}
-	names := strings.Split(info.FullMethodName, "/")
-	if len(names) != 3 {
-		if grpclog.V(2) {
-			grpclog.Infof("clientHandler.TagRPC called with info.FullMethodName bad format. got %v, want '/$service/$method/'", info.FullMethodName)
-		}
-		return ctx
-	}
-	serviceName := names[1]
-	methodName := names[2]
-
-	d := &rpcData{
-		startTime: startTime,
-	}
 
+	d := &rpcData{startTime: startTime}
 	ts := tag.FromContext(ctx)
 	encoded := tag.Encode(ts)
 	ctx = stats.SetTags(ctx, encoded)
-
 	tagMap, err := tag.NewMap(ctx,
-		tag.Upsert(keyService, serviceName),
-		tag.Upsert(keyMethod, methodName),
+		tag.Upsert(keyMethod, methodName(info.FullMethodName)),
 	)
 	if err == nil {
 		ctx = tag.NewContext(ctx, tagMap)
@@ -160,12 +146,14 @@ func (h *ClientStatsHandler) handleRPCEnd(ctx context.Context, s *stats.End) {
 	m = append(m, RPCClientRoundTripLatency.M(float64(elapsedTime)/float64(time.Millisecond)))
 
 	if s.Error != nil {
-		errorCode := s.Error.Error()
-		newTagMap, err := tag.NewMap(ctx,
-			tag.Upsert(keyOpStatus, errorCode),
-		)
-		if err == nil {
-			ctx = tag.NewContext(ctx, newTagMap)
+		s, ok := status.FromError(s.Error)
+		if ok {
+			newTagMap, err := tag.NewMap(ctx,
+				tag.Upsert(keyStatus, s.Code().String()),
+			)
+			if err == nil {
+				ctx = tag.NewContext(ctx, newTagMap)
+			}
 		}
 		m = append(m, RPCClientErrorCount.M(1))
 	}
diff --git a/plugin/grpc/grpcstats/client_metrics.go b/plugin/grpc/grpcstats/client_metrics.go
index dae9369..879bfaf 100644
--- a/plugin/grpc/grpcstats/client_metrics.go
+++ b/plugin/grpc/grpcstats/client_metrics.go
@@ -70,82 +70,221 @@ func defaultClientMeasures() {
 	var err error
 
 	// Creating client measures
-	if RPCClientErrorCount, err = stats.NewMeasureInt64("/grpc.io/client/error_count", "RPC Errors", unitCount); err != nil {
-		log.Fatalf("Cannot create measure /grpc.io/client/error_count: %v", err)
+	if RPCClientErrorCount, err = stats.NewMeasureInt64("grpc.io/client/error_count", "RPC Errors", unitCount); err != nil {
+		log.Fatalf("Cannot create measure grpc.io/client/error_count: %v", err)
 	}
-	if RPCClientRoundTripLatency, err = stats.NewMeasureFloat64("/grpc.io/client/roundtrip_latency", "RPC roundtrip latency in msecs", unitMillisecond); err != nil {
-		log.Fatalf("Cannot create measure /grpc.io/client/roundtrip_latency: %v", err)
+	if RPCClientRoundTripLatency, err = stats.NewMeasureFloat64("grpc.io/client/roundtrip_latency", "RPC roundtrip latency in msecs", unitMillisecond); err != nil {
+		log.Fatalf("Cannot create measure grpc.io/client/roundtrip_latency: %v", err)
 	}
-	if RPCClientRequestBytes, err = stats.NewMeasureInt64("/grpc.io/client/request_bytes", "Request bytes", unitByte); err != nil {
-		log.Fatalf("Cannot create measure /grpc.io/client/request_bytes: %v", err)
+	if RPCClientRequestBytes, err = stats.NewMeasureInt64("grpc.io/client/request_bytes", "Request bytes", unitByte); err != nil {
+		log.Fatalf("Cannot create measure grpc.io/client/request_bytes: %v", err)
 	}
-	if RPCClientResponseBytes, err = stats.NewMeasureInt64("/grpc.io/client/response_bytes", "Response bytes", unitByte); err != nil {
-		log.Fatalf("Cannot create measure /grpc.io/client/response_bytes: %v", err)
+	if RPCClientResponseBytes, err = stats.NewMeasureInt64("grpc.io/client/response_bytes", "Response bytes", unitByte); err != nil {
+		log.Fatalf("Cannot create measure grpc.io/client/response_bytes: %v", err)
 	}
-	if RPCClientStartedCount, err = stats.NewMeasureInt64("/grpc.io/client/started_count", "Number of client RPCs (streams) started", unitCount); err != nil {
-		log.Fatalf("Cannot create measure /grpc.io/client/started_count: %v", err)
+	if RPCClientStartedCount, err = stats.NewMeasureInt64("grpc.io/client/started_count", "Number of client RPCs (streams) started", unitCount); err != nil {
+		log.Fatalf("Cannot create measure grpc.io/client/started_count: %v", err)
 	}
-	if RPCClientFinishedCount, err = stats.NewMeasureInt64("/grpc.io/client/finished_count", "Number of client RPCs (streams) finished", unitCount); err != nil {
-		log.Fatalf("Cannot create measure /grpc.io/client/finished_count: %v", err)
+	if RPCClientFinishedCount, err = stats.NewMeasureInt64("grpc.io/client/finished_count", "Number of client RPCs (streams) finished", unitCount); err != nil {
+		log.Fatalf("Cannot create measure grpc.io/client/finished_count: %v", err)
 	}
-	if RPCClientRequestCount, err = stats.NewMeasureInt64("/grpc.io/client/request_count", "Number of client RPC request messages", unitCount); err != nil {
-		log.Fatalf("Cannot create measure /grpc.io/client/request_count: %v", err)
+	if RPCClientRequestCount, err = stats.NewMeasureInt64("grpc.io/client/request_count", "Number of client RPC request messages", unitCount); err != nil {
+		log.Fatalf("Cannot create measure grpc.io/client/request_count: %v", err)
 	}
-	if RPCClientResponseCount, err = stats.NewMeasureInt64("/grpc.io/client/response_count", "Number of client RPC response messages", unitCount); err != nil {
-		log.Fatalf("Cannot create measure /grpc.io/client/response_count: %v", err)
+	if RPCClientResponseCount, err = stats.NewMeasureInt64("grpc.io/client/response_count", "Number of client RPC response messages", unitCount); err != nil {
+		log.Fatalf("Cannot create measure grpc.io/client/response_count: %v", err)
 	}
 }
 
 func defaultClientViews() {
-	// Use the Java implementation as a reference at
-	// https://github.com/census-instrumentation/opencensus-java/blob/2b464864e3dd3f80e8e4c9dc72fccc225444a939/contrib/grpc_metrics/src/main/java/io/opencensus/contrib/grpc/metrics/RpcViewConstants.java#L113-L658
-	RPCClientErrorCountView, _ = stats.NewView("grpc.io/client/error_count/cumulative", "RPC Errors", []tag.Key{keyOpStatus, keyService, keyMethod}, RPCClientErrorCount, aggMean, windowCumulative)
-	clientViews = append(clientViews, RPCClientErrorCountView)
-	RPCClientRoundTripLatencyView, _ = stats.NewView("grpc.io/client/roundtrip_latency/cumulative", "Latency in msecs", []tag.Key{keyService, keyMethod}, RPCClientRoundTripLatency, aggDistMillis, windowCumulative)
-	clientViews = append(clientViews, RPCClientRoundTripLatencyView)
-	RPCClientRequestBytesView, _ = stats.NewView("grpc.io/client/request_bytes/cumulative", "Request bytes", []tag.Key{keyService, keyMethod}, RPCClientRequestBytes, aggDistBytes, windowCumulative)
-	clientViews = append(clientViews, RPCClientRequestBytesView)
-	RPCClientResponseBytesView, _ = stats.NewView("grpc.io/client/response_bytes/cumulative", "Response bytes", []tag.Key{keyService, keyMethod}, RPCClientResponseBytes, aggDistBytes, windowCumulative)
-	clientViews = append(clientViews, RPCClientResponseBytesView)
-	RPCClientRequestCountView, _ = stats.NewView("grpc.io/client/request_count/cumulative", "Count of request messages per client RPC", []tag.Key{keyService, keyMethod}, RPCClientRequestCount, aggDistCounts, windowCumulative)
-	clientViews = append(clientViews, RPCClientRequestCountView)
-	RPCClientResponseCountView, _ = stats.NewView("grpc.io/client/response_count/cumulative", "Count of response messages per client RPC", []tag.Key{keyService, keyMethod}, RPCClientResponseCount, aggDistCounts, windowCumulative)
-	clientViews = append(clientViews, RPCClientResponseCountView)
-
-	RPCClientRoundTripLatencyMinuteView, _ = stats.NewView("grpc.io/client/roundtrip_latency/minute", "Minute stats for latency in msecs", []tag.Key{keyService, keyMethod}, RPCClientRoundTripLatency, aggMean, windowSlidingMinute)
-	clientViews = append(clientViews, RPCClientRoundTripLatencyMinuteView)
-	RPCClientRequestBytesMinuteView, _ = stats.NewView("grpc.io/client/request_bytes/minute", "Minute stats for request size in bytes", []tag.Key{keyService, keyMethod}, RPCClientRequestBytes, aggMean, windowSlidingMinute)
-	clientViews = append(clientViews, RPCClientRequestBytesMinuteView)
-	RPCClientResponseBytesMinuteView, _ = stats.NewView("grpc.io/client/response_bytes/minute", "Minute stats for response size in bytes", []tag.Key{keyService, keyMethod}, RPCClientResponseBytes, aggMean, windowSlidingMinute)
-	clientViews = append(clientViews, RPCClientResponseBytesMinuteView)
-	RPCClientErrorCountMinuteView, _ = stats.NewView("grpc.io/client/error_count/minute", "Minute stats for rpc errors", []tag.Key{keyService, keyMethod}, RPCClientErrorCount, aggMean, windowSlidingMinute)
-	clientViews = append(clientViews, RPCClientErrorCountMinuteView)
-	RPCClientStartedCountMinuteView, _ = stats.NewView("grpc.io/client/started_count/minute", "Minute stats on the number of client RPCs started", []tag.Key{keyService, keyMethod}, RPCClientStartedCount, aggMean, windowSlidingMinute)
-	clientViews = append(clientViews, RPCClientStartedCountMinuteView)
-	RPCClientFinishedCountMinuteView, _ = stats.NewView("grpc.io/client/finished_count/minute", "Minute stats on the number of client RPCs finished", []tag.Key{keyService, keyMethod}, RPCClientFinishedCount, aggMean, windowSlidingMinute)
-	clientViews = append(clientViews, RPCClientFinishedCountMinuteView)
-	RPCClientRequestCountMinuteView, _ = stats.NewView("grpc.io/client/request_count/minute", "Minute stats on the count of request messages per client RPC", []tag.Key{keyService, keyMethod}, RPCClientRequestCount, aggMean, windowSlidingMinute)
-	clientViews = append(clientViews, RPCClientRequestCountMinuteView)
-	RPCClientResponseCountMinuteView, _ = stats.NewView("grpc.io/client/response_count/minute", "Minute stats on the count of response messages per client RPC", []tag.Key{keyService, keyMethod}, RPCClientResponseCount, aggMean, windowSlidingMinute)
-	clientViews = append(clientViews, RPCClientResponseCountMinuteView)
-
-	RPCClientRoundTripLatencyHourView, _ = stats.NewView("grpc.io/client/roundtrip_latency/hour", "Hour stats for latency in msecs", []tag.Key{keyService, keyMethod}, RPCClientRoundTripLatency, aggMean, windowSlidingHour)
-	clientViews = append(clientViews, RPCClientRoundTripLatencyHourView)
-	RPCClientRequestBytesHourView, _ = stats.NewView("grpc.io/client/request_bytes/hour", "Hour stats for request size in bytes", []tag.Key{keyService, keyMethod}, RPCClientRequestBytes, aggMean, windowSlidingHour)
-	clientViews = append(clientViews, RPCClientRequestBytesHourView)
-	RPCClientResponseBytesHourView, _ = stats.NewView("grpc.io/client/response_bytes/hour", "Hour stats for response size in bytes", []tag.Key{keyService, keyMethod}, RPCClientResponseBytes, aggMean, windowSlidingHour)
-	clientViews = append(clientViews, RPCClientResponseBytesHourView)
-	RPCClientErrorCountHourView, _ = stats.NewView("grpc.io/client/error_count/hour", "Hour stats for rpc errors", []tag.Key{keyService, keyMethod}, RPCClientErrorCount, aggMean, windowSlidingHour)
-	clientViews = append(clientViews, RPCClientErrorCountHourView)
-	RPCClientStartedCountHourView, _ = stats.NewView("grpc.io/client/started_count/hour", "Hour stats on the number of client RPCs started", []tag.Key{keyService, keyMethod}, RPCClientStartedCount, aggMean, windowSlidingHour)
-	clientViews = append(clientViews, RPCClientStartedCountHourView)
-	RPCClientFinishedCountHourView, _ = stats.NewView("grpc.io/client/finished_count/hour", "Hour stats on the number of client RPCs finished", []tag.Key{keyService, keyMethod}, RPCClientFinishedCount, aggMean, windowSlidingHour)
-	clientViews = append(clientViews, RPCClientFinishedCountHourView)
-	RPCClientRequestCountHourView, _ = stats.NewView("grpc.io/client/request_count/hour", "Hour stats on the count of request messages per client RPC", []tag.Key{keyService, keyMethod}, RPCClientRequestCount, aggMean, windowSlidingHour)
-	clientViews = append(clientViews, RPCClientRequestCountHourView)
-	RPCClientResponseCountHourView, _ = stats.NewView("grpc.io/client/response_count/hour", "Hour stats on the count of response messages per client RPC", []tag.Key{keyService, keyMethod}, RPCClientResponseCount, aggMean, windowSlidingHour)
-	clientViews = append(clientViews, RPCClientResponseCountHourView)
-	// TODO(jbd): Register it when constructing the stats handler.
+	RPCClientErrorCountView, _ = stats.NewView(
+		"grpc.io/client/error_count/cumulative",
+		"RPC Errors",
+		[]tag.Key{keyStatus, keyMethod},
+		RPCClientErrorCount,
+		aggMean,
+		windowCumulative)
+	RPCClientRoundTripLatencyView, _ = stats.NewView(
+		"grpc.io/client/roundtrip_latency/cumulative",
+		"Latency in msecs",
+		[]tag.Key{keyMethod},
+		RPCClientRoundTripLatency,
+		aggDistMillis,
+		windowCumulative)
+	RPCClientRequestBytesView, _ = stats.NewView(
+		"grpc.io/client/request_bytes/cumulative",
+		"Request bytes",
+		[]tag.Key{keyMethod},
+		RPCClientRequestBytes,
+		aggDistBytes,
+		windowCumulative)
+	RPCClientResponseBytesView, _ = stats.NewView(
+		"grpc.io/client/response_bytes/cumulative",
+		"Response bytes",
+		[]tag.Key{keyMethod},
+		RPCClientResponseBytes,
+		aggDistBytes,
+		windowCumulative)
+	RPCClientRequestCountView, _ = stats.NewView(
+		"grpc.io/client/request_count/cumulative",
+		"Count of request messages per client RPC",
+		[]tag.Key{keyMethod},
+		RPCClientRequestCount,
+		aggDistCounts,
+		windowCumulative)
+	RPCClientResponseCountView, _ = stats.NewView(
+		"grpc.io/client/response_count/cumulative",
+		"Count of response messages per client RPC",
+		[]tag.Key{keyMethod},
+		RPCClientResponseCount,
+		aggDistCounts,
+		windowCumulative)
+
+	clientViews = append(clientViews,
+		RPCClientErrorCountView,
+		RPCClientRoundTripLatencyView,
+		RPCClientRequestBytesView,
+		RPCClientResponseBytesView,
+		RPCClientRequestCountView,
+		RPCClientResponseCountView,
+	)
+	// TODO(jbd): Add roundtrip_latency, uncompressed_request_bytes, uncompressed_response_bytes, request_count, response_count.
+
+	RPCClientRoundTripLatencyMinuteView, _ = stats.NewView(
+		"grpc.io/client/roundtrip_latency/minute",
+		"Minute stats for latency in msecs",
+		[]tag.Key{keyMethod},
+		RPCClientRoundTripLatency,
+		aggMean,
+		windowSlidingMinute)
+	RPCClientRequestBytesMinuteView, _ = stats.NewView(
+		"grpc.io/client/request_bytes/minute",
+		"Minute stats for request size in bytes",
+		[]tag.Key{keyMethod},
+		RPCClientRequestBytes,
+		aggMean,
+		windowSlidingMinute)
+	RPCClientResponseBytesMinuteView, _ = stats.NewView(
+		"grpc.io/client/response_bytes/minute",
+		"Minute stats for response size in bytes",
+		[]tag.Key{keyMethod},
+		RPCClientResponseBytes,
+		aggMean,
+		windowSlidingMinute)
+	RPCClientErrorCountMinuteView, _ = stats.NewView(
+		"grpc.io/client/error_count/minute",
+		"Minute stats for rpc errors",
+		[]tag.Key{keyMethod},
+		RPCClientErrorCount,
+		aggMean,
+		windowSlidingMinute)
+	RPCClientStartedCountMinuteView, _ = stats.NewView(
+		"grpc.io/client/started_count/minute",
+		"Minute stats on the number of client RPCs started",
+		[]tag.Key{keyMethod},
+		RPCClientStartedCount,
+		aggMean,
+		windowSlidingMinute)
+	RPCClientFinishedCountMinuteView, _ = stats.NewView(
+		"grpc.io/client/finished_count/minute",
+		"Minute stats on the number of client RPCs finished",
+		[]tag.Key{keyMethod},
+		RPCClientFinishedCount,
+		aggMean,
+		windowSlidingMinute)
+	RPCClientRequestCountMinuteView, _ = stats.NewView(
+		"grpc.io/client/request_count/minute",
+		"Minute stats on the count of request messages per client RPC",
+		[]tag.Key{keyMethod},
+		RPCClientRequestCount,
+		aggMean,
+		windowSlidingMinute)
+	RPCClientResponseCountMinuteView, _ = stats.NewView(
+		"grpc.io/client/response_count/minute",
+		"Minute stats on the count of response messages per client RPC",
+		[]tag.Key{keyMethod},
+		RPCClientResponseCount,
+		aggMean,
+		windowSlidingMinute)
+
+	clientViews = append(clientViews,
+		RPCClientRoundTripLatencyMinuteView,
+		RPCClientRequestBytesMinuteView,
+		RPCClientResponseBytesMinuteView,
+		RPCClientErrorCountMinuteView,
+		RPCClientStartedCountMinuteView,
+		RPCClientFinishedCountMinuteView,
+		RPCClientRequestCountMinuteView,
+		RPCClientResponseCountMinuteView,
+	)
+
+	RPCClientRoundTripLatencyHourView, _ = stats.NewView(
+		"grpc.io/client/roundtrip_latency/hour",
+		"Hour stats for latency in msecs",
+		[]tag.Key{keyMethod},
+		RPCClientRoundTripLatency,
+		aggMean,
+		windowSlidingHour)
+	RPCClientRequestBytesHourView, _ = stats.NewView(
+		"grpc.io/client/request_bytes/hour",
+		"Hour stats for request size in bytes",
+		[]tag.Key{keyMethod},
+		RPCClientRequestBytes,
+		aggMean,
+		windowSlidingHour)
+	RPCClientResponseBytesHourView, _ = stats.NewView(
+		"grpc.io/client/response_bytes/hour",
+		"Hour stats for response size in bytes",
+		[]tag.Key{keyMethod},
+		RPCClientResponseBytes,
+		aggMean,
+		windowSlidingHour)
+	RPCClientErrorCountHourView, _ = stats.NewView(
+		"grpc.io/client/error_count/hour",
+		"Hour stats for rpc errors",
+		[]tag.Key{keyMethod},
+		RPCClientErrorCount,
+		aggMean,
+		windowSlidingHour)
+	RPCClientStartedCountHourView, _ = stats.NewView(
+		"grpc.io/client/started_count/hour",
+		"Hour stats on the number of client RPCs started",
+		[]tag.Key{keyMethod},
+		RPCClientStartedCount,
+		aggMean,
+		windowSlidingHour)
+	RPCClientFinishedCountHourView, _ = stats.NewView(
+		"grpc.io/client/finished_count/hour",
+		"Hour stats on the number of client RPCs finished",
+		[]tag.Key{keyMethod},
+		RPCClientFinishedCount,
+		aggMean,
+		windowSlidingHour)
+	RPCClientRequestCountHourView, _ = stats.NewView(
+		"grpc.io/client/request_count/hour",
+		"Hour stats on the count of request messages per client RPC",
+		[]tag.Key{keyMethod},
+		RPCClientRequestCount,
+		aggMean,
+		windowSlidingHour)
+	RPCClientResponseCountHourView, _ = stats.NewView(
+		"grpc.io/client/response_count/hour",
+		"Hour stats on the count of response messages per client RPC",
+		[]tag.Key{keyMethod},
+		RPCClientResponseCount,
+		aggMean,
+		windowSlidingHour)
+
+	clientViews = append(clientViews,
+		RPCClientRoundTripLatencyHourView,
+		RPCClientRequestBytesHourView,
+		RPCClientResponseBytesHourView,
+		RPCClientErrorCountHourView,
+		RPCClientStartedCountHourView,
+		RPCClientFinishedCountHourView,
+		RPCClientRequestCountHourView,
+		RPCClientResponseCountHourView,
+	)
 }
 
 // initClient registers the default metrics (measures and views)
diff --git a/plugin/grpc/grpcstats/grpcstats.go b/plugin/grpc/grpcstats/grpcstats.go
index e9224fe..9fa7258 100644
--- a/plugin/grpc/grpcstats/grpcstats.go
+++ b/plugin/grpc/grpcstats/grpcstats.go
@@ -18,6 +18,7 @@ package grpcstats // import "go.opencensus.io/plugin/grpc/grpcstats"
 
 import (
 	"log"
+	"strings"
 	"time"
 
 	istats "go.opencensus.io/stats"
@@ -65,21 +66,17 @@ var (
 	windowSlidingHour   = istats.Interval{Duration: 1 * time.Hour, Intervals: 6}
 	windowSlidingMinute = istats.Interval{Duration: 1 * time.Minute, Intervals: 6}
 
-	keyService  tag.Key
-	keyMethod   tag.Key
-	keyOpStatus tag.Key
+	keyMethod tag.Key
+	keyStatus tag.Key
 )
 
 func init() {
 	var err error
-	if keyService, err = tag.NewKey("grpc.service"); err != nil {
-		log.Fatalf("Cannot create grpc.service key: %v", err)
+	if keyMethod, err = tag.NewKey("method"); err != nil {
+		log.Fatalf("Cannot create method key: %v", err)
 	}
-	if keyMethod, err = tag.NewKey("grpc.method"); err != nil {
-		log.Fatalf("Cannot create grpc.method key: %v", err)
-	}
-	if keyOpStatus, err = tag.NewKey("grpc.opstatus"); err != nil {
-		log.Fatalf("Cannot create grpc.opstatus key: %v", err)
+	if keyStatus, err = tag.NewKey("canonical_status"); err != nil {
+		log.Fatalf("Cannot create canonical_status key: %v", err)
 	}
 	initServer()
 	initClient()
@@ -90,3 +87,7 @@ var (
 	grpcServerRPCKey  = grpcInstrumentationKey("server-rpc")
 	grpcClientRPCKey  = grpcInstrumentationKey("client-rpc")
 )
+
+func methodName(fullname string) string {
+	return strings.TrimLeft(fullname, "/")
+}
diff --git a/plugin/grpc/grpcstats/server_handler.go b/plugin/grpc/grpcstats/server_handler.go
index 47df0f6..2c439be 100644
--- a/plugin/grpc/grpcstats/server_handler.go
+++ b/plugin/grpc/grpcstats/server_handler.go
@@ -17,7 +17,6 @@ package grpcstats
 
 import (
 	"fmt"
-	"strings"
 	"sync/atomic"
 	"time"
 
@@ -27,6 +26,7 @@ import (
 	"go.opencensus.io/tag"
 	"google.golang.org/grpc/grpclog"
 	"google.golang.org/grpc/stats"
+	"google.golang.org/grpc/status"
 )
 
 // ServerStatsHandler is a stats.Handler implementation
@@ -65,21 +65,8 @@ func (h *ServerStatsHandler) TagRPC(ctx context.Context, info *stats.RPCTagInfo)
 		}
 		return ctx
 	}
-	names := strings.Split(info.FullMethodName, "/")
-	if len(names) != 3 {
-		if grpclog.V(2) {
-			grpclog.Infof("serverHandler.TagRPC called with info.FullMethodName bad format. got %v, want '/$service/$method/'", info.FullMethodName)
-		}
-		return ctx
-	}
-	serviceName := names[1]
-	methodName := names[2]
-
-	d := &rpcData{
-		startTime: startTime,
-	}
-
-	ts, err := h.createTagMap(ctx, serviceName, methodName)
+	d := &rpcData{startTime: startTime}
+	ts, err := h.createTagMap(ctx, info.FullMethodName)
 	if err != nil {
 		return ctx
 	}
@@ -151,12 +138,14 @@ func (h *ServerStatsHandler) handleRPCEnd(ctx context.Context, s *stats.End) {
 	m = append(m, RPCServerFinishedCount.M(1))
 	m = append(m, RPCServerServerElapsedTime.M(float64(elapsedTime)/float64(time.Millisecond)))
 	if s.Error != nil {
-		errorCode := s.Error.Error()
-		tm, err := tag.NewMap(ctx,
-			tag.Upsert(keyOpStatus, errorCode),
-		)
-		if err == nil {
-			ctx = tag.NewContext(ctx, tm)
+		s, ok := status.FromError(s.Error)
+		if ok {
+			newTagMap, err := tag.NewMap(ctx,
+				tag.Upsert(keyStatus, s.Code().String()),
+			)
+			if err == nil {
+				ctx = tag.NewContext(ctx, newTagMap)
+			}
 		}
 		m = append(m, RPCServerErrorCount.M(1))
 	}
@@ -166,10 +155,9 @@ func (h *ServerStatsHandler) handleRPCEnd(ctx context.Context, s *stats.End) {
 
 // createTagMap creates a new tag map containing the tags extracted from the
 // gRPC metadata.
-func (h *ServerStatsHandler) createTagMap(ctx context.Context, serviceName, methodName string) (*tag.Map, error) {
+func (h *ServerStatsHandler) createTagMap(ctx context.Context, fullinfo string) (*tag.Map, error) {
 	mods := []tag.Mutator{
-		tag.Upsert(keyService, serviceName),
-		tag.Upsert(keyMethod, methodName),
+		tag.Upsert(keyMethod, methodName(fullinfo)),
 	}
 	if tagsBin := stats.Tags(ctx); tagsBin != nil {
 		old, err := tag.Decode([]byte(tagsBin))
diff --git a/plugin/grpc/grpcstats/server_metrics.go b/plugin/grpc/grpcstats/server_metrics.go
index 6f17f61..c0297d2 100644
--- a/plugin/grpc/grpcstats/server_metrics.go
+++ b/plugin/grpc/grpcstats/server_metrics.go
@@ -69,85 +69,226 @@ var (
 func defaultServerMeasures() {
 	var err error
 
-	// Creating server measures
-	if RPCServerErrorCount, err = stats.NewMeasureInt64("/grpc.io/server/error_count", "RPC Errors", unitCount); err != nil {
-		log.Fatalf("Cannot create measure /grpc.io/server/error_count: %v", err)
+	if RPCServerErrorCount, err = stats.NewMeasureInt64("grpc.io/server/error_count", "RPC Errors", unitCount); err != nil {
+		log.Fatalf("Cannot create measure grpc.io/server/error_count: %v", err)
 	}
-	if RPCServerServerElapsedTime, err = stats.NewMeasureFloat64("/grpc.io/server/server_elapsed_time", "Server elapsed time in msecs", unitMillisecond); err != nil {
-		log.Fatalf("Cannot create measure /grpc.io/server/server_elapsed_time: %v", err)
+	if RPCServerServerElapsedTime, err = stats.NewMeasureFloat64("grpc.io/server/server_elapsed_time", "Server elapsed time in msecs", unitMillisecond); err != nil {
+		log.Fatalf("Cannot create measure grpc.io/server/server_elapsed_time: %v", err)
 	}
-	if RPCServerRequestBytes, err = stats.NewMeasureInt64("/grpc.io/server/request_bytes", "Request bytes", unitByte); err != nil {
-		log.Fatalf("Cannot create measure /grpc.io/server/request_bytes: %v", err)
+	if RPCServerRequestBytes, err = stats.NewMeasureInt64("grpc.io/server/request_bytes", "Request bytes", unitByte); err != nil {
+		log.Fatalf("Cannot create measure grpc.io/server/request_bytes: %v", err)
 	}
-	if RPCServerResponseBytes, err = stats.NewMeasureInt64("/grpc.io/server/response_bytes", "Response bytes", unitByte); err != nil {
-		log.Fatalf("Cannot create measure /grpc.io/server/response_bytes: %v", err)
+	if RPCServerResponseBytes, err = stats.NewMeasureInt64("grpc.io/server/response_bytes", "Response bytes", unitByte); err != nil {
+		log.Fatalf("Cannot create measure grpc.io/server/response_bytes: %v", err)
 	}
-	if RPCServerStartedCount, err = stats.NewMeasureInt64("/grpc.io/server/started_count", "Number of server RPCs (streams) started", unitCount); err != nil {
-		log.Fatalf("Cannot create measure /grpc.io/server/started_count: %v", err)
+	if RPCServerStartedCount, err = stats.NewMeasureInt64("grpc.io/server/started_count", "Number of server RPCs (streams) started", unitCount); err != nil {
+		log.Fatalf("Cannot create measure grpc.io/server/started_count: %v", err)
 	}
-	if RPCServerFinishedCount, err = stats.NewMeasureInt64("/grpc.io/server/finished_count", "Number of server RPCs (streams) finished", unitCount); err != nil {
-		log.Fatalf("Cannot create measure /grpc.io/server/finished_count: %v", err)
+	if RPCServerFinishedCount, err = stats.NewMeasureInt64("grpc.io/server/finished_count", "Number of server RPCs (streams) finished", unitCount); err != nil {
+		log.Fatalf("Cannot create measure grpc.io/server/finished_count: %v", err)
 	}
-	if RPCServerRequestCount, err = stats.NewMeasureInt64("/grpc.io/server/request_count", "Number of server RPC request messages", unitCount); err != nil {
-		log.Fatalf("Cannot create measure /grpc.io/server/request_count: %v", err)
+	if RPCServerRequestCount, err = stats.NewMeasureInt64("grpc.io/server/request_count", "Number of server RPC request messages", unitCount); err != nil {
+		log.Fatalf("Cannot create measure grpc.io/server/request_count: %v", err)
 	}
-	if RPCServerResponseCount, err = stats.NewMeasureInt64("/grpc.io/server/response_count", "Number of server RPC response messages", unitCount); err != nil {
-		log.Fatalf("Cannot create measure /grpc.io/server/response_count: %v", err)
+	if RPCServerResponseCount, err = stats.NewMeasureInt64("grpc.io/server/response_count", "Number of server RPC response messages", unitCount); err != nil {
+		log.Fatalf("Cannot create measure grpc.io/server/response_count: %v", err)
 	}
 }
 
-func defaultServerserverViews() {
-	RPCServerErrorCountView, _ = stats.NewView("grpc.io/server/error_count/cumulative", "RPC Errors", []tag.Key{keyMethod, keyOpStatus, keyService}, RPCServerErrorCount, aggCount, windowCumulative)
-	serverViews = append(serverViews, RPCServerErrorCountView)
-	RPCServerServerElapsedTimeView, _ = stats.NewView("grpc.io/server/server_elapsed_time/cumulative", "Server elapsed time in msecs", []tag.Key{keyService, keyMethod}, RPCServerServerElapsedTime, aggDistMillis, windowCumulative)
-	serverViews = append(serverViews, RPCServerServerElapsedTimeView)
-	RPCServerRequestBytesView, _ = stats.NewView("grpc.io/server/request_bytes/cumulative", "Request bytes", []tag.Key{keyService, keyMethod}, RPCServerRequestBytes, aggDistBytes, windowCumulative)
-	serverViews = append(serverViews, RPCServerRequestBytesView)
-	RPCServerResponseBytesView, _ = stats.NewView("grpc.io/server/response_bytes/cumulative", "Response bytes", []tag.Key{keyService, keyMethod}, RPCServerResponseBytes, aggDistBytes, windowCumulative)
-	serverViews = append(serverViews, RPCServerResponseBytesView)
-	RPCServerRequestCountView, _ = stats.NewView("grpc.io/server/request_count/cumulative", "Count of request messages per server RPC", []tag.Key{keyService, keyMethod}, RPCServerRequestCount, aggDistCounts, windowCumulative)
-	serverViews = append(serverViews, RPCServerRequestCountView)
-	RPCServerResponseCountView, _ = stats.NewView("grpc.io/server/response_count/cumulative", "Count of response messages per server RPC", []tag.Key{keyService, keyMethod}, RPCServerResponseCount, aggDistCounts, windowCumulative)
-	serverViews = append(serverViews, RPCServerResponseCountView)
-
-	RPCServerServerElapsedTimeMinuteView, _ = stats.NewView("grpc.io/server/server_elapsed_time/minute", "Minute stats for server elapsed time in msecs", []tag.Key{keyService, keyMethod}, RPCServerServerElapsedTime, aggDistMillis, windowSlidingMinute)
-	serverViews = append(serverViews, RPCServerServerElapsedTimeMinuteView)
-	RPCServerRequestBytesMinuteView, _ = stats.NewView("grpc.io/server/request_bytes/minute", "Minute stats for request size in bytes", []tag.Key{keyService, keyMethod}, RPCServerRequestBytes, aggCount, windowSlidingMinute)
-	serverViews = append(serverViews, RPCServerRequestBytesMinuteView)
-	RPCServerResponseBytesMinuteView, _ = stats.NewView("grpc.io/server/response_bytes/minute", "Minute stats for response size in bytes", []tag.Key{keyService, keyMethod}, RPCServerResponseBytes, aggCount, windowSlidingMinute)
-	serverViews = append(serverViews, RPCServerResponseBytesMinuteView)
-	RPCServerErrorCountMinuteView, _ = stats.NewView("grpc.io/server/error_count/minute", "Minute stats for rpc errors", []tag.Key{keyService, keyMethod}, RPCServerErrorCount, aggCount, windowSlidingMinute)
-	serverViews = append(serverViews, RPCServerErrorCountMinuteView)
-	RPCServerStartedCountMinuteView, _ = stats.NewView("grpc.io/server/started_count/minute", "Minute stats on the number of server RPCs started", []tag.Key{keyService, keyMethod}, RPCServerStartedCount, aggCount, windowSlidingMinute)
-	serverViews = append(serverViews, RPCServerStartedCountMinuteView)
-	RPCServerFinishedCountMinuteView, _ = stats.NewView("grpc.io/server/finished_count/minute", "Minute stats on the number of server RPCs finished", []tag.Key{keyService, keyMethod}, RPCServerFinishedCount, aggCount, windowSlidingMinute)
-	serverViews = append(serverViews, RPCServerFinishedCountMinuteView)
-	RPCServerRequestCountMinuteView, _ = stats.NewView("grpc.io/server/request_count/minute", "Minute stats on the count of request messages per server RPC", []tag.Key{keyService, keyMethod}, RPCServerRequestCount, aggCount, windowSlidingMinute)
-	serverViews = append(serverViews, RPCServerRequestCountMinuteView)
-	RPCServerResponseCountMinuteView, _ = stats.NewView("grpc.io/server/response_count/minute", "Minute stats on the count of response messages per server RPC", []tag.Key{keyService, keyMethod}, RPCServerResponseCount, aggCount, windowSlidingMinute)
-	serverViews = append(serverViews, RPCServerResponseCountMinuteView)
-
-	RPCServerServerElapsedTimeHourView, _ = stats.NewView("grpc.io/server/server_elapsed_time/hour", "Hour stats for server elapsed time in msecs", []tag.Key{keyService, keyMethod}, RPCServerServerElapsedTime, aggDistMillis, windowSlidingHour)
-	serverViews = append(serverViews, RPCServerServerElapsedTimeHourView)
-	RPCServerRequestBytesHourView, _ = stats.NewView("grpc.io/server/request_bytes/hour", "Hour stats for request size in bytes", []tag.Key{keyService, keyMethod}, RPCServerRequestBytes, aggCount, windowSlidingHour)
-	serverViews = append(serverViews, RPCServerRequestBytesHourView)
-	RPCServerResponseBytesHourView, _ = stats.NewView("grpc.io/server/response_bytes/hour", "Hour stats for response size in bytes", []tag.Key{keyService, keyMethod}, RPCServerResponseBytes, aggCount, windowSlidingHour)
-	serverViews = append(serverViews, RPCServerResponseBytesHourView)
-	RPCServerErrorCountHourView, _ = stats.NewView("grpc.io/server/error_count/hour", "Hour stats for rpc errors", []tag.Key{keyService, keyMethod}, RPCServerErrorCount, aggCount, windowSlidingHour)
-	serverViews = append(serverViews, RPCServerErrorCountHourView)
-	RPCServerStartedCountHourView, _ = stats.NewView("grpc.io/server/started_count/hour", "Hour stats on the number of server RPCs started", []tag.Key{keyService, keyMethod}, RPCServerStartedCount, aggCount, windowSlidingHour)
-	serverViews = append(serverViews, RPCServerStartedCountHourView)
-	RPCServerFinishedCountHourView, _ = stats.NewView("grpc.io/server/finished_count/hour", "Hour stats on the number of server RPCs finished", []tag.Key{keyService, keyMethod}, RPCServerFinishedCount, aggCount, windowSlidingHour)
-	serverViews = append(serverViews, RPCServerFinishedCountHourView)
-	RPCServerRequestCountHourView, _ = stats.NewView("grpc.io/server/request_count/hour", "Hour stats on the count of request messages per server RPC", []tag.Key{keyService, keyMethod}, RPCServerRequestCount, aggCount, windowSlidingHour)
-	serverViews = append(serverViews, RPCServerRequestCountHourView)
-	RPCServerResponseCountHourView, _ = stats.NewView("grpc.io/server/response_count/hour", "Hour stats on the count of response messages per server RPC", []tag.Key{keyService, keyMethod}, RPCServerResponseCount, aggCount, windowSlidingHour)
-	serverViews = append(serverViews, RPCServerResponseCountHourView)
+func defaultServerViews() {
+	RPCServerErrorCountView, _ = stats.NewView(
+		"grpc.io/server/error_count/cumulative",
+		"RPC Errors",
+		[]tag.Key{keyMethod, keyStatus},
+		RPCServerErrorCount,
+		aggCount,
+		windowCumulative)
+	RPCServerServerElapsedTimeView, _ = stats.NewView(
+		"grpc.io/server/server_elapsed_time/cumulative",
+		"Server elapsed time in msecs",
+		[]tag.Key{keyMethod},
+		RPCServerServerElapsedTime,
+		aggDistMillis,
+		windowCumulative)
+	RPCServerRequestBytesView, _ = stats.NewView(
+		"grpc.io/server/request_bytes/cumulative",
+		"Request bytes",
+		[]tag.Key{keyMethod},
+		RPCServerRequestBytes,
+		aggDistBytes,
+		windowCumulative)
+	RPCServerResponseBytesView, _ = stats.NewView(
+		"grpc.io/server/response_bytes/cumulative",
+		"Response bytes",
+		[]tag.Key{keyMethod},
+		RPCServerResponseBytes,
+		aggDistBytes,
+		windowCumulative)
+	RPCServerRequestCountView, _ = stats.NewView(
+		"grpc.io/server/request_count/cumulative",
+		"Count of request messages per server RPC",
+		[]tag.Key{keyMethod},
+		RPCServerRequestCount,
+		aggDistCounts,
+		windowCumulative)
+	RPCServerResponseCountView, _ = stats.NewView(
+		"grpc.io/server/response_count/cumulative",
+		"Count of response messages per server RPC",
+		[]tag.Key{keyMethod},
+		RPCServerResponseCount,
+		aggDistCounts,
+		windowCumulative)
+
+	serverViews = append(serverViews,
+		RPCServerErrorCountView,
+		RPCServerServerElapsedTimeView,
+		RPCServerRequestBytesView,
+		RPCServerResponseBytesView,
+		RPCServerRequestCountView,
+		RPCServerResponseCountView)
+
+	// TODO(jbd): Add roundtrip_latency, uncompressed_request_bytes, uncompressed_response_bytes, request_count, response_count.
+
+	RPCServerServerElapsedTimeMinuteView, _ = stats.NewView(
+		"grpc.io/server/server_elapsed_time/minute",
+		"Minute stats for server elapsed time in msecs",
+		[]tag.Key{keyMethod},
+		RPCServerServerElapsedTime,
+		aggDistMillis,
+		windowSlidingMinute)
+	RPCServerRequestBytesMinuteView, _ = stats.NewView(
+		"grpc.io/server/request_bytes/minute",
+		"Minute stats for request size in bytes",
+		[]tag.Key{keyMethod},
+		RPCServerRequestBytes,
+		aggCount,
+		windowSlidingMinute)
+	RPCServerResponseBytesMinuteView, _ = stats.NewView(
+		"grpc.io/server/response_bytes/minute",
+		"Minute stats for response size in bytes",
+		[]tag.Key{keyMethod},
+		RPCServerResponseBytes,
+		aggCount,
+		windowSlidingMinute)
+	RPCServerErrorCountMinuteView, _ = stats.NewView(
+		"grpc.io/server/error_count/minute",
+		"Minute stats for rpc errors",
+		[]tag.Key{keyMethod},
+		RPCServerErrorCount,
+		aggCount,
+		windowSlidingMinute)
+	RPCServerStartedCountMinuteView, _ = stats.NewView(
+		"grpc.io/server/started_count/minute",
+		"Minute stats on the number of server RPCs started",
+		[]tag.Key{keyMethod},
+		RPCServerStartedCount,
+		aggCount,
+		windowSlidingMinute)
+	RPCServerFinishedCountMinuteView, _ = stats.NewView(
+		"grpc.io/server/finished_count/minute",
+		"Minute stats on the number of server RPCs finished",
+		[]tag.Key{keyMethod},
+		RPCServerFinishedCount,
+		aggCount,
+		windowSlidingMinute)
+	RPCServerRequestCountMinuteView, _ = stats.NewView(
+		"grpc.io/server/request_count/minute",
+		"Minute stats on the count of request messages per server RPC",
+		[]tag.Key{keyMethod},
+		RPCServerRequestCount,
+		aggCount,
+		windowSlidingMinute)
+	RPCServerResponseCountMinuteView, _ = stats.NewView(
+		"grpc.io/server/response_count/minute",
+		"Minute stats on the count of response messages per server RPC",
+		[]tag.Key{keyMethod},
+		RPCServerResponseCount,
+		aggCount,
+		windowSlidingMinute)
+
+	serverViews = append(serverViews,
+		RPCServerServerElapsedTimeMinuteView,
+		RPCServerRequestBytesMinuteView,
+		RPCServerResponseBytesMinuteView,
+		RPCServerErrorCountMinuteView,
+		RPCServerStartedCountMinuteView,
+		RPCServerFinishedCountMinuteView,
+		RPCServerRequestCountMinuteView,
+		RPCServerResponseCountMinuteView,
+	)
+
+	RPCServerServerElapsedTimeHourView, _ = stats.NewView(
+		"grpc.io/server/server_elapsed_time/hour",
+		"Hour stats for server elapsed time in msecs",
+		[]tag.Key{keyMethod},
+		RPCServerServerElapsedTime,
+		aggDistMillis,
+		windowSlidingHour)
+	RPCServerRequestBytesHourView, _ = stats.NewView(
+		"grpc.io/server/request_bytes/hour",
+		"Hour stats for request size in bytes",
+		[]tag.Key{keyMethod},
+		RPCServerRequestBytes,
+		aggCount,
+		windowSlidingHour)
+	RPCServerResponseBytesHourView, _ = stats.NewView(
+		"grpc.io/server/response_bytes/hour",
+		"Hour stats for response size in bytes",
+		[]tag.Key{keyMethod},
+		RPCServerResponseBytes,
+		aggCount,
+		windowSlidingHour)
+	RPCServerErrorCountHourView, _ = stats.NewView(
+		"grpc.io/server/error_count/hour",
+		"Hour stats for rpc errors",
+		[]tag.Key{keyMethod},
+		RPCServerErrorCount,
+		aggCount,
+		windowSlidingHour)
+	RPCServerStartedCountHourView, _ = stats.NewView(
+		"grpc.io/server/started_count/hour",
+		"Hour stats on the number of server RPCs started",
+		[]tag.Key{keyMethod},
+		RPCServerStartedCount,
+		aggCount,
+		windowSlidingHour)
+	RPCServerFinishedCountHourView, _ = stats.NewView(
+		"grpc.io/server/finished_count/hour",
+		"Hour stats on the number of server RPCs finished",
+		[]tag.Key{keyMethod},
+		RPCServerFinishedCount,
+		aggCount,
+		windowSlidingHour)
+	RPCServerRequestCountHourView, _ = stats.NewView(
+		"grpc.io/server/request_count/hour",
+		"Hour stats on the count of request messages per server RPC",
+		[]tag.Key{keyMethod},
+		RPCServerRequestCount,
+		aggCount,
+		windowSlidingHour)
+	RPCServerResponseCountHourView, _ = stats.NewView(
+		"grpc.io/server/response_count/hour",
+		"Hour stats on the count of response messages per server RPC",
+		[]tag.Key{keyMethod},
+		RPCServerResponseCount,
+		aggCount,
+		windowSlidingHour)
+
+	serverViews = append(serverViews,
+		RPCServerResponseCountHourView,
+		RPCServerServerElapsedTimeHourView,
+		RPCServerRequestBytesHourView,
+		RPCServerResponseBytesHourView,
+		RPCServerErrorCountHourView,
+		RPCServerStartedCountHourView,
+		RPCServerFinishedCountHourView,
+		RPCServerRequestCountHourView,
+	)
 }
 
 func initServer() {
 	defaultServerMeasures()
-	defaultServerserverViews()
+	defaultServerViews()
 }
 
 var serverViews []*stats.View
diff --git a/stats/aggregation.go b/stats/aggregation.go
index bff1728..fa9bf8e 100644
--- a/stats/aggregation.go
+++ b/stats/aggregation.go
@@ -91,15 +91,13 @@ func (a DistributionAggregation) newData() func() AggregationData {
 // aggregatorCumulative indicates that the aggregation occurs over all samples
 // seen since the view collection started.
 type aggregatorCumulative struct {
-	started time.Time
-	av      AggregationData
+	data AggregationData
 }
 
 // newAggregatorCumulative creates an aggregatorCumulative.
 func newAggregatorCumulative(now time.Time, newAggregationValue func() AggregationData) *aggregatorCumulative {
 	return &aggregatorCumulative{
-		started: now,
-		av:      newAggregationValue(),
+		data: newAggregationValue(),
 	}
 }
 
@@ -108,11 +106,11 @@ func (a *aggregatorCumulative) isAggregator() bool {
 }
 
 func (a *aggregatorCumulative) addSample(v interface{}, now time.Time) {
-	a.av.addSample(v)
+	a.data.addSample(v)
 }
 
 func (a *aggregatorCumulative) retrieveCollected(now time.Time) AggregationData {
-	return a.av
+	return a.data
 }
 
 // aggregatorInterval indicates that the aggregation occurs over a
@@ -178,7 +176,7 @@ func (a *aggregatorInterval) retrieveCollected(now time.Time) AggregationData {
 	for j := 1; j < len(a.entries); j++ {
 		oldestIdx = (oldestIdx + 1) % len(a.entries)
 		e = a.entries[oldestIdx]
-		ret.addToIt(e.av)
+		ret.addOther(e.av)
 	}
 	return ret
 }
diff --git a/stats/aggregation_data.go b/stats/aggregation_data.go
index 9f43ad7..63f8302 100644
--- a/stats/aggregation_data.go
+++ b/stats/aggregation_data.go
@@ -23,12 +23,13 @@ import (
 // They are reported on the view data during exporting.
 // Mosts users won't directly access aggregration data.
 type AggregationData interface {
-	equal(other AggregationData) bool
-	isAggregate() bool
+	isAggregationData() bool
 	addSample(v interface{})
+	addOther(other AggregationData)
 	multiplyByFraction(fraction float64) AggregationData
-	addToIt(other AggregationData)
 	clear()
+	clone() AggregationData
+	equal(other AggregationData) bool
 }
 
 const epsilon = 1e-9
@@ -44,18 +45,21 @@ func newCountData(v int64) *CountData {
 	return &tmp
 }
 
-func (a *CountData) isAggregate() bool { return true }
+func (a *CountData) isAggregationData() bool { return true }
 
 func (a *CountData) addSample(v interface{}) {
 	*a = *a + 1
 }
 
+func (a *CountData) clone() AggregationData {
+	return &(*a)
+}
+
 func (a *CountData) multiplyByFraction(fraction float64) AggregationData {
 	return newCountData(int64(float64(int64(*a))*fraction + 0.5)) // adding 0.5 because go runtime will take floor instead of rounding
-
 }
 
-func (a *CountData) addToIt(av AggregationData) {
+func (a *CountData) addOther(av AggregationData) {
 	other, ok := av.(*CountData)
 	if !ok {
 		return
@@ -87,7 +91,7 @@ func newSumData(v float64) *SumData {
 	return &tmp
 }
 
-func (a *SumData) isAggregate() bool { return true }
+func (a *SumData) isAggregationData() bool { return true }
 
 func (a *SumData) addSample(v interface{}) {
 	// Both float64 and int64 values will be cast to float64
@@ -107,7 +111,11 @@ func (a *SumData) multiplyByFraction(fraction float64) AggregationData {
 	return newSumData(float64(*a) * fraction)
 }
 
-func (a *SumData) addToIt(av AggregationData) {
+func (a *SumData) clone() AggregationData {
+	return &(*a)
+}
+
+func (a *SumData) addOther(av AggregationData) {
 	other, ok := av.(*SumData)
 	if !ok {
 		return
@@ -146,7 +154,7 @@ func newMeanData(mean float64, count float64) *MeanData {
 // Sum returns the sum of all samples collected.
 func (a *MeanData) Sum() float64 { return a.Mean * float64(a.Count) }
 
-func (a *MeanData) isAggregate() bool { return true }
+func (a *MeanData) isAggregationData() bool { return true }
 
 func (a *MeanData) addSample(v interface{}) {
 	var f float64
@@ -167,12 +175,16 @@ func (a *MeanData) addSample(v interface{}) {
 	a.Mean = a.Mean + (f-a.Mean)/float64(a.Count)
 }
 
+func (a *MeanData) clone() AggregationData {
+	return &(*a)
+}
+
 // Only Count will be mutiplied by the fraction, Mean will remain the same.
 func (a *MeanData) multiplyByFraction(fraction float64) AggregationData {
 	return newMeanData(a.Mean, a.Count*fraction)
 }
 
-func (a *MeanData) addToIt(av AggregationData) {
+func (a *MeanData) addOther(av AggregationData) {
 	other, ok := av.(*MeanData)
 	if !ok {
 		return
@@ -232,7 +244,7 @@ func (a *DistributionData) variance() float64 {
 	return a.SumOfSquaredDev / float64(a.Count-1)
 }
 
-func (a *DistributionData) isAggregate() bool { return true }
+func (a *DistributionData) isAggregationData() bool { return true }
 
 func (a *DistributionData) addSample(v interface{}) {
 	var f float64
@@ -299,7 +311,7 @@ func (a *DistributionData) multiplyByFraction(fraction float64) AggregationData
 	return ret
 }
 
-func (a *DistributionData) addToIt(av AggregationData) {
+func (a *DistributionData) addOther(av AggregationData) {
 	other, ok := av.(*DistributionData)
 	if !ok {
 		return
@@ -334,6 +346,14 @@ func (a *DistributionData) clear() {
 	}
 }
 
+func (a *DistributionData) clone() AggregationData {
+	counts := make([]int64, len(a.CountPerBucket))
+	copy(counts, a.CountPerBucket)
+	c := *a
+	c.CountPerBucket = counts
+	return &c
+}
+
 func (a *DistributionData) equal(other AggregationData) bool {
 	a2, ok := other.(*DistributionData)
 	if !ok {
diff --git a/stats/measure.go b/stats/measure.go
index 2ed8728..bb83e7a 100644
--- a/stats/measure.go
+++ b/stats/measure.go
@@ -29,10 +29,6 @@ type Measure interface {
 	Name() string
 	Description() string
 	Unit() string
-
-	addView(v *View)
-	removeView(v *View)
-	viewsCount() int
 }
 
 // Measurement is the numeric value measured when recording stats. Each measure
diff --git a/stats/measure_float64.go b/stats/measure_float64.go
index afddeec..168243b 100644
--- a/stats/measure_float64.go
+++ b/stats/measure_float64.go
@@ -20,7 +20,6 @@ type MeasureFloat64 struct {
 	name        string
 	unit        string
 	description string
-	views       map[*View]bool
 }
 
 // Name returns the name of the measure.
@@ -38,16 +37,6 @@ func (m *MeasureFloat64) Unit() string {
 	return m.unit
 }
 
-func (m *MeasureFloat64) addView(v *View) {
-	m.views[v] = true
-}
-
-func (m *MeasureFloat64) removeView(v *View) {
-	delete(m.views, v)
-}
-
-func (m *MeasureFloat64) viewsCount() int { return len(m.views) }
-
 // M creates a new float64 measurement.
 // Use Record to record measurements.
 func (m *MeasureFloat64) M(v float64) Measurement {
diff --git a/stats/measure_int64.go b/stats/measure_int64.go
index 9651de1..66758fe 100644
--- a/stats/measure_int64.go
+++ b/stats/measure_int64.go
@@ -20,7 +20,6 @@ type MeasureInt64 struct {
 	name        string
 	unit        string
 	description string
-	views       map[*View]bool
 }
 
 // Name returns the name of the measure.
@@ -38,16 +37,6 @@ func (m *MeasureInt64) Unit() string {
 	return m.unit
 }
 
-func (m *MeasureInt64) addView(v *View) {
-	m.views[v] = true
-}
-
-func (m *MeasureInt64) removeView(v *View) {
-	delete(m.views, v)
-}
-
-func (m *MeasureInt64) viewsCount() int { return len(m.views) }
-
 // M creates a new int64 measurement.
 // Use Record to record measurements.
 func (m *MeasureInt64) M(v int64) Measurement {
diff --git a/stats/view.go b/stats/view.go
index 2ecf2d7..22dca80 100644
--- a/stats/view.go
+++ b/stats/view.go
@@ -39,9 +39,6 @@ type View struct {
 	// Examples of measures are cpu:tickCount, diskio:time...
 	m Measure
 
-	// start is time when view collection was started originally.
-	start time.Time
-
 	subscribed uint32 // 1 if someone is subscribed and data need to be exported, use atomic to access
 
 	collector *collector
@@ -66,7 +63,6 @@ func NewView(name, description string, keys []tag.Key, measure Measure, agg Aggr
 		description: description,
 		tagKeys:     keys,
 		m:           measure,
-		start:       time.Time{},
 		collector:   &collector{make(map[string]aggregator), agg, window},
 	}, nil
 }
@@ -168,6 +164,5 @@ func (r *Row) Equal(other *Row) bool {
 	if r == other {
 		return true
 	}
-
 	return reflect.DeepEqual(r.Tags, other.Tags) && r.Data.equal(other.Data)
 }
diff --git a/stats/window.go b/stats/window.go
index 9700da7..9522944 100644
--- a/stats/window.go
+++ b/stats/window.go
@@ -28,7 +28,7 @@ type aggregator interface {
 // which the aggregation occurs.
 type Window interface {
 	isWindow()
-	newAggregator(now time.Time, aggregationValueConstructor func() AggregationData) aggregator
+	newAggregator(now time.Time, newAggregationData func() AggregationData) aggregator
 }
 
 // Cumulative is a window that indicates that the aggregation occurs
@@ -37,8 +37,8 @@ type Cumulative struct{}
 
 func (w Cumulative) isWindow() {}
 
-func (w Cumulative) newAggregator(now time.Time, aggregationValueConstructor func() AggregationData) aggregator {
-	return newAggregatorCumulative(now, aggregationValueConstructor)
+func (w Cumulative) newAggregator(now time.Time, newAggregationData func() AggregationData) aggregator {
+	return newAggregatorCumulative(now, newAggregationData)
 }
 
 // Interval is a window that indicates that the aggregation occurs over a sliding
@@ -50,6 +50,6 @@ type Interval struct {
 
 func (w Interval) isWindow() {}
 
-func (w Interval) newAggregator(now time.Time, aggregationValueConstructor func() AggregationData) aggregator {
-	return newAggregatorInterval(now, w.Duration, w.Intervals, aggregationValueConstructor)
+func (w Interval) newAggregator(now time.Time, newAggregationData func() AggregationData) aggregator {
+	return newAggregatorInterval(now, w.Duration, w.Intervals, newAggregationData)
 }
diff --git a/stats/worker.go b/stats/worker.go
index e53b8b7..e065c9a 100644
--- a/stats/worker.go
+++ b/stats/worker.go
@@ -29,11 +29,15 @@ func init() {
 	go defaultWorker.start()
 }
 
+type measureRef struct {
+	measure Measure
+	views   map[*View]struct{}
+}
+
 type worker struct {
-	measuresByName map[string]Measure
-	measures       map[Measure]bool
-	viewsByName    map[string]*View
-	views          map[*View]bool
+	measures   map[string]*measureRef
+	views      map[string]*View
+	startTimes map[*View]time.Time
 
 	timer      *time.Ticker
 	c          chan command
@@ -54,7 +58,6 @@ func NewMeasureFloat64(name, description, unit string) (*MeasureFloat64, error)
 		name:        name,
 		description: description,
 		unit:        unit,
-		views:       make(map[*View]bool),
 	}
 
 	req := &registerMeasureReq{
@@ -79,7 +82,6 @@ func NewMeasureInt64(name, description, unit string) (*MeasureInt64, error) {
 		name:        name,
 		description: description,
 		unit:        unit,
-		views:       make(map[*View]bool),
 	}
 
 	req := &registerMeasureReq{
@@ -122,10 +124,10 @@ func RegisterView(v *View) error {
 	return <-req.err
 }
 
-// Unregister removes the previously registered view. It returns an error
+// UnregisterView removes the previously registered view. It returns an error
 // if the view wasn't registered. All data collected and not reported for the
 // corresponding view will be lost. The view is automatically be unsubscribed.
-func (v *View) Unregister() error {
+func UnregisterView(v *View) error {
 	req := &unregisterViewReq{
 		v:   v,
 		err: make(chan error),
@@ -204,14 +206,13 @@ func SetReportingPeriod(d time.Duration) {
 
 func newWorker() *worker {
 	return &worker{
-		measuresByName: make(map[string]Measure),
-		measures:       make(map[Measure]bool),
-		viewsByName:    make(map[string]*View),
-		views:          make(map[*View]bool),
-		timer:          time.NewTicker(defaultReportingDuration),
-		c:              make(chan command),
-		quit:           make(chan bool),
-		done:           make(chan bool),
+		measures:   make(map[string]*measureRef),
+		views:      make(map[string]*View),
+		startTimes: make(map[*View]time.Time),
+		timer:      time.NewTicker(defaultReportingDuration),
+		c:          make(chan command),
+		quit:       make(chan bool),
+		done:       make(chan bool),
 	}
 }
 
@@ -239,8 +240,8 @@ func (w *worker) stop() {
 }
 
 func (w *worker) tryRegisterMeasure(m Measure) error {
-	if x, ok := w.measuresByName[m.Name()]; ok {
-		if x != m {
+	if ref, ok := w.measures[m.Name()]; ok {
+		if ref.measure != m {
 			return fmt.Errorf("cannot register measure %q; another measure with the same name is already registered", m.Name())
 		}
 		// the measure is already registered so there is nothing to do and the
@@ -248,13 +249,15 @@ func (w *worker) tryRegisterMeasure(m Measure) error {
 		return nil
 	}
 
-	w.measuresByName[m.Name()] = m
-	w.measures[m] = true
+	w.measures[m.Name()] = &measureRef{
+		measure: m,
+		views:   make(map[*View]struct{}),
+	}
 	return nil
 }
 
 func (w *worker) tryRegisterView(v *View) error {
-	if x, ok := w.viewsByName[v.Name()]; ok {
+	if x, ok := w.views[v.Name()]; ok {
 		if x != v {
 			return fmt.Errorf("cannot register view %q; another view with the same name is already registered", v.Name())
 		}
@@ -270,22 +273,33 @@ func (w *worker) tryRegisterView(v *View) error {
 		return fmt.Errorf("cannot register view %q: %v", v.Name(), err)
 	}
 
-	w.viewsByName[v.Name()] = v
-	w.views[v] = true
+	w.views[v.Name()] = v
+	ref := w.measures[v.Measure().Name()]
+	ref.views[v] = struct{}{}
 
-	v.Measure().addView(v)
 	return nil
 }
 
-func (w *worker) reportUsage(now time.Time) {
-	for v := range w.views {
+func (w *worker) reportUsage(start time.Time) {
+	for _, v := range w.views {
 		if !v.isSubscribed() {
 			continue
 		}
-		rows := v.collectedRows(now)
+		rows := v.collectedRows(start)
+		if isCumulative(v) {
+			s, ok := w.startTimes[v]
+			if !ok {
+				w.startTimes[v] = start
+			} else {
+				start = s
+			}
+		}
+		// Make sure collector is never going
+		// to mutate the exported data.
+		rows = deepCopyRowData(rows)
 		viewData := &ViewData{
 			View:  v,
-			Start: now,
+			Start: start,
 			End:   time.Now(),
 			Rows:  rows,
 		}
@@ -294,13 +308,29 @@ func (w *worker) reportUsage(now time.Time) {
 			e.Export(viewData)
 		}
 		exportersMu.Unlock()
-		switch v.Window().(type) {
-		case *Cumulative:
-			// noop
-		case Cumulative:
-			// noop
-		default:
+		if !isCumulative(v) {
 			v.clearRows()
 		}
 	}
 }
+
+func isCumulative(v *View) bool {
+	switch v.Window().(type) {
+	case *Cumulative:
+		return true
+	case Cumulative:
+		return true
+	}
+	return false
+}
+
+func deepCopyRowData(rows []*Row) []*Row {
+	newRows := make([]*Row, 0, len(rows))
+	for _, r := range rows {
+		newRows = append(newRows, &Row{
+			Data: r.Data.clone(),
+			Tags: r.Tags,
+		})
+	}
+	return newRows
+}
diff --git a/stats/worker_commands.go b/stats/worker_commands.go
index b4e945f..c4fa363 100644
--- a/stats/worker_commands.go
+++ b/stats/worker_commands.go
@@ -37,7 +37,12 @@ type getMeasureByNameResp struct {
 }
 
 func (cmd *getMeasureByNameReq) handleCommand(w *worker) {
-	cmd.c <- &getMeasureByNameResp{w.measuresByName[cmd.name]}
+	ref, ok := w.measures[cmd.name]
+	if ok {
+		cmd.c <- &getMeasureByNameResp{ref.measure}
+		return
+	}
+	cmd.c <- &getMeasureByNameResp{nil}
 }
 
 // registerMeasureReq is the command to register a measure with the library.
@@ -57,24 +62,23 @@ type deleteMeasureReq struct {
 }
 
 func (cmd *deleteMeasureReq) handleCommand(w *worker) {
-	m, ok := w.measuresByName[cmd.m.Name()]
+	ref, ok := w.measures[cmd.m.Name()]
 	if !ok {
 		cmd.err <- nil
 		return
 	}
 
-	if m != cmd.m {
+	if ref.measure != cmd.m {
 		cmd.err <- nil
 		return
 	}
 
-	if c := m.viewsCount(); c > 0 {
+	if c := len(ref.views); c > 0 {
 		cmd.err <- fmt.Errorf("cannot delete; measure %q used by %v registered views", cmd.m.Name(), c)
 		return
 	}
 
-	delete(w.measuresByName, cmd.m.Name())
-	delete(w.measures, cmd.m)
+	delete(w.measures, cmd.m.Name())
 	cmd.err <- nil
 }
 
@@ -89,7 +93,7 @@ type getViewByNameResp struct {
 }
 
 func (cmd *getViewByNameReq) handleCommand(w *worker) {
-	cmd.c <- &getViewByNameResp{w.viewsByName[cmd.name]}
+	cmd.c <- &getViewByNameResp{w.views[cmd.name]}
 }
 
 // registerViewReq is the command to register a view with the library.
@@ -109,7 +113,7 @@ type unregisterViewReq struct {
 }
 
 func (cmd *unregisterViewReq) handleCommand(w *worker) {
-	v, ok := w.viewsByName[cmd.v.Name()]
+	v, ok := w.views[cmd.v.Name()]
 	if !ok {
 		cmd.err <- nil
 		return
@@ -122,9 +126,9 @@ func (cmd *unregisterViewReq) handleCommand(w *worker) {
 		cmd.err <- fmt.Errorf("cannot unregister view %q; all subscriptions must be unsubscribed first", cmd.v.Name())
 		return
 	}
-	delete(w.viewsByName, cmd.v.Name())
-	delete(w.views, cmd.v)
-	cmd.v.Measure().removeView(v)
+	delete(w.views, cmd.v.Name())
+	ref := w.measures[v.Measure().Name()]
+	delete(ref.views, v)
 	cmd.err <- nil
 }
 
@@ -181,7 +185,7 @@ type retrieveDataResp struct {
 }
 
 func (cmd *retrieveDataReq) handleCommand(w *worker) {
-	if _, ok := w.views[cmd.v]; !ok {
+	if _, ok := w.views[cmd.v.Name()]; !ok {
 		cmd.c <- &retrieveDataResp{
 			nil,
 			fmt.Errorf("cannot retrieve data; view %q is not registered", cmd.v.Name()),
@@ -214,11 +218,13 @@ func (cmd *recordReq) handleCommand(w *worker) {
 	for _, m := range cmd.ms {
 		switch measurement := m.(type) {
 		case *measurementFloat64:
-			for v := range measurement.m.views {
+			ref := w.measures[measurement.m.Name()]
+			for v := range ref.views {
 				v.addSample(cmd.tm, measurement.v, cmd.now)
 			}
 		case *measurementInt64:
-			for v := range measurement.m.views {
+			ref := w.measures[measurement.m.Name()]
+			for v := range ref.views {
 				v.addSample(cmd.tm, measurement.v, cmd.now)
 			}
 		default:


Test patch:
diff --git a/exporter/stats/stackdriver/stackdriver_test.go b/exporter/stats/stackdriver/stackdriver_test.go
index 994c3ee..e8aa625 100644
--- a/exporter/stats/stackdriver/stackdriver_test.go
+++ b/exporter/stats/stackdriver/stackdriver_test.go
@@ -53,7 +53,7 @@ func TestExporter_makeReq(t *testing.T) {
 	if err := stats.RegisterView(cumView); err != nil {
 		t.Fatal(err)
 	}
-	defer cumView.Unregister()
+	defer stats.UnregisterView(cumView)
 
 	distView, err := stats.NewView("distview", "desc", nil, m, stats.DistributionAggregation([]float64{2, 4, 7}), stats.Interval{})
 	if err != nil {
@@ -62,7 +62,7 @@ func TestExporter_makeReq(t *testing.T) {
 	if err := stats.RegisterView(distView); err != nil {
 		t.Fatal(err)
 	}
-	defer distView.Unregister()
+	defer stats.UnregisterView(distView)
 
 	start := time.Now()
 	end := start.Add(time.Minute)
@@ -78,6 +78,7 @@ func TestExporter_makeReq(t *testing.T) {
 		Mean:  -7.7,
 		Count: 5,
 	}
+	taskValue := getTaskValue()
 
 	tests := []struct {
 		name   string
@@ -94,8 +95,11 @@ func TestExporter_makeReq(t *testing.T) {
 				TimeSeries: []*monitoringpb.TimeSeries{
 					{
 						Metric: &metricpb.Metric{
-							Type:   "custom.googleapis.com/opencensus/cumview",
-							Labels: map[string]string{"test_key": "test-value-1"},
+							Type: "custom.googleapis.com/opencensus/cumview",
+							Labels: map[string]string{
+								"test_key":        "test-value-1",
+								opencensusTaskKey: taskValue,
+							},
 						},
 						Resource: &monitoredrespb.MonitoredResource{
 							Type: "global",
@@ -120,8 +124,11 @@ func TestExporter_makeReq(t *testing.T) {
 					},
 					{
 						Metric: &metricpb.Metric{
-							Type:   "custom.googleapis.com/opencensus/cumview",
-							Labels: map[string]string{"test_key": "test-value-2"},
+							Type: "custom.googleapis.com/opencensus/cumview",
+							Labels: map[string]string{
+								"test_key":        "test-value-2",
+								opencensusTaskKey: taskValue,
+							},
 						},
 						Resource: &monitoredrespb.MonitoredResource{
 							Type: "global",
@@ -156,8 +163,11 @@ func TestExporter_makeReq(t *testing.T) {
 				TimeSeries: []*monitoringpb.TimeSeries{
 					{
 						Metric: &metricpb.Metric{
-							Type:   "custom.googleapis.com/opencensus/cumview",
-							Labels: map[string]string{"test_key": "test-value-1"},
+							Type: "custom.googleapis.com/opencensus/cumview",
+							Labels: map[string]string{
+								"test_key":        "test-value-1",
+								opencensusTaskKey: taskValue,
+							},
 						},
 						Resource: &monitoredrespb.MonitoredResource{
 							Type: "global",
@@ -182,8 +192,11 @@ func TestExporter_makeReq(t *testing.T) {
 					},
 					{
 						Metric: &metricpb.Metric{
-							Type:   "custom.googleapis.com/opencensus/cumview",
-							Labels: map[string]string{"test_key": "test-value-2"},
+							Type: "custom.googleapis.com/opencensus/cumview",
+							Labels: map[string]string{
+								"test_key":        "test-value-2",
+								opencensusTaskKey: taskValue,
+							},
 						},
 						Resource: &monitoredrespb.MonitoredResource{
 							Type: "global",
@@ -218,8 +231,11 @@ func TestExporter_makeReq(t *testing.T) {
 				TimeSeries: []*monitoringpb.TimeSeries{
 					{
 						Metric: &metricpb.Metric{
-							Type:   "custom.googleapis.com/opencensus/cumview",
-							Labels: map[string]string{"test_key": "test-value-1"},
+							Type: "custom.googleapis.com/opencensus/cumview",
+							Labels: map[string]string{
+								"test_key":        "test-value-1",
+								opencensusTaskKey: taskValue,
+							},
 						},
 						Resource: &monitoredrespb.MonitoredResource{
 							Type: "global",
@@ -256,8 +272,11 @@ func TestExporter_makeReq(t *testing.T) {
 					},
 					{
 						Metric: &metricpb.Metric{
-							Type:   "custom.googleapis.com/opencensus/cumview",
-							Labels: map[string]string{"test_key": "test-value-2"},
+							Type: "custom.googleapis.com/opencensus/cumview",
+							Labels: map[string]string{
+								"test_key":        "test-value-2",
+								opencensusTaskKey: taskValue,
+							},
 						},
 						Resource: &monitoredrespb.MonitoredResource{
 							Type: "global",
@@ -304,7 +323,10 @@ func TestExporter_makeReq(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			e := &Exporter{o: Options{ProjectID: tt.projID}}
+			e := &Exporter{
+				o:         Options{ProjectID: tt.projID},
+				taskValue: taskValue,
+			}
 			resps := e.makeReq([]*stats.ViewData{tt.vd}, maxTimeSeriesPerUpload)
 			if got, want := len(resps), len(tt.want); got != want {
 				t.Fatalf("%v: Exporter.makeReq() returned %d responses; want %d", tt.name, got, want)
@@ -338,7 +360,7 @@ func TestExporter_makeReq_batching(t *testing.T) {
 	if err := stats.RegisterView(view); err != nil {
 		t.Fatal(err)
 	}
-	defer view.Unregister()
+	defer stats.UnregisterView(view)
 
 	tests := []struct {
 		name      string
@@ -411,6 +433,7 @@ func TestEqualAggWindowTagKeys(t *testing.T) {
 			md: &metricpb.MetricDescriptor{
 				MetricKind: metricpb.MetricDescriptor_CUMULATIVE,
 				ValueType:  metricpb.MetricDescriptor_INT64,
+				Labels:     []*label.LabelDescriptor{{Key: opencensusTaskKey}},
 			},
 			agg:     stats.CountAggregation{},
 			window:  stats.Cumulative{},
@@ -421,6 +444,7 @@ func TestEqualAggWindowTagKeys(t *testing.T) {
 			md: &metricpb.MetricDescriptor{
 				MetricKind: metricpb.MetricDescriptor_CUMULATIVE,
 				ValueType:  metricpb.MetricDescriptor_DOUBLE,
+				Labels:     []*label.LabelDescriptor{{Key: opencensusTaskKey}},
 			},
 			agg:     stats.SumAggregation{},
 			window:  stats.Cumulative{},
@@ -431,6 +455,7 @@ func TestEqualAggWindowTagKeys(t *testing.T) {
 			md: &metricpb.MetricDescriptor{
 				MetricKind: metricpb.MetricDescriptor_CUMULATIVE,
 				ValueType:  metricpb.MetricDescriptor_DISTRIBUTION,
+				Labels:     []*label.LabelDescriptor{{Key: opencensusTaskKey}},
 			},
 			agg:     stats.MeanAggregation{},
 			window:  stats.Cumulative{},
@@ -441,6 +466,7 @@ func TestEqualAggWindowTagKeys(t *testing.T) {
 			md: &metricpb.MetricDescriptor{
 				MetricKind: metricpb.MetricDescriptor_CUMULATIVE,
 				ValueType:  metricpb.MetricDescriptor_DISTRIBUTION,
+				Labels:     []*label.LabelDescriptor{{Key: opencensusTaskKey}},
 			},
 			agg:     stats.CountAggregation{},
 			window:  stats.Cumulative{},
@@ -451,6 +477,7 @@ func TestEqualAggWindowTagKeys(t *testing.T) {
 			md: &metricpb.MetricDescriptor{
 				MetricKind: metricpb.MetricDescriptor_CUMULATIVE,
 				ValueType:  metricpb.MetricDescriptor_DOUBLE,
+				Labels:     []*label.LabelDescriptor{{Key: opencensusTaskKey}},
 			},
 			agg:     stats.MeanAggregation{},
 			window:  stats.Cumulative{},
@@ -461,6 +488,7 @@ func TestEqualAggWindowTagKeys(t *testing.T) {
 			md: &metricpb.MetricDescriptor{
 				MetricKind: metricpb.MetricDescriptor_DELTA,
 				ValueType:  metricpb.MetricDescriptor_DISTRIBUTION,
+				Labels:     []*label.LabelDescriptor{{Key: opencensusTaskKey}},
 			},
 			agg:     stats.DistributionAggregation{},
 			window:  stats.Interval{},
@@ -471,6 +499,7 @@ func TestEqualAggWindowTagKeys(t *testing.T) {
 			md: &metricpb.MetricDescriptor{
 				MetricKind: metricpb.MetricDescriptor_CUMULATIVE,
 				ValueType:  metricpb.MetricDescriptor_DISTRIBUTION,
+				Labels:     []*label.LabelDescriptor{{Key: opencensusTaskKey}},
 			},
 			agg:     stats.DistributionAggregation{},
 			window:  stats.Interval{},
@@ -484,6 +513,7 @@ func TestEqualAggWindowTagKeys(t *testing.T) {
 				Labels: []*label.LabelDescriptor{
 					{Key: "test_key_one"},
 					{Key: "test_key_two"},
+					{Key: opencensusTaskKey},
 				},
 			},
 			agg:     stats.DistributionAggregation{},
@@ -507,6 +537,7 @@ func TestEqualAggWindowTagKeys(t *testing.T) {
 			md: &metricpb.MetricDescriptor{
 				MetricKind: metricpb.MetricDescriptor_CUMULATIVE,
 				ValueType:  metricpb.MetricDescriptor_INT64,
+				Labels:     []*label.LabelDescriptor{{Key: opencensusTaskKey}},
 			},
 			agg:     &stats.CountAggregation{},
 			window:  &stats.Cumulative{},
@@ -591,6 +622,129 @@ func TestExporter_createMeasure(t *testing.T) {
 	}
 }
 
+func TestExporter_makeReq_withCustomMonitoredResource(t *testing.T) {
+	m, err := stats.NewMeasureFloat64("test-measure", "measure desc", "unit")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer stats.DeleteMeasure(m)
+
+	key, err := tag.NewKey("test_key")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	cumView, err := stats.NewView("cumview", "desc", []tag.Key{key}, m, stats.CountAggregation{}, stats.Cumulative{})
+	if err != nil {
+		t.Fatal(err)
+	}
+	if err := cumView.Subscribe(); err != nil {
+		t.Fatal(err)
+	}
+	defer cumView.Unsubscribe()
+
+	start := time.Now()
+	end := start.Add(time.Minute)
+	count1 := stats.CountData(10)
+	count2 := stats.CountData(16)
+	taskValue := getTaskValue()
+
+	resource := &monitoredrespb.MonitoredResource{
+		Type:   "gce_instance",
+		Labels: map[string]string{"instance_id": "instance", "zone": "us-west-1a"},
+	}
+
+	tests := []struct {
+		name   string
+		projID string
+		vd     *stats.ViewData
+		want   []*monitoringpb.CreateTimeSeriesRequest
+	}{
+		{
+			name:   "count agg + cum timeline",
+			projID: "proj-id",
+			vd:     newTestCumViewData(cumView, start, end, &count1, &count2),
+			want: []*monitoringpb.CreateTimeSeriesRequest{{
+				Name: monitoring.MetricProjectPath("proj-id"),
+				TimeSeries: []*monitoringpb.TimeSeries{
+					{
+						Metric: &metricpb.Metric{
+							Type: "custom.googleapis.com/opencensus/cumview",
+							Labels: map[string]string{
+								"test_key":        "test-value-1",
+								opencensusTaskKey: taskValue,
+							},
+						},
+						Resource: resource,
+						Points: []*monitoringpb.Point{
+							{
+								Interval: &monitoringpb.TimeInterval{
+									StartTime: &timestamp.Timestamp{
+										Seconds: start.Unix(),
+										Nanos:   int32(start.Nanosecond()),
+									},
+									EndTime: &timestamp.Timestamp{
+										Seconds: end.Unix(),
+										Nanos:   int32(end.Nanosecond()),
+									},
+								},
+								Value: &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_Int64Value{
+									Int64Value: 10,
+								}},
+							},
+						},
+					},
+					{
+						Metric: &metricpb.Metric{
+							Type: "custom.googleapis.com/opencensus/cumview",
+							Labels: map[string]string{
+								"test_key":        "test-value-2",
+								opencensusTaskKey: taskValue,
+							},
+						},
+						Resource: resource,
+						Points: []*monitoringpb.Point{
+							{
+								Interval: &monitoringpb.TimeInterval{
+									StartTime: &timestamp.Timestamp{
+										Seconds: start.Unix(),
+										Nanos:   int32(start.Nanosecond()),
+									},
+									EndTime: &timestamp.Timestamp{
+										Seconds: end.Unix(),
+										Nanos:   int32(end.Nanosecond()),
+									},
+								},
+								Value: &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_Int64Value{
+									Int64Value: 16,
+								}},
+							},
+						},
+					},
+				},
+			}},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			e := &Exporter{
+				o:         Options{ProjectID: tt.projID, Resource: resource},
+				taskValue: taskValue,
+			}
+			resps := e.makeReq([]*stats.ViewData{tt.vd}, maxTimeSeriesPerUpload)
+			if got, want := len(resps), len(tt.want); got != want {
+				t.Fatalf("%v: Exporter.makeReq() returned %d responses; want %d", tt.name, got, want)
+			}
+			if len(tt.want) == 0 {
+				return
+			}
+			if !reflect.DeepEqual(resps, tt.want) {
+				t.Errorf("%v: Exporter.makeReq() = %v, want %v", tt.name, resps, tt.want)
+			}
+		})
+	}
+}
+
 func newTestCumViewData(v *stats.View, start, end time.Time, data1, data2 stats.AggregationData) *stats.ViewData {
 	key, _ := tag.NewKey("test-key")
 	tag1 := tag.Tag{Key: key, Value: "test-value-1"}
diff --git a/exporter/trace/zipkin/zipkin_test.go b/exporter/trace/zipkin/zipkin_test.go
new file mode 100644
index 0000000..d74496b
--- /dev/null
+++ b/exporter/trace/zipkin/zipkin_test.go
@@ -0,0 +1,242 @@
+// Copyright 2017, 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 zipkin
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+	"reflect"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/openzipkin/zipkin-go/model"
+	httpreporter "github.com/openzipkin/zipkin-go/reporter/http"
+	"go.opencensus.io/trace"
+)
+
+type roundTripper func(*http.Request) (*http.Response, error)
+
+func (r roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+	return r(req)
+}
+
+func TestExport(t *testing.T) {
+	now := time.Now()
+	tests := []struct {
+		span *trace.SpanData
+		want model.SpanModel
+	}{
+		{
+			span: &trace.SpanData{
+				SpanContext: trace.SpanContext{
+					TraceID:      trace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16},
+					SpanID:       trace.SpanID{17, 18, 19, 20, 21, 22, 23, 24},
+					TraceOptions: 1,
+				},
+				Name:      "name",
+				StartTime: now,
+				EndTime:   now.Add(24 * time.Hour),
+				Attributes: map[string]interface{}{
+					"stringkey": "value",
+					"intkey":    int64(42),
+					"boolkey":   true,
+				},
+				MessageEvents: []trace.MessageEvent{
+					trace.MessageEvent{
+						Time:                 now,
+						EventType:            trace.MessageEventTypeSent,
+						MessageID:            12,
+						UncompressedByteSize: 99,
+						CompressedByteSize:   98,
+					},
+				},
+				Status: trace.Status{
+					Code:    3,
+					Message: "error",
+				},
+			},
+			want: model.SpanModel{
+				SpanContext: model.SpanContext{
+					TraceID: model.TraceID{
+						High: 0x102030405060708,
+						Low:  0x90a0b0c0d0e0f10,
+					},
+					ID:      0x1112131415161718,
+					Sampled: &sampledTrue,
+				},
+				Name:      "name",
+				Kind:      "CLIENT",
+				Timestamp: time.Time{},
+				Duration:  24 * time.Hour,
+				Shared:    false,
+				Annotations: []model.Annotation{
+					model.Annotation{
+						Timestamp: time.Time{},
+						Value:     "SENT",
+					},
+				},
+				Tags: map[string]string{
+					"stringkey": "value",
+					"intkey":    "42",
+					"boolkey":   "true",
+					"error":     "INVALID_ARGUMENT",
+					"opencensus.status_description": "error",
+				},
+			},
+		},
+		{
+			span: &trace.SpanData{
+				SpanContext: trace.SpanContext{
+					TraceID:      trace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16},
+					SpanID:       trace.SpanID{17, 18, 19, 20, 21, 22, 23, 24},
+					TraceOptions: 1,
+				},
+				Name:      "name",
+				StartTime: now,
+				EndTime:   now.Add(24 * time.Hour),
+			},
+			want: model.SpanModel{
+				SpanContext: model.SpanContext{
+					TraceID: model.TraceID{
+						High: 0x102030405060708,
+						Low:  0x90a0b0c0d0e0f10,
+					},
+					ID:      0x1112131415161718,
+					Sampled: &sampledTrue,
+				},
+				Name:      "name",
+				Timestamp: time.Time{},
+				Duration:  24 * time.Hour,
+				Shared:    false,
+			},
+		},
+		{
+			span: &trace.SpanData{
+				SpanContext: trace.SpanContext{
+					TraceID:      trace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16},
+					SpanID:       trace.SpanID{17, 18, 19, 20, 21, 22, 23, 24},
+					TraceOptions: 1,
+				},
+				Name:      "name",
+				StartTime: now,
+				EndTime:   now.Add(24 * time.Hour),
+				Status: trace.Status{
+					Code:    0,
+					Message: "there is no cause for alarm",
+				},
+			},
+			want: model.SpanModel{
+				SpanContext: model.SpanContext{
+					TraceID: model.TraceID{
+						High: 0x102030405060708,
+						Low:  0x90a0b0c0d0e0f10,
+					},
+					ID:      0x1112131415161718,
+					Sampled: &sampledTrue,
+				},
+				Name:      "name",
+				Timestamp: time.Time{},
+				Duration:  24 * time.Hour,
+				Shared:    false,
+				Tags: map[string]string{
+					"opencensus.status_description": "there is no cause for alarm",
+				},
+			},
+		},
+		{
+			span: &trace.SpanData{
+				SpanContext: trace.SpanContext{
+					TraceID:      trace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16},
+					SpanID:       trace.SpanID{17, 18, 19, 20, 21, 22, 23, 24},
+					TraceOptions: 1,
+				},
+				Name:      "name",
+				StartTime: now,
+				EndTime:   now.Add(24 * time.Hour),
+				Status: trace.Status{
+					Code: 1234,
+				},
+			},
+			want: model.SpanModel{
+				SpanContext: model.SpanContext{
+					TraceID: model.TraceID{
+						High: 0x102030405060708,
+						Low:  0x90a0b0c0d0e0f10,
+					},
+					ID:      0x1112131415161718,
+					Sampled: &sampledTrue,
+				},
+				Name:      "name",
+				Timestamp: time.Time{},
+				Duration:  24 * time.Hour,
+				Shared:    false,
+				Tags: map[string]string{
+					"error": "error code 1234",
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		got := zipkinSpan(tt.span, nil)
+		got.Timestamp = time.Time{}
+		if len(got.Annotations) != len(tt.want.Annotations) {
+			t.Fatalf("zipkinSpan: got %d annotations in span, want %d", len(got.Annotations), len(tt.want.Annotations))
+		}
+		for i := range got.Annotations {
+			got.Annotations[i].Timestamp = time.Time{}
+		}
+		if !reflect.DeepEqual(got, tt.want) {
+			t.Errorf("zipkinSpan: got %#v want %#v", got, tt.want)
+		}
+	}
+	for _, tt := range tests {
+		ch := make(chan []byte)
+		client := http.Client{
+			Transport: roundTripper(func(req *http.Request) (*http.Response, error) {
+				body, _ := ioutil.ReadAll(req.Body)
+				ch <- body
+				return &http.Response{StatusCode: 200, Body: ioutil.NopCloser(strings.NewReader(""))}, nil
+			}),
+		}
+		reporter := httpreporter.NewReporter("foo", httpreporter.Client(&client), httpreporter.BatchInterval(time.Millisecond))
+		exporter := NewExporter(reporter, nil)
+		exporter.Export(tt.span)
+		var data []byte
+		select {
+		case data = <-ch:
+		case <-time.After(2 * time.Second):
+			t.Fatalf("span was not exported")
+		}
+		var spans []model.SpanModel
+		json.Unmarshal(data, &spans)
+		if len(spans) != 1 {
+			t.Fatalf("Export: got %d spans, want 1", len(spans))
+		}
+		got := spans[0]
+		got.SpanContext.Sampled = &sampledTrue // Sampled is not set when the span is reported.
+		got.Timestamp = time.Time{}
+		if len(got.Annotations) != len(tt.want.Annotations) {
+			t.Fatalf("Export: got %d annotations in span, want %d", len(got.Annotations), len(tt.want.Annotations))
+		}
+		for i := range got.Annotations {
+			got.Annotations[i].Timestamp = time.Time{}
+		}
+		if !reflect.DeepEqual(got, tt.want) {
+			t.Errorf("Export: got %#v want %#v", got, tt.want)
+		}
+	}
+}
diff --git a/plugin/grpc/grpcstats/client_handler_test.go b/plugin/grpc/grpcstats/client_handler_test.go
index 9f315b1..8031b89 100644
--- a/plugin/grpc/grpcstats/client_handler_test.go
+++ b/plugin/grpc/grpcstats/client_handler_test.go
@@ -16,9 +16,11 @@
 package grpcstats
 
 import (
-	"errors"
 	"testing"
 
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
+
 	"golang.org/x/net/context"
 
 	istats "go.opencensus.io/stats"
@@ -75,8 +77,7 @@ func TestClientDefaultCollections(t *testing.T) {
 					[]*istats.Row{
 						{
 							[]tag.Tag{
-								{Key: keyMethod, Value: "method"},
-								{Key: keyService, Value: "package.service"},
+								{Key: keyMethod, Value: "package.service/method"},
 							},
 							newDistributionData(rpcCountBucketBoundaries, []int64{0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 1, 1, 1, 1, 0),
 						},
@@ -87,8 +88,7 @@ func TestClientDefaultCollections(t *testing.T) {
 					[]*istats.Row{
 						{
 							[]tag.Tag{
-								{Key: keyMethod, Value: "method"},
-								{Key: keyService, Value: "package.service"},
+								{Key: keyMethod, Value: "package.service/method"},
 							},
 							newDistributionData(rpcCountBucketBoundaries, []int64{0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 1, 1, 1, 1, 0),
 						},
@@ -99,8 +99,7 @@ func TestClientDefaultCollections(t *testing.T) {
 					[]*istats.Row{
 						{
 							[]tag.Tag{
-								{Key: keyMethod, Value: "method"},
-								{Key: keyService, Value: "package.service"},
+								{Key: keyMethod, Value: "package.service/method"},
 							},
 							newDistributionData(rpcBytesBucketBoundaries, []int64{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 1, 10, 10, 10, 0),
 						},
@@ -111,8 +110,7 @@ func TestClientDefaultCollections(t *testing.T) {
 					[]*istats.Row{
 						{
 							[]tag.Tag{
-								{Key: keyMethod, Value: "method"},
-								{Key: keyService, Value: "package.service"},
+								{Key: keyMethod, Value: "package.service/method"},
 							},
 							newDistributionData(rpcBytesBucketBoundaries, []int64{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 1, 10, 10, 10, 0),
 						},
@@ -147,7 +145,7 @@ func TestClientDefaultCollections(t *testing.T) {
 						{Length: 10},
 						{Length: 10},
 					},
-					&stats.End{Error: errors.New("someError")},
+					&stats.End{Error: status.Error(codes.Canceled, "canceled")},
 				},
 			},
 			[]*wantData{
@@ -156,9 +154,8 @@ func TestClientDefaultCollections(t *testing.T) {
 					[]*istats.Row{
 						{
 							[]tag.Tag{
-								{Key: keyMethod, Value: "method"},
-								{Key: keyOpStatus, Value: "someError"},
-								{Key: keyService, Value: "package.service"},
+								{Key: keyStatus, Value: "Canceled"},
+								{Key: keyMethod, Value: "package.service/method"},
 							},
 							newMeanData(1, 1),
 						},
@@ -169,8 +166,7 @@ func TestClientDefaultCollections(t *testing.T) {
 					[]*istats.Row{
 						{
 							[]tag.Tag{
-								{Key: keyMethod, Value: "method"},
-								{Key: keyService, Value: "package.service"},
+								{Key: keyMethod, Value: "package.service/method"},
 							},
 							newDistributionData(rpcCountBucketBoundaries, []int64{0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 2, 2, 3, 2.5, 0.5),
 						},
@@ -181,8 +177,7 @@ func TestClientDefaultCollections(t *testing.T) {
 					[]*istats.Row{
 						{
 							[]tag.Tag{
-								{Key: keyMethod, Value: "method"},
-								{Key: keyService, Value: "package.service"},
+								{Key: keyMethod, Value: "package.service/method"},
 							},
 							newDistributionData(rpcCountBucketBoundaries, []int64{0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 2, 1, 2, 1.5, 0.5),
 						},
@@ -216,7 +211,7 @@ func TestClientDefaultCollections(t *testing.T) {
 						{Length: 4096},
 						{Length: 16384},
 					},
-					&stats.End{Error: errors.New("someError1")},
+					&stats.End{Error: status.Error(codes.Canceled, "canceled")},
 				},
 				{
 					[]tagPair{{k1, "v11"}, {k2, "v22"}},
@@ -230,7 +225,7 @@ func TestClientDefaultCollections(t *testing.T) {
 						{Length: 4096},
 						{Length: 16384},
 					},
-					&stats.End{Error: errors.New("someError2")},
+					&stats.End{Error: status.Error(codes.Aborted, "aborted")},
 				},
 			},
 			[]*wantData{
@@ -239,17 +234,15 @@ func TestClientDefaultCollections(t *testing.T) {
 					[]*istats.Row{
 						{
 							[]tag.Tag{
-								{Key: keyMethod, Value: "method"},
-								{Key: keyOpStatus, Value: "someError1"},
-								{Key: keyService, Value: "package.service"},
+								{Key: keyStatus, Value: "Canceled"},
+								{Key: keyMethod, Value: "package.service/method"},
 							},
 							newMeanData(1, 1),
 						},
 						{
 							[]tag.Tag{
-								{Key: keyMethod, Value: "method"},
-								{Key: keyOpStatus, Value: "someError2"},
-								{Key: keyService, Value: "package.service"},
+								{Key: keyStatus, Value: "Aborted"},
+								{Key: keyMethod, Value: "package.service/method"},
 							},
 							newMeanData(1, 1),
 						},
@@ -260,8 +253,7 @@ func TestClientDefaultCollections(t *testing.T) {
 					[]*istats.Row{
 						{
 							[]tag.Tag{
-								{Key: keyMethod, Value: "method"},
-								{Key: keyService, Value: "package.service"},
+								{Key: keyMethod, Value: "package.service/method"},
 							},
 							newDistributionData(rpcCountBucketBoundaries, []int64{0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 3, 2, 3, 2.666666666, 0.333333333*2),
 						},
@@ -272,8 +264,7 @@ func TestClientDefaultCollections(t *testing.T) {
 					[]*istats.Row{
 						{
 							[]tag.Tag{
-								{Key: keyMethod, Value: "method"},
-								{Key: keyService, Value: "package.service"},
+								{Key: keyMethod, Value: "package.service/method"},
 							},
 							newDistributionData(rpcCountBucketBoundaries, []int64{0, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 3, 1, 2, 1.333333333, 0.333333333*2),
 						},
@@ -284,8 +275,7 @@ func TestClientDefaultCollections(t *testing.T) {
 					[]*istats.Row{
 						{
 							[]tag.Tag{
-								{Key: keyMethod, Value: "method"},
-								{Key: keyService, Value: "package.service"},
+								{Key: keyMethod, Value: "package.service/method"},
 							},
 							newDistributionData(rpcBytesBucketBoundaries, []int64{0, 1, 1, 1, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0}, 8, 1, 65536, 13696.125, 481423542.982143*7),
 						},
@@ -296,8 +286,7 @@ func TestClientDefaultCollections(t *testing.T) {
 					[]*istats.Row{
 						{
 							[]tag.Tag{
-								{Key: keyMethod, Value: "method"},
-								{Key: keyService, Value: "package.service"},
+								{Key: keyMethod, Value: "package.service/method"},
 							},
 							newDistributionData(rpcBytesBucketBoundaries, []int64{0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 4, 1, 16384, 4864.25, 59678208.25*3),
 						},
@@ -327,17 +316,13 @@ func TestClientDefaultCollections(t *testing.T) {
 			}
 			encoded := tag.Encode(tm)
 			ctx := stats.SetTags(context.Background(), encoded)
-
 			ctx = h.TagRPC(ctx, rpc.tagInfo)
-
 			for _, out := range rpc.outPayloads {
 				h.HandleRPC(ctx, out)
 			}
-
 			for _, in := range rpc.inPayloads {
 				h.HandleRPC(ctx, in)
 			}
-
 			h.HandleRPC(ctx, rpc.end)
 		}
 
diff --git a/plugin/grpc/grpcstats/server_handler_test.go b/plugin/grpc/grpcstats/server_handler_test.go
index 1a4fdbd..4fad7d8 100644
--- a/plugin/grpc/grpcstats/server_handler_test.go
+++ b/plugin/grpc/grpcstats/server_handler_test.go
@@ -16,7 +16,6 @@
 package grpcstats
 
 import (
-	"errors"
 	"testing"
 
 	"golang.org/x/net/context"
@@ -24,7 +23,9 @@ import (
 	istats "go.opencensus.io/stats"
 	"go.opencensus.io/tag"
 
+	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/stats"
+	"google.golang.org/grpc/status"
 )
 
 func TestServerDefaultCollections(t *testing.T) {
@@ -75,8 +76,7 @@ func TestServerDefaultCollections(t *testing.T) {
 					[]*istats.Row{
 						{
 							[]tag.Tag{
-								{Key: keyMethod, Value: "method"},
-								{Key: keyService, Value: "package.service"},
+								{Key: keyMethod, Value: "package.service/method"},
 							},
 							newDistributionData(rpcCountBucketBoundaries, []int64{0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 1, 1, 1, 1, 0),
 						},
@@ -87,8 +87,7 @@ func TestServerDefaultCollections(t *testing.T) {
 					[]*istats.Row{
 						{
 							[]tag.Tag{
-								{Key: keyMethod, Value: "method"},
-								{Key: keyService, Value: "package.service"},
+								{Key: keyMethod, Value: "package.service/method"},
 							},
 							newDistributionData(rpcCountBucketBoundaries, []int64{0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 1, 1, 1, 1, 0),
 						},
@@ -99,8 +98,7 @@ func TestServerDefaultCollections(t *testing.T) {
 					[]*istats.Row{
 						{
 							[]tag.Tag{
-								{Key: keyMethod, Value: "method"},
-								{Key: keyService, Value: "package.service"},
+								{Key: keyMethod, Value: "package.service/method"},
 							},
 							newDistributionData(rpcBytesBucketBoundaries, []int64{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 1, 10, 10, 10, 0),
 						},
@@ -111,8 +109,7 @@ func TestServerDefaultCollections(t *testing.T) {
 					[]*istats.Row{
 						{
 							[]tag.Tag{
-								{Key: keyMethod, Value: "method"},
-								{Key: keyService, Value: "package.service"},
+								{Key: keyMethod, Value: "package.service/method"},
 							},
 							newDistributionData(rpcBytesBucketBoundaries, []int64{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 1, 10, 10, 10, 0),
 						},
@@ -147,7 +144,7 @@ func TestServerDefaultCollections(t *testing.T) {
 						{Length: 10},
 						{Length: 10},
 					},
-					&stats.End{Error: errors.New("someError")},
+					&stats.End{Error: status.Error(codes.Canceled, "canceled")},
 				},
 			},
 			[]*wantData{
@@ -156,9 +153,8 @@ func TestServerDefaultCollections(t *testing.T) {
 					[]*istats.Row{
 						{
 							[]tag.Tag{
-								{Key: keyMethod, Value: "method"},
-								{Key: keyOpStatus, Value: "someError"},
-								{Key: keyService, Value: "package.service"},
+								{Key: keyStatus, Value: "Canceled"},
+								{Key: keyMethod, Value: "package.service/method"},
 							},
 							newCountData(1),
 						},
@@ -169,8 +165,7 @@ func TestServerDefaultCollections(t *testing.T) {
 					[]*istats.Row{
 						{
 							[]tag.Tag{
-								{Key: keyMethod, Value: "method"},
-								{Key: keyService, Value: "package.service"},
+								{Key: keyMethod, Value: "package.service/method"},
 							},
 							newDistributionData(rpcCountBucketBoundaries, []int64{0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 2, 1, 2, 1.5, 0.5),
 						},
@@ -181,8 +176,7 @@ func TestServerDefaultCollections(t *testing.T) {
 					[]*istats.Row{
 						{
 							[]tag.Tag{
-								{Key: keyMethod, Value: "method"},
-								{Key: keyService, Value: "package.service"},
+								{Key: keyMethod, Value: "package.service/method"},
 							},
 							newDistributionData(rpcCountBucketBoundaries, []int64{0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 2, 2, 3, 2.5, 0.5),
 						},
@@ -216,7 +210,7 @@ func TestServerDefaultCollections(t *testing.T) {
 						{Length: 4096},
 						{Length: 16384},
 					},
-					&stats.End{Error: errors.New("someError1")},
+					&stats.End{Error: status.Error(codes.Aborted, "aborted")},
 				},
 				{
 					[]tagPair{{k1, "v11"}, {k2, "v22"}},
@@ -230,7 +224,7 @@ func TestServerDefaultCollections(t *testing.T) {
 						{Length: 4096},
 						{Length: 16384},
 					},
-					&stats.End{Error: errors.New("someError2")},
+					&stats.End{Error: status.Error(codes.Canceled, "canceled")},
 				},
 			},
 			[]*wantData{
@@ -239,17 +233,15 @@ func TestServerDefaultCollections(t *testing.T) {
 					[]*istats.Row{
 						{
 							[]tag.Tag{
-								{Key: keyMethod, Value: "method"},
-								{Key: keyOpStatus, Value: "someError1"},
-								{Key: keyService, Value: "package.service"},
+								{Key: keyStatus, Value: "Canceled"},
+								{Key: keyMethod, Value: "package.service/method"},
 							},
 							newCountData(1),
 						},
 						{
 							[]tag.Tag{
-								{Key: keyMethod, Value: "method"},
-								{Key: keyOpStatus, Value: "someError2"},
-								{Key: keyService, Value: "package.service"},
+								{Key: keyStatus, Value: "Aborted"},
+								{Key: keyMethod, Value: "package.service/method"},
 							},
 							newCountData(1),
 						},
@@ -260,8 +252,7 @@ func TestServerDefaultCollections(t *testing.T) {
 					[]*istats.Row{
 						{
 							[]tag.Tag{
-								{Key: keyMethod, Value: "method"},
-								{Key: keyService, Value: "package.service"},
+								{Key: keyMethod, Value: "package.service/method"},
 							},
 							newDistributionData(rpcCountBucketBoundaries, []int64{0, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 3, 1, 2, 1.333333333, 0.333333333*2),
 						},
@@ -272,8 +263,7 @@ func TestServerDefaultCollections(t *testing.T) {
 					[]*istats.Row{
 						{
 							[]tag.Tag{
-								{Key: keyMethod, Value: "method"},
-								{Key: keyService, Value: "package.service"},
+								{Key: keyMethod, Value: "package.service/method"},
 							},
 							newDistributionData(rpcCountBucketBoundaries, []int64{0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 3, 2, 3, 2.666666666, 0.333333333*2),
 						},
@@ -284,8 +274,7 @@ func TestServerDefaultCollections(t *testing.T) {
 					[]*istats.Row{
 						{
 							[]tag.Tag{
-								{Key: keyMethod, Value: "method"},
-								{Key: keyService, Value: "package.service"},
+								{Key: keyMethod, Value: "package.service/method"},
 							},
 							newDistributionData(rpcBytesBucketBoundaries, []int64{0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 4, 1, 16384, 4864.25, 59678208.25*3),
 						},
@@ -296,8 +285,7 @@ func TestServerDefaultCollections(t *testing.T) {
 					[]*istats.Row{
 						{
 							[]tag.Tag{
-								{Key: keyMethod, Value: "method"},
-								{Key: keyService, Value: "package.service"},
+								{Key: keyMethod, Value: "package.service/method"},
 							},
 							newDistributionData(rpcBytesBucketBoundaries, []int64{0, 1, 1, 1, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0}, 8, 1, 65536, 13696.125, 481423542.982143*7),
 						},
@@ -332,11 +320,9 @@ func TestServerDefaultCollections(t *testing.T) {
 			for _, in := range rpc.inPayloads {
 				h.HandleRPC(ctx, in)
 			}
-
 			for _, out := range rpc.outPayloads {
 				h.HandleRPC(ctx, out)
 			}
-
 			h.HandleRPC(ctx, rpc.end)
 		}
 
diff --git a/stats/worker_test.go b/stats/worker_test.go
index a2d0595..fe3da50 100644
--- a/stats/worker_test.go
+++ b/stats/worker_test.go
@@ -442,9 +442,9 @@ func Test_Worker_ViewRegistration(t *testing.T) {
 
 		for _, unreg := range tc.unregs {
 			v := views[unreg.vID]
-			err := v.Unregister()
+			err := UnregisterView(v)
 			if (err != nil) != (unreg.err != nil) {
-				t.Errorf("%v: Unregister() = %v; want %v", tc.label, err, unreg.err)
+				t.Errorf("%v: UnregisterView() = %v; want %v", tc.label, err, unreg.err)
 			}
 		}
 
@@ -605,7 +605,7 @@ func Test_Worker_RecordFloat64(t *testing.T) {
 		}
 
 		for _, v := range tc.registrations {
-			if err := v.Unregister(); err != nil {
+			if err := UnregisterView(v); err != nil {
 				t.Fatalf("%v: Unregistering view %v errrored with %v; want no error", tc.label, v.Name(), err)
 			}
 		}
@@ -654,7 +654,7 @@ func TestReportUsage(t *testing.T) {
 			t.Fatalf("%v: cannot subscribe: %v", tt.name, err)
 		}
 
-		e := &testExporter{}
+		e := &countExporter{}
 		RegisterExporter(e)
 
 		Record(ctx, m.M(1))
@@ -685,7 +685,6 @@ func Test_SetReportingPeriodReqNeverBlocks(t *testing.T) {
 	t.Parallel()
 
 	worker := newWorker()
-
 	durations := []time.Duration{-1, 0, 10, 100 * time.Millisecond}
 	for i, duration := range durations {
 		ackChan := make(chan bool, 1)
@@ -700,12 +699,65 @@ func Test_SetReportingPeriodReqNeverBlocks(t *testing.T) {
 	}
 }
 
-type testExporter struct {
+func TestWorkerCumStarttime(t *testing.T) {
+	restart()
+
+	ctx := context.Background()
+	m, err := NewMeasureInt64("measure", "desc", "unit")
+	if err != nil {
+		t.Fatalf("NewMeasureInt64() = %v", err)
+	}
+	view, err := NewView("cum", "", nil, m, CountAggregation{}, Cumulative{})
+	if err != nil {
+		t.Fatalf("NewView() = %v", err)
+	}
+
+	SetReportingPeriod(25 * time.Millisecond)
+	if err := view.Subscribe(); err != nil {
+		t.Fatalf("cannot subscribe to %v: %v", view.Name(), err)
+	}
+
+	e := &vdExporter{}
+	RegisterExporter(e)
+	defer UnregisterExporter(e)
+
+	Record(ctx, m.M(1))
+	Record(ctx, m.M(1))
+	Record(ctx, m.M(1))
+	Record(ctx, m.M(1))
+
+	time.Sleep(50 * time.Millisecond)
+
+	Record(ctx, m.M(1))
+	Record(ctx, m.M(1))
+	Record(ctx, m.M(1))
+	Record(ctx, m.M(1))
+
+	time.Sleep(50 * time.Millisecond)
+
+	e.Lock()
+	if len(e.vds) == 0 {
+		t.Fatal("Got no view data; want at least one")
+	}
+
+	var start time.Time
+	for _, vd := range e.vds {
+		if start.IsZero() {
+			start = vd.Start
+		}
+		if !vd.Start.Equal(start) {
+			t.Errorf("Cumulative view data start time = %v; want %v", vd.Start, start)
+		}
+	}
+	e.Unlock()
+}
+
+type countExporter struct {
 	sync.Mutex
 	count int64
 }
 
-func (e *testExporter) Export(vd *ViewData) {
+func (e *countExporter) Export(vd *ViewData) {
 	if len(vd.Rows) == 0 {
 		return
 	}
@@ -716,6 +768,18 @@ func (e *testExporter) Export(vd *ViewData) {
 	e.count = int64(*d)
 }
 
+type vdExporter struct {
+	sync.Mutex
+	vds []*ViewData
+}
+
+func (e *vdExporter) Export(vd *ViewData) {
+	e.Lock()
+	defer e.Unlock()
+
+	e.vds = append(e.vds, vd)
+}
+
 // restart stops the current processors and creates a new one.
 func restart() {
 	defaultWorker.stop()