diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 2cd831068..1368fedf0 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -12,7 +12,6 @@ import ( "encoding/base64" "encoding/json" "errors" - "expvar" "fmt" "io" "log" @@ -119,11 +118,8 @@ import ( "tailscale.com/wgengine/wgcfg/nmcfg" ) -var metricAdvertisedRoutes expvar.Int - -func init() { - usermetric.Publish("tailscaled_advertised_routes", &metricAdvertisedRoutes) -} +var metricAdvertisedRoutes = usermetric.NewGauge( + "tailscaled_advertised_routes", "Number of routes advertised by tailscaled") var controlDebugFlags = getControlDebugFlags() @@ -4633,11 +4629,13 @@ func (b *LocalBackend) applyPrefsToHostinfoLocked(hi *tailcfg.Hostinfo, prefs ip hi.AllowsUpdate = envknob.AllowsRemoteUpdate() || prefs.AutoUpdate().Apply.EqualBool(true) // count routes without exit node routes + var routes int64 for _, route := range hi.RoutableIPs { if route.Bits() != 0 { - metricAdvertisedRoutes.Add(1) + routes++ } } + metricAdvertisedRoutes.Set(float64(routes)) var sshHostKeys []string if prefs.RunSSH() && envknob.CanSSHD() { diff --git a/util/usermetric/usermetric.go b/util/usermetric/usermetric.go index 04fbc2ea4..6e27c4111 100644 --- a/util/usermetric/usermetric.go +++ b/util/usermetric/usermetric.go @@ -7,6 +7,8 @@ package usermetric import ( "expvar" + "fmt" + "io" "net/http" "tailscale.com/metrics" @@ -33,14 +35,40 @@ func NewMultiLabelMap[T comparable](name string, promType, helpText string) *met return m } -// Publish declares a named exported variable. This should be called from a -// package's init function when it creates its Vars. -// -// Note that usermetric are not protected against duplicate -// metrics name. It is the caller's responsibility to ensure that -// the name is unique. -func Publish(name string, v expvar.Var) { - vars.Set(name, v) +// Gauge is a gauge metric with no labels. +type Gauge struct { + m *expvar.Float + help string +} + +// NewGauge creates and register a new gauge metric with the given name and help text. +func NewGauge(name, help string) *Gauge { + m := &expvar.Float{} + vars.Set(name, m) + return &Gauge{m, help} +} + +// Set sets the gauge to the given value. +func (g *Gauge) Set(v float64) { + g.m.Set(v) +} + +// WritePrometheus writes the gauge metric in Prometheus format to the given writer. +// This satisfies the varz.PrometheusWriter interface. +func (g *Gauge) WritePrometheus(w io.Writer, name string) { + io.WriteString(w, "# TYPE ") + io.WriteString(w, name) + io.WriteString(w, " gauge\n") + if g.help != "" { + io.WriteString(w, "# HELP ") + io.WriteString(w, name) + io.WriteString(w, " ") + io.WriteString(w, g.help) + io.WriteString(w, "\n") + } + + io.WriteString(w, name) + fmt.Fprintf(w, " %v\n", g.m.Value()) } // Handler returns a varz.Handler that serves the userfacing expvar contained diff --git a/util/usermetric/usermetric_test.go b/util/usermetric/usermetric_test.go new file mode 100644 index 000000000..aa0e82ea6 --- /dev/null +++ b/util/usermetric/usermetric_test.go @@ -0,0 +1,25 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package usermetric + +import ( + "bytes" + "testing" +) + +func TestGauge(t *testing.T) { + g := NewGauge("test_gauge", "This is a test gauge") + g.Set(15) + + var buf bytes.Buffer + g.WritePrometheus(&buf, "test_gauge") + const want = `# TYPE test_gauge gauge +# HELP test_gauge This is a test gauge +test_gauge 15 +` + if got := buf.String(); got != want { + t.Errorf("got %q; want %q", got, want) + } + +}