event/otel: metrics

Add an event handler for OpenTelemetry metrics.

The first time it sees an event.Metric, the handler creates a matching
otel instrument and caches it. On each call, it uses the instrument to
record the metric value.

Change-Id: I07d6f40601c7d2a801ed9fbe3cf7c24d5698f3f1
Reviewed-on: https://go-review.googlesource.com/c/exp/+/320350
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
diff --git a/event/common.go b/event/common.go
index 2bd82a4..f12b84a 100644
--- a/event/common.go
+++ b/event/common.go
@@ -11,7 +11,7 @@
 )
 
 const (
-	MetricKey      = "metric"
+	MetricKey      = interfaceKey("metric")
 	MetricVal      = "metricValue"
 	DurationMetric = interfaceKey("durationMetric")
 )
diff --git a/event/event_test.go b/event/event_test.go
index d918bcd..1757a97 100644
--- a/event/event_test.go
+++ b/event/event_test.go
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build !disable_events
 // +build !disable_events
 
 package event_test
@@ -24,9 +25,9 @@
 	l1      = event.Int64("l1", 1)
 	l2      = event.Int64("l2", 2)
 	l3      = event.Int64("l3", 3)
-	counter = event.NewCounter("hits", "cache hits")
-	gauge   = event.NewFloatGauge("temperature", "CPU board temperature in Celsius")
-	latency = event.NewDuration("latency", "how long it took")
+	counter = event.NewCounter("hits", nil)
+	gauge   = event.NewFloatGauge("temperature", nil)
+	latency = event.NewDuration("latency", nil)
 	err     = errors.New("an error")
 )
 
