internal/lsp: add new stats library

This is the basic library that allows for recording of stats about the program
operation.

Change-Id: I09f7e3de5fc37aaf29bc0db46f15b15056fc0eb2
Reviewed-on: https://go-review.googlesource.com/c/tools/+/185338
Run-TryBot: Ian Cottrell <iancottrell@google.com>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/jsonrpc2/jsonrpc2.go b/internal/jsonrpc2/jsonrpc2.go
index 52b0188..4e9cd70 100644
--- a/internal/jsonrpc2/jsonrpc2.go
+++ b/internal/jsonrpc2/jsonrpc2.go
@@ -16,7 +16,6 @@
 	"time"
 
 	"golang.org/x/tools/internal/lsp/telemetry"
-	"golang.org/x/tools/internal/lsp/telemetry/stats"
 	"golang.org/x/tools/internal/lsp/telemetry/tag"
 	"golang.org/x/tools/internal/lsp/telemetry/trace"
 )
@@ -81,18 +80,12 @@
 type Canceler func(context.Context, *Conn, ID)
 
 type rpcStats struct {
-	server   bool
-	method   string
-	span     trace.Span
-	start    time.Time
-	received int64
-	sent     int64
+	server bool
+	method string
+	span   trace.Span
+	start  time.Time
 }
 
-type statsKeyType string
-
-const rpcStatsKey = statsKeyType("rpcStatsKey")
-
 func start(ctx context.Context, server bool, method string, id *ID) (context.Context, *rpcStats) {
 	if method == "" {
 		panic("no method in rpc stats")
@@ -102,7 +95,6 @@
 		method: method,
 		start:  time.Now(),
 	}
-	ctx = context.WithValue(ctx, rpcStatsKey, s)
 	mode := telemetry.Outbound
 	if server {
 		mode = telemetry.Inbound
@@ -112,7 +104,7 @@
 		tag.Tag{Key: telemetry.RPCDirection, Value: mode},
 		tag.Tag{Key: telemetry.RPCID, Value: id},
 	)
-	stats.Record(ctx, telemetry.Started.M(1))
+	telemetry.Started.Record(ctx, 1)
 	return ctx, s
 }
 
@@ -124,13 +116,7 @@
 	}
 	elapsedTime := time.Since(s.start)
 	latencyMillis := float64(elapsedTime) / float64(time.Millisecond)
-
-	stats.Record(ctx,
-		telemetry.ReceivedBytes.M(s.received),
-		telemetry.SentBytes.M(s.sent),
-		telemetry.Latency.M(latencyMillis),
-	)
-
+	telemetry.Latency.Record(ctx, latencyMillis)
 	s.span.End()
 }
 
@@ -199,7 +185,7 @@
 	}
 	c.Logger(Send, nil, -1, request.Method, request.Params, nil)
 	n, err := c.stream.Write(ctx, data)
-	rpcStats.sent += n
+	telemetry.SentBytes.Record(ctx, n)
 	return err
 }
 
@@ -241,7 +227,7 @@
 	before := time.Now()
 	c.Logger(Send, request.ID, -1, request.Method, request.Params, nil)
 	n, err := c.stream.Write(ctx, data)
-	rpcStats.sent += n
+	telemetry.SentBytes.Record(ctx, n)
 	if err != nil {
 		// sending failed, we will never get a response, so don't leave it pending
 		return err
@@ -336,13 +322,7 @@
 	}
 	r.conn.Logger(Send, response.ID, elapsed, r.Method, response.Result, response.Error)
 	n, err := r.conn.stream.Write(ctx, data)
