usermetric: add initial user-facing metrics

This commit adds a new usermetric package and wires
up metrics across the tailscale client.

Updates tailscale/corp#22075

Co-authored-by: Anton Tolchanov <anton@tailscale.com>
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
Kristoffer Dalby
2024-08-01 13:00:36 +02:00
committed by Kristoffer Dalby
parent 06c31f4e91
commit a2c42d3cd4
17 changed files with 368 additions and 22 deletions

View File

@@ -39,7 +39,7 @@ func NewMultiLabelMap[T comparable](name string, promType, helpText string) *Mul
Help: helpText,
}
var zero T
_ = labelString(zero) // panic early if T is invalid
_ = LabelString(zero) // panic early if T is invalid
expvar.Publish(name, m)
return m
}
@@ -50,8 +50,10 @@ type labelsAndValue[T comparable] struct {
val expvar.Var
}
// labelString returns a Prometheus-formatted label string for the given key.
func labelString(k any) string {
// LabelString returns a Prometheus-formatted label string for the given key.
// k must be a struct type with scalar fields, as required by MultiLabelMap,
// if k is not a struct, it will panic.
func LabelString(k any) string {
rv := reflect.ValueOf(k)
t := rv.Type()
if t.Kind() != reflect.Struct {
@@ -150,7 +152,7 @@ func (v *MultiLabelMap[T]) Init() *MultiLabelMap[T] {
//
// v.mu must be held.
func (v *MultiLabelMap[T]) addKeyLocked(key T, val expvar.Var) {
ls := labelString(key)
ls := LabelString(key)
ent := labelsAndValue[T]{key, ls, val}
// Using insertion sort to place key into the already-sorted v.keys.
@@ -209,6 +211,26 @@ func (v *MultiLabelMap[T]) Set(key T, val expvar.Var) {
v.m.Store(key, val)
}
// SetInt sets val to the *[expvar.Int] value stored under the given map key,
// creating it if it doesn't exist yet.
// It does nothing if key exists but is of the wrong type.
func (v *MultiLabelMap[T]) SetInt(key T, val int64) {
// Set to Int; ignore otherwise.
if iv, ok := v.getOrFill(key, newInt).(*expvar.Int); ok {
iv.Set(val)
}
}
// SetFloat sets val to the *[expvar.Float] value stored under the given map key,
// creating it if it doesn't exist yet.
// It does nothing if key exists but is of the wrong type.
func (v *MultiLabelMap[T]) SetFloat(key T, val float64) {
// Set to Float; ignore otherwise.
if iv, ok := v.getOrFill(key, newFloat).(*expvar.Float); ok {
iv.Set(val)
}
}
// Add adds delta to the *[expvar.Int] value stored under the given map key,
// creating it if it doesn't exist yet.
// It does nothing if key exists but is of the wrong type.
@@ -234,7 +256,7 @@ func (v *MultiLabelMap[T]) AddFloat(key T, delta float64) {
// This is not optimized for highly concurrent usage; it's presumed to only be
// used rarely, at startup.
func (v *MultiLabelMap[T]) Delete(key T) {
ls := labelString(key)
ls := LabelString(key)
v.mu.Lock()
defer v.mu.Unlock()

View File

@@ -5,6 +5,7 @@ package metrics
import (
"bytes"
"expvar"
"fmt"
"io"
"testing"
@@ -22,6 +23,12 @@ func TestMultilabelMap(t *testing.T) {
m.Add(L2{"b", "b"}, 3)
m.Add(L2{"a", "a"}, 1)
m.SetFloat(L2{"sf", "sf"}, 3.5)
m.SetFloat(L2{"sf", "sf"}, 5.5)
m.Set(L2{"sfunc", "sfunc"}, expvar.Func(func() any { return 3 }))
m.SetInt(L2{"si", "si"}, 3)
m.SetInt(L2{"si", "si"}, 5)
cur := func() string {
var buf bytes.Buffer
m.Do(func(kv KeyValue[L2]) {
@@ -33,7 +40,7 @@ func TestMultilabelMap(t *testing.T) {
return buf.String()
}
if g, w := cur(), "a/a=1,a/b=2,b/b=3,b/c=4"; g != w {
if g, w := cur(), "a/a=1,a/b=2,b/b=3,b/c=4,sf/sf=5.5,sfunc/sfunc=3,si/si=5"; g != w {
t.Errorf("got %q; want %q", g, w)
}
@@ -43,6 +50,9 @@ func TestMultilabelMap(t *testing.T) {
metricname{foo="a",bar="b"} 2
metricname{foo="b",bar="b"} 3
metricname{foo="b",bar="c"} 4
metricname{foo="sf",bar="sf"} 5.5
metricname{foo="sfunc",bar="sfunc"} 3
metricname{foo="si",bar="si"} 5
`
if got := buf.String(); got != want {
t.Errorf("promtheus output = %q; want %q", got, want)
@@ -50,7 +60,7 @@ metricname{foo="b",bar="c"} 4
m.Delete(L2{"b", "b"})
if g, w := cur(), "a/a=1,a/b=2,b/c=4"; g != w {
if g, w := cur(), "a/a=1,a/b=2,b/c=4,sf/sf=5.5,sfunc/sfunc=3,si/si=5"; g != w {
t.Errorf("got %q; want %q", g, w)
}