@@ -275,7 +276,7 @@
 
 func TestTraceDuration(t *testing.T) {
 	// Verify that a trace can can emit a latency metric.
-	dur := event.NewDuration("test", "")
+	dur := event.NewDuration("test", nil)
 	want := time.Second
 
 	check := func(t *testing.T, h *testTraceDurationHandler) {
@@ -313,7 +314,7 @@
 
 func (t *testTraceDurationHandler) Event(ctx context.Context, ev *event.Event) context.Context {
 	for _, l := range ev.Labels {
-		if l.Name == event.MetricVal {
+		if l.Name == string(event.MetricVal) {
 			t.got = l
 		}
 	}
@@ -322,7 +323,7 @@
 
 func BenchmarkBuildContext(b *testing.B) {
 	// How long does it take to deliver an event from a nested context?
-	c := event.NewCounter("c", "")
+	c := event.NewCounter("c", nil)
 	for _, depth := range []int{1, 5, 7, 10} {
 		b.Run(fmt.Sprintf("depth %d", depth), func(b *testing.B) {
 			ctx := event.WithExporter(context.Background(), event.NewExporter(nopHandler{}, eventtest.ExporterOptions()))
diff --git a/event/metric.go b/event/metric.go
index a603a10..e74689e 100644
--- a/event/metric.go
+++ b/event/metric.go
@@ -6,81 +6,71 @@
 
 import (
 	"context"
-	"fmt"
 	"time"
 )
 
+// A Unit is a unit of measurement for a metric.
+type Unit string
+
+const (
+	UnitDimensionless Unit = "1"
+	UnitBytes         Unit = "By"
+	UnitMilliseconds  Unit = "ms"
+)
+
 // A Metric represents a kind of recorded measurement.
 type Metric interface {
-	Descriptor() *MetricDescriptor
+	Name() string
+	Options() MetricOptions
 }
 
-// A MetricDescriptor describes a metric.
-type MetricDescriptor struct {
-	namespace   string
-	name        string
-	description string
-	// TODO: deal with units. Follow otel, or define Go types for common units.
-	// We don't need a time unit because we'll use time.Duration, and the only
-	// other unit otel currently defines (besides dimensionless) is bytes.
-}
+type MetricOptions struct {
+	// A string that should be common for all metrics of an application or
+	// service. Defaults to the import path of the package calling
+	// the metric construction function (NewCounter, etc.).
+	Namespace string
 
-// NewMetricDescriptor creates a MetricDescriptor with the given name.
-// The namespace defaults to the import path of the caller of NewMetricDescriptor.
-// Use SetNamespace to provide a different one.
-// Neither the name nor the namespace can be empty.
-func NewMetricDescriptor(name, description string) *MetricDescriptor {
-	return newMetricDescriptor(name, description)
-}
+	// Optional description of the metric.
+	Description string
 
-func newMetricDescriptor(name, description string) *MetricDescriptor {
-	if name == "" {
-		panic("name cannot be empty")
-	}
-	return &MetricDescriptor{
-		name:        name,
-		namespace:   scanStack().Space,
-		description: description,
-	}
+	// Optional unit for the metric. Defaults to UnitDimensionless.
+	Unit Unit
 }
 
-// SetNamespace sets the namespace of m to a non-empty string.
-func (m *MetricDescriptor) SetNamespace(ns string) {
-	if ns == "" {
-		panic("namespace cannot be empty")
-	}
-	m.namespace = ns
-}
-
-func (m *MetricDescriptor) String() string {
-	return fmt.Sprintf("Metric(\"%s/%s\")", m.namespace, m.name)
-}
-
-func (m *MetricDescriptor) Name() string        { return m.name }
-func (m *MetricDescriptor) Namespace() string   { return m.namespace }
-func (m *MetricDescriptor) Description() string { return m.description }
-
 // A Counter is a metric that counts something cumulatively.
 type Counter struct {
-	*MetricDescriptor
+	name string
+	opts MetricOptions
+}
+
+func initOpts(popts *MetricOptions) MetricOptions {
+	var opts MetricOptions
+	if popts != nil {
+		opts = *popts
+	}
+	if opts.Namespace == "" {
+		opts.Namespace = scanStack().Space
+	}
+	if opts.Unit == "" {
+		opts.Unit = UnitDimensionless
+	}
+	return opts
 }
 
 // NewCounter creates a counter with the given name.
-func NewCounter(name, description string) *Counter {
-	return &Counter{newMetricDescriptor(name, description)}
+func NewCounter(name string, opts *MetricOptions) *Counter {
+	return &Counter{name, initOpts(opts)}
 }
 
-// Descriptor returns the receiver's MetricDescriptor.
-func (c *Counter) Descriptor() *MetricDescriptor {
-	return c.MetricDescriptor
-}
+func (c *Counter) Name() string           { return c.name }
+func (c *Counter) Options() MetricOptions { return c.opts }
 
 // Record delivers a metric event with the given metric, value and labels to the
 // exporter in the context.
 func (c *Counter) Record(ctx context.Context, v int64, labels ...Label) {
 	ev := New(ctx, MetricKind)
 	if ev != nil {
-		record(ev, c, Int64(MetricVal, v))
+		record(ev, c, Int64(string(MetricVal), v))
 		ev.Labels = append(ev.Labels, labels...)
 		ev.Deliver()
 	}
@@ -89,26 +79,24 @@
 // A FloatGauge records a single floating-point value that may go up or down.
 // TODO(generics): Gauge[T]
 type FloatGauge struct {
-	*MetricDescriptor
+	name string
+	opts MetricOptions
 }
 
 // NewFloatGauge creates a new FloatGauge with the given name.
-func NewFloatGauge(name, description string) *FloatGauge {
-	return &FloatGauge{newMetricDescriptor(name, description)}
+func NewFloatGauge(name string, opts *MetricOptions) *FloatGauge {
+	return &FloatGauge{name, initOpts(opts)}
 }
 
-// Descriptor returns the receiver's MetricDescriptor.
-func (g *FloatGauge) Descriptor() *MetricDescriptor {
-	return g.MetricDescriptor
-}
+func (g *FloatGauge) Name() string           { return g.name }
+func (g *FloatGauge) Options() MetricOptions { return g.opts }
 
 // Record converts its argument into a Value and returns a MetricValue with the
-// receiver and the value. It is intended to be used as an argument to
-// Builder.Metric.
+// receiver and the value.
 func (g *FloatGauge) Record(ctx context.Context, v float64, labels ...Label) {
 	ev := New(ctx, MetricKind)
 	if ev != nil {
-		record(ev, g, Float64(MetricVal, v))
+		record(ev, g, Float64(string(MetricVal), v))
 		ev.Labels = append(ev.Labels, labels...)
 		ev.Deliver()
 	}
@@ -117,26 +105,24 @@
 // A DurationDistribution records a distribution of durations.
 // TODO(generics): Distribution[T]
 type DurationDistribution struct {
-	*MetricDescriptor
+	name string
+	opts MetricOptions
 }
 
 // NewDuration creates a new Duration with the given name.
-func NewDuration(name, description string) *DurationDistribution {
-	return &DurationDistribution{newMetricDescriptor(name, description)}
+func NewDuration(name string, opts *MetricOptions) *DurationDistribution {
+	return &DurationDistribution{name, initOpts(opts)}
 }
 
-// Descriptor returns the receiver's MetricDescriptor.
-func (d *DurationDistribution) Descriptor() *MetricDescriptor {
-	return d.MetricDescriptor
-}
+func (d *DurationDistribution) Name() string           { return d.name }
+func (d *DurationDistribution) Options() MetricOptions { return d.opts }
 
 // Record converts its argument into a Value and returns a MetricValue with the
-// receiver and the value. It is intended to be used as an argument to
-// Builder.Metric.
+// receiver and the value.
 func (d *DurationDistribution) Record(ctx context.Context, v time.Duration, labels ...Label) {
 	ev := New(ctx, MetricKind)
 	if ev != nil {
-		record(ev, d, Duration(MetricVal, v))
+		record(ev, d, Duration(string(MetricVal), v))
 		ev.Labels = append(ev.Labels, labels...)
 		ev.Deliver()
 	}
@@ -144,31 +130,29 @@
 
 // An IntDistribution records a distribution of int64s.
 type IntDistribution struct {
-	*MetricDescriptor
+	name string
+	opts MetricOptions
 }
 
+func (d *IntDistribution) Name() string           { return d.name }
+func (d *IntDistribution) Options() MetricOptions { return d.opts }
+
 // NewIntDistribution creates a new IntDistribution with the given name.
-func NewIntDistribution(name, description string) *IntDistribution {
-	return &IntDistribution{newMetricDescriptor(name, description)}
-}
-
-// Descriptor returns the receiver's MetricDescriptor.
-func (d *IntDistribution) Descriptor() *MetricDescriptor {
-	return d.MetricDescriptor
+func NewIntDistribution(name string, opts *MetricOptions) *IntDistribution {
+	return &IntDistribution{name, initOpts(opts)}
 }
 
 // Record converts its argument into a Value and returns a MetricValue with the
-// receiver and the value. It is intended to be used as an argument to
-// Builder.Metric.
+// receiver and the value.
 func (d *IntDistribution) Record(ctx context.Context, v int64, labels ...Label) {
 	ev := New(ctx, MetricKind)
 	if ev != nil {
-		record(ev, d, Int64(MetricVal, v))
+		record(ev, d, Int64(string(MetricVal), v))
 		ev.Labels = append(ev.Labels, labels...)
 		ev.Deliver()
 	}
 }
 
 func record(ev *Event, m Metric, l Label) {
-	ev.Labels = append(ev.Labels, l, Value(MetricKey, m))
+	ev.Labels = append(ev.Labels, l, MetricKey.Of(m))
 }
diff --git a/event/otel/metric.go b/event/otel/metric.go
new file mode 100644
index 0000000..910cc94
--- /dev/null
+++ b/event/otel/metric.go
@@ -0,0 +1,131 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package otel
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"sync"
+
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/metric"
+	otelunit "go.opentelemetry.io/otel/metric/unit"
+	"golang.org/x/exp/event"
+)
+
+// MetricHandler is an event.Handler for OpenTelemetry metrics.
+// Its Event method handles Metric events and ignores all others.
+type MetricHandler struct {
+	meter metric.MeterMust
+	mu    sync.Mutex
+	// A map from event.Metrics to, effectively, otel Meters.
+	// But since the only thing we need from the Meter is recording a value, we
+	// use a function for that that closes over the Meter itself.
+	recordFuncs map[event.Metric]recordFunc
+}
+
+type recordFunc func(context.Context, event.Label, []attribute.KeyValue)
+
+var _ event.Handler = (*MetricHandler)(nil)
+
+// NewMetricHandler creates a new MetricHandler.
+func NewMetricHandler(m metric.Meter) *MetricHandler {
+	return &MetricHandler{
+		meter:       metric.Must(m),
+		recordFuncs: map[event.Metric]recordFunc{},
+	}
+}
+
+func (m *MetricHandler) Event(ctx context.Context, e *event.Event) context.Context {
+	if e.Kind != event.MetricKind {
+		return ctx
+	}
+	// Get the otel instrument corresponding to the event's MetricDescriptor,
+	// or create a new one.
+	mi, ok := event.MetricKey.Find(e)
+	if !ok {
+		panic(errors.New("no metric key for metric event"))
+	}
+	em := mi.(event.Metric)
+	lval := e.Find(event.MetricVal)
+	if !lval.HasValue() {
+		panic(errors.New("no metric value for metric event"))
+	}
+	rf := m.getRecordFunc(em)
+	if rf == nil {
+		panic(fmt.Errorf("unable to record for metric %v", em))
+	}
+	rf(ctx, lval, labelsToAttributes(e.Labels))
+	return ctx
+}
+
+func (m *MetricHandler) getRecordFunc(em event.Metric) recordFunc {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	if f, ok := m.recordFuncs[em]; ok {
+		return f
+	}
+	f := m.newRecordFunc(em)
+	m.recordFuncs[em] = f
+	return f
+}
+
+func (m *MetricHandler) newRecordFunc(em event.Metric) recordFunc {
+	opts := em.Options()
+	name := opts.Namespace + "/" + em.Name()
+	otelOpts := []metric.InstrumentOption{
+		metric.WithDescription(opts.Description),
+		metric.WithUnit(otelunit.Unit(opts.Unit)), // cast OK: same strings
+	}
+	switch em.(type) {
+	case *event.Counter:
+		c := m.meter.NewInt64Counter(name, otelOpts...)
+		return func(ctx context.Context, l event.Label, attrs []attribute.KeyValue) {
+			c.Add(ctx, l.Int64(), attrs...)
+		}
+
+	case *event.FloatGauge:
+		g := m.meter.NewFloat64UpDownCounter(name, otelOpts...)
+		return func(ctx context.Context, l event.Label, attrs []attribute.KeyValue) {
+			g.Add(ctx, l.Float64(), attrs...)
+		}
+
+	case *event.DurationDistribution:
+		r := m.meter.NewInt64Histogram(name, otelOpts...)
+		return func(ctx context.Context, l event.Label, attrs []attribute.KeyValue) {
+			r.Record(ctx, l.Duration().Nanoseconds(), attrs...)
+		}
+
+	default:
+		return nil
+	}
+}
+
+func labelsToAttributes(ls []event.Label) []attribute.KeyValue {
+	var attrs []attribute.KeyValue
+	for _, l := range ls {
+		if l.Name == string(event.MetricKey) || l.Name == string(event.MetricVal) {
+			continue
+		}
+		attrs = append(attrs, labelToAttribute(l))
+	}
+	return attrs
+}
+
+func labelToAttribute(l event.Label) attribute.KeyValue {
+	switch {
+	case l.IsString():
+		return attribute.String(l.Name, l.String())
+	case l.IsInt64():
+		return attribute.Int64(l.Name, l.Int64())
+	case l.IsFloat64():
+		return attribute.Float64(l.Name, l.Float64())
+	case l.IsBool():
+		return attribute.Bool(l.Name, l.Bool())
+	default: // including uint64
+		panic(fmt.Errorf("cannot convert label value of type %T to attribute.KeyValue", l.Interface()))
+	}
+}
diff --git a/event/otel/metric_test.go b/event/otel/metric_test.go
new file mode 100644
index 0000000..300f4a7
--- /dev/null
+++ b/event/otel/metric_test.go
@@ -0,0 +1,75 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package otel_test
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/metric/metrictest"
+	"go.opentelemetry.io/otel/metric/number"
+	"golang.org/x/exp/event"
+	"golang.org/x/exp/event/otel"
+)
+
+func TestMeter(t *testing.T) {
+	ctx := context.Background()
+	mp := metrictest.NewMeterProvider()
+	mh := otel.NewMetricHandler(mp.Meter("test"))
+	ctx = event.WithExporter(ctx, event.NewExporter(mh, nil))
+	recordMetrics(ctx)
+
+	lib := metrictest.Library{InstrumentationName: "test"}
+	emptyLabels := map[attribute.Key]attribute.Value{}
+	got := metrictest.AsStructs(mp.MeasurementBatches)
+	want := []metrictest.Measured{
+		{
+			Name:    "golang.org/x/exp/event/otel_test/hits",
+			Number:  number.NewInt64Number(8),
+			Labels:  emptyLabels,
+			Library: lib,
+		},
+		{
+			Name:    "golang.org/x/exp/event/otel_test/temp",
+			Number:  number.NewFloat64Number(-100),
+			Labels:  map[attribute.Key]attribute.Value{"location": attribute.StringValue("Mare Imbrium")},
+			Library: lib,
+		},
+		{
+			Name:    "golang.org/x/exp/event/otel_test/latency",
+			Number:  number.NewInt64Number(int64(1248 * time.Millisecond)),
+			Labels:  emptyLabels,
+			Library: lib,
+		},
+		{
+			Name:    "golang.org/x/exp/event/otel_test/latency",
+			Number:  number.NewInt64Number(int64(1255 * time.Millisecond)),
+			Labels:  emptyLabels,
+			Library: lib,
+		},
+	}
+
+	if diff := cmp.Diff(want, got, cmp.Comparer(valuesEqual)); diff != "" {
+		t.Errorf("mismatch (-want, got):\n%s", diff)
+	}
+}
+
+func valuesEqual(v1, v2 attribute.Value) bool {
+	return v1.AsInterface() == v2.AsInterface()
+}
+
+func recordMetrics(ctx context.Context) {
+	c := event.NewCounter("hits", &event.MetricOptions{Description: "Earth meteorite hits"})
+	g := event.NewFloatGauge("temp", &event.MetricOptions{Description: "moon surface temperature in Kelvin"})
+	d := event.NewDuration("latency", &event.MetricOptions{Description: "Earth-moon comms lag, milliseconds"})
+
+	c.Record(ctx, 8)
+	g.Record(ctx, -100, event.String("location", "Mare Imbrium"))
+	d.Record(ctx, 1248*time.Millisecond)
+	d.Record(ctx, 1255*time.Millisecond)
+}
diff --git a/go.mod b/go.mod
index 7cd7c67..cc530ad 100644
--- a/go.mod
+++ b/go.mod
@@ -11,6 +11,8 @@
 	github.com/google/go-cmp v0.5.6
 	github.com/rs/zerolog v1.21.0
 	github.com/sirupsen/logrus v1.8.1
+	go.opentelemetry.io/otel v1.3.0
+	go.opentelemetry.io/otel/metric v0.26.0
 	go.opentelemetry.io/otel/sdk v1.3.0
 	go.opentelemetry.io/otel/trace v1.3.0
 	go.uber.org/zap v1.16.0
@@ -25,10 +27,8 @@
 require (
 	github.com/go-logfmt/logfmt v0.5.0 // indirect
 	github.com/go-logr/stdr v1.2.0 // indirect
-	go.opentelemetry.io/otel v1.3.0 // indirect
-	go.opentelemetry.io/otel/metric v0.20.0 // indirect
+	go.opentelemetry.io/otel/internal/metric v0.26.0 // indirect
 	go.uber.org/atomic v1.7.0 // indirect
 	go.uber.org/multierr v1.6.0 // indirect
 	honnef.co/go/tools v0.1.3 // indirect
 )
-
diff --git a/go.sum b/go.sum
index d7654bd..1445e46 100644
--- a/go.sum
+++ b/go.sum
@@ -69,8 +69,6 @@
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4=
 github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
-github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc=
-github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
 github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.2.2 h1:ahHml/yUpnlb96Rp8HCvtYVPY8ZYpxq3g7UYchIYwbs=
@@ -97,8 +95,6 @@
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
-github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -259,26 +255,19 @@
 github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
 go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
 go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opentelemetry.io/otel v0.20.0 h1:eaP0Fqu7SXHwvjiqDq83zImeehOHX8doTvU9AwXON8g=
-go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo=
 go.opentelemetry.io/otel v1.3.0 h1:APxLf0eiBwLl+SOXiJJCVYzA1OOJNyAoV8C5RNRyy7Y=
 go.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs=
-go.opentelemetry.io/otel/metric v0.20.0 h1:4kzhXFP+btKm4jwxpjIqjs41A7MakRFUS86bqLHTIw8=
-go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU=
-go.opentelemetry.io/otel/oteltest v0.20.0 h1:HiITxCawalo5vQzdHfKeZurV8x7ljcqAgiWzF6Vaeaw=
-go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw=
-go.opentelemetry.io/otel/sdk v0.20.0 h1:JsxtGXd06J8jrnya7fdI/U/MR6yXA5DtbZy+qoHQlr8=
-go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc=
+go.opentelemetry.io/otel/internal/metric v0.26.0 h1:dlrvawyd/A+X8Jp0EBT4wWEe4k5avYaXsXrBr4dbfnY=
+go.opentelemetry.io/otel/internal/metric v0.26.0/go.mod h1:CbBP6AxKynRs3QCbhklyLUtpfzbqCLiafV9oY2Zj1Jk=
+go.opentelemetry.io/otel/metric v0.26.0 h1:VaPYBTvA13h/FsiWfxa3yZnZEm15BhStD8JZQSA773M=
+go.opentelemetry.io/otel/metric v0.26.0/go.mod h1:c6YL0fhRo4YVoNs6GoByzUgBp36hBL523rECoZA5UWg=
 go.opentelemetry.io/otel/sdk v1.3.0 h1:3278edCoH89MEJ0Ky8WQXVmDQv3FX4ZJ3Pp+9fJreAI=
 go.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs=
-go.opentelemetry.io/otel/trace v0.20.0 h1:1DL6EXUdcg95gukhuRRvLDO/4X5THh/5dIV52lqtnbw=
-go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw=
 go.opentelemetry.io/otel/trace v1.3.0 h1:doy8Hzb1RJ+I3yFhtDmwNc7tIyw1tNMOIsyPzp1NOGY=
 go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk=
 go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@@ -303,7 +292,6 @@
 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
@@ -323,7 +311,6 @@
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
 golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 h1:LQmS1nU0twXLA96Kt7U9qtHJEbBk3z6Q0V4UXjZkpr4=
 golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -343,8 +330,6 @@
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -354,7 +339,6 @@
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -372,19 +356,13 @@
 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0=
 golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/jsonrpc2/defs.go b/jsonrpc2/defs.go
index 80b25c1..e3677de 100644
--- a/jsonrpc2/defs.go
+++ b/jsonrpc2/defs.go
@@ -14,11 +14,20 @@
 func StatusCode(v string) event.Label   { return event.String("status.code", v) }
 
 var (
-	Started       = event.NewCounter("started", "Count of started RPCs.")
-	Finished      = event.NewCounter("finished", "Count of finished RPCs (includes error).")
-	ReceivedBytes = event.NewIntDistribution("received_bytes", "Bytes received.") //, unit.Bytes)
-	SentBytes     = event.NewIntDistribution("sent_bytes", "Bytes sent.")         //, unit.Bytes)
-	Latency       = event.NewDuration("latency", "Elapsed time of an RPC.")       //, unit.Milliseconds)
+	Started       = event.NewCounter("started", &event.MetricOptions{Description: "Count of started RPCs."})
+	Finished      = event.NewCounter("finished", &event.MetricOptions{Description: "Count of finished RPCs (includes error)."})
+	ReceivedBytes = event.NewIntDistribution("received_bytes", &event.MetricOptions{
+		Description: "Bytes received.",
+		Unit:        event.UnitBytes,
+	})
+	SentBytes = event.NewIntDistribution("sent_bytes", &event.MetricOptions{
+		Description: "Bytes sent.",
+		Unit:        event.UnitBytes,
+	})
+	Latency = event.NewDuration("latency", &event.MetricOptions{
+		Description: "Elapsed time of an RPC.",
+		Unit:        event.UnitMilliseconds,
+	})
 )
 
 const (