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