Repository: census-instrumentation/opencensus-go
Pull request #639 (2018-03-26 19:13:17)

Remove charset restrictions on tag values
See: https://github.com/census-instrumentation/opencensus-specs/pull/71


Patch:
diff --git a/exporter/prometheus/prometheus.go b/exporter/prometheus/prometheus.go
index 4f5183d..7030749 100644
--- a/exporter/prometheus/prometheus.go
+++ b/exporter/prometheus/prometheus.go
@@ -233,20 +233,14 @@ func (c *collector) toMetric(desc *prometheus.Desc, v *view.View, row *view.Row)
 	switch data := row.Data.(type) {
 	case *view.CountData:
 		return prometheus.NewConstMetric(desc, prometheus.CounterValue, float64(*data), tagValues(row.Tags)...)
-
 	case *view.DistributionData:
 		points := make(map[float64]uint64)
 		for i, b := range v.Aggregation.Buckets {
 			points[b] = uint64(data.CountPerBucket[i])
 		}
 		return prometheus.NewConstHistogram(desc, uint64(data.Count), data.Sum(), points, tagValues(row.Tags)...)
-
-	case *view.MeanData:
-		return prometheus.NewConstSummary(desc, uint64(data.Count), data.Sum(), make(map[float64]float64), tagValues(row.Tags)...)
-
 	case *view.SumData:
 		return prometheus.NewConstMetric(desc, prometheus.UntypedValue, float64(*data), tagValues(row.Tags)...)
-
 	default:
 		return nil, fmt.Errorf("aggregation %T is not yet supported", v.Aggregation)
 	}
diff --git a/exporter/stackdriver/stats.go b/exporter/stackdriver/stats.go
index dbd1cba..71156c1 100644
--- a/exporter/stackdriver/stats.go
+++ b/exporter/stackdriver/stats.go
@@ -255,7 +255,12 @@ func (e *statsExporter) createMeasure(ctx context.Context, vd *view.Data) error
 		// because this view does not apply to the recorded values.
 		unit = stats.UnitNone
 	case view.AggTypeSum:
-		valueType = metricpb.MetricDescriptor_DOUBLE
+		switch m.(type) {
+		case *stats.Int64Measure:
+			valueType = metricpb.MetricDescriptor_INT64
+		case *stats.Float64Measure:
+			valueType = metricpb.MetricDescriptor_DOUBLE
+		}
 	case view.AggTypeMean:
 		valueType = metricpb.MetricDescriptor_DISTRIBUTION
 	case view.AggTypeDistribution:
@@ -314,25 +319,16 @@ func newTypedValue(vd *view.View, r *view.Row) *monitoringpb.TypedValue {
 			Int64Value: int64(*v),
 		}}
 	case *view.SumData:
