metrics: add histogram support

Add initial histogram support.

Updates tailscale/corp#8641

Signed-off-by: Maisem Ali <maisem@tailscale.com>
maisem/histgram
Maisem Ali 2023-05-08 01:22:23 -07:00
parent 6e967446e4
commit 484d80f8fc
2 changed files with 93 additions and 1 deletions

View File

@ -5,7 +5,14 @@
// Tailscale for monitoring.
package metrics
import "expvar"
import (
"expvar"
"fmt"
"io"
"strings"
"golang.org/x/exp/slices"
)
// Set is a string-to-Var map variable that satisfies the expvar.Var
// interface.
@ -58,3 +65,86 @@ func (m *LabelMap) GetFloat(key string) *expvar.Float {
func CurrentFDs() int {
return currentFDs()
}
type Histogram struct {
name string
// buckets is a list of bucket boundaries, in increasing order.
buckets []float64
// buckStrings is a list of the same buckets, but as strings.
// To avoid allocations, this is only populated on first use.
bucketStrings []string
bucketVars []expvar.Int
sum expvar.Float
count expvar.Int
}
// NewHistogram returns a new histogram that reports to the given
// expvar map under the given name.
//
// The buckets are the boundaries of the histogram buckets, in
// increasing order. The last bucket is +Inf.
func NewHistogram(buckets []float64) *Histogram {
if !slices.IsSorted(buckets) {
panic("buckets must be sorted")
}
labels := make([]string, len(buckets))
for i, b := range buckets {
labels[i] = fmt.Sprintf("%v", b)
}
h := &Histogram{
buckets: buckets,
bucketStrings: labels,
bucketVars: make([]expvar.Int, len(buckets)),
}
return h
}
// Observe records a new observation in the histogram.
func (h *Histogram) Observe(v float64) {
h.sum.Add(v)
h.count.Add(1)
for i, b := range h.buckets {
if v <= b {
h.bucketVars[i].Add(1)
}
}
}
func (h *Histogram) String() string {
var b strings.Builder
fmt.Fprintf(&b, "{")
first := true
h.Do(func(kv expvar.KeyValue) {
if !first {
fmt.Fprintf(&b, ", ")
}
fmt.Fprintf(&b, "%q: ", kv.Key)
if kv.Value != nil {
fmt.Fprintf(&b, "%v", kv.Value)
} else {
fmt.Fprint(&b, "null")
}
first = false
})
fmt.Fprintf(&b, "}")
return b.String()
}
// Do calls f for each bucket in the histogram.
func (h *Histogram) Do(f func(expvar.KeyValue)) {
for i := range h.bucketVars {
f(expvar.KeyValue{Key: h.bucketStrings[i], Value: &h.bucketVars[i]})
}
f(expvar.KeyValue{Key: "+Inf", Value: &h.count})
}
// PromExport writes the histogram to w in Prometheus exposition format.
func (h *Histogram) PromExport(w io.Writer, name string) {
fmt.Fprintf(w, "# TYPE %s histogram\n", name)
h.Do(func(kv expvar.KeyValue) {
fmt.Fprintf(w, "%s_bucket{le=%q} %v\n", name, kv.Key, kv.Value)
})
fmt.Fprintf(w, "%s_sum %v\n", name, &h.sum)
fmt.Fprintf(w, "%s_count %v\n", name, &h.count)
}

View File

@ -196,6 +196,8 @@ func writePromExpVar(w io.Writer, prefix string, kv expvar.KeyValue) {
v.Do(func(kv expvar.KeyValue) {
fmt.Fprintf(w, "%s{%s=%q} %v\n", name, v.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)