Files
frp/pkg/metrics/text/runtime.go
T
fatedier 5c0a125c16 deps: bump prometheus/client_golang to v1.25.0 (#5599)
Move client_golang to v1.25.0 with its required module updates
(client_model v0.6.3, common v0.72.0, procfs v0.22.0, xxhash v2.3.0,
protobuf v1.36.12, munnerz/goautoneg). go.mod keeps go 1.26.0 and has
no toolchain line.

Sync the frpc text metrics clone with the new default Go collector:
add go_gc_gogc_percent, go_gc_gomemlimit_bytes and
go_sched_gomaxprocs_threads, update the go_memstats help texts, and
remove go_memstats_lookups_total. frpc still exposes go_* metrics only.
2026-11-04 14:43:00 +08:00

425 lines
16 KiB
Go

// Copyright 2026 The frp 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 text
import (
"runtime"
"runtime/debug"
"runtime/metrics"
"sync"
"time"
)
// memstats is one go_memstats_* family. The values are read the same way
// client_golang reads them since Go 1.17: from runtime/metrics, which does not
// stop the world, and then mapped onto runtime.MemStats fields.
type memstats struct {
name string
help string
typ metricType
get func(*runtime.MemStats) float64
}
// memstatsFamilies matches the go_memstats_* families of client_golang's default
// registry, including their help strings and types.
var memstatsFamilies = []memstats{
{
name: "go_memstats_alloc_bytes",
help: "Number of bytes allocated in heap and currently in use. Equals to /memory/classes/heap/objects:bytes.",
typ: typeGauge,
get: func(ms *runtime.MemStats) float64 { return float64(ms.Alloc) },
},
{
name: "go_memstats_alloc_bytes_total",
help: "Total number of bytes allocated in heap until now, even if released already. Equals to /gc/heap/allocs:bytes.",
typ: typeCounter,
get: func(ms *runtime.MemStats) float64 { return float64(ms.TotalAlloc) },
},
{
name: "go_memstats_buck_hash_sys_bytes",
help: "Number of bytes used by the profiling bucket hash table. Equals to /memory/classes/profiling/buckets:bytes.",
typ: typeGauge,
get: func(ms *runtime.MemStats) float64 { return float64(ms.BuckHashSys) },
},
{
name: "go_memstats_frees_total",
help: "Total number of heap objects frees. Equals to /gc/heap/frees:objects + /gc/heap/tiny/allocs:objects.",
typ: typeCounter,
get: func(ms *runtime.MemStats) float64 { return float64(ms.Frees) },
},
{
name: "go_memstats_gc_sys_bytes",
help: "Number of bytes used for garbage collection system metadata. Equals to /memory/classes/metadata/other:bytes.",
typ: typeGauge,
get: func(ms *runtime.MemStats) float64 { return float64(ms.GCSys) },
},
{
name: "go_memstats_heap_alloc_bytes",
help: "Number of heap bytes allocated and currently in use, same as go_memstats_alloc_bytes. Equals to /memory/classes/heap/objects:bytes.",
typ: typeGauge,
get: func(ms *runtime.MemStats) float64 { return float64(ms.HeapAlloc) },
},
{
name: "go_memstats_heap_idle_bytes",
help: "Number of heap bytes waiting to be used. Equals to /memory/classes/heap/released:bytes + /memory/classes/heap/free:bytes.",
typ: typeGauge,
get: func(ms *runtime.MemStats) float64 { return float64(ms.HeapIdle) },
},
{
name: "go_memstats_heap_inuse_bytes",
help: "Number of heap bytes that are in use. Equals to /memory/classes/heap/objects:bytes + /memory/classes/heap/unused:bytes",
typ: typeGauge,
get: func(ms *runtime.MemStats) float64 { return float64(ms.HeapInuse) },
},
{
name: "go_memstats_heap_objects",
help: "Number of currently allocated objects. Equals to /gc/heap/objects:objects.",
typ: typeGauge,
get: func(ms *runtime.MemStats) float64 { return float64(ms.HeapObjects) },
},
{
name: "go_memstats_heap_released_bytes",
help: "Number of heap bytes released to OS. Equals to /memory/classes/heap/released:bytes.",
typ: typeGauge,
get: func(ms *runtime.MemStats) float64 { return float64(ms.HeapReleased) },
},
{
name: "go_memstats_heap_sys_bytes",
help: "Number of heap bytes obtained from system. Equals to /memory/classes/heap/objects:bytes + /memory/classes/heap/unused:bytes + /memory/classe" +
"s/heap/released:bytes + /memory/classes/heap/free:bytes.",
typ: typeGauge,
get: func(ms *runtime.MemStats) float64 { return float64(ms.HeapSys) },
},
{
name: "go_memstats_mallocs_total",
help: "Total number of heap objects allocated, both live and gc-ed. Semantically a counter version for go_memstats_heap_objects gauge. Equals to /g" +
"c/heap/allocs:objects + /gc/heap/tiny/allocs:objects.",
typ: typeCounter,
get: func(ms *runtime.MemStats) float64 { return float64(ms.Mallocs) },
},
{
name: "go_memstats_mcache_inuse_bytes",
help: "Number of bytes in use by mcache structures. Equals to /memory/classes/metadata/mcache/inuse:bytes.",
typ: typeGauge,
get: func(ms *runtime.MemStats) float64 { return float64(ms.MCacheInuse) },
},
{
name: "go_memstats_mcache_sys_bytes",
help: "Number of bytes used for mcache structures obtained from system. Equals to /memory/classes/metadata/mcache/inuse:bytes + /memory/classes/met" +
"adata/mcache/free:bytes.",
typ: typeGauge,
get: func(ms *runtime.MemStats) float64 { return float64(ms.MCacheSys) },
},
{
name: "go_memstats_mspan_inuse_bytes",
help: "Number of bytes in use by mspan structures. Equals to /memory/classes/metadata/mspan/inuse:bytes.",
typ: typeGauge,
get: func(ms *runtime.MemStats) float64 { return float64(ms.MSpanInuse) },
},
{
name: "go_memstats_mspan_sys_bytes",
help: "Number of bytes used for mspan structures obtained from system. Equals to /memory/classes/metadata/mspan/inuse:bytes + /memory/classes/metad" +
"ata/mspan/free:bytes.",
typ: typeGauge,
get: func(ms *runtime.MemStats) float64 { return float64(ms.MSpanSys) },
},
{
name: "go_memstats_next_gc_bytes",
help: "Number of heap bytes when next garbage collection will take place. Equals to /gc/heap/goal:bytes.",
typ: typeGauge,
get: func(ms *runtime.MemStats) float64 { return float64(ms.NextGC) },
},
{
name: "go_memstats_other_sys_bytes",
help: "Number of bytes used for other system allocations. Equals to /memory/classes/other:bytes.",
typ: typeGauge,
get: func(ms *runtime.MemStats) float64 { return float64(ms.OtherSys) },
},
{
name: "go_memstats_stack_inuse_bytes",
help: "Number of bytes obtained from system for stack allocator in non-CGO environments. Equals to /memory/classes/heap/stacks:bytes.",
typ: typeGauge,
get: func(ms *runtime.MemStats) float64 { return float64(ms.StackInuse) },
},
{
name: "go_memstats_stack_sys_bytes",
help: "Number of bytes obtained from system for stack allocator. Equals to /memory/classes/heap/stacks:bytes + /memory/classes/os-stacks:bytes.",
typ: typeGauge,
get: func(ms *runtime.MemStats) float64 { return float64(ms.StackSys) },
},
{
name: "go_memstats_sys_bytes",
help: "Number of bytes obtained from system. Equals to /memory/classes/total:byte.",
typ: typeGauge,
get: func(ms *runtime.MemStats) float64 { return float64(ms.Sys) },
},
}
// The runtime/metrics names below are the ones client_golang v1.19.1 reads to
// build the go_memstats_* families. See prometheus/go_collector_latest.go in
// that module (Apache-2.0).
const (
rmGCHeapTinyAllocsObjects = "/gc/heap/tiny/allocs:objects"
rmGCHeapAllocsObjects = "/gc/heap/allocs:objects"
rmGCHeapFreesObjects = "/gc/heap/frees:objects"
rmGCHeapAllocsBytes = "/gc/heap/allocs:bytes"
rmGCHeapObjects = "/gc/heap/objects:objects"
rmGCHeapGoalBytes = "/gc/heap/goal:bytes"
rmMemoryClassesTotalBytes = "/memory/classes/total:bytes"
rmMemoryClassesHeapObjectsBytes = "/memory/classes/heap/objects:bytes"
rmMemoryClassesHeapUnusedBytes = "/memory/classes/heap/unused:bytes"
rmMemoryClassesHeapReleasedBytes = "/memory/classes/heap/released:bytes"
rmMemoryClassesHeapFreeBytes = "/memory/classes/heap/free:bytes"
rmMemoryClassesHeapStacksBytes = "/memory/classes/heap/stacks:bytes"
rmMemoryClassesOSStacksBytes = "/memory/classes/os-stacks:bytes"
rmMemoryClassesMetadataMSpanInuseBytes = "/memory/classes/metadata/mspan/inuse:bytes"
rmMemoryClassesMetadataMSpanFreeBytes = "/memory/classes/metadata/mspan/free:bytes"
rmMemoryClassesMetadataMCacheInuseBytes = "/memory/classes/metadata/mcache/inuse:bytes"
rmMemoryClassesMetadataMCacheFreeBytes = "/memory/classes/metadata/mcache/free:bytes"
rmMemoryClassesProfilingBucketsBytes = "/memory/classes/profiling/buckets:bytes"
rmMemoryClassesMetadataOtherBytes = "/memory/classes/metadata/other:bytes"
rmMemoryClassesOtherBytes = "/memory/classes/other:bytes"
)
// memoryNames lists every runtime/metrics name memStatsFromRM reads.
var memoryNames = []string{
rmGCHeapTinyAllocsObjects,
rmGCHeapAllocsObjects,
rmGCHeapFreesObjects,
rmGCHeapAllocsBytes,
rmGCHeapObjects,
rmGCHeapGoalBytes,
rmMemoryClassesTotalBytes,
rmMemoryClassesHeapObjectsBytes,
rmMemoryClassesHeapUnusedBytes,
rmMemoryClassesHeapReleasedBytes,
rmMemoryClassesHeapFreeBytes,
rmMemoryClassesHeapStacksBytes,
rmMemoryClassesOSStacksBytes,
rmMemoryClassesMetadataMSpanInuseBytes,
rmMemoryClassesMetadataMSpanFreeBytes,
rmMemoryClassesMetadataMCacheInuseBytes,
rmMemoryClassesMetadataMCacheFreeBytes,
rmMemoryClassesProfilingBucketsBytes,
rmMemoryClassesMetadataOtherBytes,
rmMemoryClassesOtherBytes,
}
// memorySamples holds the sample buffer of one scrape and the index that maps a
// runtime/metrics name to its slot.
type memorySamples struct {
samples []metrics.Sample
index map[string]int
}
// memStatsSnapshot takes the Go memory statistics frpc reports. It reads the
// runtime/metrics package instead of calling runtime.ReadMemStats, which stops
// the world on every call, and memStatsFromRM maps the samples onto the old
// field names.
//
// The sample buffer is reused between scrapes, so concurrent scrapes take turns:
// runtime/metrics.Read writes into the samples it is handed.
func memStatsSnapshot() runtime.MemStats {
memoryMu.Lock()
defer memoryMu.Unlock()
if memoryBuffer == nil {
memoryBuffer = newMemorySamples()
}
// metrics.Read indexes into the slice, so an empty buffer has to be skipped.
if len(memoryBuffer.samples) > 0 {
metrics.Read(memoryBuffer.samples)
}
var ms runtime.MemStats
memStatsFromRM(&ms, memoryBuffer.value)
return ms
}
var (
memoryMu sync.Mutex
memoryBuffer *memorySamples
)
// newMemorySamples builds the sample buffer from the names this runtime supports.
// A name the runtime does not know is left out and reads as zero, which is what
// client_golang does as well.
func newMemorySamples() *memorySamples {
supported := make(map[string]struct{}, len(memoryNames))
for _, d := range metrics.All() {
supported[d.Name] = struct{}{}
}
s := &memorySamples{index: make(map[string]int, len(memoryNames))}
for _, name := range memoryNames {
if _, ok := supported[name]; !ok {
continue
}
s.index[name] = len(s.samples)
s.samples = append(s.samples, metrics.Sample{Name: name})
}
return s
}
// value returns the sampled value of name, or zero when the runtime does not
// expose it.
func (s *memorySamples) value(name string) uint64 {
i, ok := s.index[name]
if !ok {
return 0
}
return s.samples[i].Value.Uint64()
}
// memStatsFromRM maps runtime/metrics samples onto a runtime.MemStats, using the
// same field combinations as client_golang v1.19.1, so the go_memstats_* values
// match the ones the official collector reports for the same process. GCCPUFraction
// is left at zero, as client_golang omits it too.
func memStatsFromRM(ms *runtime.MemStats, lookup func(string) uint64) {
// MemStats counts tiny allocations in both Mallocs and Frees. client_golang
// keeps that behavior because Mallocs-Frees is then a live object count.
tinyAllocs := lookup(rmGCHeapTinyAllocsObjects)
ms.Mallocs = lookup(rmGCHeapAllocsObjects) + tinyAllocs
ms.Frees = lookup(rmGCHeapFreesObjects) + tinyAllocs
ms.TotalAlloc = lookup(rmGCHeapAllocsBytes)
ms.Sys = lookup(rmMemoryClassesTotalBytes)
ms.Lookups = 0 // Already always zero.
ms.HeapAlloc = lookup(rmMemoryClassesHeapObjectsBytes)
ms.Alloc = ms.HeapAlloc
ms.HeapInuse = ms.HeapAlloc + lookup(rmMemoryClassesHeapUnusedBytes)
ms.HeapReleased = lookup(rmMemoryClassesHeapReleasedBytes)
ms.HeapIdle = ms.HeapReleased + lookup(rmMemoryClassesHeapFreeBytes)
ms.HeapSys = ms.HeapInuse + ms.HeapIdle
ms.HeapObjects = lookup(rmGCHeapObjects)
ms.StackInuse = lookup(rmMemoryClassesHeapStacksBytes)
ms.StackSys = ms.StackInuse + lookup(rmMemoryClassesOSStacksBytes)
ms.MSpanInuse = lookup(rmMemoryClassesMetadataMSpanInuseBytes)
ms.MSpanSys = ms.MSpanInuse + lookup(rmMemoryClassesMetadataMSpanFreeBytes)
ms.MCacheInuse = lookup(rmMemoryClassesMetadataMCacheInuseBytes)
ms.MCacheSys = ms.MCacheInuse + lookup(rmMemoryClassesMetadataMCacheFreeBytes)
ms.BuckHashSys = lookup(rmMemoryClassesProfilingBucketsBytes)
ms.GCSys = lookup(rmMemoryClassesMetadataOtherBytes)
ms.OtherSys = lookup(rmMemoryClassesOtherBytes)
ms.NextGC = lookup(rmGCHeapGoalBytes)
}
// gcQuantileLabels matches the quantiles client_golang reports in the
// go_gc_duration_seconds summary.
var gcQuantileLabels = []string{"0", "0.25", "0.5", "0.75", "1"}
// runtimeFamilies reads the runtime once per scrape rather than once per metric.
func runtimeFamilies() []family {
ms := memStatsSnapshot()
out := make([]family, 0, len(memstatsFamilies)+5)
for _, m := range memstatsFamilies {
out = append(out, family{
name: m.name,
help: m.help,
typ: m.typ,
samples: []sample{{value: m.get(&ms)}},
})
}
// debug.ReadGCStats only takes the mheap lock; it does not stop the world.
var gc debug.GCStats
gc.PauseQuantiles = make([]time.Duration, len(gcQuantileLabels))
debug.ReadGCStats(&gc)
gcSamples := make([]sample, 0, len(gcQuantileLabels)+2)
for i, q := range gcQuantileLabels {
var pause time.Duration
if i < len(gc.PauseQuantiles) {
pause = gc.PauseQuantiles[i]
}
gcSamples = append(gcSamples, sample{
labels: []label{{name: "quantile", value: q}},
value: pause.Seconds(),
})
}
gcSamples = append(gcSamples,
sample{name: "go_gc_duration_seconds_sum", value: gc.PauseTotal.Seconds()},
sample{name: "go_gc_duration_seconds_count", value: float64(gc.NumGC)},
)
return append(out,
family{
name: "go_gc_duration_seconds",
help: "A summary of the wall-time pause (stop-the-world) duration in garbage collection cycles.",
typ: typeSummary,
samples: gcSamples,
},
family{
name: "go_goroutines",
help: "Number of goroutines that currently exist.",
typ: typeGauge,
samples: []sample{{value: float64(runtime.NumGoroutine())}},
},
family{
name: "go_info",
help: "Information about the Go environment.",
typ: typeGauge,
samples: []sample{{labels: []label{{name: "version", value: runtime.Version()}}, value: 1}},
},
family{
name: "go_memstats_last_gc_time_seconds",
help: "Number of seconds since 1970 of last garbage collection.",
typ: typeGauge,
samples: []sample{{
value: float64(gc.LastGC.UnixNano()) / float64(time.Second),
}},
},
family{
name: "go_threads",
help: "Number of OS threads created.",
typ: typeGauge,
samples: []sample{{value: float64(threadCount())}},
},
family{
name: "go_gc_gogc_percent",
help: "Heap size target percentage configured by the user, otherwise 100. This value is set by the GOGC environment variable, and the runtime/debug" +
".SetGCPercent function. Sourced from /gc/gogc:percent.",
typ: typeGauge,
samples: []sample{{value: float64(readRuntimeMetric("/gc/gogc:percent"))}},
},
family{
name: "go_gc_gomemlimit_bytes",
help: "Go runtime memory limit configured by the user, otherwise math.MaxInt64. This value is set by the GOMEMLIMIT environment variable, and the r" +
"untime/debug.SetMemoryLimit function. Sourced from /gc/gomemlimit:bytes.",
typ: typeGauge,
samples: []sample{{value: float64(readRuntimeMetric("/gc/gomemlimit:bytes"))}},
},
family{
name: "go_sched_gomaxprocs_threads",
help: "The current runtime.GOMAXPROCS setting, or the number of operating system threads that can execute user-level Go code simultaneously. Source" +
"d from /sched/gomaxprocs:threads.",
typ: typeGauge,
samples: []sample{{value: float64(runtime.GOMAXPROCS(0))}},
},
)
}
// readRuntimeMetric reads one runtime/metrics gauge.
func readRuntimeMetric(name string) uint64 {
s := []metrics.Sample{{Name: name}}
metrics.Read(s)
return s[0].Value.Uint64()
}
func threadCount() int {
n, _ := runtime.ThreadCreateProfile(nil)
return n
}