client/local: add method to set gauge metric to a value

The existing client metric methods only support incrementing (or
decrementing) a delta value.  This new method allows setting the metric
to a specific value.

Updates tailscale/corp#35327

Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d
Signed-off-by: Will Norris <will@tailscale.com>
This commit is contained in:
Will Norris
2025-12-15 14:01:00 -08:00
committed by Will Norris
parent f174ecb6fd
commit 0fd1670a59
6 changed files with 52 additions and 25 deletions
+17 -12
View File
@@ -43,6 +43,7 @@ import (
"tailscale.com/types/appctype" "tailscale.com/types/appctype"
"tailscale.com/types/dnstype" "tailscale.com/types/dnstype"
"tailscale.com/types/key" "tailscale.com/types/key"
"tailscale.com/util/clientmetric"
"tailscale.com/util/eventbus" "tailscale.com/util/eventbus"
) )
@@ -385,18 +386,14 @@ func (lc *Client) IncrementCounter(ctx context.Context, name string, delta int)
if !buildfeatures.HasClientMetrics { if !buildfeatures.HasClientMetrics {
return nil return nil
} }
type metricUpdate struct {
Name string `json:"name"`
Type string `json:"type"`
Value int `json:"value"` // amount to increment by
}
if delta < 0 { if delta < 0 {
return errors.New("negative delta not allowed") return errors.New("negative delta not allowed")
} }
_, err := lc.send(ctx, "POST", "/localapi/v0/upload-client-metrics", 200, jsonBody([]metricUpdate{{ _, err := lc.send(ctx, "POST", "/localapi/v0/upload-client-metrics", 200, jsonBody([]clientmetric.MetricUpdate{{
Name: name, Name: name,
Type: "counter", Type: "counter",
Value: delta, Value: delta,
Op: "add",
}})) }}))
return err return err
} }
@@ -405,15 +402,23 @@ func (lc *Client) IncrementCounter(ctx context.Context, name string, delta int)
// metric by the given delta. If the metric has yet to exist, a new gauge // metric by the given delta. If the metric has yet to exist, a new gauge
// metric is created and initialized to delta. The delta value can be negative. // metric is created and initialized to delta. The delta value can be negative.
func (lc *Client) IncrementGauge(ctx context.Context, name string, delta int) error { func (lc *Client) IncrementGauge(ctx context.Context, name string, delta int) error {
type metricUpdate struct { _, err := lc.send(ctx, "POST", "/localapi/v0/upload-client-metrics", 200, jsonBody([]clientmetric.MetricUpdate{{
Name string `json:"name"`
Type string `json:"type"`
Value int `json:"value"` // amount to increment by
}
_, err := lc.send(ctx, "POST", "/localapi/v0/upload-client-metrics", 200, jsonBody([]metricUpdate{{
Name: name, Name: name,
Type: "gauge", Type: "gauge",
Value: delta, Value: delta,
Op: "add",
}}))
return err
}
// SetGauge sets the value of a Tailscale daemon's gauge metric to the given value.
// If the metric has yet to exist, a new gauge metric is created and initialized to value.
func (lc *Client) SetGauge(ctx context.Context, name string, value int) error {
_, err := lc.send(ctx, "POST", "/localapi/v0/upload-client-metrics", 200, jsonBody([]clientmetric.MetricUpdate{{
Name: name,
Type: "gauge",
Value: value,
Op: "set",
}})) }}))
return err return err
} }
+2 -2
View File
@@ -66,8 +66,8 @@ func (menu *Menu) Run(client *local.Client) {
case <-menu.bgCtx.Done(): case <-menu.bgCtx.Done():
} }
}() }()
go menu.lc.IncrementGauge(menu.bgCtx, "systray_running", 1) go menu.lc.SetGauge(menu.bgCtx, "systray_running", 1)
defer menu.lc.IncrementGauge(menu.bgCtx, "systray_running", -1) defer menu.lc.SetGauge(menu.bgCtx, "systray_running", 0)
systray.Run(menu.onReady, menu.onExit) systray.Run(menu.onReady, menu.onExit)
} }
+1 -1
View File
@@ -143,7 +143,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/types/tkatype from tailscale.com/client/local+ tailscale.com/types/tkatype from tailscale.com/client/local+
tailscale.com/types/views from tailscale.com/ipn+ tailscale.com/types/views from tailscale.com/ipn+
tailscale.com/util/cibuild from tailscale.com/health+ tailscale.com/util/cibuild from tailscale.com/health+
tailscale.com/util/clientmetric from tailscale.com/net/netmon tailscale.com/util/clientmetric from tailscale.com/net/netmon+
tailscale.com/util/cloudenv from tailscale.com/hostinfo+ tailscale.com/util/cloudenv from tailscale.com/hostinfo+
tailscale.com/util/ctxkey from tailscale.com/tsweb+ tailscale.com/util/ctxkey from tailscale.com/tsweb+
💣 tailscale.com/util/deephash from tailscale.com/util/syspolicy/setting 💣 tailscale.com/util/deephash from tailscale.com/util/syspolicy/setting
+11 -10
View File
@@ -1283,13 +1283,8 @@ func (h *Handler) serveUploadClientMetrics(w http.ResponseWriter, r *http.Reques
http.Error(w, "unsupported method", http.StatusMethodNotAllowed) http.Error(w, "unsupported method", http.StatusMethodNotAllowed)
return return
} }
type clientMetricJSON struct {
Name string `json:"name"`
Type string `json:"type"` // one of "counter" or "gauge"
Value int `json:"value"` // amount to increment metric by
}
var clientMetrics []clientMetricJSON var clientMetrics []clientmetric.MetricUpdate
if err := json.NewDecoder(r.Body).Decode(&clientMetrics); err != nil { if err := json.NewDecoder(r.Body).Decode(&clientMetrics); err != nil {
http.Error(w, "invalid JSON body", http.StatusBadRequest) http.Error(w, "invalid JSON body", http.StatusBadRequest)
return return
@@ -1299,14 +1294,12 @@ func (h *Handler) serveUploadClientMetrics(w http.ResponseWriter, r *http.Reques
defer metricsMu.Unlock() defer metricsMu.Unlock()
for _, m := range clientMetrics { for _, m := range clientMetrics {
if metric, ok := metrics[m.Name]; ok { metric, ok := metrics[m.Name]
metric.Add(int64(m.Value)) if !ok {
} else {
if clientmetric.HasPublished(m.Name) { if clientmetric.HasPublished(m.Name) {
http.Error(w, "Already have a metric named "+m.Name, http.StatusBadRequest) http.Error(w, "Already have a metric named "+m.Name, http.StatusBadRequest)
return return
} }
var metric *clientmetric.Metric
switch m.Type { switch m.Type {
case "counter": case "counter":
metric = clientmetric.NewCounter(m.Name) metric = clientmetric.NewCounter(m.Name)
@@ -1317,7 +1310,15 @@ func (h *Handler) serveUploadClientMetrics(w http.ResponseWriter, r *http.Reques
return return
} }
metrics[m.Name] = metric metrics[m.Name] = metric
}
switch m.Op {
case "add", "":
metric.Add(int64(m.Value)) metric.Add(int64(m.Value))
case "set":
metric.Set(int64(m.Value))
default:
http.Error(w, "Unknown metric op "+m.Op, http.StatusBadRequest)
return
} }
} }
+14
View File
@@ -58,6 +58,20 @@ const (
TypeCounter TypeCounter
) )
// MetricUpdate requests that a client metric value be updated.
//
// This is the request body sent to /localapi/v0/upload-client-metrics.
type MetricUpdate struct {
Name string `json:"name"`
Type string `json:"type"` // one of "counter" or "gauge"
Value int `json:"value"` // amount to increment by or set
// Op indicates if Value is added to the existing metric value,
// or if the metric is set to Value.
// One of "add" or "set". If empty, defaults to "add".
Op string `json:"op"`
}
// Metric is an integer metric value that's tracked over time. // Metric is an integer metric value that's tracked over time.
// //
// It's safe for concurrent use. // It's safe for concurrent use.
+7
View File
@@ -13,6 +13,13 @@ func (*Metric) Value() int64 { return 0 }
func (*Metric) Register(expvarInt any) {} func (*Metric) Register(expvarInt any) {}
func (*Metric) UnregisterAll() {} func (*Metric) UnregisterAll() {}
type MetricUpdate struct {
Name string `json:"name"`
Type string `json:"type"`
Value int `json:"value"`
Op string `json:"op"`
}
func HasPublished(string) bool { panic("unreachable") } func HasPublished(string) bool { panic("unreachable") }
func EncodeLogTailMetricsDelta() string { return "" } func EncodeLogTailMetricsDelta() string { return "" }
func WritePrometheusExpositionFormat(any) {} func WritePrometheusExpositionFormat(any) {}