tailscale/tsweb/varz/varz.go
Brad Fitzpatrick 3090461961 tsweb/varz: optimize some allocs, add helper func for others
Updates #cleanup
Updates tailscale/corp#23546 (noticed when doing this)

Change-Id: Ia9f627fe32bb4955739b2787210ba18f5de27f4d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-07 08:09:16 -08:00

421 lines
13 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package varz contains code to export metrics in Prometheus format.
package varz
import (
"cmp"
"expvar"
"fmt"
"io"
"net/http"
"reflect"
"runtime"
"sort"
"strings"
"sync"
"time"
"unicode"
"unicode/utf8"
"tailscale.com/metrics"
"tailscale.com/version"
)
// StaticStringVar returns a new expvar.Var that always returns s.
func StaticStringVar(s string) expvar.Var {
var v any = s // box s into an interface just once
return expvar.Func(func() any { return v })
}
func init() {
expvar.Publish("process_start_unix_time", expvar.Func(func() any { return timeStart.Unix() }))
expvar.Publish("version", StaticStringVar(version.Long()))
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() }))
}
const (
gaugePrefix = "gauge_"
counterPrefix = "counter_"
labelMapPrefix = "labelmap_"
histogramPrefix = "histogram_"
)
// prefixesToTrim contains key prefixes to remove when exporting and sorting metrics.
var prefixesToTrim = []string{gaugePrefix, counterPrefix, labelMapPrefix, histogramPrefix}
var timeStart = time.Now()
func Uptime() time.Duration { return time.Since(timeStart).Round(time.Second) }
// WritePrometheusExpvar writes kv to w in Prometheus metrics format.
//
// See VarzHandler for conventions. This is exported primarily for
// people to test their varz.
func WritePrometheusExpvar(w io.Writer, kv expvar.KeyValue) {
writePromExpVar(w, "", kv)
}
type prometheusMetricDetails struct {
Name string
Type string
Label string
}
var prometheusMetricCache sync.Map // string => *prometheusMetricDetails
func prometheusMetric(prefix string, key string) (string, string, string) {
cachekey := prefix + key
if v, ok := prometheusMetricCache.Load(cachekey); ok {
d := v.(*prometheusMetricDetails)
return d.Name, d.Type, d.Label
}
var typ string
var label string
switch {
case strings.HasPrefix(key, gaugePrefix):
typ = "gauge"
key = strings.TrimPrefix(key, gaugePrefix)
case strings.HasPrefix(key, counterPrefix):
typ = "counter"
key = strings.TrimPrefix(key, counterPrefix)
case strings.HasPrefix(key, histogramPrefix):
typ = "histogram"
key = strings.TrimPrefix(key, histogramPrefix)
}
if strings.HasPrefix(key, labelMapPrefix) {
key = strings.TrimPrefix(key, labelMapPrefix)
if a, b, ok := strings.Cut(key, "_"); ok {
label, key = a, b
}
}
// Convert the metric to a valid Prometheus metric name.
// "Metric names may contain ASCII letters, digits, underscores, and colons.
// It must match the regex [a-zA-Z_:][a-zA-Z0-9_:]*"
mapInvalidMetricRunes := func(r rune) rune {
if r >= 'a' && r <= 'z' ||
r >= 'A' && r <= 'Z' ||
r >= '0' && r <= '9' ||
r == '_' || r == ':' {
return r
}
if r < utf8.RuneSelf && unicode.IsPrint(r) {
return '_'
}
return -1
}
metricName := strings.Map(mapInvalidMetricRunes, prefix+key)
if metricName == "" || unicode.IsDigit(rune(metricName[0])) {
metricName = "_" + metricName
}
d := &prometheusMetricDetails{
Name: metricName,
Type: typ,
Label: label,
}
prometheusMetricCache.Store(cachekey, d)
return d.Name, d.Type, d.Label
}
func writePromExpVar(w io.Writer, prefix string, kv expvar.KeyValue) {
key := kv.Key
name, typ, label := prometheusMetric(prefix, key)
switch v := kv.Value.(type) {
case *expvar.Int:
fmt.Fprintf(w, "# TYPE %s %s\n%s %v\n", name, cmp.Or(typ, "counter"), name, v.Value())
return
case *expvar.Float:
fmt.Fprintf(w, "# TYPE %s %s\n%s %v\n", name, cmp.Or(typ, "gauge"), name, v.Value())
return
case *metrics.Set:
v.Do(func(kv expvar.KeyValue) {
writePromExpVar(w, name+"_", kv)
})
return
case PrometheusWriter:
v.WritePrometheus(w, name)
return
case PrometheusMetricsReflectRooter:
root := v.PrometheusMetricsReflectRoot()
rv := reflect.ValueOf(root)
if rv.Type().Kind() == reflect.Ptr {
if rv.IsNil() {
return
}
rv = rv.Elem()
}
if rv.Type().Kind() != reflect.Struct {
fmt.Fprintf(w, "# skipping expvar %q; unknown root type\n", name)
return
}
foreachExportedStructField(rv, func(fieldOrJSONName, metricType string, rv reflect.Value) {
mname := name + "_" + fieldOrJSONName
switch rv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
fmt.Fprintf(w, "# TYPE %s %s\n%s %v\n", mname, metricType, mname, rv.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
fmt.Fprintf(w, "# TYPE %s %s\n%s %v\n", mname, metricType, mname, rv.Uint())
case reflect.Float32, reflect.Float64:
fmt.Fprintf(w, "# TYPE %s %s\n%s %v\n", mname, metricType, mname, rv.Float())
case reflect.Struct:
if rv.CanAddr() {
// Slight optimization, not copying big structs if they're addressable:
writePromExpVar(w, name+"_", expvar.KeyValue{Key: fieldOrJSONName, Value: expVarPromStructRoot{rv.Addr().Interface()}})
} else {
writePromExpVar(w, name+"_", expvar.KeyValue{Key: fieldOrJSONName, Value: expVarPromStructRoot{rv.Interface()}})
}
}
return
})
return
}
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)
return
}
if vs, ok := v.(string); ok && strings.HasSuffix(name, "version") {
fmt.Fprintf(w, "%s{version=%q} 1\n", name, vs)
return
}
switch v := v.(type) {
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64:
fmt.Fprintf(w, "%s %v\n", name, v)
return
}
funcRet = fmt.Sprintf(" returning %T", v)
}
switch kv.Value.(type) {
default:
fmt.Fprintf(w, "# skipping expvar %q (Go type %T%s) with undeclared Prometheus type\n", name, kv.Value, funcRet)
return
case *metrics.LabelMap, *expvar.Map:
// Permit typeless LabelMap and expvar.Map for
// compatibility with old expvar-registered
// metrics.LabelMap.
}
}
switch v := kv.Value.(type) {
case expvar.Func:
val := v()
switch val.(type) {
case float64, int64, int:
fmt.Fprintf(w, "# TYPE %s %s\n%s %v\n", name, typ, name, val)
default:
fmt.Fprintf(w, "# skipping expvar func %q returning unknown type %T\n", name, val)
}
case *metrics.LabelMap:
if typ != "" {
fmt.Fprintf(w, "# TYPE %s %s\n", name, typ)
}
// IntMap uses expvar.Map on the inside, which presorts
// keys. The output ordering is deterministic.
v.Do(func(kv expvar.KeyValue) {
fmt.Fprintf(w, "%s{%s=%q} %v\n", name, cmp.Or(v.Label, "label"), kv.Key, kv.Value)
})
case *metrics.Histogram:
v.PromExport(w, name)
case *expvar.Map:
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)
})
} else {
v.Do(func(kv expvar.KeyValue) {
fmt.Fprintf(w, "%s_%s %v\n", name, kv.Key, kv.Value)
})
}
}
}
// PrometheusWriter is the interface implemented by metrics that can write
// themselves into Prometheus exposition format.
//
// As of 2024-03-25, this is only *metrics.MultiLabelMap.
type PrometheusWriter interface {
WritePrometheus(w io.Writer, name string)
}
var sortedKVsPool = &sync.Pool{New: func() any { return new(sortedKVs) }}
// sortedKV is a KeyValue with a sort key.
type sortedKV struct {
expvar.KeyValue
sortKey string // KeyValue.Key with type prefix removed
}
type sortedKVs struct {
kvs []sortedKV
}
// Handler is an HTTP handler to write expvar values into the
// prometheus export format:
//
// https://github.com/prometheus/docs/blob/master/content/docs/instrumenting/exposition_formats.md
//
// It makes the following assumptions:
//
// - *expvar.Int are counters (unless marked as a gauge_; see below)
// - a *tailscale/metrics.Set is descended into, joining keys with
// underscores. So use underscores as your metric names.
// - an expvar named starting with "gauge_" or "counter_" is of that
// Prometheus type, and has that prefix stripped.
// - anything else is untyped and thus not exported.
// - expvar.Func can return an int or int64 (for now) and anything else
// is not exported.
//
// This will evolve over time, or perhaps be replaced.
func Handler(w http.ResponseWriter, r *http.Request) {
ExpvarDoHandler(expvarDo)(w, r)
}
// 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.
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")
s := sortedKVsPool.Get().(*sortedKVs)
defer sortedKVsPool.Put(s)
s.kvs = s.kvs[:0]
expvarDoFunc(func(kv expvar.KeyValue) {
s.kvs = append(s.kvs, sortedKV{kv, removeTypePrefixes(kv.Key)})
})
sort.Slice(s.kvs, func(i, j int) bool {
return s.kvs[i].sortKey < s.kvs[j].sortKey
})
for _, e := range s.kvs {
writePromExpVar(w, "", e.KeyValue)
}
}
}
// PrometheusMetricsReflectRooter is an optional interface that expvar.Var implementations
// can implement to indicate that they should be walked recursively with reflect to find
// sets of fields to export.
type PrometheusMetricsReflectRooter interface {
expvar.Var
// PrometheusMetricsReflectRoot returns the struct or struct pointer to walk.
PrometheusMetricsReflectRoot() any
}
var expvarDo = expvar.Do // pulled out for tests
func writeMemstats(w io.Writer, ms *runtime.MemStats) {
out := func(name, typ string, v uint64, help string) {
if help != "" {
fmt.Fprintf(w, "# HELP memstats_%s %s\n", name, help)
}
fmt.Fprintf(w, "# TYPE memstats_%s %s\nmemstats_%s %v\n", name, typ, name, v)
}
g := func(name string, v uint64, help string) { out(name, "gauge", v, help) }
c := func(name string, v uint64, help string) { out(name, "counter", v, help) }
g("heap_alloc", ms.HeapAlloc, "current bytes of allocated heap objects (up/down smoothly)")
c("total_alloc", ms.TotalAlloc, "cumulative bytes allocated for heap objects")
g("sys", ms.Sys, "total bytes of memory obtained from the OS")
c("mallocs", ms.Mallocs, "cumulative count of heap objects allocated")
c("frees", ms.Frees, "cumulative count of heap objects freed")
c("num_gc", uint64(ms.NumGC), "number of completed GC cycles")
}
// sortedStructField is metadata about a struct field used both for sorting once
// (by structTypeSortedFields) and at serving time (by
// foreachExportedStructField).
type sortedStructField struct {
Index int // index of struct field in struct
Name string // struct field name, or "json" name
SortName string // Name with "foo_" type prefixes removed
MetricType string // the "metrictype" struct tag
StructFieldType *reflect.StructField
}
var structSortedFieldsCache sync.Map // reflect.Type => []sortedStructField
// structTypeSortedFields returns the sorted fields of t, caching as needed.
func structTypeSortedFields(t reflect.Type) []sortedStructField {
if v, ok := structSortedFieldsCache.Load(t); ok {
return v.([]sortedStructField)
}
fields := make([]sortedStructField, 0, t.NumField())
for i, n := 0, t.NumField(); i < n; i++ {
sf := t.Field(i)
name := sf.Name
if v := sf.Tag.Get("json"); v != "" {
v, _, _ = strings.Cut(v, ",")
if v == "-" {
// Skip it, regardless of its metrictype.
continue
}
if v != "" {
name = v
}
}
fields = append(fields, sortedStructField{
Index: i,
Name: name,
SortName: removeTypePrefixes(name),
MetricType: sf.Tag.Get("metrictype"),
StructFieldType: &sf,
})
}
sort.Slice(fields, func(i, j int) bool {
return fields[i].SortName < fields[j].SortName
})
structSortedFieldsCache.Store(t, fields)
return fields
}
// removeTypePrefixes returns s with the first "foo_" prefix in prefixesToTrim
// removed.
func removeTypePrefixes(s string) string {
for _, prefix := range prefixesToTrim {
if trimmed, ok := strings.CutPrefix(s, prefix); ok {
return trimmed
}
}
return s
}
// foreachExportedStructField iterates over the fields in sorted order of
// their name, after removing metric prefixes. This is not necessarily the
// order they were declared in the struct
func foreachExportedStructField(rv reflect.Value, f func(fieldOrJSONName, metricType string, rv reflect.Value)) {
t := rv.Type()
for _, ssf := range structTypeSortedFields(t) {
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 {
fv := rv.Field(ssf.Index)
if !fv.IsNil() {
f(ssf.Name, ssf.MetricType, fv.Elem())
}
}
}
}
type expVarPromStructRoot struct{ v any }
func (r expVarPromStructRoot) PrometheusMetricsReflectRoot() any { return r.v }
func (r expVarPromStructRoot) String() string { panic("unused") }
var (
_ PrometheusMetricsReflectRooter = expVarPromStructRoot{}
_ expvar.Var = expVarPromStructRoot{}
)