From fa651b05963cfb6060755dc887e7d156ba66e792 Mon Sep 17 00:00:00 2001 From: Brian Hoffmann Date: Tue, 1 Oct 2019 06:45:06 +0200 Subject: [PATCH] Add runtime metrics support (#1156) * Add runtime metrics support * Rename Options to RunMetricOptions * Make runmetrics producer registration easier with Enable/Disable * Rename and cleanup metric names --- go.mod | 2 + go.sum | 10 + plugin/runmetrics/doc.go | 23 +++ plugin/runmetrics/example_test.go | 77 ++++++++ plugin/runmetrics/producer.go | 290 +++++++++++++++++++++++++++++ plugin/runmetrics/producer_test.go | 177 ++++++++++++++++++ 6 files changed, 579 insertions(+) create mode 100644 plugin/runmetrics/doc.go create mode 100644 plugin/runmetrics/example_test.go create mode 100644 plugin/runmetrics/producer.go create mode 100644 plugin/runmetrics/producer_test.go diff --git a/go.mod b/go.mod index 7c1886e9e..139157cd3 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 212b6b73b..ed2a1d844 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/runmetrics/doc.go b/plugin/runmetrics/doc.go new file mode 100644 index 000000000..2bb53d4c7 --- /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/example_test.go b/plugin/runmetrics/example_test.go new file mode 100644 index 000000000..7cf8dbe03 --- /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.go b/plugin/runmetrics/producer.go new file mode 100644 index 000000000..eb307fea9 --- /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 +} diff --git a/plugin/runmetrics/producer_test.go b/plugin/runmetrics/producer_test.go new file mode 100644 index 000000000..a89c1a55e --- /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") +}