Repository: census-instrumentation/opencensus-go (go)

Issue:
Prometheus View of runtime.MemStats
Motivation
-----------

I'm not sure if this belongs in this repo but I think there's some common runtime stats that most monitored services would want to expose similar to those available in `/debug/pprof`. I would see this feature being responsible for creating a measurement and view for each of the fields in the `runtime.MemStats` struct.

Impact
-------

As this would be a new function this is an API expansion. The performance impact of adding this view is expected to be approximately equivalent to the cost of calling `runtime.ReadMemStats` and populating all of the measurements.

Proposed Implementation
--------------------------

Add a package `go.opencensus.io/stats/view/runtime` with the following functions and data structs;

- `New(prefix string) RuntimeViews`.
- `RuntimeViews.Views() []View` - returns a collection of all of the runtime views.
- `RuntimeViews.Start(freq uint64)` - starts the background polling goroutine.
- `RuntimeViews.Stop()` - stops the background polling goroutine.

**param - prefix**

- Used to prefix each of the stats.

**param - freq**

- Not sure if this is desirable or if it should make the `ReadMemStats` call on each request.
- Idea would be to periodically call `ReadMemStats` in a background goroutine and cache the values until the timer has triggered.

**return - []View**

- Each element in the `[]View` would correspond to a field in the MemStats struct.
- Descriptions would be the first line of each of the fields in the MemStats struct documentation.
- The full names would be `${prefix}/runtime/${SNAKE_CASE_FIELD_NAME}` (I'll map these out more fully later).

Pull request description:
Add runtime metrics support
This adds a `metricproducer.Producer` implementation under `/plugin/` which enables the collection of runtime metrics.
 
Fixes #784 

Patch:
diff --git a/README.md b/README.md
index a8cd09e..1d7e837 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,8 @@ OpenCensus Go is a Go implementation of OpenCensus, a toolkit for
 collecting application performance and behavior monitoring data.
 Currently it consists of three major components: tags, stats and tracing.
 
+#### OpenCensus and OpenTracing have merged to form OpenTelemetry, which serves as the next major version of OpenCensus and OpenTracing. OpenTelemetry will offer backwards compatibility with existing OpenCensus integrations, and we will continue to make security patches to existing OpenCensus libraries for two years. Read more about the merger [here](https://medium.com/opentracing/a-roadmap-to-convergence-b074e5815289).
+
 ## Installation
 
 ```
@@ -57,6 +59,7 @@ can implement their own exporters by implementing the exporter interfaces
 * [Datadog][exporter-datadog] for stats and traces
 * [Graphite][exporter-graphite] for stats
 * [Honeycomb][exporter-honeycomb] for traces
+* [New Relic][exporter-newrelic] for stats and traces
 
 ## Overview
 
@@ -261,3 +264,4 @@ release in which the functionality was marked *Deprecated*.
 [exporter-datadog]: https://github.com/DataDog/opencensus-go-exporter-datadog
 [exporter-graphite]: https://github.com/census-ecosystem/opencensus-go-exporter-graphite
 [exporter-honeycomb]: https://github.com/honeycombio/opencensus-exporter
+[exporter-newrelic]: https://github.com/newrelic/newrelic-opencensus-exporter-go
diff --git a/go.mod b/go.mod
index 7c1886e..139157c 100644
--- a/go.mod
+++ b/go.mod
@@ -4,9 +4,11 @@ require (
 	github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6
 	github.com/golang/protobuf v1.3.1
 	github.com/google/go-cmp v0.3.0
+	github.com/stretchr/testify v1.4.0
 	golang.org/x/net v0.0.0-20190620200207-3b0461eec859
 	golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd // indirect
 	golang.org/x/text v0.3.2 // indirect
+	google.golang.org/appengine v1.4.0 // indirect
 	google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb // indirect
 	google.golang.org/grpc v1.20.1
 )
diff --git a/go.sum b/go.sum
index 212b6b7..ed2a1d8 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,8 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I=
@@ -12,6 +14,11 @@ github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -60,4 +67,7 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/plugin/ochttp/server.go b/plugin/ochttp/server.go
index dc6563a..c7ea642 100644
--- a/plugin/ochttp/server.go
+++ b/plugin/ochttp/server.go
@@ -70,6 +70,12 @@ type Handler struct {
 	// from the information found in the incoming HTTP Request. By default the
 	// name equals the URL Path.
 	FormatSpanName func(*http.Request) string
+
+	// IsHealthEndpoint holds the function to use for determining if the
+	// incoming HTTP request should be considered a health check. This is in
+	// addition to the private isHealthEndpoint func which may also indicate
+	// tracing should be skipped.
+	IsHealthEndpoint func(*http.Request) bool
 }
 
 func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -87,7 +93,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 }
 
 func (h *Handler) startTrace(w http.ResponseWriter, r *http.Request) (*http.Request, func()) {
-	if isHealthEndpoint(r.URL.Path) {
+	if h.IsHealthEndpoint != nil && h.IsHealthEndpoint(r) || isHealthEndpoint(r.URL.Path) {
 		return r, func() {}
 	}
 	var name string
diff --git a/plugin/runmetrics/doc.go b/plugin/runmetrics/doc.go
new file mode 100644
index 0000000..2bb53d4
--- /dev/null
+++ b/plugin/runmetrics/doc.go
@@ -0,0 +1,23 @@
+// Copyright 2019, 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 runmetrics contains support for runtime metrics.
+//
+// To enable collecting runtime metrics, just call Enable():
+//
+//     _ := runmetrics.Enable(runmetrics.RunMetricOptions{
+//         EnableCPU: true,
+//         EnableMemory: true,
+//     })
+package runmetrics // import "go.opencensus.io/plugin/runmetrics"
diff --git a/plugin/runmetrics/producer.go b/plugin/runmetrics/producer.go
new file mode 100644
index 0000000..eb307fe
--- /dev/null
+++ b/plugin/runmetrics/producer.go
@@ -0,0 +1,290 @@
+package runmetrics
+
+import (
+	"errors"
+	"go.opencensus.io/metric"
+	"go.opencensus.io/metric/metricdata"
+	"go.opencensus.io/metric/metricproducer"
+	"runtime"
+	"sync"
+)
+
+type (
+	// producer produces runtime metrics.
+	//
+	// Enable collection of runtime metrics with Enable().
+	producer struct {
+		options RunMetricOptions
+		reg     *metric.Registry
+
+		memStats *memStats
+		cpuStats *cpuStats
+	}
+
+	// RunMetricOptions allows to configure runtime metrics.
+	RunMetricOptions struct {
+		EnableCPU    bool   // EnableCPU whether CPU metrics shall be recorded
+		EnableMemory bool   // EnableMemory whether memory metrics shall be recorded
+		Prefix       string // Prefix is a custom prefix for metric names
+	}
+
+	memStats struct {
+		memStats runtime.MemStats
+
+		memAlloc   *metric.Int64GaugeEntry
+		memTotal   *metric.Int64GaugeEntry
+		memSys     *metric.Int64GaugeEntry
+		memLookups *metric.Int64GaugeEntry
+		memMalloc  *metric.Int64GaugeEntry
+		memFrees   *metric.Int64GaugeEntry
+
+		heapAlloc    *metric.Int64GaugeEntry
+		heapSys      *metric.Int64GaugeEntry
+		heapIdle     *metric.Int64GaugeEntry
+		heapInuse    *metric.Int64GaugeEntry
+		heapObjects  *metric.Int64GaugeEntry
+		heapReleased *metric.Int64GaugeEntry
+
+		stackInuse       *metric.Int64GaugeEntry
+		stackSys         *metric.Int64GaugeEntry
+		stackMSpanInuse  *metric.Int64GaugeEntry
+		stackMSpanSys    *metric.Int64GaugeEntry
+		stackMCacheInuse *metric.Int64GaugeEntry
+		stackMCacheSys   *metric.Int64GaugeEntry
+	}
+
+	cpuStats struct {
+		numGoroutines *metric.Int64GaugeEntry
+		numCgoCalls   *metric.Int64GaugeEntry
+	}
+)
+
+var (
+	_               metricproducer.Producer = (*producer)(nil)
+	enableMutex     sync.Mutex
+	enabledProducer *producer
+)
+
+// Enable enables collection of runtime metrics.
+//
+// Supply RunMetricOptions to configure the behavior of metrics collection.
+// An error might be returned, if creating metrics gauges fails.
+//
+// Previous calls will be overwritten by subsequent ones.
+func Enable(options RunMetricOptions) error {
+	producer := &producer{options: options, reg: metric.NewRegistry()}
+	var err error
+
+	if options.EnableMemory {
+		producer.memStats, err = newMemStats(producer)
+		if err != nil {
+			return err
+		}
+	}
+
+	if options.EnableCPU {
+		producer.cpuStats, err = newCPUStats(producer)
+		if err != nil {
+			return err
+		}
+	}
+
+	enableMutex.Lock()
+	defer enableMutex.Unlock()
+
+	metricproducer.GlobalManager().DeleteProducer(enabledProducer)
+	metricproducer.GlobalManager().AddProducer(producer)
+	enabledProducer = producer
+
+	return nil
+}
+
+// Disable disables collection of runtime metrics.
+func Disable() {
+	enableMutex.Lock()
+	defer enableMutex.Unlock()
+
+	metricproducer.GlobalManager().DeleteProducer(enabledProducer)
+	enabledProducer = nil
+}
+
+// Read reads the current runtime metrics.
+func (p *producer) Read() []*metricdata.Metric {
+	if p.memStats != nil {
+		p.memStats.read()
+	}
+
+	if p.cpuStats != nil {
+		p.cpuStats.read()
+	}
+
+	return p.reg.Read()
+}
+
+func newMemStats(producer *producer) (*memStats, error) {
+	var err error
+	memStats := &memStats{}
+
+	// General
+	memStats.memAlloc, err = producer.createInt64GaugeEntry("process/memory_alloc", "Number of bytes currently allocated in use", metricdata.UnitBytes)
+	if err != nil {
+		return nil, err
+	}
+
+	memStats.memTotal, err = producer.createInt64GaugeEntry("process/total_memory_alloc", "Number of allocations in total", metricdata.UnitBytes)
+	if err != nil {
+		return nil, err
+	}
+
+	memStats.memSys, err = producer.createInt64GaugeEntry("process/sys_memory_alloc", "Number of bytes given to the process to use in total", metricdata.UnitBytes)
+	if err != nil {
+		return nil, err
+	}
+
+	memStats.memLookups, err = producer.createInt64GaugeEntry("process/memory_lookups", "Number of pointer lookups performed by the runtime", metricdata.UnitDimensionless)
+	if err != nil {
+		return nil, err
+	}
+
+	memStats.memMalloc, err = producer.createInt64GaugeEntry("process/memory_malloc", "Cumulative count of heap objects allocated", metricdata.UnitDimensionless)
+	if err != nil {
+		return nil, err
+	}
+
+	memStats.memFrees, err = producer.createInt64GaugeEntry("process/memory_frees", "Cumulative count of heap objects freed", metricdata.UnitDimensionless)
+	if err != nil {
+		return nil, err
+	}
+
+	// Heap
+	memStats.heapAlloc, err = producer.createInt64GaugeEntry("process/heap_alloc", "Process heap allocation", metricdata.UnitBytes)
+	if err != nil {
+		return nil, err
+	}
+
+	memStats.heapSys, err = producer.createInt64GaugeEntry("process/sys_heap", "Bytes of heap memory obtained from the OS", metricdata.UnitBytes)
+	if err != nil {
+		return nil, err
+	}
+
+	memStats.heapIdle, err = producer.createInt64GaugeEntry("process/heap_idle", "Bytes in idle (unused) spans", metricdata.UnitBytes)
+	if err != nil {
+		return nil, err
+	}
+
+	memStats.heapInuse, err = producer.createInt64GaugeEntry("process/heap_inuse", "Bytes in in-use spans", metricdata.UnitBytes)
+	if err != nil {
+		return nil, err
+	}
+
+	memStats.heapObjects, err = producer.createInt64GaugeEntry("process/heap_objects", "The number of objects allocated on the heap", metricdata.UnitDimensionless)
+	if err != nil {
+		return nil, err
+	}
+
+	memStats.heapReleased, err = producer.createInt64GaugeEntry("process/heap_release", "The number of objects released from the heap", metricdata.UnitBytes)
+	if err != nil {
+		return nil, err
+	}
+
+	// Stack
+	memStats.stackInuse, err = producer.createInt64GaugeEntry("process/stack_inuse", "Bytes in stack spans", metricdata.UnitBytes)
+	if err != nil {
+		return nil, err
+	}
+
+	memStats.stackSys, err = producer.createInt64GaugeEntry("process/sys_stack", "The memory used by stack spans and OS thread stacks", metricdata.UnitBytes)
+	if err != nil {
+		return nil, err
+	}
+
+	memStats.stackMSpanInuse, err = producer.createInt64GaugeEntry("process/stack_mspan_inuse", "Bytes of allocated mspan structures", metricdata.UnitBytes)
+	if err != nil {
+		return nil, err
+	}
+
+	memStats.stackMSpanSys, err = producer.createInt64GaugeEntry("process/sys_stack_mspan", "Bytes of memory obtained from the OS for mspan structures", metricdata.UnitBytes)
+	if err != nil {
+		return nil, err
+	}
+
+	memStats.stackMCacheInuse, err = producer.createInt64GaugeEntry("process/stack_mcache_inuse", "Bytes of allocated mcache structures", metricdata.UnitBytes)
+	if err != nil {
+		return nil, err
+	}
+
+	memStats.stackMCacheSys, err = producer.createInt64GaugeEntry("process/sys_stack_mcache", "Bytes of memory obtained from the OS for mcache structures", metricdata.UnitBytes)
+	if err != nil {
+		return nil, err
+	}
+
+	return memStats, nil
+}
+
+func (m *memStats) read() {
+	runtime.ReadMemStats(&m.memStats)
+
+	m.memAlloc.Set(int64(m.memStats.Alloc))
+	m.memTotal.Set(int64(m.memStats.TotalAlloc))
+	m.memSys.Set(int64(m.memStats.Sys))
+	m.memLookups.Set(int64(m.memStats.Lookups))
+	m.memMalloc.Set(int64(m.memStats.Mallocs))
+	m.memFrees.Set(int64(m.memStats.Frees))
+
+	m.heapAlloc.Set(int64(m.memStats.HeapAlloc))
+	m.heapSys.Set(int64(m.memStats.HeapSys))
+	m.heapIdle.Set(int64(m.memStats.HeapIdle))
+	m.heapInuse.Set(int64(m.memStats.HeapInuse))
+	m.heapReleased.Set(int64(m.memStats.HeapReleased))
+	m.heapObjects.Set(int64(m.memStats.HeapObjects))
+
+	m.stackInuse.Set(int64(m.memStats.StackInuse))
+	m.stackSys.Set(int64(m.memStats.StackSys))
+	m.stackMSpanInuse.Set(int64(m.memStats.MSpanInuse))
+	m.stackMSpanSys.Set(int64(m.memStats.MSpanSys))
+	m.stackMCacheInuse.Set(int64(m.memStats.MCacheInuse))
+	m.stackMCacheSys.Set(int64(m.memStats.MCacheSys))
+}
+
+func newCPUStats(producer *producer) (*cpuStats, error) {
+	cpuStats := &cpuStats{}
+	var err error
+
+	cpuStats.numGoroutines, err = producer.createInt64GaugeEntry("process/cpu_goroutines", "Number of goroutines that currently exist", metricdata.UnitDimensionless)
+	if err != nil {
+		return nil, err
+	}
+
+	cpuStats.numCgoCalls, err = producer.createInt64GaugeEntry("process/cpu_cgo_calls", "Number of cgo calls made by the current process", metricdata.UnitDimensionless)
+	if err != nil {
+		return nil, err
+	}
+
+	return cpuStats, nil
+}
+
+func (c *cpuStats) read() {
+	c.numGoroutines.Set(int64(runtime.NumGoroutine()))
+	c.numCgoCalls.Set(runtime.NumCgoCall())
+}
+
+func (p *producer) createInt64GaugeEntry(name string, description string, unit metricdata.Unit) (*metric.Int64GaugeEntry, error) {
+	if len(p.options.Prefix) > 0 {
+		name = p.options.Prefix + name
+	}
+
+	gauge, err := p.reg.AddInt64Gauge(
+		name,
+		metric.WithDescription(description),
+		metric.WithUnit(unit))
+	if err != nil {
+		return nil, errors.New("error creating gauge for " + name + ": " + err.Error())
+	}
+
+	entry, err := gauge.GetEntry()
+	if err != nil {
+		return nil, errors.New("error getting gauge entry for " + name + ": " + err.Error())
+	}
+
+	return entry, nil
+}


Test patch:
diff --git a/plugin/ochttp/server_test.go b/plugin/ochttp/server_test.go
index 4cca1bc..6e4ed5f 100644
--- a/plugin/ochttp/server_test.go
+++ b/plugin/ochttp/server_test.go
@@ -555,40 +555,61 @@ func TestHandlerImplementsHTTPCloseNotify(t *testing.T) {
 	}
 }
 
-func TestIgnoreHealthz(t *testing.T) {
-	var spans int
+func testHealthEndpointSkipArray(r *http.Request) bool {
+	for _, toSkip := range []string{"/health", "/metrics"} {
+		if r.URL.Path == toSkip {
+			return true
+		}
+	}
+	return false
+}
 
-	ts := httptest.NewServer(&Handler{
-		Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-			span := trace.FromContext(r.Context())
-			if span != nil {
-				spans++
-			}
-			fmt.Fprint(w, "ok")
-		}),
-		StartOptions: trace.StartOptions{
-			Sampler: trace.AlwaysSample(),
-		},
-	})
-	defer ts.Close()
+func TestIgnoreHealthEndpoints(t *testing.T) {
+	var spans int
 
 	client := &http.Client{}
+	tests := []struct {
+		path               string
+		healthEndpointFunc func(*http.Request) bool
+	}{
+		{"/healthz", nil},
+		{"/_ah/health", nil},
+		{"/healthz", testHealthEndpointSkipArray},
+		{"/_ah/health", testHealthEndpointSkipArray},
+		{"/health", testHealthEndpointSkipArray},
+		{"/metrics", testHealthEndpointSkipArray},
+	}
+	for _, tt := range tests {
+		t.Run(tt.path, func(t *testing.T) {
+			ts := httptest.NewServer(&Handler{
+				Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+					span := trace.FromContext(r.Context())
+					if span != nil {
+						spans++
+					}
+					fmt.Fprint(w, "ok")
+				}),
+				StartOptions: trace.StartOptions{
+					Sampler: trace.AlwaysSample(),
+				},
+				IsHealthEndpoint: tt.healthEndpointFunc,
+			})
+			defer ts.Close()
 
-	for _, path := range []string{"/healthz", "/_ah/health"} {
-		resp, err := client.Get(ts.URL + path)
-		if err != nil {
-			t.Fatalf("Cannot GET %q: %v", path, err)
-		}
-
-		b, err := ioutil.ReadAll(resp.Body)
-		if err != nil {
-			t.Fatalf("Cannot read body for %q: %v", path, err)
-		}
+			resp, err := client.Get(ts.URL + tt.path)
+			if err != nil {
+				t.Fatalf("Cannot GET %q: %v", tt.path, err)
+			}
+			b, err := ioutil.ReadAll(resp.Body)
+			if err != nil {
+				t.Fatalf("Cannot read body for %q: %v", tt.path, err)
+			}
 
-		if got, want := string(b), "ok"; got != want {
-			t.Fatalf("Body for %q = %q; want %q", path, got, want)
-		}
-		resp.Body.Close()
+			if got, want := string(b), "ok"; got != want {
+				t.Fatalf("Body for %q = %q; want %q", tt.path, got, want)
+			}
+			resp.Body.Close()
+		})
 	}
 
 	if spans > 0 {
diff --git a/plugin/runmetrics/example_test.go b/plugin/runmetrics/example_test.go
new file mode 100644
index 0000000..7cf8dbe
--- /dev/null
+++ b/plugin/runmetrics/example_test.go
@@ -0,0 +1,77 @@
+package runmetrics_test
+
+import (
+	"context"
+	"fmt"
+	"go.opencensus.io/metric/metricdata"
+	"go.opencensus.io/metric/metricexport"
+	"go.opencensus.io/plugin/runmetrics"
+	"log"
+	"sort"
+)
+
+type printExporter struct {
+}
+
+func (l *printExporter) ExportMetrics(ctx context.Context, data []*metricdata.Metric) error {
+	mapData := make(map[string]metricdata.Metric, 0)
+
+	for _, v := range data {
+		mapData[v.Descriptor.Name] = *v
+	}
+
+	mapKeys := make([]string, 0, len(mapData))
+	for key := range mapData {
+		mapKeys = append(mapKeys, key)
+	}
+	sort.Strings(mapKeys)
+
+	// for the sake of a simple example, we cannot use the real value here
+	simpleVal := func(v interface{}) int { return 42 }
+
+	for _, k := range mapKeys {
+		v := mapData[k]
+		fmt.Printf("%s %d\n", k, simpleVal(v.TimeSeries[0].Points[0].Value))
+	}
+
+	return nil
+}
+
+func ExampleEnable() {
+
+	// Enable collection of runtime metrics and supply options
+	err := runmetrics.Enable(runmetrics.RunMetricOptions{
+		EnableCPU:    true,
+		EnableMemory: true,
+		Prefix:       "mayapp/",
+	})
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Use your reader/exporter to extract values
+	// This part is not specific to runtime metrics and only here to make it a complete example.
+	metricexport.NewReader().ReadAndExport(&printExporter{})
+
+	// output:
+	// mayapp/process/cpu_cgo_calls 42
+	// mayapp/process/cpu_goroutines 42
+	// mayapp/process/heap_alloc 42
+	// mayapp/process/heap_idle 42
+	// mayapp/process/heap_inuse 42
+	// mayapp/process/heap_objects 42
+	// mayapp/process/heap_release 42
+	// mayapp/process/memory_alloc 42
+	// mayapp/process/memory_frees 42
+	// mayapp/process/memory_lookups 42
+	// mayapp/process/memory_malloc 42
+	// mayapp/process/stack_inuse 42
+	// mayapp/process/stack_mcache_inuse 42
+	// mayapp/process/stack_mspan_inuse 42
+	// mayapp/process/sys_heap 42
+	// mayapp/process/sys_memory_alloc 42
+	// mayapp/process/sys_stack 42
+	// mayapp/process/sys_stack_mcache 42
+	// mayapp/process/sys_stack_mspan 42
+	// mayapp/process/total_memory_alloc 42
+}
diff --git a/plugin/runmetrics/producer_test.go b/plugin/runmetrics/producer_test.go
new file mode 100644
index 0000000..a89c1a5
--- /dev/null
+++ b/plugin/runmetrics/producer_test.go
@@ -0,0 +1,177 @@
+package runmetrics_test
+
+import (
+	"context"
+	"github.com/stretchr/testify/assert"
+	"go.opencensus.io/metric/metricdata"
+	"go.opencensus.io/metric/metricexport"
+	"go.opencensus.io/metric/metricproducer"
+	"go.opencensus.io/plugin/runmetrics"
+	"testing"
+)
+
+type testExporter struct {
+	data []*metricdata.Metric
+}
+
+func (t *testExporter) ExportMetrics(ctx context.Context, data []*metricdata.Metric) error {
+	t.data = append(t.data, data...)
+	return nil
+}
+
+func TestEnable(t *testing.T) {
+	tests := []struct {
+		name                string
+		options             runmetrics.RunMetricOptions
+		wantMetricNames     [][]string
+		dontWantMetricNames [][]string
+	}{
+		{
+			"no stats",
+			runmetrics.RunMetricOptions{
+				EnableCPU:    false,
+				EnableMemory: false,
+			},
+			[][]string{},
+			[][]string{},
+		},
+		{
+			"cpu and memory stats",
+			runmetrics.RunMetricOptions{
+				EnableCPU:    true,
+				EnableMemory: true,
+			},
+			[][]string{
+				{"process/memory_alloc", "process/total_memory_alloc", "process/sys_memory_alloc", "process/memory_lookups", "process/memory_malloc", "process/memory_frees"},
+				{"process/heap_alloc", "process/sys_heap", "process/heap_idle", "process/heap_inuse", "process/heap_objects", "process/heap_release"},
+				{"process/stack_inuse", "process/sys_stack", "process/stack_mspan_inuse", "process/sys_stack_mspan", "process/stack_mcache_inuse", "process/sys_stack_mcache"},
+				{"process/cpu_goroutines", "process/cpu_cgo_calls"},
+			},
+			[][]string{},
+		},
+		{
+			"only cpu stats",
+			runmetrics.RunMetricOptions{
+				EnableCPU:    true,
+				EnableMemory: false,
+			},
+			[][]string{
+				{"process/cpu_goroutines", "process/cpu_cgo_calls"},
+			},
+			[][]string{
+				{"process/memory_alloc", "process/total_memory_alloc", "process/sys_memory_alloc", "process/memory_lookups", "process/memory_malloc", "process/memory_frees"},
+				{"process/heap_alloc", "process/sys_heap", "process/heap_idle", "process/heap_inuse", "process/heap_objects", "process/heap_release"},
+				{"process/stack_inuse", "process/sys_stack", "process/stack_mspan_inuse", "process/sys_stack_mspan", "process/stack_mcache_inuse", "process/sys_stack_mcache"},
+			},
+		},
+		{
+			"only memory stats",
+			runmetrics.RunMetricOptions{
+				EnableCPU:    false,
+				EnableMemory: true,
+			},
+			[][]string{
+				{"process/memory_alloc", "process/total_memory_alloc", "process/sys_memory_alloc", "process/memory_lookups", "process/memory_malloc", "process/memory_frees"},
+				{"process/heap_alloc", "process/sys_heap", "process/heap_idle", "process/heap_inuse", "process/heap_objects", "process/heap_release"},
+				{"process/stack_inuse", "process/sys_stack", "process/stack_mspan_inuse", "process/sys_stack_mspan", "process/stack_mcache_inuse", "process/sys_stack_mcache"},
+			},
+			[][]string{
+				{"process/cpu_goroutines", "process/cpu_cgo_calls"},
+			},
+		},
+		{
+			"cpu and memory stats with custom prefix",
+			runmetrics.RunMetricOptions{
+				EnableCPU:    true,
+				EnableMemory: true,
+				Prefix:       "test_",
+			},
+			[][]string{
+				{"test_process/memory_alloc", "test_process/total_memory_alloc", "test_process/sys_memory_alloc", "test_process/memory_lookups", "test_process/memory_malloc", "test_process/memory_frees"},
+				{"test_process/heap_alloc", "test_process/sys_heap", "test_process/heap_idle", "test_process/heap_inuse", "test_process/heap_objects", "test_process/heap_release"},
+				{"test_process/stack_inuse", "test_process/sys_stack", "test_process/stack_mspan_inuse", "test_process/sys_stack_mspan", "test_process/stack_mcache_inuse", "test_process/sys_stack_mcache"},
+				{"test_process/cpu_goroutines", "test_process/cpu_cgo_calls"},
+			},
+			[][]string{},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+
+			err := runmetrics.Enable(test.options)
+
+			if err != nil {
+				t.Errorf("want: nil, got: %v", err)
+			}
+
+			defer runmetrics.Disable()
+
+			exporter := &testExporter{}
+			reader := metricexport.NewReader()
+			reader.ReadAndExport(exporter)
+
+			for _, want := range test.wantMetricNames {
+				assertNames(t, true, exporter, want)
+			}
+
+			for _, dontWant := range test.dontWantMetricNames {
+				assertNames(t, false, exporter, dontWant)
+			}
+		})
+	}
+}
+
+func assertNames(t *testing.T, wantIncluded bool, exporter *testExporter, expectedNames []string) {
+	t.Helper()
+
+	metricNames := make([]string, 0)
+	for _, v := range exporter.data {
+		metricNames = append(metricNames, v.Descriptor.Name)
+	}
+
+	for _, want := range expectedNames {
+		if wantIncluded {
+			assert.Contains(t, metricNames, want)
+		} else {
+			assert.NotContains(t, metricNames, want)
+		}
+	}
+}
+
+func TestEnable_RegistersWithGlobalManager(t *testing.T) {
+	err := runmetrics.Enable(runmetrics.RunMetricOptions{})
+	if err != nil {
+		t.Errorf("want: nil, got: %v", err)
+	}
+
+	registeredCount := len(metricproducer.GlobalManager().GetAll())
+	assert.Equal(t, 1, registeredCount, "expected a producer to be registered")
+}
+
+func TestEnable_RegistersNoDuplicates(t *testing.T) {
+	err := runmetrics.Enable(runmetrics.RunMetricOptions{})
+	if err != nil {
+		t.Errorf("want: nil, got: %v", err)
+	}
+
+	err = runmetrics.Enable(runmetrics.RunMetricOptions{})
+	if err != nil {
+		t.Errorf("want: nil, got: %v", err)
+	}
+
+	producerCount := len(metricproducer.GlobalManager().GetAll())
+	assert.Equal(t, 1, producerCount, "expected one registered producer")
+}
+
+func TestDisable(t *testing.T) {
+	err := runmetrics.Enable(runmetrics.RunMetricOptions{})
+	if err != nil {
+		t.Errorf("want: nil, got: %v", err)
+	}
+
+	runmetrics.Disable()
+
+	producerCount := len(metricproducer.GlobalManager().GetAll())
+	assert.Equal(t, 0, producerCount, "expected one registered producer")
+}