Update
This commit is contained in:
1 parent
3530afb854
commit
6903fcff27
2146 files changed
+112256
-190703
No files matched your search
+88
@@ -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
@@ -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
@@ -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())
|
||||
|
||||
Reference in new issue
Block a user