This commit is contained in:
bluepython508 committed 2026-10-22 12:27:19 +01:00
1 parent 3530afb854
commit 6903fcff27
2146 files changed
+112256 -190703

No files matched your search

+88
View File
@@ -0,0 +1,88 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !android
package varz
import (
"bytes"
"expvar"
"os"
"runtime/debug"
"strings"
)
func init() {
var capable any = hostGOAMD64Level()
expvar.Publish("gauge_goamd64_capable", expvar.Func(func() any { return capable }))
var compiled any = compiledGOAMD64Level()
expvar.Publish("gauge_goamd64_compiled", expvar.Func(func() any { return compiled }))
}
// goamd64LevelFlags maps each x86-64 microarchitecture level to the
// /proc/cpuinfo flag names it requires beyond the previous level.
// It matches the per-level feature checks in Go's runtime/asm_amd64.s.
// Note that /proc/cpuinfo spells SSE3 as "pni" and LZCNT as "abm",
// and the kernel clears the AVX and AVX-512 flags when the OS lacks
// XSAVE support, so OSXSAVE needs no separate check.
var goamd64LevelFlags = [...][]string{
2: {"cx16", "lahf_lm", "popcnt", "pni", "sse4_1", "sse4_2", "ssse3"},
3: {"abm", "avx", "avx2", "bmi1", "bmi2", "f16c", "fma", "movbe"},
4: {"avx512f", "avx512bw", "avx512cd", "avx512dq", "avx512vl"},
}
// hostGOAMD64Level returns the maximum GOAMD64 microarchitecture
// level (1-4) that the host CPU supports, regardless of what level
// this binary was compiled for. It returns 0 on error.
func hostGOAMD64Level() int {
cpuinfo, err := os.ReadFile("/proc/cpuinfo")
if err != nil {
return 0
}
for line := range bytes.Lines(cpuinfo) {
name, rest, ok := bytes.Cut(line, []byte(":"))
if ok && string(bytes.TrimSpace(name)) == "flags" {
return goamd64Level(strings.Fields(string(rest)))
}
}
return 0
}
// goamd64Level returns the maximum GOAMD64 level (1-4) supported by
// a CPU with the given /proc/cpuinfo flags.
func goamd64Level(flags []string) int {
has := make(map[string]bool, len(flags))
for _, f := range flags {
has[f] = true
}
level := 1
for next := 2; next < len(goamd64LevelFlags); next++ {
for _, f := range goamd64LevelFlags[next] {
if !has[f] {
return level
}
}
level = next
}
return level
}
// compiledGOAMD64Level returns the GOAMD64 microarchitecture level
// (1-4) that this binary was compiled for, as recorded in its build
// info. It returns 0 if unknown.
func compiledGOAMD64Level() int {
bi, ok := debug.ReadBuildInfo()
if !ok {
return 0
}
for _, s := range bi.Settings {
if s.Key == "GOAMD64" {
if v, ok := strings.CutPrefix(s.Value, "v"); ok && len(v) == 1 && v[0] >= '1' && v[0] <= '4' {
return int(v[0] - '0')
}
return 0
}
}
return 0
}
+508
View File
@@ -0,0 +1,508 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package varz
import (
"io"
"math"
"runtime/metrics"
"strconv"
"strings"
"sync"
)
// runtimeMetricSpec describes one Go runtime/metrics metric that
// [Handler] exports in Prometheus format.
type runtimeMetricSpec struct {
// name is the runtime/metrics name, such as "/gc/heap/allocs:bytes".
name string
// typ is the Prometheus type: "gauge", "counter", or "histogram".
// Counters get a "_total" suffix, as in the Prometheus naming
// conventions.
typ string
// buckets says how a histogram's runtime buckets map to Prometheus
// buckets. It is required for histograms and ignored otherwise.
buckets histBuckets
// hidden means the metric is read (because it backs one of the
// legacy memstats_* metrics) but is not exported under its own
// go_runtime_ name.
hidden bool
}
// histBuckets says how the runtime's lower-bound-inclusive buckets
// [lo, hi) of a histogram become Prometheus's upper-bound-inclusive
// le buckets.
type histBuckets uint8
const (
// histTime is for the runtime's time histograms, which have
// hundreds of buckets. They are reduced as described at
// [runtimeHistMinBucket], and a runtime bucket [lo, hi) becomes
// le=hi, which is only wrong for a value exactly equal to hi.
histTime histBuckets = iota + 1
// histPowersOfTwo is for histograms whose observed values are all
// exactly equal to bucket lower bounds, such as goroutine stack
// sizes. A runtime bucket [lo, hi) becomes le=lo, so the labels
// name the values they count, and the runtime's final catch-all
// bucket is folded into +Inf.
histPowersOfTwo
// histInts is for histograms of small integer values, such as
// counts of GC cycles. A runtime bucket [lo, hi) becomes le=hi-1,
// which is exact for integers.
histInts
)
// runtimeMetricSpecs is the set of Go runtime/metrics metrics that
// [Handler] reads. Everything here is cheap for the runtime to read: no
// metric in this list stops the world or walks the heap or the
// goroutine list.
//
// The list is deliberately short. The hidden entries only back the
// legacy memstats_* metrics. The rest are either specific to the
// Tailscale fork of Go or have no runtime.MemStats equivalent and earn
// their place on a dashboard.
//
// Names that the running toolchain does not know (such as the
// /tailscale/ metrics when not built with the fork) are dropped on
// first use, so it is fine for this list to be a superset of what
// exists.
//
// Go's Cumulative metrics are declared as counters here and the rest
// as gauges; TestRuntimeMetricSpecs checks that against the
// runtime's own descriptions.
var runtimeMetricSpecs = []runtimeMetricSpec{
// These back the memstats_* metrics; see memstatsIndexes.
{name: "/memory/classes/heap/objects:bytes", typ: "gauge", hidden: true},
{name: "/gc/heap/allocs:bytes", typ: "counter", hidden: true},
{name: "/memory/classes/total:bytes", typ: "gauge", hidden: true},
{name: "/gc/heap/allocs:objects", typ: "counter", hidden: true},
{name: "/gc/heap/frees:objects", typ: "counter", hidden: true},
{name: "/gc/heap/tiny/allocs:objects", typ: "counter", hidden: true},
{name: "/gc/cycles/total:gc-cycles", typ: "counter", hidden: true},
{name: "/cpu/classes/gc/mark/idle:cpu-seconds", typ: "counter", hidden: true},
// GC CPU time as counters, so that rate() over a window works.
// memstats_gc_cpu_fraction, derived from these two (less the
// hidden idle mark time above), is a lifetime average and cannot
// show a recent change.
{name: "/cpu/classes/gc/total:cpu-seconds", typ: "counter"},
{name: "/cpu/classes/total:cpu-seconds", typ: "counter"},
// Live heap after the last GC. Unlike memstats_heap_alloc it
// excludes garbage not yet swept, so it is the number to compare
// against GOMEMLIMIT or a container memory limit.
{name: "/gc/heap/live:bytes", typ: "gauge"},
// How long runnable goroutines wait to be scheduled, and how long
// GC stop-the-world pauses take. Neither is visible any other way.
{name: "/sched/latencies:seconds", typ: "histogram", buckets: histTime},
{name: "/sched/pauses/total/gc:seconds", typ: "histogram", buckets: histTime},
// OS threads owned by the runtime, for spotting thread leaks from
// blocking syscalls or cgo.
{name: "/sched/threads/total:threads", typ: "gauge"},
// Time spent blocked on mutexes. cmd/derper used to export this
// itself as go_sync_mutex_wait_seconds.
{name: "/sync/mutex/wait/total:seconds", typ: "counter"},
// The /tailscale/ metrics exist only in the Tailscale fork of Go
// (github.com/tailscale/go), used when building with the
// tailscale_go build tag.
{name: "/tailscale/sched/goroutines-by-stack-size:bytes", typ: "histogram", buckets: histPowersOfTwo},
{name: "/tailscale/sched/stacks/copied:bytes", typ: "counter"},
{name: "/tailscale/sched/stacks/growths:events", typ: "counter"},
{name: "/tailscale/sched/stacks/shrinks:events", typ: "counter"},
{name: "/tailscale/sched/timers/tracked:timers", typ: "gauge"},
{name: "/tailscale/sched/timers/zombies/chan:timers", typ: "gauge"},
{name: "/tailscale/sched/timers/zombies/func:timers", typ: "gauge"},
{name: "/tailscale/sched/timers/zombies/func/created:timers", typ: "counter"},
{name: "/tailscale/sched/timers/zombies/func/removed:timers", typ: "counter"},
{name: "/tailscale/sched/timers/zombies/func/lifetime:gc-cycles", typ: "histogram", buckets: histInts},
{name: "/tailscale/sched/timers/zombies:timers", typ: "gauge"},
}
// runtimeMetricPrefix is the namespace prefix of exported runtime
// metrics. It is deliberately not "go_", which is the namespace used
// by the Prometheus Go client's collector; binaries that also use that
// client (see tailscale.com/tsweb/promvarz) would otherwise export
// some series twice, and Prometheus rejects a scrape containing
// duplicate series.
const runtimeMetricPrefix = "go_runtime_"
// runtimeMetricPromName converts a runtime/metrics name such as
// "/gc/heap/allocs:bytes" to a Prometheus metric name such as
// "go_runtime_gc_heap_allocs_bytes_total". The conversion follows the
// same rules as the Prometheus Go client's collector, other than the
// namespace: the path becomes the name with slashes and dashes
// replaced by underscores, the unit is appended, and counters get a
// "_total" suffix.
func runtimeMetricPromName(name, typ string) string {
path, unit, _ := strings.Cut(strings.TrimPrefix(name, "/"), ":")
r := strings.NewReplacer("/", "_", "-", "_")
unit = strings.ReplaceAll(unit, "/", "_per_")
unit = strings.ReplaceAll(unit, "*", "_")
s := runtimeMetricPrefix + r.Replace(path) + "_" + r.Replace(unit)
if typ == "counter" {
s += "_total"
}
return s
}
// Time histograms from the runtime have hundreds of buckets, four per
// power of two from 256ns (finer below that) to about a day. That is
// far too many series for Prometheus, so they are reduced to one
// bucket per factor of runtimeHistBucketFactor starting at
// runtimeHistMinBucket, and everything above runtimeHistMaxBucket is
// folded into the +Inf bucket. Because the runtime's bucket boundaries
// are at powers of two times small integers, a factor of 4 starting at
// 256ns lands exactly on runtime boundaries, so the reduction only
// merges runtime buckets and never splits one.
const (
runtimeHistMinBucket = 256e-9 // seconds
runtimeHistMaxBucket = 1.0 // seconds
runtimeHistBucketFactor = 4
)
// runtimeHistLayout describes how a runtime histogram's buckets are
// merged into the exported Prometheus buckets. It is computed once per
// metric from the runtime's bucket boundaries, which never change.
type runtimeHistLayout struct {
// lines[i] is the exported line prefix for output bucket i, up to
// and including the space before the value:
// `name_bucket{le="0.001"} `. The last entry is the +Inf bucket.
lines [][]byte
// ends[i] is the number of leading runtime bucket counts summed
// into output bucket i, which is cumulative like Prometheus
// buckets. The last entry, for +Inf, is the total number of
// runtime buckets.
ends []int
}
// newRuntimeHistLayout computes the export layout for a runtime
// histogram with the given bucket boundaries (as in
// [metrics.Float64Histogram.Buckets], so the runtime bucket i counts
// values in [buckets[i], buckets[i+1])).
func newRuntimeHistLayout(promName string, buckets []float64, kind histBuckets) *runtimeHistLayout {
numCounts := len(buckets) - 1
lay := new(runtimeHistLayout)
addBucket := func(le float64, end int) {
line := append([]byte(promName), `_bucket{le="`...)
if math.IsInf(le, 1) {
line = append(line, "+Inf"...)
} else {
line = strconv.AppendFloat(line, le, 'g', -1, 64)
}
line = append(line, `"} `...)
lay.lines = append(lay.lines, line)
lay.ends = append(lay.ends, end)
}
switch kind {
case histPowersOfTwo:
// All but the final catch-all runtime bucket, each labeled
// with its lower bound.
for i := 0; i < numCounts-1; i++ {
addBucket(buckets[i], i+1)
}
case histInts:
// All but the final catch-all runtime bucket, each labeled
// with the largest integer it contains.
for i := 1; i < numCounts; i++ {
addBucket(buckets[i]-1, i)
}
case histTime:
last := 0.0
for i := 1; i < numCounts; i++ {
hi := buckets[i]
if math.IsInf(hi, 0) || hi < runtimeHistMinBucket {
continue
}
if last > runtimeHistMaxBucket {
break
}
if last == 0 || hi >= last*runtimeHistBucketFactor {
addBucket(hi, i)
last = hi
}
}
}
addBucket(math.Inf(1), numCounts)
return lay
}
// runtimeMetricState is a metric being exported, alongside its
// [metrics.Sample] in [runtimeMetricsExporter.samples].
type runtimeMetricState struct {
spec runtimeMetricSpec
// typeLine is the "# TYPE name type\n" line.
typeLine []byte
// valuePrefix is "name " for scalar metrics, or nil for histograms.
valuePrefix []byte
// countPrefix is "name_count " for histograms, or nil otherwise.
countPrefix []byte
// hist is the bucket layout for histograms, or nil otherwise.
hist *runtimeHistLayout
}
// runtimeMetricsExporter reads a fixed set of Go runtime/metrics and
// writes them in Prometheus format. There is one, [runtimeMetrics],
// and it keeps its sample slice across reads so that the runtime can
// reuse histogram buffers and a steady-state scrape does not allocate.
type runtimeMetricsExporter struct {
mu sync.Mutex
inited bool
samples []metrics.Sample // reused across reads; parallel to states
states []*runtimeMetricState // parallel to samples
// Indexes into samples of the metrics backing the legacy
// memstats_* names, or -1 if the runtime lacks them.
memstatsIdx memstatsIndexes
}
// memstatsIndexes holds indexes into [runtimeMetricsExporter.samples]
// for the runtime/metrics equivalents of the runtime.MemStats fields
// that varz has exported as memstats_* since before runtime/metrics
// existed. Those names are kept for existing dashboards, but they are
// now read from runtime/metrics rather than runtime.ReadMemStats,
// which stops the world.
type memstatsIndexes struct {
heapAlloc int // /memory/classes/heap/objects:bytes
totalAlloc int // /gc/heap/allocs:bytes
sys int // /memory/classes/total:bytes
mallocs int // /gc/heap/allocs:objects
frees int // /gc/heap/frees:objects
tinyAllocs int // /gc/heap/tiny/allocs:objects
numGC int // /gc/cycles/total:gc-cycles
gcCPU int // /cpu/classes/gc/total:cpu-seconds
gcIdleCPU int // /cpu/classes/gc/mark/idle:cpu-seconds
totalCPU int // /cpu/classes/total:cpu-seconds
}
// runtimeMetrics is the process-wide exporter used by [Handler].
var runtimeMetrics = new(runtimeMetricsExporter)
// runtimeMetricsBufPool holds *[]byte output buffers so that a scrape
// can be formatted under the exporter's lock and written to the
// (possibly slow) client after releasing it.
var runtimeMetricsBufPool = sync.Pool{New: func() any { return new([]byte) }}
// init reads every metric in [runtimeMetricSpecs] once, drops the ones
// this toolchain does not know, and precomputes the per-metric output
// prefixes and histogram layouts.
//
// e.mu must be held.
func (e *runtimeMetricsExporter) init() {
e.inited = true
samples := make([]metrics.Sample, len(runtimeMetricSpecs))
for i, spec := range runtimeMetricSpecs {
samples[i].Name = spec.name
}
metrics.Read(samples)
byName := map[string]int{}
for i, spec := range runtimeMetricSpecs {
s := samples[i]
var ok bool
switch s.Value.Kind() {
case metrics.KindUint64, metrics.KindFloat64:
ok = spec.typ != "histogram"
case metrics.KindFloat64Histogram:
ok = spec.typ == "histogram"
}
if !ok {
// Unknown to this toolchain, or its kind changed
// out from under our table.
continue
}
st := &runtimeMetricState{spec: spec}
if !spec.hidden {
name := runtimeMetricPromName(spec.name, spec.typ)
st.typeLine = []byte("# TYPE " + name + " " + spec.typ + "\n")
if spec.typ == "histogram" {
st.hist = newRuntimeHistLayout(name, s.Value.Float64Histogram().Buckets, spec.buckets)
st.countPrefix = []byte(name + "_count ")
} else {
st.valuePrefix = []byte(name + " ")
}
}
byName[spec.name] = len(e.samples)
e.samples = append(e.samples, s)
e.states = append(e.states, st)
}
idx := func(name string) int {
if i, ok := byName[name]; ok {
return i
}
return -1
}
e.memstatsIdx = memstatsIndexes{
heapAlloc: idx("/memory/classes/heap/objects:bytes"),
totalAlloc: idx("/gc/heap/allocs:bytes"),
sys: idx("/memory/classes/total:bytes"),
mallocs: idx("/gc/heap/allocs:objects"),
frees: idx("/gc/heap/frees:objects"),
tinyAllocs: idx("/gc/heap/tiny/allocs:objects"),
numGC: idx("/gc/cycles/total:gc-cycles"),
gcCPU: idx("/cpu/classes/gc/total:cpu-seconds"),
gcIdleCPU: idx("/cpu/classes/gc/mark/idle:cpu-seconds"),
totalCPU: idx("/cpu/classes/total:cpu-seconds"),
}
}
// writeTo reads the current values of the exported metrics and writes
// them to w in Prometheus text format, preceded by the legacy
// memstats_* metrics.
func (e *runtimeMetricsExporter) writeTo(w io.Writer) error {
bp := runtimeMetricsBufPool.Get().(*[]byte)
defer runtimeMetricsBufPool.Put(bp)
e.mu.Lock()
if !e.inited {
e.init()
} else {
metrics.Read(e.samples)
}
*bp = e.appendMemstats((*bp)[:0])
for i, st := range e.states {
if !st.spec.hidden {
*bp = st.appendTo(*bp, &e.samples[i].Value)
}
}
e.mu.Unlock()
_, err := w.Write(*bp)
return err
}
// appendTo appends the Prometheus lines for st with value v to b.
func (st *runtimeMetricState) appendTo(b []byte, v *metrics.Value) []byte {
b = append(b, st.typeLine...)
switch v.Kind() {
case metrics.KindUint64:
b = append(b, st.valuePrefix...)
b = strconv.AppendUint(b, v.Uint64(), 10)
b = append(b, '\n')
case metrics.KindFloat64:
b = append(b, st.valuePrefix...)
b = strconv.AppendFloat(b, v.Float64(), 'g', -1, 64)
b = append(b, '\n')
case metrics.KindFloat64Histogram:
counts := v.Float64Histogram().Counts
var sum uint64
var next int
for i, end := range st.hist.ends {
for ; next < end && next < len(counts); next++ {
sum += counts[next]
}
b = append(b, st.hist.lines[i]...)
b = strconv.AppendUint(b, sum, 10)
b = append(b, '\n')
}
b = append(b, st.countPrefix...)
b = strconv.AppendUint(b, sum, 10)
b = append(b, '\n')
}
return b
}
// appendMemstats appends the legacy memstats_* metrics to b, sourced
// from the runtime/metrics samples named by e.memstatsIdx.
//
// e.mu must be held and e.samples must have been read.
func (e *runtimeMetricsExporter) appendMemstats(b []byte) []byte {
idx := &e.memstatsIdx
u := func(i int) (uint64, bool) {
if i < 0 {
return 0, false
}
v := &e.samples[i].Value
if v.Kind() != metrics.KindUint64 {
return 0, false
}
return v.Uint64(), true
}
f := func(i int) (float64, bool) {
if i < 0 {
return 0, false
}
v := &e.samples[i].Value
if v.Kind() != metrics.KindFloat64 {
return 0, false
}
return v.Float64(), true
}
appendUint := func(typ, name string, v uint64, ok bool, help string) {
if ok {
b = appendMemstat(b, typ, name, help)
b = strconv.AppendUint(b, v, 10)
b = append(b, '\n')
}
}
appendIdx := func(typ, name string, i int, help string) {
v, ok := u(i)
appendUint(typ, name, v, ok, help)
}
appendIdx("gauge", "heap_alloc", idx.heapAlloc, "current bytes of allocated heap objects (up/down smoothly)")
appendIdx("counter", "total_alloc", idx.totalAlloc, "cumulative bytes allocated for heap objects")
appendIdx("gauge", "sys", idx.sys, "total bytes of memory obtained from the OS")
// runtime.ReadMemStats counts tiny allocations in both Mallocs
// and Frees, but /gc/heap/allocs:objects and /gc/heap/frees:objects
// exclude them, so add them back.
mallocs, ok1 := u(idx.mallocs)
frees, ok2 := u(idx.frees)
tiny, ok3 := u(idx.tinyAllocs)
appendUint("counter", "mallocs", mallocs+tiny, ok1 && ok3, "cumulative count of heap objects allocated")
appendUint("counter", "frees", frees+tiny, ok2 && ok3, "cumulative count of heap objects freed")
appendIdx("counter", "num_gc", idx.numGC, "number of completed GC cycles")
gc, ok1 := f(idx.gcCPU)
idle, ok2 := f(idx.gcIdleCPU)
total, ok3 := f(idx.totalCPU)
if ok1 && ok2 && ok3 {
// This is how the runtime computes MemStats.GCCPUFraction:
// GC time less idle-priority marking, which is "free", over
// total time. The runtime updates its CPU class stats at
// the end of each GC cycle, so total is 0 until the first
// one, as GCCPUFraction was too.
var frac float64
if total > 0 {
frac = (gc - idle) / total
}
b = appendMemstat(b, "gauge", "gc_cpu_fraction", "fraction of CPU time used by GC")
b = strconv.AppendFloat(b, frac, 'f', -1, 64)
b = append(b, '\n')
}
return b
}
// appendMemstat appends the HELP and TYPE lines for the memstats_*
// metric name to b, followed by the metric name and a space, leaving
// the caller to append the value and newline.
func appendMemstat(b []byte, typ, name, help string) []byte {
b = append(b, "# HELP memstats_"...)
b = append(b, name...)
b = append(b, ' ')
b = append(b, help...)
b = append(b, "\n# TYPE memstats_"...)
b = append(b, name...)
b = append(b, ' ')
b = append(b, typ...)
b = append(b, "\nmemstats_"...)
b = append(b, name...)
b = append(b, ' ')
return b
}
+57 -64
View File
@@ -1,11 +1,10 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package varz contains code to export metrics in Prometheus format.
package varz
import (
"bufio"
"cmp"
"expvar"
"fmt"
@@ -23,10 +22,8 @@ import (
"unicode"
"unicode/utf8"
"golang.org/x/exp/constraints"
"tailscale.com/metrics"
"tailscale.com/syncs"
"tailscale.com/types/logger"
"tailscale.com/version"
)
@@ -42,8 +39,34 @@ func init() {
expvar.Publish("go_version", StaticStringVar(runtime.Version()))
expvar.Publish("counter_uptime_sec", expvar.Func(func() any { return int64(Uptime().Seconds()) }))
expvar.Publish("gauge_goroutines", expvar.Func(func() any { return runtime.NumGoroutine() }))
if v := nodeBootTime(); v != 0 {
var vi any = v // box once
// The name matches what Prometheus's node exporter uses
// for the same value.
expvar.Publish("node_boot_time_seconds", expvar.Func(func() any { return vi }))
}
}
// nodeBootTime returns the machine's boot time in Unix seconds,
// as reported by the "btime" line of Linux's /proc/stat.
// It returns 0 if unavailable, such as on non-Linux systems.
var nodeBootTime = sync.OnceValue(func() int64 {
stat, err := os.ReadFile("/proc/stat")
if err != nil {
return 0
}
for line := range strings.Lines(string(stat)) {
if rest, ok := strings.CutPrefix(line, "btime "); ok {
sec, err := strconv.ParseInt(strings.TrimSpace(rest), 10, 64)
if err != nil {
return 0
}
return sec
}
}
return 0
})
const (
gaugePrefix = "gauge_"
counterPrefix = "counter_"
@@ -93,8 +116,8 @@ func prometheusMetric(prefix string, key string) (string, string, string) {
typ = "histogram"
key = strings.TrimPrefix(key, histogramPrefix)
}
if strings.HasPrefix(key, labelMapPrefix) {
key = strings.TrimPrefix(key, labelMapPrefix)
if after, ok := strings.CutPrefix(key, labelMapPrefix); ok {
key = after
if a, b, ok := strings.Cut(key, "_"); ok {
label, key = a, b
}
@@ -154,7 +177,7 @@ func writePromExpVar(w io.Writer, prefix string, kv expvar.KeyValue) {
case PrometheusMetricsReflectRooter:
root := v.PrometheusMetricsReflectRoot()
rv := reflect.ValueOf(root)
if rv.Type().Kind() == reflect.Ptr {
if rv.Type().Kind() == reflect.Pointer {
if rv.IsNil() {
return
}
@@ -189,11 +212,14 @@ func writePromExpVar(w io.Writer, prefix string, kv expvar.KeyValue) {
if typ == "" {
var funcRet string
if f, ok := kv.Value.(expvar.Func); ok {
v := f()
if ms, ok := v.(runtime.MemStats); ok && name == "memstats" {
writeMemstats(w, &ms)
if key == "memstats" {
// The expvar package's own memstats var calls
// runtime.ReadMemStats, which stops the world and
// allocates. [Handler] exports the memstats_* metrics
// from runtime/metrics instead, so don't even call it.
return
}
v := f()
if vs, ok := v.(string); ok && strings.HasSuffix(name, "version") {
if name == "version" {
fmt.Fprintf(w, "%s{version=%q,binary=%q} 1\n", name, vs, binaryName())
@@ -245,11 +271,21 @@ func writePromExpVar(w io.Writer, prefix string, kv expvar.KeyValue) {
if label != "" && typ != "" {
fmt.Fprintf(w, "# TYPE %s %s\n", name, typ)
v.Do(func(kv expvar.KeyValue) {
fmt.Fprintf(w, "%s{%s=%q} %v\n", name, label, kv.Key, kv.Value)
switch kv.Value.(type) {
case *expvar.Int, *expvar.Float:
fmt.Fprintf(w, "%s{%s=%q} %v\n", name, label, kv.Key, kv.Value)
default:
fmt.Fprintf(w, "# skipping %q expvar map key %q with unknown value type %T\n", name, kv.Key, kv.Value)
}
})
} else {
v.Do(func(kv expvar.KeyValue) {
fmt.Fprintf(w, "%s_%s %v\n", name, kv.Key, kv.Value)
switch kv.Value.(type) {
case *expvar.Int, *expvar.Float:
fmt.Fprintf(w, "%s_%s %v\n", name, kv.Key, kv.Value)
default:
fmt.Fprintf(w, "# skipping %q expvar map key %q with unknown value type %T\n", name, kv.Key, kv.Value)
}
})
}
}
@@ -292,13 +328,19 @@ type sortedKVs struct {
// is not exported.
//
// This will evolve over time, or perhaps be replaced.
//
// It also exports the Go runtime/metrics in [runtimeMetricSpecs] and
// the legacy memstats_* metrics, both read from runtime/metrics.
func Handler(w http.ResponseWriter, r *http.Request) {
ExpvarDoHandler(expvarDo)(w, r)
runtimeMetrics.writeTo(w)
}
// ExpvarDoHandler handler returns a Handler like above, but takes an optional
// expvar.Do func allow the usage of alternative containers of metrics, other
// than the global expvar.Map.
//
// Unlike [Handler], it does not export the Go runtime/metrics.
func ExpvarDoHandler(expvarDoFunc func(f func(expvar.KeyValue))) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain;version=0.0.4;charset=utf-8")
@@ -342,54 +384,6 @@ type PrometheusMetricsReflectRooter interface {
var expvarDo = expvar.Do // pulled out for tests
func writeMemstat[V constraints.Integer | constraints.Float](bw *bufio.Writer, typ, name string, v V, help string) {
if help != "" {
bw.WriteString("# HELP memstats_")
bw.WriteString(name)
bw.WriteString(" ")
bw.WriteString(help)
bw.WriteByte('\n')
}
bw.WriteString("# TYPE memstats_")
bw.WriteString(name)
bw.WriteString(" ")
bw.WriteString(typ)
bw.WriteByte('\n')
bw.WriteString("memstats_")
bw.WriteString(name)
bw.WriteByte(' ')
rt := reflect.TypeOf(v)
switch {
case rt == reflect.TypeFor[int]() ||
rt == reflect.TypeFor[uint]() ||
rt == reflect.TypeFor[int8]() ||
rt == reflect.TypeFor[uint8]() ||
rt == reflect.TypeFor[int16]() ||
rt == reflect.TypeFor[uint16]() ||
rt == reflect.TypeFor[int32]() ||
rt == reflect.TypeFor[uint32]() ||
rt == reflect.TypeFor[int64]() ||
rt == reflect.TypeFor[uint64]() ||
rt == reflect.TypeFor[uintptr]():
bw.Write(strconv.AppendInt(bw.AvailableBuffer(), int64(v), 10))
case rt == reflect.TypeFor[float32]() || rt == reflect.TypeFor[float64]():
bw.Write(strconv.AppendFloat(bw.AvailableBuffer(), float64(v), 'f', -1, 64))
}
bw.WriteByte('\n')
}
func writeMemstats(w io.Writer, ms *runtime.MemStats) {
fmt.Fprintf(w, "%v", logger.ArgWriter(func(bw *bufio.Writer) {
writeMemstat(bw, "gauge", "heap_alloc", ms.HeapAlloc, "current bytes of allocated heap objects (up/down smoothly)")
writeMemstat(bw, "counter", "total_alloc", ms.TotalAlloc, "cumulative bytes allocated for heap objects")
writeMemstat(bw, "gauge", "sys", ms.Sys, "total bytes of memory obtained from the OS")
writeMemstat(bw, "counter", "mallocs", ms.Mallocs, "cumulative count of heap objects allocated")
writeMemstat(bw, "counter", "frees", ms.Frees, "cumulative count of heap objects freed")
writeMemstat(bw, "counter", "num_gc", ms.NumGC, "number of completed GC cycles")
writeMemstat(bw, "gauge", "gc_cpu_fraction", ms.GCCPUFraction, "fraction of CPU time used by GC")
}))
}
// sortedStructField is metadata about a struct field used both for sorting once
// (by structTypeSortedFields) and at serving time (by
// foreachExportedStructField).
@@ -409,8 +403,7 @@ func structTypeSortedFields(t reflect.Type) []sortedStructField {
return v.([]sortedStructField)
}
fields := make([]sortedStructField, 0, t.NumField())
for i, n := 0, t.NumField(); i < n; i++ {
sf := t.Field(i)
for sf := range t.Fields() {
name := sf.Name
if v := sf.Tag.Get("json"); v != "" {
v, _, _ = strings.Cut(v, ",")
@@ -423,7 +416,7 @@ func structTypeSortedFields(t reflect.Type) []sortedStructField {
}
}
fields = append(fields, sortedStructField{
Index: i,
Index: sf.Index[0],
Name: name,
SortName: removeTypePrefixes(name),
MetricType: sf.Tag.Get("metrictype"),
@@ -457,7 +450,7 @@ func foreachExportedStructField(rv reflect.Value, f func(fieldOrJSONName, metric
sf := ssf.StructFieldType
if ssf.MetricType != "" || sf.Type.Kind() == reflect.Struct {
f(ssf.Name, ssf.MetricType, rv.Field(ssf.Index))
} else if sf.Type.Kind() == reflect.Ptr && sf.Type.Elem().Kind() == reflect.Struct {
} else if sf.Type.Kind() == reflect.Pointer && sf.Type.Elem().Kind() == reflect.Struct {
fv := rv.Field(ssf.Index)
if !fv.IsNil() {
f(ssf.Name, ssf.MetricType, fv.Elem())