-		return &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_DoubleValue{
-			DoubleValue: float64(*v),
-		}}
-	case *view.MeanData:
-		return &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_DistributionValue{
-			DistributionValue: &distributionpb.Distribution{
-				Count: int64(v.Count),
-				Mean:  v.Mean,
-				SumOfSquaredDeviation: 0,
-				BucketOptions: &distributionpb.Distribution_BucketOptions{
-					Options: &distributionpb.Distribution_BucketOptions_ExplicitBuckets{
-						ExplicitBuckets: &distributionpb.Distribution_BucketOptions_Explicit{
-							Bounds: []float64{0},
-						},
-					},
-				},
-				BucketCounts: []int64{0, int64(v.Count)},
-			},
-		}}
+		switch vd.Measure.(type) {
+		case *stats.Int64Measure:
+			return &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_Int64Value{
+				Int64Value: int64(*v),
+			}}
+		case *stats.Float64Measure:
+			return &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_DoubleValue{
+				DoubleValue: float64(*v),
+			}}
+		}
 	case *view.DistributionData:
 		return &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_DistributionValue{
 			DistributionValue: &distributionpb.Distribution{
diff --git a/internal/readme/stats.go b/internal/readme/stats.go
index f94e304..f1c11d9 100644
--- a/internal/readme/stats.go
+++ b/internal/readme/stats.go
@@ -38,10 +38,9 @@ func statsExamples() {
 	distAgg := view.Distribution(0, 1<<32, 2<<32, 3<<32)
 	countAgg := view.Count()
 	sumAgg := view.Sum()
-	meanAgg := view.Mean()
 	// END aggs
 
-	_, _, _, _ = distAgg, countAgg, sumAgg, meanAgg
+	_, _, _ = distAgg, countAgg, sumAgg
 
 	// START view
 	if err = view.Subscribe(&view.View{
diff --git a/plugin/ocgrpc/client_metrics.go b/plugin/ocgrpc/client_metrics.go
index e9a4d2b..3d48fff 100644
--- a/plugin/ocgrpc/client_metrics.go
+++ b/plugin/ocgrpc/client_metrics.go
@@ -43,7 +43,7 @@ var (
 		Description: "RPC Errors",
 		TagKeys:     []tag.Key{KeyStatus, KeyMethod},
 		Measure:     ClientErrorCount,
-		Aggregation: view.Mean(),
+		Aggregation: view.Count(),
 	}
 
 	ClientRoundTripLatencyView = &view.View{
diff --git a/stats/view/aggregation.go b/stats/view/aggregation.go
index 3ae6d02..c3272e5 100644
--- a/stats/view/aggregation.go
+++ b/stats/view/aggregation.go
@@ -50,12 +50,6 @@ var (
 			return newSumData(0)
 		},
 	}
-	aggMean = &Aggregation{
-		Type: AggTypeMean,
-		newData: func() AggregationData {
-			return newMeanData(0, 0)
-		},
-	}
 )
 
 // Count indicates that data collected and aggregated
@@ -74,14 +68,6 @@ func Sum() *Aggregation {
 	return aggSum
 }
 
-// Mean indicates that collect and aggregate data and maintain
-// the mean value.
-// For example, average latency in milliseconds can be aggregated by using
-// Mean, although in most cases it is preferable to use a Distribution.
-func Mean() *Aggregation {
-	return aggMean
-}
-
 // Distribution indicates that the desired aggregation is
 // a histogram distribution.
 //
diff --git a/stats/view/aggregation_data.go b/stats/view/aggregation_data.go
index 8d74bc4..09cc7c1 100644
--- a/stats/view/aggregation_data.go
+++ b/stats/view/aggregation_data.go
@@ -90,48 +90,6 @@ func (a *SumData) equal(other AggregationData) bool {
 	return math.Pow(float64(*a)-float64(*a2), 2) < epsilon
 }
 
-// MeanData is the aggregated data for the Mean aggregation.
-// A mean aggregation processes data and maintains the mean value.
-//
-// Most users won't directly access mean data.
-type MeanData struct {
-	Count int64   // number of data points aggregated
-	Mean  float64 // mean of all data points
-}
-
-func newMeanData(mean float64, count int64) *MeanData {
-	return &MeanData{
-		Mean:  mean,
-		Count: count,
-	}
-}
-
-// Sum returns the sum of all samples collected.
-func (a *MeanData) Sum() float64 { return a.Mean * float64(a.Count) }
-
-func (a *MeanData) isAggregationData() bool { return true }
-
-func (a *MeanData) addSample(f float64) {
-	a.Count++
-	if a.Count == 1 {
-		a.Mean = f
-		return
-	}
-	a.Mean = a.Mean + (f-a.Mean)/float64(a.Count)
-}
-
-func (a *MeanData) clone() AggregationData {
-	return newMeanData(a.Mean, a.Count)
-}
-
-func (a *MeanData) equal(other AggregationData) bool {
-	a2, ok := other.(*MeanData)
-	if !ok {
-		return false
-	}
-	return a.Count == a2.Count && math.Pow(a.Mean-a2.Mean, 2) < epsilon
-}
-
 // DistributionData is the aggregated data for the
 // Distribution aggregation.
 //
diff --git a/tag/doc.go b/tag/doc.go
index da16b74..3286081 100644
--- a/tag/doc.go
+++ b/tag/doc.go
@@ -22,5 +22,12 @@ the OpenCensus instrumentation data.
 Tags can be propagated on the wire and in the same
 process via context.Context. Encode and Decode should be
 used to represent tags into their binary propagation form.
+
+This package supports a restrictive set of characters in tag keys which
+we believe are supported by most metrics backends. Tag values are not limited in
+this way, but specific exporters may have their own restrictions on tag
+values and if so, should provide a way to sanitize tag values for use
+with that backend.
+
 */
 package tag // import "go.opencensus.io/tag"
diff --git a/tag/validate.go b/tag/validate.go
index 0939fc6..92c5678 100644
--- a/tag/validate.go
+++ b/tag/validate.go
@@ -26,7 +26,7 @@ const (
 
 var (
 	errInvalidKeyName = errors.New("invalid key name: only ASCII characters accepted; max length must be 255 characters")
-	errInvalidValue   = errors.New("invalid value: only ASCII characters accepted; max length must be 255 characters")
+	errInvalidValue   = errors.New("invalid value: max length must be 255 UTF-8 characters")
 )
 
 func checkKeyName(name string) bool {
@@ -49,8 +49,5 @@ func isASCII(s string) bool {
 }
 
 func checkValue(v string) bool {
-	if len(v) > maxKeyLength {
-		return false
-	}
-	return isASCII(v)
+	return len(v) <= maxKeyLength
 }
diff --git a/zpages/rpcz.go b/zpages/rpcz.go
index 663b9b3..2753f2f 100644
--- a/zpages/rpcz.go
+++ b/zpages/rpcz.go
@@ -251,9 +251,6 @@ func (s snapExporter) ExportView(vd *view.Data) {
 		case *view.DistributionData:
 			sum = v.Sum()
 			count = float64(v.Count)
-		case *view.MeanData:
-			sum = v.Sum()
-			count = float64(v.Count)
 		case *view.SumData:
 			sum = float64(*v)
 			count = float64(*v)


Test patch:
diff --git a/exporter/prometheus/prometheus_test.go b/exporter/prometheus/prometheus_test.go
index 456751f..5ca5abc 100644
--- a/exporter/prometheus/prometheus_test.go
+++ b/exporter/prometheus/prometheus_test.go
@@ -49,10 +49,6 @@ func newView(measureName string, agg *view.Aggregation) *view.View {
 func TestOnlyCumulativeWindowSupported(t *testing.T) {
 	// See Issue https://github.com/census-instrumentation/opencensus-go/issues/214.
 	count1 := view.CountData(1)
-	mean1 := view.MeanData{
-		Mean:  4.5,
-		Count: 5,
-	}
 	tests := []struct {
 		vds  *view.Data
 		want int
@@ -72,15 +68,6 @@ func TestOnlyCumulativeWindowSupported(t *testing.T) {
 			},
 			want: 1,
 		},
-		2: {
-			vds: &view.Data{
-				View: newView("TestOnlyCumulativeWindowSupported/m3", view.Mean()),
-				Rows: []*view.Row{
-					{Data: &mean1},
-				},
-			},
-			want: 1,
-		},
 	}
 
 	for i, tt := range tests {
@@ -144,9 +131,7 @@ func TestCollectNonRacy(t *testing.T) {
 
 		for i := 0; i < 1e3; i++ {
 			count1 := view.CountData(1)
-			mean1 := &view.MeanData{Mean: 4.5, Count: 5}
 			vds := []*view.Data{
-				{View: newView(fmt.Sprintf("TestCollectNonRacy/m1-%d", i), view.Mean()), Rows: []*view.Row{{Data: mean1}}},
 				{View: newView(fmt.Sprintf("TestCollectNonRacy/m2-%d", i), view.Count()), Rows: []*view.Row{{Data: &count1}}},
 			}
 			for _, v := range vds {
diff --git a/exporter/stackdriver/stats_test.go b/exporter/stackdriver/stats_test.go
index 8809845..b47e476 100644
--- a/exporter/stackdriver/stats_test.go
+++ b/exporter/stackdriver/stats_test.go
@@ -26,7 +26,6 @@ import (
 	"go.opencensus.io/stats/view"
 	"go.opencensus.io/tag"
 	"google.golang.org/api/option"
-	distributionpb "google.golang.org/genproto/googleapis/api/distribution"
 	"google.golang.org/genproto/googleapis/api/label"
 	"google.golang.org/genproto/googleapis/api/metric"
 	metricpb "google.golang.org/genproto/googleapis/api/metric"
@@ -101,14 +100,6 @@ func TestExporter_makeReq(t *testing.T) {
 	count2 := view.CountData(16)
 	sum1 := view.SumData(5.5)
 	sum2 := view.SumData(-11.1)
-	mean1 := view.MeanData{
-		Mean:  3.3,
-		Count: 7,
-	}
-	mean2 := view.MeanData{
-		Mean:  -7.7,
-		Count: 5,
-	}
 	taskValue := getTaskValue()
 
 	tests := []struct {
@@ -253,98 +244,6 @@ func TestExporter_makeReq(t *testing.T) {
 				},
 			}},
 		},
-		{
-			name:   "mean agg + timeline",
-			projID: "proj-id",
-			vd:     newTestViewData(v, start, end, &mean1, &mean2),
-			want: []*monitoringpb.CreateTimeSeriesRequest{{
-				Name: monitoring.MetricProjectPath("proj-id"),
-				TimeSeries: []*monitoringpb.TimeSeries{
-					{
-						Metric: &metricpb.Metric{
-							Type: "custom.googleapis.com/opencensus/testview",
-							Labels: map[string]string{
-								"test_key":        "test-value-1",
-								opencensusTaskKey: taskValue,
-							},
-						},
-						Resource: &monitoredrespb.MonitoredResource{
-							Type: "global",
-						},
-						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_DistributionValue{
-									DistributionValue: &distributionpb.Distribution{
-										Count: 7,
-										Mean:  3.3,
-										SumOfSquaredDeviation: 0,
-										BucketOptions: &distributionpb.Distribution_BucketOptions{
-											Options: &distributionpb.Distribution_BucketOptions_ExplicitBuckets{
-												ExplicitBuckets: &distributionpb.Distribution_BucketOptions_Explicit{
-													Bounds: []float64{0},
-												},
-											},
-										},
-										BucketCounts: []int64{0, 7},
-									},
-								}},
-							},
-						},
-					},
-					{
-						Metric: &metricpb.Metric{
-							Type: "custom.googleapis.com/opencensus/testview",
-							Labels: map[string]string{
-								"test_key":        "test-value-2",
-								opencensusTaskKey: taskValue,
-							},
-						},
-						Resource: &monitoredrespb.MonitoredResource{
-							Type: "global",
-						},
-						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_DistributionValue{
-									DistributionValue: &distributionpb.Distribution{
-										Count: 5,
-										Mean:  -7.7,
-										SumOfSquaredDeviation: 0,
-										BucketOptions: &distributionpb.Distribution_BucketOptions{
-											Options: &distributionpb.Distribution_BucketOptions_ExplicitBuckets{
-												ExplicitBuckets: &distributionpb.Distribution_BucketOptions_Explicit{
-													Bounds: []float64{0},
-												},
-											},
-										},
-										BucketCounts: []int64{0, 5},
-									},
-								}},
-							},
-						},
-					},
-				},
-			}},
-		},
 		{
 			name:   "dist agg + time window",
 			projID: "proj-id",
@@ -479,16 +378,6 @@ func TestEqualAggWindowTagKeys(t *testing.T) {
 			agg:     view.Sum(),
 			wantErr: false,
 		},
-		{
-			name: "mean agg",
-			md: &metricpb.MetricDescriptor{
-				MetricKind: metricpb.MetricDescriptor_CUMULATIVE,
-				ValueType:  metricpb.MetricDescriptor_DISTRIBUTION,
-				Labels:     []*label.LabelDescriptor{{Key: opencensusTaskKey}},
-			},
-			agg:     view.Mean(),
-			wantErr: false,
-		},
 		{
 			name: "distribution agg - mismatch",
 			md: &metricpb.MetricDescriptor{
@@ -499,16 +388,6 @@ func TestEqualAggWindowTagKeys(t *testing.T) {
 			agg:     view.Count(),
 			wantErr: true,
 		},
-		{
-			name: "mean agg - mismatch",
-			md: &metricpb.MetricDescriptor{
-				MetricKind: metricpb.MetricDescriptor_CUMULATIVE,
-				ValueType:  metricpb.MetricDescriptor_DOUBLE,
-				Labels:     []*label.LabelDescriptor{{Key: opencensusTaskKey}},
-			},
-			agg:     view.Mean(),
-			wantErr: true,
-		},
 		{
 			name: "distribution agg with keys",
 			md: &metricpb.MetricDescriptor{
diff --git a/plugin/ocgrpc/client_metrics_test.go b/plugin/ocgrpc/client_metrics_test.go
index 5a0489e..61bea17 100644
--- a/plugin/ocgrpc/client_metrics_test.go
+++ b/plugin/ocgrpc/client_metrics_test.go
@@ -40,7 +40,7 @@ func TestViewsAggregationsConform(t *testing.T) {
 		}
 	}
 
-	assertTypeOf(ClientErrorCountView, view.Mean())
+	assertTypeOf(ClientErrorCountView, view.Sum())
 	assertTypeOf(ClientRoundTripLatencyView, view.Distribution())
 	assertTypeOf(ClientRequestBytesView, view.Distribution())
 	assertTypeOf(ClientResponseBytesView, view.Distribution())
diff --git a/plugin/ocgrpc/client_stats_handler_test.go b/plugin/ocgrpc/client_stats_handler_test.go
index 90da4e5..a8c4ba5 100644
--- a/plugin/ocgrpc/client_stats_handler_test.go
+++ b/plugin/ocgrpc/client_stats_handler_test.go
@@ -158,7 +158,7 @@ func TestClientDefaultCollections(t *testing.T) {
 								{Key: KeyStatus, Value: "Canceled"},
 								{Key: KeyMethod, Value: "package.service/method"},
 							},
-							Data: newMeanData(1, 1),
+							Data: newCountData(1),
 						},
 					},
 				},
@@ -238,14 +238,14 @@ func TestClientDefaultCollections(t *testing.T) {
 								{Key: KeyStatus, Value: "Canceled"},
 								{Key: KeyMethod, Value: "package.service/method"},
 							},
-							Data: newMeanData(1, 1),
+							Data: newCountData(1),
 						},
 						{
 							Tags: []tag.Tag{
 								{Key: KeyStatus, Value: "Aborted"},
 								{Key: KeyMethod, Value: "package.service/method"},
 							},
-							Data: newMeanData(1, 1),
+							Data: newCountData(1),
 						},
 					},
 				},
diff --git a/plugin/ocgrpc/server_stats_handler_test.go b/plugin/ocgrpc/server_stats_handler_test.go
index 85a315e..d5d0c20 100644
--- a/plugin/ocgrpc/server_stats_handler_test.go
+++ b/plugin/ocgrpc/server_stats_handler_test.go
@@ -358,11 +358,6 @@ func newCountData(v int) *view.CountData {
 	return &cav
 }
 
-func newMeanData(count int64, mean float64) *view.MeanData {
-	mav := view.MeanData{Count: count, Mean: mean}
-	return &mav
-}
-
 func newDistributionData(countPerBucket []int64, count int64, min, max, mean, sumOfSquaredDev float64) *view.DistributionData {
 	return &view.DistributionData{
 		Count:           count,
diff --git a/stats/view/aggregation_data_test.go b/stats/view/aggregation_data_test.go
index 6e9d0e5..e4fcfd2 100644
--- a/stats/view/aggregation_data_test.go
+++ b/stats/view/aggregation_data_test.go
@@ -41,10 +41,6 @@ func TestDataClone(t *testing.T) {
 			name: "distribution data",
 			src:  dist,
 		},
-		{
-			name: "mean data",
-			src:  newMeanData(11.0, 5),
-		},
 		{
 			name: "sum data",
 			src:  newSumData(65.7),
diff --git a/stats/view/view_test.go b/stats/view/view_test.go
index 390a338..2312e0d 100644
--- a/stats/view/view_test.go
+++ b/stats/view/view_test.go
@@ -313,7 +313,7 @@ func TestCanonicalize(t *testing.T) {
 	k1, _ := tag.NewKey("k1")
 	k2, _ := tag.NewKey("k2")
 	m, _ := stats.Int64("TestCanonicalize/m1", "desc desc", stats.UnitNone)
-	v := &View{TagKeys: []tag.Key{k2, k1}, Measure: m, Aggregation: Mean()}
+	v := &View{TagKeys: []tag.Key{k2, k1}, Measure: m, Aggregation: Sum()}
 	err := v.canonicalize()
 	if err != nil {
 		t.Fatal(err)
@@ -332,125 +332,6 @@ func TestCanonicalize(t *testing.T) {
 	}
 }
 
-func Test_View_MeasureFloat64_AggregationMean(t *testing.T) {
-	k1, _ := tag.NewKey("k1")
-	k2, _ := tag.NewKey("k2")
-	k3, _ := tag.NewKey("k3")
-	m, _ := stats.Int64("Test_View_MeasureFloat64_AggregationMean/m1", "", stats.UnitNone)
-	viewDesc := &View{TagKeys: []tag.Key{k1, k2}, Measure: m, Aggregation: Mean()}
-	view, err := newViewInternal(viewDesc)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	type tagString struct {
-		k tag.Key
-		v string
-	}
-	type record struct {
-		f    float64
-		tags []tagString
-	}
-
-	tcs := []struct {
-		label    string
-		records  []record
-		wantRows []*Row
-	}{
-		{
-			"1",
-			[]record{
-				{1, []tagString{{k1, "v1"}}},
-				{5, []tagString{{k1, "v1"}}},
-			},
-			[]*Row{
-				{
-					[]tag.Tag{{Key: k1, Value: "v1"}},
-					newMeanData(3, 2),
-				},
-			},
-		},
-		{
-			"2",
-			[]record{
-				{1, []tagString{{k1, "v1"}}},
-				{5, []tagString{{k2, "v2"}}},
-				{-0.5, []tagString{{k2, "v2"}}},
-			},
-			[]*Row{
-				{
-					[]tag.Tag{{Key: k1, Value: "v1"}},
-					newMeanData(1, 1),
-				},
-				{
-					[]tag.Tag{{Key: k2, Value: "v2"}},
-					newMeanData(2.25, 2),
-				},
-			},
-		},
-		{
-			"3",
-			[]record{
-				{1, []tagString{{k1, "v1"}}},
-				{5, []tagString{{k1, "v1"}, {k3, "v3"}}},
-				{1, []tagString{{k1, "v1 other"}}},
-				{5, []tagString{{k2, "v2"}}},
-				{5, []tagString{{k1, "v1"}, {k2, "v2"}}},
-				{-4, []tagString{{k1, "v1"}, {k2, "v2"}}},
-			},
-			[]*Row{
-				{
-					[]tag.Tag{{Key: k1, Value: "v1"}},
-					newMeanData(3, 2),
-				},
-				{
-					[]tag.Tag{{Key: k1, Value: "v1 other"}},
-					newMeanData(1, 1),
-				},
-				{
-					[]tag.Tag{{Key: k2, Value: "v2"}},
-					newMeanData(5, 1),
-				},
-				{
-					[]tag.Tag{{Key: k1, Value: "v1"}, {Key: k2, Value: "v2"}},
-					newMeanData(0.5, 2),
-				},
-			},
-		},
-	}
-
-	for _, tt := range tcs {
-		view.clearRows()
-		view.subscribe()
-		for _, r := range tt.records {
-			mods := []tag.Mutator{}
-			for _, t := range r.tags {
-				mods = append(mods, tag.Insert(t.k, t.v))
-			}
-			ctx, err := tag.New(context.Background(), mods...)
-			if err != nil {
-				t.Errorf("%v: New = %v", tt.label, err)
-			}
-			view.addSample(tag.FromContext(ctx), r.f)
-		}
-
-		gotRows := view.collectedRows()
-		for i, got := range gotRows {
-			if !containsRow(tt.wantRows, got) {
-				t.Errorf("%v-%d: got row %v; want none", tt.label, i, got)
-				break
-			}
-		}
-
-		for i, want := range tt.wantRows {
-			if !containsRow(gotRows, want) {
-				t.Errorf("%v-%d: got none; want row %v", tt.label, i, want)
-				break
-			}
-		}
-	}
-}
-
 func TestViewSortedKeys(t *testing.T) {
 	k1, _ := tag.NewKey("a")
 	k2, _ := tag.NewKey("b")
@@ -463,7 +344,7 @@ func TestViewSortedKeys(t *testing.T) {
 		Description: "desc sort_keys",
 		TagKeys:     ks,
 		Measure:     m,
-		Aggregation: Mean(),
+		Aggregation: Sum(),
 	})
 	// Subscribe normalizes the view by sorting the tag keys, retrieve the normalized view
 	v := Find("sort_keys")
diff --git a/tag/map_codec_test.go b/tag/map_codec_test.go
index 3327ada..abb0ead 100644
--- a/tag/map_codec_test.go
+++ b/tag/map_codec_test.go
@@ -67,7 +67,7 @@ func TestEncodeDecode(t *testing.T) {
 			[]keyValue{
 				{k1, "v1"},
 				{k2, "v2"},
-				{k3, "v3"},
+				{k3, "数据库着火了 🔥🔥🔥"},
 				{k4, "v4 is very weird <>.,?/'\";:`~!@#$%^&*()_-+={[}]|\\"},
 			},
 		},
@@ -107,6 +107,7 @@ func TestEncodeDecode(t *testing.T) {
 func TestDecode(t *testing.T) {
 	k1, _ := NewKey("k1")
 	ctx, _ := New(context.Background(), Insert(k1, "v1"))
+	ctx2, _ := New(context.Background(), Insert(k1, "数据库着火了 🔥🔥🔥"))
 
 	tests := []struct {
 		name    string
@@ -121,14 +122,14 @@ func TestDecode(t *testing.T) {
 			wantErr: false,
 		},
 		{
-			name:    "non-ascii key",
-			bytes:   []byte{0, 0, 2, 107, 49, 2, 118, 49, 0, 2, 107, 25, 2, 118, 49},
-			want:    nil,
-			wantErr: true,
+			name:    "valid (non-ascii value)",
+			bytes:   []byte{0x0, 0x0, 0x2, 0x6b, 0x31, 0x1f, 0xe6, 0x95, 0xb0, 0xe6, 0x8d, 0xae, 0xe5, 0xba, 0x93, 0xe7, 0x9d, 0x80, 0xe7, 0x81, 0xab, 0xe4, 0xba, 0x86, 0x20, 0xf0, 0x9f, 0x94, 0xa5, 0xf0, 0x9f, 0x94, 0xa5, 0xf0, 0x9f, 0x94, 0xa5},
+			want:    FromContext(ctx2),
+			wantErr: false,
 		},
 		{
-			name:    "non-ascii value",
-			bytes:   []byte{0, 0, 2, 107, 49, 2, 118, 49, 0, 2, 107, 50, 2, 118, 25},
+			name:    "non-ascii key",
+			bytes:   []byte{0, 0, 2, 107, 49, 2, 118, 49, 0, 2, 107, 25, 2, 118, 49},
 			want:    nil,
 			wantErr: true,
 		},
@@ -147,7 +148,11 @@ func TestDecode(t *testing.T) {
 				return
 			}
 			if !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("Decode() = %v, want %v", got, tt.want)
+				var encoded []byte
+				if tt.want != nil {
+					encoded = Encode(tt.want)
+				}
+				t.Errorf("Decode() = %v, want %v = Decode(%#v)", got, tt.want, encoded)
 			}
 		})
 	}
diff --git a/tag/map_test.go b/tag/map_test.go
index 6d27047..aca6e88 100644
--- a/tag/map_test.go
+++ b/tag/map_test.go
@@ -129,9 +129,7 @@ func TestNewMap(t *testing.T) {
 			name:    "from empty; invalid",
 			initial: nil,
 			mods: []Mutator{
-				Insert(k5, "v\x19"),
-				Upsert(k5, "v\x19"),
-				Update(k5, "v\x19"),
+				Insert(k5, strings.Repeat("x", 256)),
 			},
 			want: nil,
 		},
@@ -140,31 +138,33 @@ func TestNewMap(t *testing.T) {
 			initial: nil,
 			mods: []Mutator{
 				Insert(k5, "v1"),
-				Update(k5, "v\x19"),
+				Update(k5, strings.Repeat("x", 256)),
 			},
 			want: nil,
 		},
 	}
 
 	for _, tt := range tests {
-		mods := []Mutator{
-			Insert(k1, "v1"),
-			Insert(k2, "v2"),
-			Update(k3, "v3"),
-			Upsert(k4, "v4"),
-			Insert(k2, "v2"),
-			Delete(k1),
-		}
-		mods = append(mods, tt.mods...)
-		ctx := NewContext(context.Background(), tt.initial)
-		ctx, err := New(ctx, mods...)
-		if tt.want != nil && err != nil {
-			t.Errorf("%v: New = %v", tt.name, err)
-		}
+		t.Run(tt.name, func(t *testing.T) {
+			mods := []Mutator{
+				Insert(k1, "v1"),
+				Insert(k2, "v2"),
+				Update(k3, "v3"),
+				Upsert(k4, "v4"),
+				Insert(k2, "v2"),
+				Delete(k1),
+			}
+			mods = append(mods, tt.mods...)
+			ctx := NewContext(context.Background(), tt.initial)
+			ctx, err := New(ctx, mods...)
+			if tt.want != nil && err != nil {
+				t.Errorf("New = %v", err)
+			}
 
-		if got, want := FromContext(ctx), tt.want; !reflect.DeepEqual(got, want) {
-			t.Errorf("%v: got %v; want %v", tt.name, got, want)
-		}
+			if got, want := FromContext(ctx), tt.want; !reflect.DeepEqual(got, want) {
+				t.Errorf("got %v; want %v", got, want)
+			}
+		})
 	}
 }
 
@@ -183,7 +183,7 @@ func TestNewMapValidation(t *testing.T) {
 		// Value validation
 		{err: "", seed: &Map{m: map[Key]string{{name: "key"}: ""}}},
 		{err: "", seed: &Map{m: map[Key]string{{name: "key"}: strings.Repeat("a", 255)}}},
-		{err: "invalid value", seed: &Map{m: map[Key]string{{name: "key"}: "Приве́т"}}},
+		{err: "", seed: &Map{m: map[Key]string{{name: "key"}: "Приве́т"}}},
 		{err: "invalid value", seed: &Map{m: map[Key]string{{name: "key"}: strings.Repeat("a", 256)}}},
 	}
 
diff --git a/tag/validate_test.go b/tag/validate_test.go
index 371a775..553069b 100644
--- a/tag/validate_test.go
+++ b/tag/validate_test.go
@@ -75,16 +75,6 @@ func TestCheckValue(t *testing.T) {
 			value:  "v1",
 			wantOK: true,
 		},
-		{
-			name:   "invalid i",
-			value:  "k\x19",
-			wantOK: false,
-		},
-		{
-			name:   "invalid ii",
-			value:  "k\x7f",
-			wantOK: false,
-		},
 		{
 			name:   "empty",
 			value:  "",
@@ -100,6 +90,21 @@ func TestCheckValue(t *testing.T) {
 			value:  strings.Repeat("a", 256),
 			wantOK: false,
 		},
+		{
+			name:   "emoji",
+			value:  "🔥🔥🔥",
+			wantOK: true,
+		},
+		{
+			name:   "Simplified Chinese",
+			value:  "上海",
+			wantOK: true,
+		},
+		{
+			name:   "Bengali",
+			value:  "বাংলা/বঙ্গ",
+			wantOK: true,
+		},
 	}
 	for _, tt := range tests {
 		ok := checkValue(tt.value)