diff --git a/client/local/local.go b/client/local/local.go index 72ddbb55f..195a91b1e 100644 --- a/client/local/local.go +++ b/client/local/local.go @@ -43,6 +43,7 @@ "tailscale.com/types/appctype" "tailscale.com/types/dnstype" "tailscale.com/types/key" + "tailscale.com/util/clientmetric" "tailscale.com/util/eventbus" ) @@ -385,18 +386,14 @@ func (lc *Client) IncrementCounter(ctx context.Context, name string, delta int) if !buildfeatures.HasClientMetrics { return nil } - type metricUpdate struct { - Name string `json:"name"` - Type string `json:"type"` - Value int `json:"value"` // amount to increment by - } if delta < 0 { 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, Type: "counter", Value: delta, + Op: "add", }})) return err } @@ -405,15 +402,23 @@ type metricUpdate struct { // 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. func (lc *Client) IncrementGauge(ctx context.Context, name string, delta int) error { - type metricUpdate struct { - 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{{ + _, err := lc.send(ctx, "POST", "/localapi/v0/upload-client-metrics", 200, jsonBody([]clientmetric.MetricUpdate{{ Name: name, Type: "gauge", 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 } diff --git a/client/systray/systray.go b/client/systray/systray.go index 330df8d06..b9e8fcc59 100644 --- a/client/systray/systray.go +++ b/client/systray/systray.go @@ -66,8 +66,8 @@ func (menu *Menu) Run(client *local.Client) { case <-menu.bgCtx.Done(): } }() - go menu.lc.IncrementGauge(menu.bgCtx, "systray_running", 1) - defer menu.lc.IncrementGauge(menu.bgCtx, "systray_running", -1) + go menu.lc.SetGauge(menu.bgCtx, "systray_running", 1) + defer menu.lc.SetGauge(menu.bgCtx, "systray_running", 0) systray.Run(menu.onReady, menu.onExit) } diff --git a/cmd/derper/depaware.txt b/cmd/derper/depaware.txt index b2465d28d..7695cf598 100644 --- a/cmd/derper/depaware.txt +++ b/cmd/derper/depaware.txt @@ -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/views from tailscale.com/ipn+ 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/ctxkey from tailscale.com/tsweb+ 💣 tailscale.com/util/deephash from tailscale.com/util/syspolicy/setting diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index 7f249fe53..4648b2c49 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -1283,13 +1283,8 @@ func (h *Handler) serveUploadClientMetrics(w http.ResponseWriter, r *http.Reques http.Error(w, "unsupported method", http.StatusMethodNotAllowed) 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 { http.Error(w, "invalid JSON body", http.StatusBadRequest) return @@ -1299,14 +1294,12 @@ type clientMetricJSON struct { defer metricsMu.Unlock() for _, m := range clientMetrics { - if metric, ok := metrics[m.Name]; ok { - metric.Add(int64(m.Value)) - } else { + metric, ok := metrics[m.Name] + if !ok { if clientmetric.HasPublished(m.Name) { http.Error(w, "Already have a metric named "+m.Name, http.StatusBadRequest) return } - var metric *clientmetric.Metric switch m.Type { case "counter": metric = clientmetric.NewCounter(m.Name) @@ -1317,7 +1310,15 @@ type clientMetricJSON struct { return } metrics[m.Name] = metric + } + switch m.Op { + case "add", "": metric.Add(int64(m.Value)) + case "set": + metric.Set(int64(m.Value)) + default: + http.Error(w, "Unknown metric op "+m.Op, http.StatusBadRequest) + return } } diff --git a/util/clientmetric/clientmetric.go b/util/clientmetric/clientmetric.go index 9e6b03a15..50cf3b296 100644 --- a/util/clientmetric/clientmetric.go +++ b/util/clientmetric/clientmetric.go @@ -58,6 +58,20 @@ type scanEntry struct { 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. // // It's safe for concurrent use. diff --git a/util/clientmetric/omit.go b/util/clientmetric/omit.go index 5349fc724..6d678cf20 100644 --- a/util/clientmetric/omit.go +++ b/util/clientmetric/omit.go @@ -13,6 +13,13 @@ func (*Metric) Value() int64 { return 0 } func (*Metric) Register(expvarInt any) {} 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 EncodeLogTailMetricsDelta() string { return "" } func WritePrometheusExpositionFormat(any) {}