event: metric-specific builders

This CL imagines that each kind of metric is its own type, and has its
own builder.

Notes:

- Metric values are checked by the language. For example, we can enforce that
  counter values are unsigned integers.
  That means, however, that we either have to provide metric types for each common
  value type, or we have to wait for generics to allow a variety of types.

- Builder methods can be tailored to metrics. For example, metric
  builders have no way to override the metric's namespace. But it's
  not clear that's the right choice, and I think that's the only
  method that we might want to omit.

- In theory, users can define their own metric types. In practice,
  they'd have to duplicate a lot of our unexported machinery, or we'd
  have to export it somehow.

- You can't add common labels to a single Builder and re-use that builder
  for both metrics and other events.

Change-Id: Ie99cb3b953e1670fb8d6910f9412885e5dcbc219
Reviewed-on: https://go-review.googlesource.com/c/exp/+/326289
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
diff --git a/event/bench/event_test.go b/event/bench/event_test.go
index 76c0c9c..637c621 100644
--- a/event/bench/event_test.go
+++ b/event/bench/event_test.go
@@ -68,17 +68,17 @@
 		},
 	}
 
-	gauge       = event.NewMetric(event.Gauge, "gauge", "m")
+	gauge       = event.NewFloatGauge("gauge")
 	eventMetric = Hooks{
 		AStart: func(ctx context.Context, a int) context.Context {
-			event.To(ctx).With(aStat.Of(a)).Metric(gauge, event.Int64Of(1))
-			event.To(ctx).With(aCount.Of(1)).Metric(gauge, event.Int64Of(1))
+			gauge.To(ctx).With(aStat.Of(a)).Record(1)
+			gauge.To(ctx).With(aCount.Of(1)).Record(1)
 			return ctx
 		},
 		AEnd: func(ctx context.Context) {},
 		BStart: func(ctx context.Context, b string) context.Context {
-			event.To(ctx).With(bLength.Of(len(b))).Metric(gauge, event.Int64Of(1))
-			event.To(ctx).With(bCount.Of(1)).Metric(gauge, event.Int64Of(1))
+			gauge.To(ctx).With(bLength.Of(len(b))).Record(1)
+			gauge.To(ctx).With(bCount.Of(1)).Record(1)
 			return ctx
 		},
 		BEnd: func(ctx context.Context) {},
diff --git a/event/builder.go b/event/builder.go
index 0d90441..5b14267 100644
--- a/event/builder.go
+++ b/event/builder.go
@@ -16,6 +16,10 @@
 
 // Builder is a fluent builder for construction of new events.
 type Builder struct {
+	builderCommon
+}
+
+type builderCommon struct {
 	ctx       context.Context
 	data      *builder
 	builderID uint64 // equals data.id if all is well
@@ -39,21 +43,28 @@
 
 // To initializes a builder from the values stored in a context.
 func To(ctx context.Context) Builder {
-	b := Builder{ctx: ctx}
-	exporter, parent := FromContext(ctx)
-	if exporter == nil {
-		return b
+	b := Builder{builderCommon{ctx: ctx}}
+	b.data = newBuilder(ctx)
+	if b.data != nil {
+		b.builderID = b.data.id
 	}
-	b.data = allocBuilder()
-	b.builderID = b.data.id
-	b.data.exporter = exporter
-	b.data.Event.Labels = b.data.labels[:0]
-	b.data.Event.Parent = parent
 	return b
 }
 
 var builderID uint64 // atomic
 
+func newBuilder(ctx context.Context) *builder {
+	exporter, parent := FromContext(ctx)
+	if exporter == nil {
+		return nil
+	}
+	b := allocBuilder()
+	b.exporter = exporter
+	b.Event.Labels = b.labels[:0]
+	b.Event.Parent = parent
+	return b
+}
+
 func allocBuilder() *builder {
 	b := builderPool.Get().(*builder)
 	b.id = atomic.AddUint64(&builderID, 1)
@@ -63,12 +74,16 @@
 // Clone returns a copy of this builder.
 // The two copies can be independently delivered.
 func (b Builder) Clone() Builder {
+	return Builder{b.clone()}
+}
+
+func (b builderCommon) clone() builderCommon {
 	if b.data == nil {
 		return b
 	}
 	bb := allocBuilder()
 	bbid := bb.id
-	clone := Builder{ctx: b.ctx, data: bb, builderID: bb.id}
+	clone := builderCommon{ctx: b.ctx, data: bb, builderID: bb.id}
 	*clone.data = *b.data
 	clone.data.id = bbid
 	if len(b.data.Event.Labels) == 0 || &b.data.labels[0] == &b.data.Event.Labels[0] {
@@ -82,17 +97,26 @@
 
 // With adds a new label to the event being constructed.
 func (b Builder) With(label Label) Builder {
-	if b.data != nil {
-		checkValid(b.data, b.builderID)
-		b.data.Event.Labels = append(b.data.Event.Labels, label)
-	}
+	b.addLabel(label)
 	return b
 }
 
+func (b builderCommon) addLabel(label Label) {
+	if b.data != nil {
+		b.data.Event.Labels = append(b.data.Event.Labels, label)
+		checkValid(b.data, b.builderID)
+	}
+}
+
 // WithAll adds all the supplied labels to the event being constructed.
 func (b Builder) WithAll(labels ...Label) Builder {
+	b.addLabels(labels)
+	return b
+}
+
+func (b builderCommon) addLabels(labels []Label) {
 	if b.data == nil || len(labels) == 0 {
-		return b
+		return
 	}
 	checkValid(b.data, b.builderID)
 	if len(b.data.Event.Labels) == 0 {
@@ -100,23 +124,30 @@
 	} else {
 		b.data.Event.Labels = append(b.data.Event.Labels, labels...)
 	}
-	return b
 }
 
 func (b Builder) At(t time.Time) Builder {
+	b.setAt(t)
+	return b
+}
+
+func (b builderCommon) setAt(t time.Time) {
 	if b.data != nil {
 		checkValid(b.data, b.builderID)
 		b.data.Event.At = t
 	}
-	return b
 }
 
 func (b Builder) Namespace(ns string) Builder {
+	b.setNamespace(ns)
+	return b
+}
+
+func (b builderCommon) setNamespace(ns string) {
 	if b.data != nil {
 		checkValid(b.data, b.builderID)
 		b.data.Event.Namespace = ns
 	}
-	return b
 }
 
 // Log is a helper that calls Deliver with LogKind.
@@ -155,34 +186,34 @@
 }
 
 // Metric is a helper that calls Deliver with MetricKind.
-func (b Builder) Metric(m *Metric, value Value) {
-	if b.data == nil {
-		return
-	}
-	checkValid(b.data, b.builderID)
-	if b.data.exporter.metricsEnabled() {
-		b.data.exporter.mu.Lock()
-		defer b.data.exporter.mu.Unlock()
-		if b.data.Event.Namespace == "" {
-			b.data.Event.Namespace = m.Namespace()
-		}
-		b.data.Event.Labels = append(b.data.Event.Labels, MetricValue.Of(value), MetricKey.Of(ValueOf(m)))
-		b.data.exporter.prepare(&b.data.Event)
-		b.data.exporter.handler.Metric(b.ctx, &b.data.Event)
-	}
-	b.done()
-}
+// func (b Builder) Metric(m *Metric, value Value) {
+// 	if b.data == nil {
+// 		return
+// 	}
+// 	checkValid(b.data, b.builderID)
+// 	if b.data.exporter.metricsEnabled() {
+// 		b.data.exporter.mu.Lock()
+// 		defer b.data.exporter.mu.Unlock()
+// 		if b.data.Event.Namespace == "" {
+// 			b.data.Event.Namespace = m.Namespace()
+// 		}
+// 		b.data.Event.Labels = append(b.data.Event.Labels, MetricValue.Of(value), MetricKey.Of(ValueOf(m)))
+// 		b.data.exporter.prepare(&b.data.Event)
+// 		b.data.exporter.handler.Metric(b.ctx, &b.data.Event)
+// 	}
+// 	b.done()
+// }
 
-func (b Builder) Count(m *Metric) {
-	if m.Kind() != Counter {
-		panic("Builder.Count called on non-counter")
-	}
-	b.Metric(m, Int64Of(1))
-}
+// func (b Builder) Count(m *Metric) {
+// 	if m.Kind() != Counter {
+// 		panic("Builder.Count called on non-counter")
+// 	}
+// 	b.Metric(m, Int64Of(1))
+// }
 
-func (b Builder) Since(m *Metric, start time.Time) {
-	b.Metric(m, DurationOf(time.Since(start)))
-}
+// func (b Builder) Since(m *Metric, start time.Time) {
+// 	b.Metric(m, DurationOf(time.Since(start)))
+// }
 
 // Annotate is a helper that calls Deliver with AnnotateKind.
 func (b Builder) Annotate() {
@@ -226,7 +257,7 @@
 	return &clone
 }
 
-func (b Builder) done() {
+func (b builderCommon) done() {
 	*b.data = builder{}
 	builderPool.Put(b.data)
 }
diff --git a/event/common_test.go b/event/common_test.go
index a3658c7..6470f1f 100644
--- a/event/common_test.go
+++ b/event/common_test.go
@@ -16,7 +16,7 @@
 func TestCommon(t *testing.T) {
 	h := &catchHandler{}
 	ctx := event.WithExporter(context.Background(), event.NewExporter(h, nil))
-	m := event.NewMetric(event.Counter, "m", "")
+	m := event.NewCounter("m")
 
 	const simple = "simple message"
 	const trace = "a trace"
@@ -25,7 +25,7 @@
 	checkFind(t, h, "Log", event.Message, true, simple)
 	checkFind(t, h, "Log", event.Name, false, "")
 
-	event.To(ctx).Metric(m, event.Int64Of(3))
+	m.To(ctx).Record(3)
 	checkFind(t, h, "Metric", event.Message, false, "")
 	checkFind(t, h, "Metric", event.Name, false, "")
 
diff --git a/event/event_test.go b/event/event_test.go
index cf6e0cb..210f29c 100644
--- a/event/event_test.go
+++ b/event/event_test.go
@@ -23,8 +23,9 @@
 	l1      = keys.Int("l1").Of(1)
 	l2      = keys.Int("l2").Of(2)
 	l3      = keys.Int("l3").Of(3)
-	gauge   = event.NewMetric(event.Gauge, "gauge", "")
-	latency = event.NewMetric(event.Distribution, "latency", "")
+	counter = event.NewCounter("hits")
+	gauge   = event.NewFloatGauge("temperature")
+	latency = event.NewDuration("latency")
 )
 
 func TestPrint(t *testing.T) {
@@ -74,15 +75,19 @@
 time=2020-03-05T14:27:51 parent=2 end
 time=2020-03-05T14:27:52 parent=1 end
 `}, {
-		name:   "metric",
-		events: func(ctx context.Context) { event.To(ctx).With(l1).Metric(gauge, event.Int64Of(2)) },
-		expect: `time=2020-03-05T14:27:48 l1=1 metricValue=2 metric=Gauge("golang.org/x/exp/event_test/gauge")`,
+		name:   "counter",
+		events: func(ctx context.Context) { counter.To(ctx).With(l1).Record(2) },
+		expect: `time=2020-03-05T14:27:48 l1=1 metricValue=2 metric=Metric("golang.org/x/exp/event_test/hits")`,
 	}, {
-		name: "metric 2",
+		name:   "gauge",
+		events: func(ctx context.Context) { gauge.To(ctx).With(l1).Record(98.6) },
+		expect: `time=2020-03-05T14:27:48 l1=1 metricValue=98.6 metric=Metric("golang.org/x/exp/event_test/temperature")`,
+	}, {
+		name: "duration",
 		events: func(ctx context.Context) {
-			event.To(ctx).With(l1).With(l2).Metric(latency, event.DurationOf(3*time.Second))
+			latency.To(ctx).With(l1).With(l2).Record(3 * time.Second)
 		},
-		expect: `time=2020-03-05T14:27:48 l1=1 l2=2 metricValue=3s metric=Distribution("golang.org/x/exp/event_test/latency")`,
+		expect: `time=2020-03-05T14:27:48 l1=1 l2=2 metricValue=3s metric=Metric("golang.org/x/exp/event_test/latency")`,
 	}, {
 		name:   "annotate",
 		events: func(ctx context.Context) { event.To(ctx).With(l1).Annotate() },
diff --git a/event/metric.go b/event/metric.go
index 6e30323..15861b3 100644
--- a/event/metric.go
+++ b/event/metric.go
@@ -4,67 +4,49 @@
 
 package event
 
-import "fmt"
-
-// MetricKind represents the kind of a Metric.
-type MetricKind int
-
-const (
-	// A Counter is a metric that always increases, usually by 1.
-	Counter MetricKind = iota
-	// A Gauge is a metric that may go up or down.
-	Gauge
-	// A Distribution is a metric for which a summary of values is tracked.
-	Distribution
+import (
+	"context"
+	"fmt"
+	"time"
 )
 
-func (k MetricKind) String() string {
-	switch k {
-	case Counter:
-		return "Counter"
-	case Gauge:
-		return "Gauge"
-	case Distribution:
-		return "Distribution"
-	default:
-		return "!unknownMetricKind"
-	}
+type Metric interface {
+	Descriptor() MetricDescriptor
 }
 
-type Metric struct {
-	kind        MetricKind
+type MetricDescriptor struct {
 	namespace   string
 	name        string
-	description string
+	Description string
 	// For unit, 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.
 }
 
-func NewMetric(kind MetricKind, name, description string) *Metric {
+// TODO: how to force a non-empty namespace?
+
+func NewMetricDescriptor(name string) MetricDescriptor {
 	if name == "" {
 		panic("name cannot be empty")
 	}
-	m := &Metric{
-		kind:        kind,
-		name:        name,
-		description: description,
-	}
+	m := MetricDescriptor{name: name}
+	// TODO: make this work right whether called from in this package or externally.
 	// Set namespace to the caller's import path.
 	// Depth:
 	//   0  runtime.Callers
 	//   1  importPath
 	//   2  this function
-	//   3  caller of this function
-	m.namespace = importPath(3, nil)
+	//   3  caller of this function (one of the NewXXX methods in this package)
+	//   4  caller's caller
+	m.namespace = importPath(4, nil)
 	return m
 }
 
-func (m *Metric) String() string {
-	return fmt.Sprintf("%s(\"%s/%s\")", m.kind, m.namespace, m.name)
+func (m *MetricDescriptor) String() string {
+	return fmt.Sprintf("Metric(\"%s/%s\")", m.namespace, m.name)
 }
 
-func (m *Metric) WithNamespace(ns string) *Metric {
+func (m *MetricDescriptor) WithNamespace(ns string) *MetricDescriptor {
 	if ns == "" {
 		panic("namespace cannot be empty")
 	}
@@ -72,7 +54,145 @@
 	return m
 }
 
-func (m *Metric) Kind() MetricKind    { return m.kind }
-func (m *Metric) Name() string        { return m.name }
-func (m *Metric) Namespace() string   { return m.namespace }
-func (m *Metric) Description() string { return m.description }
+func (m *MetricDescriptor) Name() string      { return m.name }
+func (m *MetricDescriptor) Namespace() string { return m.namespace }
+
+// A counter is a metric that counts something cumulatively.
+type Counter struct {
+	MetricDescriptor
+}
+
+func NewCounter(name string) *Counter {
+	return &Counter{NewMetricDescriptor(name)}
+}
+
+func (c *Counter) Descriptor() MetricDescriptor {
+	return c.MetricDescriptor
+}
+
+func (c *Counter) To(ctx context.Context) CounterBuilder {
+	b := CounterBuilder{builderCommon: builderCommon{ctx: ctx}, c: c}
+	b.data = newBuilder(ctx)
+	if b.data != nil {
+		b.builderID = b.data.id
+	}
+	return b
+}
+
+type CounterBuilder struct {
+	builderCommon
+	c *Counter
+}
+
+func (b CounterBuilder) With(label Label) CounterBuilder {
+	b.addLabel(label)
+	return b
+}
+
+func (b CounterBuilder) WithAll(labels ...Label) CounterBuilder {
+	b.addLabels(labels)
+	return b
+}
+
+func (b CounterBuilder) Record(v uint64) {
+	record(b.builderCommon, b.c, Uint64Of(v))
+}
+
+func record(b builderCommon, m Metric, v Value) {
+	if b.data == nil {
+		return
+	}
+	checkValid(b.data, b.builderID)
+	if b.data.exporter.metricsEnabled() {
+		b.data.exporter.mu.Lock()
+		defer b.data.exporter.mu.Unlock()
+		b.data.Event.Labels = append(b.data.Event.Labels, MetricValue.Of(v), MetricKey.Of(ValueOf(m)))
+		b.data.exporter.prepare(&b.data.Event)
+		b.data.exporter.handler.Metric(b.ctx, &b.data.Event)
+	}
+	b.done()
+}
+
+// A FloatGauge records a single floating-point value that may go up or down.
+// TODO(generics): Gauge[T]
+type FloatGauge struct {
+	MetricDescriptor
+}
+
+func NewFloatGauge(name string) *FloatGauge {
+	return &FloatGauge{NewMetricDescriptor(name)}
+}
+
+func (g *FloatGauge) Descriptor() MetricDescriptor {
+	return g.MetricDescriptor
+}
+
+func (g *FloatGauge) To(ctx context.Context) FloatGaugeBuilder {
+	b := FloatGaugeBuilder{builderCommon: builderCommon{ctx: ctx}, g: g}
+	b.data = newBuilder(ctx)
+	if b.data != nil {
+		b.builderID = b.data.id
+	}
+	return b
+}
+
+type FloatGaugeBuilder struct {
+	builderCommon
+	g *FloatGauge
+}
+
+func (b FloatGaugeBuilder) With(label Label) FloatGaugeBuilder {
+	b.addLabel(label)
+	return b
+}
+
+func (b FloatGaugeBuilder) WithAll(labels ...Label) FloatGaugeBuilder {
+	b.addLabels(labels)
+	return b
+}
+
+func (b FloatGaugeBuilder) Record(v float64) {
+	record(b.builderCommon, b.g, Float64Of(v))
+}
+
+// A Duration records a distribution of durations.
+// TODO(generics): Distribution[T]
+type Duration struct {
+	MetricDescriptor
+}
+
+func NewDuration(name string) *Duration {
+	return &Duration{NewMetricDescriptor(name)}
+}
+
+func (d *Duration) Descriptor() MetricDescriptor {
+	return d.MetricDescriptor
+}
+
+func (d *Duration) To(ctx context.Context) DurationBuilder {
+	b := DurationBuilder{builderCommon: builderCommon{ctx: ctx}, d: d}
+	b.data = newBuilder(ctx)
+	if b.data != nil {
+		b.builderID = b.data.id
+	}
+	return b
+}
+
+type DurationBuilder struct {
+	builderCommon
+	d *Duration
+}
+
+func (b DurationBuilder) With(label Label) DurationBuilder {
+	b.addLabel(label)
+	return b
+}
+
+func (b DurationBuilder) WithAll(labels ...Label) DurationBuilder {
+	b.addLabels(labels)
+	return b
+}
+
+func (b DurationBuilder) Record(v time.Duration) {
+	record(b.builderCommon, b.d, DurationOf(v))
+}