mirror of
https://github.com/fatedier/frp.git
synced 2026-10-20 21:43:28 +08:00
Add pkg/metrics/text, a small Prometheus text-format writer, and use it for frpc's /metrics. The endpoint lives only on metricsServer and is covered by its Basic Auth, like the other endpoints there. The package emits the Go runtime families and, on Linux, the process families read from /proc. Their names, types and help strings match client_golang's default collectors, so existing Go runtime dashboards keep working. It deliberately emits no frp_ business metrics and none of client_golang's promhttp_ handler metrics. Writing the subset ourselves keeps client_golang and protobuf out of the frpc binary; both are still used by frps and by the tests here.
399 lines
14 KiB
Go
399 lines
14 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 and still in use.",
|
|
typ: typeGauge,
|
|
get: func(ms *runtime.MemStats) float64 { return float64(ms.Alloc) },
|
|
},
|
|
{
|
|
name: "go_memstats_alloc_bytes_total",
|
|
help: "Total number of bytes allocated, even if freed.",
|
|
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.",
|
|
typ: typeGauge,
|
|
get: func(ms *runtime.MemStats) float64 { return float64(ms.BuckHashSys) },
|
|
},
|
|
{
|
|
name: "go_memstats_frees_total",
|
|
help: "Total number of frees.",
|
|
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.",
|
|
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 still in use.",
|
|
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.",
|
|
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.",
|
|
typ: typeGauge,
|
|
get: func(ms *runtime.MemStats) float64 { return float64(ms.HeapInuse) },
|
|
},
|
|
{
|
|
name: "go_memstats_heap_objects",
|
|
help: "Number of allocated 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.",
|
|
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.",
|
|
typ: typeGauge,
|
|
get: func(ms *runtime.MemStats) float64 { return float64(ms.HeapSys) },
|
|
},
|
|
{
|
|
name: "go_memstats_lookups_total",
|
|
help: "Total number of pointer lookups.",
|
|
typ: typeCounter,
|
|
get: func(ms *runtime.MemStats) float64 { return float64(ms.Lookups) },
|
|
},
|
|
{
|
|
name: "go_memstats_mallocs_total",
|
|
help: "Total number of mallocs.",
|
|
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.",
|
|
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.",
|
|
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.",
|
|
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.",
|
|
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.",
|
|
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.",
|
|
typ: typeGauge,
|
|
get: func(ms *runtime.MemStats) float64 { return float64(ms.OtherSys) },
|
|
},
|
|
{
|
|
name: "go_memstats_stack_inuse_bytes",
|
|
help: "Number of bytes in use by the stack allocator.",
|
|
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.",
|
|
typ: typeGauge,
|
|
get: func(ms *runtime.MemStats) float64 { return float64(ms.StackSys) },
|
|
},
|
|
{
|
|
name: "go_memstats_sys_bytes",
|
|
help: "Number of bytes obtained from system.",
|
|
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 pause duration of 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())}},
|
|
},
|
|
)
|
|
}
|
|
|
|
func threadCount() int {
|
|
n, _ := runtime.ThreadCreateProfile(nil)
|
|
return n
|
|
}
|