-
-	v := ctx.Value(rpcStatsKey)
-	if v != nil {
-		v.(*rpcStats).sent += n
-	} else {
-		panic("no stats available in reply")
-	}
+	telemetry.SentBytes.Record(ctx, n)
 
 	if err != nil {
 		// TODO(iancottrell): if a stream write fails, we really need to shut down
@@ -407,7 +387,7 @@
 			// if method is set it must be a request
 			reqCtx, cancelReq := context.WithCancel(ctx)
 			reqCtx, rpcStats := start(reqCtx, true, msg.Method, msg.ID)
-			rpcStats.received += n
+			telemetry.ReceivedBytes.Record(ctx, n)
 			thisRequest := nextRequest
 			nextRequest = make(chan struct{})
 			req := &Request{
diff --git a/internal/lsp/debug/serve.go b/internal/lsp/debug/serve.go
index 73cbcd1..c7c6a26 100644
--- a/internal/lsp/debug/serve.go
+++ b/internal/lsp/debug/serve.go
@@ -19,7 +19,6 @@
 	"strconv"
 	"sync"
 
-	"golang.org/x/tools/internal/lsp/telemetry"
 	"golang.org/x/tools/internal/span"
 )
 
@@ -217,7 +216,6 @@
 		mux := http.NewServeMux()
 		mux.HandleFunc("/", Render(mainTmpl, func(*http.Request) interface{} { return data }))
 		mux.HandleFunc("/debug/", Render(debugTmpl, nil))
-		telemetry.Handle(mux)
 		mux.HandleFunc("/debug/pprof/", pprof.Index)
 		mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
 		mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
diff --git a/internal/lsp/telemetry/stats/stats.go b/internal/lsp/telemetry/stats/stats.go
index cade415..6da3ed3 100644
--- a/internal/lsp/telemetry/stats/stats.go
+++ b/internal/lsp/telemetry/stats/stats.go
@@ -3,45 +3,103 @@
 // license that can be found in the LICENSE file.
 
 // Package stats provides support for recording telemetry statistics.
+// It acts as a coordination point between things that want to record stats,
+// and things that want to aggregate and report stats.
 package stats
 
-import "context"
-
-type Measure interface {
-	Name() string
-	Description() string
-	Unit() string
-}
-
-type Float64Measure interface {
-	Measure
-	M(v float64) Measurement
-}
-
-type Int64Measure interface {
-	Measure
-	M(v int64) Measurement
-}
-
-type Measurement interface {
-	Measure() Measure
-	Value() float64
-}
-
-type nullMeasure struct{}
-type nullFloat64Measure struct{ nullMeasure }
-type nullInt64Measure struct{ nullMeasure }
-
-func (nullMeasure) Name() string        { return "" }
-func (nullMeasure) Description() string { return "" }
-func (nullMeasure) Unit() string        { return "" }
-
-func (nullFloat64Measure) M(v float64) Measurement { return nil }
-func (nullInt64Measure) M(v int64) Measurement     { return nil }
-
-func NullFloat64Measure() Float64Measure { return nullFloat64Measure{} }
-func NullInt64Measure() Int64Measure     { return nullInt64Measure{} }
-
-var (
-	Record = func(ctx context.Context, ms ...Measurement) {}
+import (
+	"context"
 )
+
+// Int64Measure is used to record integer values.
+type Int64Measure struct {
+	name        string
+	description string
+	unit        Unit
+	subscribers []Int64Subscriber
+}
+
+// Int64Measure is used to record floating point values.
+type Float64Measure struct {
+	name        string
+	description string
+	unit        Unit
+	subscribers []Float64Subscriber
+}
+
+// Int64Subscriber is the type for functions that want to listen to
+// integer statistic events.
+type Int64Subscriber func(context.Context, *Int64Measure, int64)
+
+// Float64Subscriber is the type for functions that want to listen to
+// floating point statistic events.
+type Float64Subscriber func(context.Context, *Float64Measure, float64)
+
+// Unit is used to specify the units for a given measure.
+// This is can used for display purposes.
+type Unit int
+
+const (
+	// UnitDimensionless indicates that a measure has no specified units.
+	UnitDimensionless = Unit(iota)
+	// UnitBytes indicates that that a measure is recording number of bytes.
+	UnitBytes
+	// UnitMilliseconds indicates that a measure is recording a duration in milliseconds.
+	UnitMilliseconds
+)
+
+// Int64 creates a new Int64Measure and prepares it for use.
+func Int64(name string, description string, unit Unit) *Int64Measure {
+	return &Int64Measure{
+		name:        name,
+		description: description,
+		unit:        unit,
+	}
+}
+
+// Float64 creates a new Float64Measure and prepares it for use.
+func Float64(name string, description string, unit Unit) *Float64Measure {
+	return &Float64Measure{
+		name:        name,
+		description: description,
+		unit:        unit,
+	}
+}
+
+// Name returns the name this measure was given on construction.
+func (m *Int64Measure) Name() string { return m.name }
+
+// Description returns the description this measure was given on construction.
+func (m *Int64Measure) Description() string { return m.description }
+
+// Unit returns the units this measure was given on construction.
+func (m *Int64Measure) Unit() Unit { return m.unit }
+
+// Subscribe adds a new subscriber to this measure.
+func (m *Int64Measure) Subscribe(s Int64Subscriber) { m.subscribers = append(m.subscribers, s) }
+
+// Record delivers a new value to the subscribers of this measure.
+func (m *Int64Measure) Record(ctx context.Context, value int64) {
+	for _, s := range m.subscribers {
+		s(ctx, m, value)
+	}
+}
+
+// Name returns the name this measure was given on construction.
+func (m *Float64Measure) Name() string { return m.name }
+
+// Description returns the description this measure was given on construction.
+func (m *Float64Measure) Description() string { return m.description }
+
+// Unit returns the units this measure was given on construction.
+func (m *Float64Measure) Unit() Unit { return m.unit }
+
+// Subscribe adds a new subscriber to this measure.
+func (m *Float64Measure) Subscribe(s Float64Subscriber) { m.subscribers = append(m.subscribers, s) }
+
+// Record delivers a new value to the subscribers of this measure.
+func (m *Float64Measure) Record(ctx context.Context, value float64) {
+	for _, s := range m.subscribers {
+		s(ctx, m, value)
+	}
+}
diff --git a/internal/lsp/telemetry/telemetry.go b/internal/lsp/telemetry/telemetry.go
index f97c1e5..6b56c07 100644
--- a/internal/lsp/telemetry/telemetry.go
+++ b/internal/lsp/telemetry/telemetry.go
@@ -7,8 +7,6 @@
 package telemetry
 
 import (
-	"net/http"
-
 	"golang.org/x/tools/internal/lsp/telemetry/stats"
 	"golang.org/x/tools/internal/lsp/telemetry/tag"
 )
@@ -25,12 +23,11 @@
 )
 
 var (
-	Handle = func(mux *http.ServeMux) {}
-
-	Started       = stats.NullInt64Measure()
-	ReceivedBytes = stats.NullInt64Measure()
-	SentBytes     = stats.NullInt64Measure()
-	Latency       = stats.NullFloat64Measure()
+	// create the stats we measure
+	Started       = stats.Int64("started", "Count of started RPCs.", stats.UnitDimensionless)
+	ReceivedBytes = stats.Int64("received_bytes", "Bytes received.", stats.UnitBytes)
+	SentBytes     = stats.Int64("sent_bytes", "Bytes sent.", stats.UnitBytes)
+	Latency       = stats.Float64("latency_ms", "Elapsed time in milliseconds", stats.UnitMilliseconds)
 )
 
 const (