gocollector: Added options to Go Collector for changing the (#1031)

* Renamed files.

Signed-off-by: Bartlomiej Plotka <bwplotka@gmail.com>

* gocollector: Added options to Go Collector for diffetent collections.

Fixes https://github.com/prometheus/client_golang/issues/983

Also:

* fixed TestMemStatsEquivalence, it was noop before (:
* Removed gc_cpu_fraction metric completely, since it's not working completely for Go1.17+

Signed-off-by: Bartlomiej Plotka <bwplotka@gmail.com>
This commit is contained in:
Bartlomiej Plotka 2022-04-13 10:55:22 +02:00 committed by Kemal Akkoyun
parent 585540a010
commit d498b3cdd9
7 changed files with 398 additions and 161 deletions

View File

@ -14,3 +14,27 @@
// Package collectors provides implementations of prometheus.Collector to // Package collectors provides implementations of prometheus.Collector to
// conveniently collect process and Go-related metrics. // conveniently collect process and Go-related metrics.
package collectors package collectors
import "github.com/prometheus/client_golang/prometheus"
// NewBuildInfoCollector returns a collector collecting a single metric
// "go_build_info" with the constant value 1 and three labels "path", "version",
// and "checksum". Their label values contain the main module path, version, and
// checksum, respectively. The labels will only have meaningful values if the
// binary is built with Go module support and from source code retrieved from
// the source repository (rather than the local file system). This is usually
// accomplished by building from outside of GOPATH, specifying the full address
// of the main package, e.g. "GO111MODULE=on go run
// github.com/prometheus/client_golang/examples/random". If built without Go
// module support, all label values will be "unknown". If built with Go module
// support but using the source code from the local file system, the "path" will
// be set appropriately, but "checksum" will be empty and "version" will be
// "(devel)".
//
// This collector uses only the build information for the main module. See
// https://github.com/povilasv/prommod for an example of a collector for the
// module dependencies.
func NewBuildInfoCollector() prometheus.Collector {
//nolint:staticcheck // Ignore SA1019 until v2.
return prometheus.NewBuildInfoCollector()
}

View File

@ -11,6 +11,9 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build !go1.17
// +build !go1.17
package collectors package collectors
import "github.com/prometheus/client_golang/prometheus" import "github.com/prometheus/client_golang/prometheus"
@ -42,28 +45,5 @@ import "github.com/prometheus/client_golang/prometheus"
// NOTE: The problem is solved in Go 1.15, see // NOTE: The problem is solved in Go 1.15, see
// https://github.com/golang/go/issues/19812 for the related Go issue. // https://github.com/golang/go/issues/19812 for the related Go issue.
func NewGoCollector() prometheus.Collector { func NewGoCollector() prometheus.Collector {
//nolint:staticcheck // Ignore SA1019 until v2.
return prometheus.NewGoCollector() return prometheus.NewGoCollector()
} }
// NewBuildInfoCollector returns a collector collecting a single metric
// "go_build_info" with the constant value 1 and three labels "path", "version",
// and "checksum". Their label values contain the main module path, version, and
// checksum, respectively. The labels will only have meaningful values if the
// binary is built with Go module support and from source code retrieved from
// the source repository (rather than the local file system). This is usually
// accomplished by building from outside of GOPATH, specifying the full address
// of the main package, e.g. "GO111MODULE=on go run
// github.com/prometheus/client_golang/examples/random". If built without Go
// module support, all label values will be "unknown". If built with Go module
// support but using the source code from the local file system, the "path" will
// be set appropriately, but "checksum" will be empty and "version" will be
// "(devel)".
//
// This collector uses only the build information for the main module. See
// https://github.com/povilasv/prommod for an example of a collector for the
// module dependencies.
func NewBuildInfoCollector() prometheus.Collector {
//nolint:staticcheck // Ignore SA1019 until v2.
return prometheus.NewBuildInfoCollector()
}

View File

@ -0,0 +1,91 @@
// Copyright 2021 The Prometheus 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.
//go:build go1.17
// +build go1.17
package collectors
import "github.com/prometheus/client_golang/prometheus"
//nolint:staticcheck // Ignore SA1019 until v2.
type goOptions = prometheus.GoCollectorOptions
type goOption func(o *goOptions)
type GoCollectionOption uint32
const (
// GoRuntimeMemStatsCollection represents the metrics represented by runtime.MemStats structure such as
// go_memstats_alloc_bytes
// go_memstats_alloc_bytes_total
// go_memstats_sys_bytes
// go_memstats_lookups_total
// go_memstats_mallocs_total
// go_memstats_frees_total
// go_memstats_heap_alloc_bytes
// go_memstats_heap_sys_bytes
// go_memstats_heap_idle_bytes
// go_memstats_heap_inuse_bytes
// go_memstats_heap_released_bytes
// go_memstats_heap_objects
// go_memstats_stack_inuse_bytes
// go_memstats_stack_sys_bytes
// go_memstats_mspan_inuse_bytes
// go_memstats_mspan_sys_bytes
// go_memstats_mcache_inuse_bytes
// go_memstats_mcache_sys_bytes
// go_memstats_buck_hash_sys_bytes
// go_memstats_gc_sys_bytes
// go_memstats_other_sys_bytes
// go_memstats_next_gc_bytes
// so the metrics known from pre client_golang v1.12.0, except skipped go_memstats_gc_cpu_fraction (see
// https://github.com/prometheus/client_golang/issues/842#issuecomment-861812034 for explanation.
//
// NOTE that this mode represents runtime.MemStats statistics, but they are
// actually implemented using new runtime/metrics package.
// Deprecated: Use GoRuntimeMetricsCollection instead going forward.
GoRuntimeMemStatsCollection GoCollectionOption = 1 << iota
// GoRuntimeMetricsCollection is the new set of metrics represented by runtime/metrics package and follows
// consistent naming. The exposed metric set depends on Go version, but it is controlled against
// unexpected cardinality. This set has overlapping information with GoRuntimeMemStatsCollection, just with
// new names. GoRuntimeMetricsCollection is what is recommended for using going forward.
GoRuntimeMetricsCollection
)
// WithGoCollections allows enabling different collections for Go collector on top of base metrics
// like go_goroutines, go_threads, go_gc_duration_seconds, go_memstats_last_gc_time_seconds, go_info.
//
// Check GoRuntimeMemStatsCollection and GoRuntimeMetricsCollection for more details. You can use none,
// one or more collections at once. For example:
// WithGoCollections(GoRuntimeMemStatsCollection | GoRuntimeMetricsCollection) means both GoRuntimeMemStatsCollection
// metrics and GoRuntimeMetricsCollection will be exposed.
//
// Use WithGoCollections(GoRuntimeMemStatsCollection) to have Go collector working in
// the compatibility mode with client_golang pre v1.12 (move to runtime/metrics).
func WithGoCollections(flags uint32) goOption {
return func(o *goOptions) {
o.EnabledCollections = flags
}
}
// NewGoCollector returns a collector that exports metrics about the current Go
// process using debug.GCStats using runtime/metrics.
func NewGoCollector(opts ...goOption) prometheus.Collector {
//nolint:staticcheck // Ignore SA1019 until v2.
promPkgOpts := make([]func(o *prometheus.GoCollectorOptions), len(opts))
for i, opt := range opts {
promPkgOpts[i] = opt
}
//nolint:staticcheck // Ignore SA1019 until v2.
return prometheus.NewGoCollector(promPkgOpts...)
}

View File

@ -197,14 +197,6 @@ func goRuntimeMemStats() memStatsMetrics {
), ),
eval: func(ms *runtime.MemStats) float64 { return float64(ms.NextGC) }, eval: func(ms *runtime.MemStats) float64 { return float64(ms.NextGC) },
valType: GaugeValue, valType: GaugeValue,
}, {
desc: NewDesc(
memstatNamespace("gc_cpu_fraction"),
"The fraction of this program's available CPU time used by the GC since the program started.",
nil, nil,
),
eval: func(ms *runtime.MemStats) float64 { return ms.GCCPUFraction },
valType: GaugeValue,
}, },
} }
} }
@ -268,7 +260,6 @@ func (c *baseGoCollector) Collect(ch chan<- Metric) {
quantiles[0.0] = stats.PauseQuantiles[0].Seconds() quantiles[0.0] = stats.PauseQuantiles[0].Seconds()
ch <- MustNewConstSummary(c.gcDesc, uint64(stats.NumGC), stats.PauseTotal.Seconds(), quantiles) ch <- MustNewConstSummary(c.gcDesc, uint64(stats.NumGC), stats.PauseTotal.Seconds(), quantiles)
ch <- MustNewConstMetric(c.gcLastTimeDesc, GaugeValue, float64(stats.LastGC.UnixNano())/1e9) ch <- MustNewConstMetric(c.gcLastTimeDesc, GaugeValue, float64(stats.LastGC.UnixNano())/1e9)
ch <- MustNewConstMetric(c.goInfoDesc, GaugeValue, 1) ch <- MustNewConstMetric(c.goInfoDesc, GaugeValue, 1)
} }
@ -278,6 +269,7 @@ func memstatNamespace(s string) string {
// memStatsMetrics provide description, evaluator, runtime/metrics name, and // memStatsMetrics provide description, evaluator, runtime/metrics name, and
// value type for memstat metrics. // value type for memstat metrics.
// TODO(bwplotka): Remove with end Go 1.16 EOL and replace with runtime/metrics.Description
type memStatsMetrics []struct { type memStatsMetrics []struct {
desc *Desc desc *Desc
eval func(*runtime.MemStats) float64 eval func(*runtime.MemStats) float64

View File

@ -40,13 +40,28 @@ type goCollector struct {
// //
// Deprecated: Use collectors.NewGoCollector instead. // Deprecated: Use collectors.NewGoCollector instead.
func NewGoCollector() Collector { func NewGoCollector() Collector {
msMetrics := goRuntimeMemStats()
msMetrics = append(msMetrics, struct {
desc *Desc
eval func(*runtime.MemStats) float64
valType ValueType
}{
// This metric is omitted in Go1.17+, see https://github.com/prometheus/client_golang/issues/842#issuecomment-861812034
desc: NewDesc(
memstatNamespace("gc_cpu_fraction"),
"The fraction of this program's available CPU time used by the GC since the program started.",
nil, nil,
),
eval: func(ms *runtime.MemStats) float64 { return ms.GCCPUFraction },
valType: GaugeValue,
})
return &goCollector{ return &goCollector{
base: newBaseGoCollector(), base: newBaseGoCollector(),
msLast: &runtime.MemStats{}, msLast: &runtime.MemStats{},
msRead: runtime.ReadMemStats, msRead: runtime.ReadMemStats,
msMaxWait: time.Second, msMaxWait: time.Second,
msMaxAge: 5 * time.Minute, msMaxAge: 5 * time.Minute,
msMetrics: goRuntimeMemStats(), msMetrics: msMetrics,
} }
} }

View File

@ -29,7 +29,66 @@ import (
dto "github.com/prometheus/client_model/go" dto "github.com/prometheus/client_model/go"
) )
const (
goGCHeapTinyAllocsObjects = "/gc/heap/tiny/allocs:objects"
goGCHeapAllocsObjects = "/gc/heap/allocs:objects"
goGCHeapFreesObjects = "/gc/heap/frees:objects"
goGCHeapAllocsBytes = "/gc/heap/allocs:bytes"
goGCHeapObjects = "/gc/heap/objects:objects"
goGCHeapGoalBytes = "/gc/heap/goal:bytes"
goMemoryClassesTotalBytes = "/memory/classes/total:bytes"
goMemoryClassesHeapObjectsBytes = "/memory/classes/heap/objects:bytes"
goMemoryClassesHeapUnusedBytes = "/memory/classes/heap/unused:bytes"
goMemoryClassesHeapReleasedBytes = "/memory/classes/heap/released:bytes"
goMemoryClassesHeapFreeBytes = "/memory/classes/heap/free:bytes"
goMemoryClassesHeapStacksBytes = "/memory/classes/heap/stacks:bytes"
goMemoryClassesOSStacksBytes = "/memory/classes/os-stacks:bytes"
goMemoryClassesMetadataMSpanInuseBytes = "/memory/classes/metadata/mspan/inuse:bytes"
goMemoryClassesMetadataMSPanFreeBytes = "/memory/classes/metadata/mspan/free:bytes"
goMemoryClassesMetadataMCacheInuseBytes = "/memory/classes/metadata/mcache/inuse:bytes"
goMemoryClassesMetadataMCacheFreeBytes = "/memory/classes/metadata/mcache/free:bytes"
goMemoryClassesProfilingBucketsBytes = "/memory/classes/profiling/buckets:bytes"
goMemoryClassesMetadataOtherBytes = "/memory/classes/metadata/other:bytes"
goMemoryClassesOtherBytes = "/memory/classes/other:bytes"
)
// runtime/metrics names required for runtimeMemStats like logic.
var rmForMemStats = []string{goGCHeapTinyAllocsObjects,
goGCHeapAllocsObjects,
goGCHeapFreesObjects,
goGCHeapAllocsBytes,
goGCHeapObjects,
goGCHeapGoalBytes,
goMemoryClassesTotalBytes,
goMemoryClassesHeapObjectsBytes,
goMemoryClassesHeapUnusedBytes,
goMemoryClassesHeapReleasedBytes,
goMemoryClassesHeapFreeBytes,
goMemoryClassesHeapStacksBytes,
goMemoryClassesOSStacksBytes,
goMemoryClassesMetadataMSpanInuseBytes,
goMemoryClassesMetadataMSPanFreeBytes,
goMemoryClassesMetadataMCacheInuseBytes,
goMemoryClassesMetadataMCacheFreeBytes,
goMemoryClassesProfilingBucketsBytes,
goMemoryClassesMetadataOtherBytes,
goMemoryClassesOtherBytes,
}
func bestEffortLookupRM(lookup []string) []metrics.Description {
ret := make([]metrics.Description, 0, len(lookup))
for _, rm := range metrics.All() {
for _, m := range lookup {
if m == rm.Name {
ret = append(ret, rm)
}
}
}
return ret
}
type goCollector struct { type goCollector struct {
opt GoCollectorOptions
base baseGoCollector base baseGoCollector
// mu protects updates to all fields ensuring a consistent // mu protects updates to all fields ensuring a consistent
@ -51,12 +110,46 @@ type goCollector struct {
msMetrics memStatsMetrics msMetrics memStatsMetrics
} }
const (
// Those are not exposed due to need to move Go collector to another package in v2.
// See issue https://github.com/prometheus/client_golang/issues/1030.
goRuntimeMemStatsCollection uint32 = 1 << iota
goRuntimeMetricsCollection
)
// GoCollectorOptions should not be used be directly by anything, except `collectors` package.
// Use it via collectors package instead. See issue
// https://github.com/prometheus/client_golang/issues/1030.
//
// Deprecated: Use collectors.WithGoCollections
type GoCollectorOptions struct {
// EnabledCollection sets what type of collections collector should expose on top of base collection.
// By default it's goMemStatsCollection | goRuntimeMetricsCollection.
EnabledCollections uint32
}
func (c GoCollectorOptions) isEnabled(flag uint32) bool {
return c.EnabledCollections&flag != 0
}
const defaultGoCollections = goRuntimeMemStatsCollection | goRuntimeMetricsCollection
// NewGoCollector is the obsolete version of collectors.NewGoCollector. // NewGoCollector is the obsolete version of collectors.NewGoCollector.
// See there for documentation. // See there for documentation.
// //
// Deprecated: Use collectors.NewGoCollector instead. // Deprecated: Use collectors.NewGoCollector instead.
func NewGoCollector() Collector { func NewGoCollector(opts ...func(o *GoCollectorOptions)) Collector {
descriptions := metrics.All() opt := GoCollectorOptions{EnabledCollections: defaultGoCollections}
for _, o := range opts {
o(&opt)
}
var descriptions []metrics.Description
if opt.isEnabled(goRuntimeMetricsCollection) {
descriptions = metrics.All()
} else if opt.isEnabled(goRuntimeMemStatsCollection) {
descriptions = bestEffortLookupRM(rmForMemStats)
}
// Collect all histogram samples so that we can get their buckets. // Collect all histogram samples so that we can get their buckets.
// The API guarantees that the buckets are always fixed for the lifetime // The API guarantees that the buckets are always fixed for the lifetime
@ -67,7 +160,11 @@ func NewGoCollector() Collector {
histograms = append(histograms, metrics.Sample{Name: d.Name}) histograms = append(histograms, metrics.Sample{Name: d.Name})
} }
} }
metrics.Read(histograms)
if len(histograms) > 0 {
metrics.Read(histograms)
}
bucketsMap := make(map[string][]float64) bucketsMap := make(map[string][]float64)
for i := range histograms { for i := range histograms {
bucketsMap[histograms[i].Name] = histograms[i].Value.Float64Histogram().Buckets bucketsMap[histograms[i].Name] = histograms[i].Value.Float64Histogram().Buckets
@ -83,7 +180,7 @@ func NewGoCollector() Collector {
if !ok { if !ok {
// Just ignore this metric; we can't do anything with it here. // Just ignore this metric; we can't do anything with it here.
// If a user decides to use the latest version of Go, we don't want // If a user decides to use the latest version of Go, we don't want
// to fail here. This condition is tested elsewhere. // to fail here. This condition is tested in TestExpectedRuntimeMetrics.
continue continue
} }
@ -123,12 +220,18 @@ func NewGoCollector() Collector {
} }
metricSet = append(metricSet, m) metricSet = append(metricSet, m)
} }
var msMetrics memStatsMetrics
if opt.isEnabled(goRuntimeMemStatsCollection) {
msMetrics = goRuntimeMemStats()
}
return &goCollector{ return &goCollector{
opt: opt,
base: newBaseGoCollector(), base: newBaseGoCollector(),
rmSampleBuf: sampleBuf, rmSampleBuf: sampleBuf,
rmSampleMap: sampleMap, rmSampleMap: sampleMap,
rmMetrics: metricSet, rmMetrics: metricSet,
msMetrics: goRuntimeMemStats(), msMetrics: msMetrics,
} }
} }
@ -163,40 +266,47 @@ func (c *goCollector) Collect(ch chan<- Metric) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
// Populate runtime/metrics sample buffer. if len(c.rmSampleBuf) > 0 {
metrics.Read(c.rmSampleBuf) // Populate runtime/metrics sample buffer.
metrics.Read(c.rmSampleBuf)
}
// Update all our metrics from rmSampleBuf. if c.opt.isEnabled(goRuntimeMetricsCollection) {
for i, sample := range c.rmSampleBuf { // Collect all our metrics from rmSampleBuf.
// N.B. switch on concrete type because it's significantly more efficient for i, sample := range c.rmSampleBuf {
// than checking for the Counter and Gauge interface implementations. In // N.B. switch on concrete type because it's significantly more efficient
// this case, we control all the types here. // than checking for the Counter and Gauge interface implementations. In
switch m := c.rmMetrics[i].(type) { // this case, we control all the types here.
case *counter: switch m := c.rmMetrics[i].(type) {
// Guard against decreases. This should never happen, but a failure case *counter:
// to do so will result in a panic, which is a harsh consequence for // Guard against decreases. This should never happen, but a failure
// a metrics collection bug. // to do so will result in a panic, which is a harsh consequence for
v0, v1 := m.get(), unwrapScalarRMValue(sample.Value) // a metrics collection bug.
if v1 > v0 { v0, v1 := m.get(), unwrapScalarRMValue(sample.Value)
m.Add(unwrapScalarRMValue(sample.Value) - m.get()) if v1 > v0 {
m.Add(unwrapScalarRMValue(sample.Value) - m.get())
}
m.Collect(ch)
case *gauge:
m.Set(unwrapScalarRMValue(sample.Value))
m.Collect(ch)
case *batchHistogram:
m.update(sample.Value.Float64Histogram(), c.exactSumFor(sample.Name))
m.Collect(ch)
default:
panic("unexpected metric type")
} }
m.Collect(ch)
case *gauge:
m.Set(unwrapScalarRMValue(sample.Value))
m.Collect(ch)
case *batchHistogram:
m.update(sample.Value.Float64Histogram(), c.exactSumFor(sample.Name))
m.Collect(ch)
default:
panic("unexpected metric type")
} }
} }
// ms is a dummy MemStats that we populate ourselves so that we can // ms is a dummy MemStats that we populate ourselves so that we can
// populate the old metrics from it. // populate the old metrics from it if goMemStatsCollection is enabled.
var ms runtime.MemStats if c.opt.isEnabled(goRuntimeMemStatsCollection) {
memStatsFromRM(&ms, c.rmSampleMap) var ms runtime.MemStats
for _, i := range c.msMetrics { memStatsFromRM(&ms, c.rmSampleMap)
ch <- MustNewConstMetric(i.desc, i.valType, i.eval(&ms)) for _, i := range c.msMetrics {
ch <- MustNewConstMetric(i.desc, i.valType, i.eval(&ms))
}
} }
} }
@ -261,35 +371,30 @@ func memStatsFromRM(ms *runtime.MemStats, rm map[string]*metrics.Sample) {
// while having Mallocs - Frees still represent a live object count. // while having Mallocs - Frees still represent a live object count.
// Unfortunately, MemStats doesn't actually export a large allocation count, // Unfortunately, MemStats doesn't actually export a large allocation count,
// so it's impossible to pull this number out directly. // so it's impossible to pull this number out directly.
tinyAllocs := lookupOrZero("/gc/heap/tiny/allocs:objects") tinyAllocs := lookupOrZero(goGCHeapTinyAllocsObjects)
ms.Mallocs = lookupOrZero("/gc/heap/allocs:objects") + tinyAllocs ms.Mallocs = lookupOrZero(goGCHeapAllocsObjects) + tinyAllocs
ms.Frees = lookupOrZero("/gc/heap/frees:objects") + tinyAllocs ms.Frees = lookupOrZero(goGCHeapFreesObjects) + tinyAllocs
ms.TotalAlloc = lookupOrZero("/gc/heap/allocs:bytes") ms.TotalAlloc = lookupOrZero(goGCHeapAllocsBytes)
ms.Sys = lookupOrZero("/memory/classes/total:bytes") ms.Sys = lookupOrZero(goMemoryClassesTotalBytes)
ms.Lookups = 0 // Already always zero. ms.Lookups = 0 // Already always zero.
ms.HeapAlloc = lookupOrZero("/memory/classes/heap/objects:bytes") ms.HeapAlloc = lookupOrZero(goMemoryClassesHeapObjectsBytes)
ms.Alloc = ms.HeapAlloc ms.Alloc = ms.HeapAlloc
ms.HeapInuse = ms.HeapAlloc + lookupOrZero("/memory/classes/heap/unused:bytes") ms.HeapInuse = ms.HeapAlloc + lookupOrZero(goMemoryClassesHeapUnusedBytes)
ms.HeapReleased = lookupOrZero("/memory/classes/heap/released:bytes") ms.HeapReleased = lookupOrZero(goMemoryClassesHeapReleasedBytes)
ms.HeapIdle = ms.HeapReleased + lookupOrZero("/memory/classes/heap/free:bytes") ms.HeapIdle = ms.HeapReleased + lookupOrZero(goMemoryClassesHeapFreeBytes)
ms.HeapSys = ms.HeapInuse + ms.HeapIdle ms.HeapSys = ms.HeapInuse + ms.HeapIdle
ms.HeapObjects = lookupOrZero("/gc/heap/objects:objects") ms.HeapObjects = lookupOrZero(goGCHeapObjects)
ms.StackInuse = lookupOrZero("/memory/classes/heap/stacks:bytes") ms.StackInuse = lookupOrZero(goMemoryClassesHeapStacksBytes)
ms.StackSys = ms.StackInuse + lookupOrZero("/memory/classes/os-stacks:bytes") ms.StackSys = ms.StackInuse + lookupOrZero(goMemoryClassesOSStacksBytes)
ms.MSpanInuse = lookupOrZero("/memory/classes/metadata/mspan/inuse:bytes") ms.MSpanInuse = lookupOrZero(goMemoryClassesMetadataMSpanInuseBytes)
ms.MSpanSys = ms.MSpanInuse + lookupOrZero("/memory/classes/metadata/mspan/free:bytes") ms.MSpanSys = ms.MSpanInuse + lookupOrZero(goMemoryClassesMetadataMSPanFreeBytes)
ms.MCacheInuse = lookupOrZero("/memory/classes/metadata/mcache/inuse:bytes") ms.MCacheInuse = lookupOrZero(goMemoryClassesMetadataMCacheInuseBytes)
ms.MCacheSys = ms.MCacheInuse + lookupOrZero("/memory/classes/metadata/mcache/free:bytes") ms.MCacheSys = ms.MCacheInuse + lookupOrZero(goMemoryClassesMetadataMCacheFreeBytes)
ms.BuckHashSys = lookupOrZero("/memory/classes/profiling/buckets:bytes") ms.BuckHashSys = lookupOrZero(goMemoryClassesProfilingBucketsBytes)
ms.GCSys = lookupOrZero("/memory/classes/metadata/other:bytes") ms.GCSys = lookupOrZero(goMemoryClassesMetadataOtherBytes)
ms.OtherSys = lookupOrZero("/memory/classes/other:bytes") ms.OtherSys = lookupOrZero(goMemoryClassesOtherBytes)
ms.NextGC = lookupOrZero("/gc/heap/goal:bytes") ms.NextGC = lookupOrZero(goGCHeapGoalBytes)
// N.B. LastGC is omitted because runtime.GCStats already has this.
// See https://github.com/prometheus/client_golang/issues/842#issuecomment-861812034
// for more details.
ms.LastGC = 0
// N.B. GCCPUFraction is intentionally omitted. This metric is not useful, // N.B. GCCPUFraction is intentionally omitted. This metric is not useful,
// and often misleading due to the fact that it's an average over the lifetime // and often misleading due to the fact that it's an average over the lifetime

View File

@ -28,78 +28,96 @@ import (
dto "github.com/prometheus/client_model/go" dto "github.com/prometheus/client_model/go"
) )
func TestGoCollectorRuntimeMetrics(t *testing.T) { func TestRmForMemStats(t *testing.T) {
metrics := collectGoMetrics(t) if got, want := len(bestEffortLookupRM(rmForMemStats)), len(rmForMemStats); got != want {
t.Errorf("got %d, want %d metrics", got, want)
msChecklist := make(map[string]bool)
for _, m := range goRuntimeMemStats() {
msChecklist[m.desc.fqName] = false
}
if len(metrics) == 0 {
t.Fatal("no metrics created by Collect")
}
// Check a few specific metrics.
//
// Checking them all is somewhat pointless because the runtime/metrics
// metrics are going to shift underneath us. Also if we try to check
// against the runtime/metrics package in an automated fashion we're kind
// of missing the point, because we have to do all the same work the code
// has to do to perform the translation. Same for supporting old metric
// names (the best we can do here is make sure they're all accounted for).
var sysBytes, allocs float64
for _, m := range metrics {
name := m.Desc().fqName
switch name {
case "go_memory_classes_total_bytes":
checkMemoryMetric(t, m, &sysBytes)
case "go_sys_bytes":
checkMemoryMetric(t, m, &sysBytes)
case "go_gc_heap_allocs_bytes_total":
checkMemoryMetric(t, m, &allocs)
case "go_alloc_bytes_total":
checkMemoryMetric(t, m, &allocs)
}
if present, ok := msChecklist[name]; ok {
if present {
t.Errorf("memstats metric %s found more than once", name)
}
msChecklist[name] = true
}
}
for name := range msChecklist {
if present := msChecklist[name]; !present {
t.Errorf("memstats metric %s not collected", name)
}
} }
} }
func checkMemoryMetric(t *testing.T, m Metric, expValue *float64) { func expectedBaseMetrics() map[string]struct{} {
t.Helper() metrics := map[string]struct{}{}
b := newBaseGoCollector()
for _, m := range []string{
b.gcDesc.fqName,
b.goInfoDesc.fqName,
b.goroutinesDesc.fqName,
b.gcLastTimeDesc.fqName,
b.threadsDesc.fqName,
} {
metrics[m] = struct{}{}
}
return metrics
}
pb := &dto.Metric{} func addExpectedRuntimeMemStats(metrics map[string]struct{}) map[string]struct{} {
m.Write(pb) for _, m := range goRuntimeMemStats() {
var value float64 metrics[m.desc.fqName] = struct{}{}
if g := pb.GetGauge(); g != nil {
value = g.GetValue()
} else {
value = pb.GetCounter().GetValue()
} }
if value <= 0 { return metrics
t.Error("bad value for total memory") }
func addExpectedRuntimeMetrics(metrics map[string]struct{}) map[string]struct{} {
for _, m := range expectedRuntimeMetrics {
metrics[m] = struct{}{}
} }
if *expValue == 0 { return metrics
*expValue = value }
} else if value != *expValue {
t.Errorf("legacy metric and runtime/metrics metric do not match: want %d, got %d", int64(*expValue), int64(value)) func TestGoCollector(t *testing.T) {
for _, tcase := range []struct {
collections uint32
expectedFQNameSet map[string]struct{}
}{
{
collections: 0,
expectedFQNameSet: expectedBaseMetrics(),
},
{
collections: goRuntimeMemStatsCollection,
expectedFQNameSet: addExpectedRuntimeMemStats(expectedBaseMetrics()),
},
{
collections: goRuntimeMetricsCollection,
expectedFQNameSet: addExpectedRuntimeMetrics(expectedBaseMetrics()),
},
{
collections: goRuntimeMemStatsCollection | goRuntimeMetricsCollection,
expectedFQNameSet: addExpectedRuntimeMemStats(addExpectedRuntimeMetrics(expectedBaseMetrics())),
},
} {
if ok := t.Run("", func(t *testing.T) {
goMetrics := collectGoMetrics(t, tcase.collections)
goMetricSet := make(map[string]Metric)
for _, m := range goMetrics {
goMetricSet[m.Desc().fqName] = m
}
for i := range goMetrics {
name := goMetrics[i].Desc().fqName
if _, ok := tcase.expectedFQNameSet[name]; !ok {
t.Errorf("found unpexpected metric %s", name)
continue
}
}
// Now iterate over the expected metrics and look for removals.
for expectedName := range tcase.expectedFQNameSet {
if _, ok := goMetricSet[expectedName]; !ok {
t.Errorf("missing expected metric %s in collection", expectedName)
continue
}
}
}); !ok {
return
}
} }
} }
var sink interface{} var sink interface{}
func TestBatchHistogram(t *testing.T) { func TestBatchHistogram(t *testing.T) {
goMetrics := collectGoMetrics(t) goMetrics := collectGoMetrics(t, defaultGoCollections)
var mhist Metric var mhist Metric
for _, m := range goMetrics { for _, m := range goMetrics {
@ -126,7 +144,7 @@ func TestBatchHistogram(t *testing.T) {
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
sink = make([]byte, 128) sink = make([]byte, 128)
} }
collectGoMetrics(t) collectGoMetrics(t, defaultGoCollections)
for i, v := range hist.counts { for i, v := range hist.counts {
if v != countsCopy[i] { if v != countsCopy[i] {
t.Error("counts changed during new collection") t.Error("counts changed during new collection")
@ -175,10 +193,12 @@ func TestBatchHistogram(t *testing.T) {
} }
} }
func collectGoMetrics(t *testing.T) []Metric { func collectGoMetrics(t *testing.T, enabledCollections uint32) []Metric {
t.Helper() t.Helper()
c := NewGoCollector().(*goCollector) c := NewGoCollector(func(o *GoCollectorOptions) {
o.EnabledCollections = enabledCollections
}).(*goCollector)
// Collect all metrics. // Collect all metrics.
ch := make(chan Metric) ch := make(chan Metric)
@ -201,7 +221,8 @@ func collectGoMetrics(t *testing.T) []Metric {
func TestMemStatsEquivalence(t *testing.T) { func TestMemStatsEquivalence(t *testing.T) {
var msReal, msFake runtime.MemStats var msReal, msFake runtime.MemStats
descs := metrics.All() descs := bestEffortLookupRM(rmForMemStats)
samples := make([]metrics.Sample, len(descs)) samples := make([]metrics.Sample, len(descs))
samplesMap := make(map[string]*metrics.Sample) samplesMap := make(map[string]*metrics.Sample)
for i := range descs { for i := range descs {
@ -214,9 +235,9 @@ func TestMemStatsEquivalence(t *testing.T) {
// Populate msReal. // Populate msReal.
runtime.ReadMemStats(&msReal) runtime.ReadMemStats(&msReal)
// Populate msFake and hope that no GC happened in between (:
// Populate msFake.
metrics.Read(samples) metrics.Read(samples)
memStatsFromRM(&msFake, samplesMap) memStatsFromRM(&msFake, samplesMap)
// Iterate over them and make sure they're somewhat close. // Iterate over them and make sure they're somewhat close.
@ -227,9 +248,16 @@ func TestMemStatsEquivalence(t *testing.T) {
for i := 0; i < msRealValue.NumField(); i++ { for i := 0; i < msRealValue.NumField(); i++ {
fr := msRealValue.Field(i) fr := msRealValue.Field(i)
ff := msFakeValue.Field(i) ff := msFakeValue.Field(i)
switch typ.Kind() {
if typ.Field(i).Name == "PauseTotalNs" || typ.Field(i).Name == "LastGC" {
// We don't use those fields for metrics,
// thus we are not interested in having this filled.
continue
}
switch fr.Kind() {
// Fields which we are interested in are all uint64s.
// The only float64 field GCCPUFraction is by design omitted.
case reflect.Uint64: case reflect.Uint64:
// N.B. Almost all fields of MemStats are uint64s.
vr := fr.Interface().(uint64) vr := fr.Interface().(uint64)
vf := ff.Interface().(uint64) vf := ff.Interface().(uint64)
if float64(vr-vf)/float64(vf) > 0.05 { if float64(vr-vf)/float64(vf) > 0.05 {
@ -240,7 +268,7 @@ func TestMemStatsEquivalence(t *testing.T) {
} }
func TestExpectedRuntimeMetrics(t *testing.T) { func TestExpectedRuntimeMetrics(t *testing.T) {
goMetrics := collectGoMetrics(t) goMetrics := collectGoMetrics(t, goRuntimeMetricsCollection)
goMetricSet := make(map[string]Metric) goMetricSet := make(map[string]Metric)
for _, m := range goMetrics { for _, m := range goMetrics {
goMetricSet[m.Desc().fqName] = m goMetricSet[m.Desc().fqName] = m
@ -253,6 +281,7 @@ func TestExpectedRuntimeMetrics(t *testing.T) {
rmName := descs[i].Name rmName := descs[i].Name
rmSet[rmName] = struct{}{} rmSet[rmName] = struct{}{}
// expectedRuntimeMetrics depends on Go version.
expFQName, ok := expectedRuntimeMetrics[rmName] expFQName, ok := expectedRuntimeMetrics[rmName]
if !ok { if !ok {
t.Errorf("found new runtime/metrics metric %s", rmName) t.Errorf("found new runtime/metrics metric %s", rmName)
@ -268,6 +297,7 @@ func TestExpectedRuntimeMetrics(t *testing.T) {
continue continue
} }
} }
// Now iterate over the expected metrics and look for removals. // Now iterate over the expected metrics and look for removals.
cardinality := 0 cardinality := 0
for rmName, fqName := range expectedRuntimeMetrics { for rmName, fqName := range expectedRuntimeMetrics {