util/usermetrics: make usermetrics non-global

this commit changes usermetrics to be non-global, this is a building
block for correct metrics if a go process runs multiple tsnets or
in tests.

Updates #13420
Updates tailscale/corp#22075

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
Kristoffer Dalby 2024-09-23 18:34:00 +02:00 committed by Kristoffer Dalby
parent e1bbe1bf45
commit 0e0e53d3b3
27 changed files with 372 additions and 129 deletions

View File

@ -680,12 +680,15 @@ func tryEngine(logf logger.Logf, sys *tsd.System, name string) (onlyNetstack boo
ListenPort: args.port, ListenPort: args.port,
NetMon: sys.NetMon.Get(), NetMon: sys.NetMon.Get(),
HealthTracker: sys.HealthTracker(), HealthTracker: sys.HealthTracker(),
Metrics: sys.UserMetricsRegistry(),
Dialer: sys.Dialer.Get(), Dialer: sys.Dialer.Get(),
SetSubsystem: sys.Set, SetSubsystem: sys.Set,
ControlKnobs: sys.ControlKnobs(), ControlKnobs: sys.ControlKnobs(),
DriveForLocal: driveimpl.NewFileSystemForLocal(logf), DriveForLocal: driveimpl.NewFileSystemForLocal(logf),
} }
sys.HealthTracker().SetMetricsRegistry(sys.UserMetricsRegistry())
onlyNetstack = name == "userspace-networking" onlyNetstack = name == "userspace-networking"
netstackSubnetRouter := onlyNetstack // but mutated later on some platforms netstackSubnetRouter := onlyNetstack // but mutated later on some platforms
netns.SetEnabled(!onlyNetstack) netns.SetEnabled(!onlyNetstack)

2
go.mod
View File

@ -320,7 +320,7 @@ require (
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e // indirect github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/polyfloyd/go-errorlint v1.4.1 // indirect github.com/polyfloyd/go-errorlint v1.4.1 // indirect
github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/client_model v0.5.0
github.com/prometheus/procfs v0.12.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect
github.com/quasilyte/go-ruleguard v0.3.19 // indirect github.com/quasilyte/go-ruleguard v0.3.19 // indirect
github.com/quasilyte/gogrep v0.5.0 // indirect github.com/quasilyte/gogrep v0.5.0 // indirect

View File

@ -20,6 +20,7 @@
"time" "time"
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/metrics"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/opt" "tailscale.com/types/opt"
"tailscale.com/util/cibuild" "tailscale.com/util/cibuild"
@ -111,6 +112,7 @@ type Tracker struct {
lastLoginErr error lastLoginErr error
localLogConfigErr error localLogConfigErr error
tlsConnectionErrors map[string]error // map[ServerName]error tlsConnectionErrors map[string]error // map[ServerName]error
metricHealthMessage *metrics.MultiLabelMap[metricHealthMessageLabel]
} }
// Subsystem is the name of a subsystem whose health can be monitored. // Subsystem is the name of a subsystem whose health can be monitored.
@ -317,6 +319,33 @@ func (w *Warnable) IsVisible(ws *warningState) bool {
return time.Since(ws.BrokenSince) >= w.TimeToVisible return time.Since(ws.BrokenSince) >= w.TimeToVisible
} }
// SetMetricsRegistry sets up the metrics for the Tracker. It takes
// a usermetric.Registry and registers the metrics there.
func (t *Tracker) SetMetricsRegistry(reg *usermetric.Registry) {
if reg == nil || t.metricHealthMessage != nil {
return
}
t.metricHealthMessage = usermetric.NewMultiLabelMapWithRegistry[metricHealthMessageLabel](
reg,
"tailscaled_health_messages",
"gauge",
"Number of health messages broken down by type.",
)
t.metricHealthMessage.Set(metricHealthMessageLabel{
Type: "warning",
}, expvar.Func(func() any {
if t.nil() {
return 0
}
t.mu.Lock()
defer t.mu.Unlock()
t.updateBuiltinWarnablesLocked()
return int64(len(t.stringsLocked()))
}))
}
// SetUnhealthy sets a warningState for the given Warnable with the provided Args, and should be // SetUnhealthy sets a warningState for the given Warnable with the provided Args, and should be
// called when a Warnable becomes unhealthy, or its unhealthy status needs to be updated. // called when a Warnable becomes unhealthy, or its unhealthy status needs to be updated.
// SetUnhealthy takes ownership of args. The args can be nil if no additional information is // SetUnhealthy takes ownership of args. The args can be nil if no additional information is
@ -1205,18 +1234,6 @@ func (t *Tracker) ReceiveFuncStats(which ReceiveFunc) *ReceiveFuncStats {
} }
func (t *Tracker) doOnceInit() { func (t *Tracker) doOnceInit() {
metricHealthMessage.Set(metricHealthMessageLabel{
Type: "warning",
}, expvar.Func(func() any {
if t.nil() {
return 0
}
t.mu.Lock()
defer t.mu.Unlock()
t.updateBuiltinWarnablesLocked()
return int64(len(t.stringsLocked()))
}))
for i := range t.MagicSockReceiveFuncs { for i := range t.MagicSockReceiveFuncs {
f := &t.MagicSockReceiveFuncs[i] f := &t.MagicSockReceiveFuncs[i]
f.name = (ReceiveFunc(i)).String() f.name = (ReceiveFunc(i)).String()
@ -1252,9 +1269,3 @@ type metricHealthMessageLabel struct {
// TODO: break down by warnable.severity as well? // TODO: break down by warnable.severity as well?
Type string Type string
} }
var metricHealthMessage = usermetric.NewMultiLabelMap[metricHealthMessageLabel](
"tailscaled_health_messages",
"gauge",
"Number of health messages broken down by type.",
)

View File

@ -119,9 +119,6 @@
"tailscale.com/wgengine/wgcfg/nmcfg" "tailscale.com/wgengine/wgcfg/nmcfg"
) )
var metricAdvertisedRoutes = usermetric.NewGauge(
"tailscaled_advertised_routes", "Number of advertised network routes (e.g. by a subnet router)")
var controlDebugFlags = getControlDebugFlags() var controlDebugFlags = getControlDebugFlags()
func getControlDebugFlags() []string { func getControlDebugFlags() []string {
@ -184,6 +181,7 @@ type LocalBackend struct {
statsLogf logger.Logf // for printing peers stats on change statsLogf logger.Logf // for printing peers stats on change
sys *tsd.System sys *tsd.System
health *health.Tracker // always non-nil health *health.Tracker // always non-nil
metrics metrics
e wgengine.Engine // non-nil; TODO(bradfitz): remove; use sys e wgengine.Engine // non-nil; TODO(bradfitz): remove; use sys
store ipn.StateStore // non-nil; TODO(bradfitz): remove; use sys store ipn.StateStore // non-nil; TODO(bradfitz): remove; use sys
dialer *tsdial.Dialer // non-nil; TODO(bradfitz): remove; use sys dialer *tsdial.Dialer // non-nil; TODO(bradfitz): remove; use sys
@ -377,6 +375,11 @@ func (b *LocalBackend) HealthTracker() *health.Tracker {
return b.health return b.health
} }
// UserMetricsRegistry returns the usermetrics registry for the backend
func (b *LocalBackend) UserMetricsRegistry() *usermetric.Registry {
return b.sys.UserMetricsRegistry()
}
// NetMon returns the network monitor for the backend. // NetMon returns the network monitor for the backend.
func (b *LocalBackend) NetMon() *netmon.Monitor { func (b *LocalBackend) NetMon() *netmon.Monitor {
return b.sys.NetMon.Get() return b.sys.NetMon.Get()
@ -386,6 +389,12 @@ type updateStatus struct {
started bool started bool
} }
type metrics struct {
// advertisedRoutes is a metric that counts the number of network routes that are advertised by the local node.
// This informs the user of how many routes are being advertised by the local node, excluding exit routes.
advertisedRoutes *usermetric.Gauge
}
// clientGen is a func that creates a control plane client. // clientGen is a func that creates a control plane client.
// It's the type used by LocalBackend.SetControlClientGetterForTesting. // It's the type used by LocalBackend.SetControlClientGetterForTesting.
type clientGen func(controlclient.Options) (controlclient.Client, error) type clientGen func(controlclient.Options) (controlclient.Client, error)
@ -429,6 +438,11 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
captiveCtx, captiveCancel := context.WithCancel(ctx) captiveCtx, captiveCancel := context.WithCancel(ctx)
captiveCancel() captiveCancel()
m := metrics{
advertisedRoutes: sys.UserMetricsRegistry().NewGauge(
"tailscaled_advertised_routes", "Number of advertised network routes (e.g. by a subnet router)"),
}
b := &LocalBackend{ b := &LocalBackend{
ctx: ctx, ctx: ctx,
ctxCancel: cancel, ctxCancel: cancel,
@ -437,6 +451,7 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
statsLogf: logger.LogOnChange(logf, 5*time.Minute, clock.Now), statsLogf: logger.LogOnChange(logf, 5*time.Minute, clock.Now),
sys: sys, sys: sys,
health: sys.HealthTracker(), health: sys.HealthTracker(),
metrics: m,
e: e, e: e,
dialer: dialer, dialer: dialer,
store: store, store: store,
@ -4760,7 +4775,7 @@ func (b *LocalBackend) applyPrefsToHostinfoLocked(hi *tailcfg.Hostinfo, prefs ip
routes++ routes++
} }
} }
metricAdvertisedRoutes.Set(float64(routes)) b.metrics.advertisedRoutes.Set(float64(routes))
var sshHostKeys []string var sshHostKeys []string
if prefs.RunSSH() && envknob.CanSSHD() { if prefs.RunSSH() && envknob.CanSSHD() {

View File

@ -432,7 +432,7 @@ func newTestLocalBackend(t testing.TB) *LocalBackend {
sys := new(tsd.System) sys := new(tsd.System)
store := new(mem.Store) store := new(mem.Store)
sys.Set(store) sys.Set(store)
eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker()) eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry())
if err != nil { if err != nil {
t.Fatalf("NewFakeUserspaceEngine: %v", err) t.Fatalf("NewFakeUserspaceEngine: %v", err)
} }

View File

@ -50,7 +50,7 @@ func TestLocalLogLines(t *testing.T) {
sys := new(tsd.System) sys := new(tsd.System)
store := new(mem.Store) store := new(mem.Store)
sys.Set(store) sys.Set(store)
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker()) e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry())
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -35,6 +35,7 @@
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/types/netmap" "tailscale.com/types/netmap"
"tailscale.com/util/must" "tailscale.com/util/must"
"tailscale.com/util/usermetric"
"tailscale.com/wgengine" "tailscale.com/wgengine"
"tailscale.com/wgengine/filter" "tailscale.com/wgengine/filter"
) )
@ -643,7 +644,8 @@ func TestPeerAPIReplyToDNSQueries(t *testing.T) {
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345") h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
ht := new(health.Tracker) ht := new(health.Tracker)
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht) reg := new(usermetric.Registry)
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht, reg)
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht)) pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht))
h.ps = &peerAPIServer{ h.ps = &peerAPIServer{
b: &LocalBackend{ b: &LocalBackend{
@ -694,7 +696,8 @@ func TestPeerAPIPrettyReplyCNAME(t *testing.T) {
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345") h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
ht := new(health.Tracker) ht := new(health.Tracker)
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht) reg := new(usermetric.Registry)
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht, reg)
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht)) pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht))
var a *appc.AppConnector var a *appc.AppConnector
if shouldStore { if shouldStore {
@ -767,7 +770,8 @@ func TestPeerAPIReplyToDNSQueriesAreObserved(t *testing.T) {
rc := &appctest.RouteCollector{} rc := &appctest.RouteCollector{}
ht := new(health.Tracker) ht := new(health.Tracker)
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht) reg := new(usermetric.Registry)
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht, reg)
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht)) pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht))
var a *appc.AppConnector var a *appc.AppConnector
if shouldStore { if shouldStore {
@ -830,8 +834,9 @@ func TestPeerAPIReplyToDNSQueriesAreObservedWithCNAMEFlattening(t *testing.T) {
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345") h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
ht := new(health.Tracker) ht := new(health.Tracker)
reg := new(usermetric.Registry)
rc := &appctest.RouteCollector{} rc := &appctest.RouteCollector{}
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht) eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht, reg)
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht)) pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht))
var a *appc.AppConnector var a *appc.AppConnector
if shouldStore { if shouldStore {

View File

@ -684,6 +684,7 @@ func newTestBackend(t *testing.T) *LocalBackend {
e, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{ e, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
SetSubsystem: sys.Set, SetSubsystem: sys.Set,
HealthTracker: sys.HealthTracker(), HealthTracker: sys.HealthTracker(),
Metrics: sys.UserMetricsRegistry(),
}) })
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)

View File

@ -298,7 +298,7 @@ func TestStateMachine(t *testing.T) {
sys := new(tsd.System) sys := new(tsd.System)
store := new(testStateStorage) store := new(testStateStorage)
sys.Set(store) sys.Set(store)
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker()) e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry())
if err != nil { if err != nil {
t.Fatalf("NewFakeUserspaceEngine: %v", err) t.Fatalf("NewFakeUserspaceEngine: %v", err)
} }
@ -931,7 +931,7 @@ func TestEditPrefsHasNoKeys(t *testing.T) {
logf := tstest.WhileTestRunningLogger(t) logf := tstest.WhileTestRunningLogger(t)
sys := new(tsd.System) sys := new(tsd.System)
sys.Set(new(mem.Store)) sys.Set(new(mem.Store))
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker()) e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry())
if err != nil { if err != nil {
t.Fatalf("NewFakeUserspaceEngine: %v", err) t.Fatalf("NewFakeUserspaceEngine: %v", err)
} }

View File

@ -64,7 +64,6 @@
"tailscale.com/util/progresstracking" "tailscale.com/util/progresstracking"
"tailscale.com/util/rands" "tailscale.com/util/rands"
"tailscale.com/util/testenv" "tailscale.com/util/testenv"
"tailscale.com/util/usermetric"
"tailscale.com/version" "tailscale.com/version"
"tailscale.com/wgengine/magicsock" "tailscale.com/wgengine/magicsock"
) )
@ -581,7 +580,7 @@ func (h *Handler) serveUserMetrics(w http.ResponseWriter, r *http.Request) {
http.Error(w, "usermetrics debug flag not enabled", http.StatusForbidden) http.Error(w, "usermetrics debug flag not enabled", http.StatusForbidden)
return return
} }
usermetric.Handler(w, r) h.b.UserMetricsRegistry().Handler(w, r)
} }
func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) { func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) {

View File

@ -356,7 +356,7 @@ func newTestLocalBackend(t testing.TB) *ipnlocal.LocalBackend {
sys := new(tsd.System) sys := new(tsd.System)
store := new(mem.Store) store := new(mem.Store)
sys.Set(store) sys.Set(store)
eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker()) eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry())
if err != nil { if err != nil {
t.Fatalf("NewFakeUserspaceEngine: %v", err) t.Fatalf("NewFakeUserspaceEngine: %v", err)
} }

View File

@ -97,7 +97,12 @@ type KeyValue[T comparable] struct {
} }
func (v *MultiLabelMap[T]) String() string { func (v *MultiLabelMap[T]) String() string {
return `"MultiLabelMap"` var sb strings.Builder
sb.WriteString("MultiLabelMap:\n")
v.Do(func(kv KeyValue[T]) {
fmt.Fprintf(&sb, "\t%v: %v\n", kv.Key, kv.Value)
})
return sb.String()
} }
// WritePrometheus writes v to w in Prometheus exposition format. // WritePrometheus writes v to w in Prometheus exposition format.
@ -281,3 +286,16 @@ func (v *MultiLabelMap[T]) Do(f func(KeyValue[T])) {
f(KeyValue[T]{e.key, e.val}) f(KeyValue[T]{e.key, e.val})
} }
} }
// ResetAllForTest resets all values for metrics to zero.
// Should only be used in tests.
func (v *MultiLabelMap[T]) ResetAllForTest() {
v.Do(func(kv KeyValue[T]) {
switch v := kv.Value.(type) {
case *expvar.Int:
v.Set(0)
case *expvar.Float:
v.Set(0)
}
})
}

View File

@ -24,6 +24,7 @@
"go4.org/mem" "go4.org/mem"
"gvisor.dev/gvisor/pkg/tcpip/stack" "gvisor.dev/gvisor/pkg/tcpip/stack"
"tailscale.com/disco" "tailscale.com/disco"
tsmetrics "tailscale.com/metrics"
"tailscale.com/net/connstats" "tailscale.com/net/connstats"
"tailscale.com/net/packet" "tailscale.com/net/packet"
"tailscale.com/net/packet/checksum" "tailscale.com/net/packet/checksum"
@ -209,6 +210,30 @@ type Wrapper struct {
stats atomic.Pointer[connstats.Statistics] stats atomic.Pointer[connstats.Statistics]
captureHook syncs.AtomicValue[capture.Callback] captureHook syncs.AtomicValue[capture.Callback]
metrics *metrics
}
type metrics struct {
inboundDroppedPacketsTotal *tsmetrics.MultiLabelMap[dropPacketLabel]
outboundDroppedPacketsTotal *tsmetrics.MultiLabelMap[dropPacketLabel]
}
func registerMetrics(reg *usermetric.Registry) *metrics {
return &metrics{
inboundDroppedPacketsTotal: usermetric.NewMultiLabelMapWithRegistry[dropPacketLabel](
reg,
"tailscaled_inbound_dropped_packets_total",
"counter",
"Counts the number of dropped packets received by the node from other peers",
),
outboundDroppedPacketsTotal: usermetric.NewMultiLabelMapWithRegistry[dropPacketLabel](
reg,
"tailscaled_outbound_dropped_packets_total",
"counter",
"Counts the number of packets dropped while being sent to other peers",
),
}
} }
// tunInjectedRead is an injected packet pretending to be a tun.Read(). // tunInjectedRead is an injected packet pretending to be a tun.Read().
@ -248,15 +273,15 @@ func (w *Wrapper) Start() {
close(w.startCh) close(w.startCh)
} }
func WrapTAP(logf logger.Logf, tdev tun.Device) *Wrapper { func WrapTAP(logf logger.Logf, tdev tun.Device, m *usermetric.Registry) *Wrapper {
return wrap(logf, tdev, true) return wrap(logf, tdev, true, m)
} }
func Wrap(logf logger.Logf, tdev tun.Device) *Wrapper { func Wrap(logf logger.Logf, tdev tun.Device, m *usermetric.Registry) *Wrapper {
return wrap(logf, tdev, false) return wrap(logf, tdev, false, m)
} }
func wrap(logf logger.Logf, tdev tun.Device, isTAP bool) *Wrapper { func wrap(logf logger.Logf, tdev tun.Device, isTAP bool, m *usermetric.Registry) *Wrapper {
logf = logger.WithPrefix(logf, "tstun: ") logf = logger.WithPrefix(logf, "tstun: ")
w := &Wrapper{ w := &Wrapper{
logf: logf, logf: logf,
@ -274,6 +299,7 @@ func wrap(logf logger.Logf, tdev tun.Device, isTAP bool) *Wrapper {
// TODO(dmytro): (highly rate-limited) hexdumps should happen on unknown packets. // TODO(dmytro): (highly rate-limited) hexdumps should happen on unknown packets.
filterFlags: filter.LogAccepts | filter.LogDrops, filterFlags: filter.LogAccepts | filter.LogDrops,
startCh: make(chan struct{}), startCh: make(chan struct{}),
metrics: registerMetrics(m),
} }
w.vectorBuffer = make([][]byte, tdev.BatchSize()) w.vectorBuffer = make([][]byte, tdev.BatchSize())
@ -872,7 +898,7 @@ func (t *Wrapper) filterPacketOutboundToWireGuard(p *packet.Parsed, pc *peerConf
if filt.RunOut(p, t.filterFlags) != filter.Accept { if filt.RunOut(p, t.filterFlags) != filter.Accept {
metricPacketOutDropFilter.Add(1) metricPacketOutDropFilter.Add(1)
metricOutboundDroppedPacketsTotal.Add(dropPacketLabel{ t.metrics.outboundDroppedPacketsTotal.Add(dropPacketLabel{
Reason: DropReasonACL, Reason: DropReasonACL,
}, 1) }, 1)
return filter.Drop, gro return filter.Drop, gro
@ -1144,7 +1170,7 @@ func (t *Wrapper) filterPacketInboundFromWireGuard(p *packet.Parsed, captHook ca
if outcome != filter.Accept { if outcome != filter.Accept {
metricPacketInDropFilter.Add(1) metricPacketInDropFilter.Add(1)
metricInboundDroppedPacketsTotal.Add(dropPacketLabel{ t.metrics.inboundDroppedPacketsTotal.Add(dropPacketLabel{
Reason: DropReasonACL, Reason: DropReasonACL,
}, 1) }, 1)
@ -1225,7 +1251,7 @@ func (t *Wrapper) Write(buffs [][]byte, offset int) (int, error) {
t.noteActivity() t.noteActivity()
_, err := t.tdevWrite(buffs, offset) _, err := t.tdevWrite(buffs, offset)
if err != nil { if err != nil {
metricInboundDroppedPacketsTotal.Add(dropPacketLabel{ t.metrics.inboundDroppedPacketsTotal.Add(dropPacketLabel{
Reason: DropReasonError, Reason: DropReasonError,
}, int64(len(buffs))) }, int64(len(buffs)))
} }
@ -1482,19 +1508,6 @@ type dropPacketLabel struct {
Reason DropReason Reason DropReason
} }
var (
metricInboundDroppedPacketsTotal = usermetric.NewMultiLabelMap[dropPacketLabel](
"tailscaled_inbound_dropped_packets_total",
"counter",
"Counts the number of dropped packets received by the node from other peers",
)
metricOutboundDroppedPacketsTotal = usermetric.NewMultiLabelMap[dropPacketLabel](
"tailscaled_outbound_dropped_packets_total",
"counter",
"Counts the number of packets dropped while being sent to other peers",
)
)
func (t *Wrapper) InstallCaptureHook(cb capture.Callback) { func (t *Wrapper) InstallCaptureHook(cb capture.Callback) {
t.captureHook.Store(cb) t.captureHook.Store(cb)
} }

View File

@ -8,6 +8,7 @@
"context" "context"
"encoding/binary" "encoding/binary"
"encoding/hex" "encoding/hex"
"expvar"
"fmt" "fmt"
"net/netip" "net/netip"
"reflect" "reflect"
@ -38,6 +39,7 @@
"tailscale.com/types/ptr" "tailscale.com/types/ptr"
"tailscale.com/types/views" "tailscale.com/types/views"
"tailscale.com/util/must" "tailscale.com/util/must"
"tailscale.com/util/usermetric"
"tailscale.com/wgengine/capture" "tailscale.com/wgengine/capture"
"tailscale.com/wgengine/filter" "tailscale.com/wgengine/filter"
"tailscale.com/wgengine/wgcfg" "tailscale.com/wgengine/wgcfg"
@ -173,7 +175,8 @@ func setfilter(logf logger.Logf, tun *Wrapper) {
func newChannelTUN(logf logger.Logf, secure bool) (*tuntest.ChannelTUN, *Wrapper) { func newChannelTUN(logf logger.Logf, secure bool) (*tuntest.ChannelTUN, *Wrapper) {
chtun := tuntest.NewChannelTUN() chtun := tuntest.NewChannelTUN()
tun := Wrap(logf, chtun.TUN()) reg := new(usermetric.Registry)
tun := Wrap(logf, chtun.TUN(), reg)
if secure { if secure {
setfilter(logf, tun) setfilter(logf, tun)
} else { } else {
@ -185,7 +188,8 @@ func newChannelTUN(logf logger.Logf, secure bool) (*tuntest.ChannelTUN, *Wrapper
func newFakeTUN(logf logger.Logf, secure bool) (*fakeTUN, *Wrapper) { func newFakeTUN(logf logger.Logf, secure bool) (*fakeTUN, *Wrapper) {
ftun := NewFake() ftun := NewFake()
tun := Wrap(logf, ftun) reg := new(usermetric.Registry)
tun := Wrap(logf, ftun, reg)
if secure { if secure {
setfilter(logf, tun) setfilter(logf, tun)
} else { } else {
@ -315,15 +319,15 @@ func mustHexDecode(s string) []byte {
} }
func TestFilter(t *testing.T) { func TestFilter(t *testing.T) {
// Reset the metrics before test. These are global
// so the different tests might have affected them.
metricInboundDroppedPacketsTotal.SetInt(dropPacketLabel{Reason: DropReasonACL}, 0)
metricInboundDroppedPacketsTotal.SetInt(dropPacketLabel{Reason: DropReasonError}, 0)
metricOutboundDroppedPacketsTotal.SetInt(dropPacketLabel{Reason: DropReasonACL}, 0)
chtun, tun := newChannelTUN(t.Logf, true) chtun, tun := newChannelTUN(t.Logf, true)
defer tun.Close() defer tun.Close()
// Reset the metrics before test. These are global
// so the different tests might have affected them.
tun.metrics.inboundDroppedPacketsTotal.ResetAllForTest()
tun.metrics.outboundDroppedPacketsTotal.ResetAllForTest()
type direction int type direction int
const ( const (
@ -436,20 +440,26 @@ func TestFilter(t *testing.T) {
}) })
} }
inACL := metricInboundDroppedPacketsTotal.Get(dropPacketLabel{Reason: DropReasonACL}) var metricInboundDroppedPacketsACL, metricInboundDroppedPacketsErr, metricOutboundDroppedPacketsACL int64
inError := metricInboundDroppedPacketsTotal.Get(dropPacketLabel{Reason: DropReasonError}) if m, ok := tun.metrics.inboundDroppedPacketsTotal.Get(dropPacketLabel{Reason: DropReasonACL}).(*expvar.Int); ok {
outACL := metricOutboundDroppedPacketsTotal.Get(dropPacketLabel{Reason: DropReasonACL}) metricInboundDroppedPacketsACL = m.Value()
}
assertMetricPackets(t, "inACL", "3", inACL.String()) if m, ok := tun.metrics.inboundDroppedPacketsTotal.Get(dropPacketLabel{Reason: DropReasonError}).(*expvar.Int); ok {
assertMetricPackets(t, "inError", "0", inError.String()) metricInboundDroppedPacketsErr = m.Value()
assertMetricPackets(t, "outACL", "1", outACL.String()) }
if m, ok := tun.metrics.outboundDroppedPacketsTotal.Get(dropPacketLabel{Reason: DropReasonACL}).(*expvar.Int); ok {
metricOutboundDroppedPacketsACL = m.Value()
}
assertMetricPackets(t, "inACL", 3, metricInboundDroppedPacketsACL)
assertMetricPackets(t, "inError", 0, metricInboundDroppedPacketsErr)
assertMetricPackets(t, "outACL", 1, metricOutboundDroppedPacketsACL)
} }
func assertMetricPackets(t *testing.T, metricName, want, got string) { func assertMetricPackets(t *testing.T, metricName string, want, got int64) {
t.Helper() t.Helper()
if want != got { if want != got {
t.Errorf("%s got unexpected value, got %s, want %s", metricName, got, want) t.Errorf("%s got unexpected value, got %d, want %d", metricName, got, want)
} }
} }
@ -512,6 +522,7 @@ func TestAtomic64Alignment(t *testing.T) {
} }
func TestPeerAPIBypass(t *testing.T) { func TestPeerAPIBypass(t *testing.T) {
reg := new(usermetric.Registry)
wrapperWithPeerAPI := &Wrapper{ wrapperWithPeerAPI := &Wrapper{
PeerAPIPort: func(ip netip.Addr) (port uint16, ok bool) { PeerAPIPort: func(ip netip.Addr) (port uint16, ok bool) {
if ip == netip.MustParseAddr("100.64.1.2") { if ip == netip.MustParseAddr("100.64.1.2") {
@ -519,6 +530,7 @@ func TestPeerAPIBypass(t *testing.T) {
} }
return return
}, },
metrics: registerMetrics(reg),
} }
tests := []struct { tests := []struct {
@ -534,13 +546,16 @@ func TestPeerAPIBypass(t *testing.T) {
PeerAPIPort: func(netip.Addr) (port uint16, ok bool) { PeerAPIPort: func(netip.Addr) (port uint16, ok bool) {
return 60000, true return 60000, true
}, },
metrics: registerMetrics(reg),
}, },
pkt: tcp4syn("1.2.3.4", "100.64.1.2", 1234, 60000), pkt: tcp4syn("1.2.3.4", "100.64.1.2", 1234, 60000),
want: filter.Drop, want: filter.Drop,
}, },
{ {
name: "reject_with_filter", name: "reject_with_filter",
w: &Wrapper{}, w: &Wrapper{
metrics: registerMetrics(reg),
},
filter: filter.NewAllowNone(logger.Discard, new(netipx.IPSet)), filter: filter.NewAllowNone(logger.Discard, new(netipx.IPSet)),
pkt: tcp4syn("1.2.3.4", "100.64.1.2", 1234, 60000), pkt: tcp4syn("1.2.3.4", "100.64.1.2", 1234, 60000),
want: filter.Drop, want: filter.Drop,

View File

@ -826,7 +826,7 @@ func TestSSHAuthFlow(t *testing.T) {
func TestSSH(t *testing.T) { func TestSSH(t *testing.T) {
var logf logger.Logf = t.Logf var logf logger.Logf = t.Logf
sys := &tsd.System{} sys := &tsd.System{}
eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker()) eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry())
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -32,6 +32,7 @@
"tailscale.com/net/tstun" "tailscale.com/net/tstun"
"tailscale.com/proxymap" "tailscale.com/proxymap"
"tailscale.com/types/netmap" "tailscale.com/types/netmap"
"tailscale.com/util/usermetric"
"tailscale.com/wgengine" "tailscale.com/wgengine"
"tailscale.com/wgengine/magicsock" "tailscale.com/wgengine/magicsock"
"tailscale.com/wgengine/router" "tailscale.com/wgengine/router"
@ -65,7 +66,8 @@ type System struct {
controlKnobs controlknobs.Knobs controlKnobs controlknobs.Knobs
proxyMap proxymap.Mapper proxyMap proxymap.Mapper
healthTracker health.Tracker healthTracker health.Tracker
userMetricsRegistry usermetric.Registry
} }
// NetstackImpl is the interface that *netstack.Impl implements. // NetstackImpl is the interface that *netstack.Impl implements.
@ -142,6 +144,11 @@ func (s *System) HealthTracker() *health.Tracker {
return &s.healthTracker return &s.healthTracker
} }
// UserMetricsRegistry returns the system usermetrics.
func (s *System) UserMetricsRegistry() *usermetric.Registry {
return &s.userMetricsRegistry
}
// SubSystem represents some subsystem of the Tailscale node daemon. // SubSystem represents some subsystem of the Tailscale node daemon.
// //
// A subsystem can be set to a value, and then later retrieved. A subsystem // A subsystem can be set to a value, and then later retrieved. A subsystem

View File

@ -536,12 +536,14 @@ func (s *Server) start() (reterr error) {
SetSubsystem: sys.Set, SetSubsystem: sys.Set,
ControlKnobs: sys.ControlKnobs(), ControlKnobs: sys.ControlKnobs(),
HealthTracker: sys.HealthTracker(), HealthTracker: sys.HealthTracker(),
Metrics: sys.UserMetricsRegistry(),
}) })
if err != nil { if err != nil {
return err return err
} }
closePool.add(s.dialer) closePool.add(s.dialer)
sys.Set(eng) sys.Set(eng)
sys.HealthTracker().SetMetricsRegistry(sys.UserMetricsRegistry())
// TODO(oxtoacart): do we need to support Taildrive on tsnet, and if so, how? // TODO(oxtoacart): do we need to support Taildrive on tsnet, and if so, how?
ns, err := netstack.Create(tsLogf, sys.Tun.Get(), eng, sys.MagicSock.Get(), s.dialer, sys.DNSManager.Get(), sys.ProxyMapper(), nil) ns, err := netstack.Create(tsLogf, sys.Tun.Get(), eng, sys.MagicSock.Get(), s.dialer, sys.DNSManager.Get(), sys.ProxyMapper(), nil)

View File

@ -5,6 +5,7 @@
import ( import (
"bufio" "bufio"
"bytes"
"context" "context"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic" "crypto/elliptic"
@ -31,7 +32,8 @@
"testing" "testing"
"time" "time"
"github.com/google/go-cmp/cmp" dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/expfmt"
"golang.org/x/net/proxy" "golang.org/x/net/proxy"
"tailscale.com/cmd/testwrapper/flakytest" "tailscale.com/cmd/testwrapper/flakytest"
"tailscale.com/health" "tailscale.com/health"
@ -818,65 +820,166 @@ func TestUDPConn(t *testing.T) {
} }
} }
// testWarnable is a Warnable that is used within this package for testing purposes only.
var testWarnable = health.Register(&health.Warnable{
Code: "test-warnable-tsnet",
Title: "Test warnable",
Severity: health.SeverityLow,
Text: func(args health.Args) string {
return args[health.ArgError]
},
})
func parseMetrics(m []byte) (map[string]float64, error) {
metrics := make(map[string]float64)
var parser expfmt.TextParser
mf, err := parser.TextToMetricFamilies(bytes.NewReader(m))
if err != nil {
return nil, err
}
for _, f := range mf {
for _, ff := range f.Metric {
val := float64(0)
switch f.GetType() {
case dto.MetricType_COUNTER:
val = ff.GetCounter().GetValue()
case dto.MetricType_GAUGE:
val = ff.GetGauge().GetValue()
}
metrics[f.GetName()+promMetricLabelsStr(ff.GetLabel())] = val
}
}
return metrics, nil
}
func promMetricLabelsStr(labels []*dto.LabelPair) string {
if len(labels) == 0 {
return ""
}
var b strings.Builder
b.WriteString("{")
for i, l := range labels {
if i > 0 {
b.WriteString(",")
}
b.WriteString(fmt.Sprintf("%s=%q", l.GetName(), l.GetValue()))
}
b.WriteString("}")
return b.String()
}
func TestUserMetrics(t *testing.T) { func TestUserMetrics(t *testing.T) {
flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/13420")
tstest.ResourceCheck(t) tstest.ResourceCheck(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel() defer cancel()
// testWarnable is a Warnable that is used within this package for testing purposes only.
var testWarnable = health.Register(&health.Warnable{
Code: "test-warnable-tsnet",
Title: "Test warnable",
Severity: health.SeverityLow,
Text: func(args health.Args) string {
return args[health.ArgError]
},
})
controlURL, c := startControl(t) controlURL, c := startControl(t)
s1, _, s1PubKey := startServer(t, ctx, controlURL, "s1") s1, s1ip, s1PubKey := startServer(t, ctx, controlURL, "s1")
s2, _, _ := startServer(t, ctx, controlURL, "s2")
s1.lb.EditPrefs(&ipn.MaskedPrefs{ s1.lb.EditPrefs(&ipn.MaskedPrefs{
Prefs: ipn.Prefs{ Prefs: ipn.Prefs{
AdvertiseRoutes: []netip.Prefix{ AdvertiseRoutes: []netip.Prefix{
netip.MustParsePrefix("192.0.2.0/24"), netip.MustParsePrefix("192.0.2.0/24"),
netip.MustParsePrefix("192.0.3.0/24"), netip.MustParsePrefix("192.0.3.0/24"),
netip.MustParsePrefix("192.0.5.1/32"),
netip.MustParsePrefix("0.0.0.0/0"),
}, },
}, },
AdvertiseRoutesSet: true, AdvertiseRoutesSet: true,
}) })
c.SetSubnetRoutes(s1PubKey, []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24")}) c.SetSubnetRoutes(s1PubKey, []netip.Prefix{
netip.MustParsePrefix("192.0.2.0/24"),
netip.MustParsePrefix("192.0.5.1/32"),
netip.MustParsePrefix("0.0.0.0/0"),
})
lc1, err := s1.LocalClient() lc1, err := s1.LocalClient()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
ht := s1.lb.HealthTracker() lc2, err := s2.LocalClient()
ht.SetUnhealthy(testWarnable, health.Args{"Text": "Hello world 1"})
metrics1, err := lc1.UserMetrics(ctx)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// Note that this test will check for two warnings because the health // ping to make sure the connection is up.
// tracker will have two warnings: one from the testWarnable, added in res, err := lc2.Ping(ctx, s1ip, tailcfg.PingICMP)
// this test, and one because we are running the dev/unstable version if err != nil {
// of tailscale. t.Fatalf("pinging: %s", err)
want := `# TYPE tailscaled_advertised_routes gauge }
# HELP tailscaled_advertised_routes Number of advertised network routes (e.g. by a subnet router) t.Logf("ping success: %#+v", res)
tailscaled_advertised_routes 2
# TYPE tailscaled_health_messages gauge
# HELP tailscaled_health_messages Number of health messages broken down by type.
tailscaled_health_messages{type="warning"} 2
# TYPE tailscaled_inbound_dropped_packets_total counter
# HELP tailscaled_inbound_dropped_packets_total Counts the number of dropped packets received by the node from other peers
# TYPE tailscaled_outbound_dropped_packets_total counter
# HELP tailscaled_outbound_dropped_packets_total Counts the number of packets dropped while being sent to other peers
`
if diff := cmp.Diff(want, string(metrics1)); diff != "" { ht := s1.lb.HealthTracker()
t.Fatalf("unexpected metrics (-want +got):\n%s", diff) ht.SetUnhealthy(testWarnable, health.Args{"Text": "Hello world 1"})
// Force an update to the netmap to ensure that the metrics are up-to-date.
s1.lb.DebugForceNetmapUpdate()
s2.lb.DebugForceNetmapUpdate()
ctxLc, cancelLc := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelLc()
metrics1, err := lc1.UserMetrics(ctxLc)
if err != nil {
t.Fatal(err)
}
status1, err := lc1.Status(ctxLc)
if err != nil {
t.Fatal(err)
}
parsedMetrics1, err := parseMetrics(metrics1)
if err != nil {
t.Fatal(err)
}
t.Logf("Metrics1:\n%s\n", metrics1)
// The node is advertising 4 routes:
// - 192.0.2.0/24
// - 192.0.3.0/24
// - 192.0.5.1/32
if got, want := parsedMetrics1["tailscaled_advertised_routes"], 3.0; got != want {
t.Errorf("metrics1, tailscaled_advertised_routes: got %v, want %v", got, want)
}
// Validate the health counter metric against the status of the node
if got, want := parsedMetrics1[`tailscaled_health_messages{type="warning"}`], float64(len(status1.Health)); got != want {
t.Errorf("metrics1, tailscaled_health_messages: got %v, want %v", got, want)
}
metrics2, err := lc2.UserMetrics(ctx)
if err != nil {
t.Fatal(err)
}
status2, err := lc2.Status(ctx)
if err != nil {
t.Fatal(err)
}
parsedMetrics2, err := parseMetrics(metrics2)
if err != nil {
t.Fatal(err)
}
t.Logf("Metrics2:\n%s\n", metrics2)
// The node is advertising 0 routes
if got, want := parsedMetrics2["tailscaled_advertised_routes"], 0.0; got != want {
t.Errorf("metrics2, tailscaled_advertised_routes: got %v, want %v", got, want)
}
// Validate the health counter metric against the status of the node
if got, want := parsedMetrics2[`tailscaled_health_messages{type="warning"}`], float64(len(status2.Health)); got != want {
t.Errorf("metrics2, tailscaled_health_messages: got %v, want %v", got, want)
} }
} }

View File

@ -10,29 +10,33 @@
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"strings"
"tailscale.com/metrics" "tailscale.com/metrics"
"tailscale.com/tsweb/varz" "tailscale.com/tsweb/varz"
) )
var vars expvar.Map // Registry tracks user-facing metrics of various Tailscale subsystems.
type Registry struct {
vars expvar.Map
}
// NewMultiLabelMap creates and register a new // NewMultiLabelMapWithRegistry creates and register a new
// MultiLabelMap[T] variable with the given name and returns it. // MultiLabelMap[T] variable with the given name and returns it.
// The variable is registered with the userfacing metrics package. // The variable is registered with the userfacing metrics package.
// //
// Note that usermetric are not protected against duplicate // Note that usermetric are not protected against duplicate
// metrics name. It is the caller's responsibility to ensure that // metrics name. It is the caller's responsibility to ensure that
// the name is unique. // the name is unique.
func NewMultiLabelMap[T comparable](name string, promType, helpText string) *metrics.MultiLabelMap[T] { func NewMultiLabelMapWithRegistry[T comparable](m *Registry, name string, promType, helpText string) *metrics.MultiLabelMap[T] {
m := &metrics.MultiLabelMap[T]{ ml := &metrics.MultiLabelMap[T]{
Type: promType, Type: promType,
Help: helpText, Help: helpText,
} }
var zero T var zero T
_ = metrics.LabelString(zero) // panic early if T is invalid _ = metrics.LabelString(zero) // panic early if T is invalid
vars.Set(name, m) m.vars.Set(name, ml)
return m return ml
} }
// Gauge is a gauge metric with no labels. // Gauge is a gauge metric with no labels.
@ -42,20 +46,26 @@ type Gauge struct {
} }
// NewGauge creates and register a new gauge metric with the given name and help text. // NewGauge creates and register a new gauge metric with the given name and help text.
func NewGauge(name, help string) *Gauge { func (r *Registry) NewGauge(name, help string) *Gauge {
g := &Gauge{&expvar.Float{}, help} g := &Gauge{&expvar.Float{}, help}
vars.Set(name, g) r.vars.Set(name, g)
return g return g
} }
// Set sets the gauge to the given value. // Set sets the gauge to the given value.
func (g *Gauge) Set(v float64) { func (g *Gauge) Set(v float64) {
if g == nil {
return
}
g.m.Set(v) g.m.Set(v)
} }
// String returns the string of the underlying expvar.Float. // String returns the string of the underlying expvar.Float.
// This satisfies the expvar.Var interface. // This satisfies the expvar.Var interface.
func (g *Gauge) String() string { func (g *Gauge) String() string {
if g == nil {
return ""
}
return g.m.String() return g.m.String()
} }
@ -79,6 +89,17 @@ func (g *Gauge) WritePrometheus(w io.Writer, name string) {
// Handler returns a varz.Handler that serves the userfacing expvar contained // Handler returns a varz.Handler that serves the userfacing expvar contained
// in this package. // in this package.
func Handler(w http.ResponseWriter, r *http.Request) { func (r *Registry) Handler(w http.ResponseWriter, req *http.Request) {
varz.ExpvarDoHandler(vars.Do)(w, r) varz.ExpvarDoHandler(r.vars.Do)(w, req)
}
// String returns the string representation of all the metrics and their
// values in the registry. It is useful for debugging.
func (r *Registry) String() string {
var sb strings.Builder
r.vars.Do(func(kv expvar.KeyValue) {
fmt.Fprintf(&sb, "%s: %v\n", kv.Key, kv.Value)
})
return sb.String()
} }

View File

@ -9,7 +9,8 @@
) )
func TestGauge(t *testing.T) { func TestGauge(t *testing.T) {
g := NewGauge("test_gauge", "This is a test gauge") var reg Registry
g := reg.NewGauge("test_gauge", "This is a test gauge")
g.Set(15) g.Set(15)
var buf bytes.Buffer var buf bytes.Buffer

View File

@ -60,6 +60,7 @@
"tailscale.com/util/set" "tailscale.com/util/set"
"tailscale.com/util/testenv" "tailscale.com/util/testenv"
"tailscale.com/util/uniq" "tailscale.com/util/uniq"
"tailscale.com/util/usermetric"
"tailscale.com/wgengine/capture" "tailscale.com/wgengine/capture"
"tailscale.com/wgengine/wgint" "tailscale.com/wgengine/wgint"
) )
@ -386,6 +387,9 @@ type Options struct {
// report errors and warnings to. // report errors and warnings to.
HealthTracker *health.Tracker HealthTracker *health.Tracker
// Metrics specifies the metrics registry to record metrics to.
Metrics *usermetric.Registry
// ControlKnobs are the set of control knobs to use. // ControlKnobs are the set of control knobs to use.
// If nil, they're ignored and not updated. // If nil, they're ignored and not updated.
ControlKnobs *controlknobs.Knobs ControlKnobs *controlknobs.Knobs

View File

@ -64,6 +64,7 @@
"tailscale.com/util/cibuild" "tailscale.com/util/cibuild"
"tailscale.com/util/racebuild" "tailscale.com/util/racebuild"
"tailscale.com/util/set" "tailscale.com/util/set"
"tailscale.com/util/usermetric"
"tailscale.com/wgengine/filter" "tailscale.com/wgengine/filter"
"tailscale.com/wgengine/wgcfg" "tailscale.com/wgengine/wgcfg"
"tailscale.com/wgengine/wgcfg/nmcfg" "tailscale.com/wgengine/wgcfg/nmcfg"
@ -156,6 +157,7 @@ type magicStack struct {
dev *device.Device // the wireguard-go Device that connects the previous things dev *device.Device // the wireguard-go Device that connects the previous things
wgLogger *wglog.Logger // wireguard-go log wrapper wgLogger *wglog.Logger // wireguard-go log wrapper
netMon *netmon.Monitor // always non-nil netMon *netmon.Monitor // always non-nil
metrics *usermetric.Registry
} }
// newMagicStack builds and initializes an idle magicsock and // newMagicStack builds and initializes an idle magicsock and
@ -174,9 +176,11 @@ func newMagicStackWithKey(t testing.TB, logf logger.Logf, l nettype.PacketListen
t.Fatalf("netmon.New: %v", err) t.Fatalf("netmon.New: %v", err)
} }
var reg usermetric.Registry
epCh := make(chan []tailcfg.Endpoint, 100) // arbitrary epCh := make(chan []tailcfg.Endpoint, 100) // arbitrary
conn, err := NewConn(Options{ conn, err := NewConn(Options{
NetMon: netMon, NetMon: netMon,
Metrics: &reg,
Logf: logf, Logf: logf,
DisablePortMapper: true, DisablePortMapper: true,
TestOnlyPacketListener: l, TestOnlyPacketListener: l,
@ -193,7 +197,7 @@ func newMagicStackWithKey(t testing.TB, logf logger.Logf, l nettype.PacketListen
} }
tun := tuntest.NewChannelTUN() tun := tuntest.NewChannelTUN()
tsTun := tstun.Wrap(logf, tun.TUN()) tsTun := tstun.Wrap(logf, tun.TUN(), &reg)
tsTun.SetFilter(filter.NewAllowAllForTest(logf)) tsTun.SetFilter(filter.NewAllowAllForTest(logf))
tsTun.Start() tsTun.Start()
@ -219,6 +223,7 @@ func newMagicStackWithKey(t testing.TB, logf logger.Logf, l nettype.PacketListen
dev: dev, dev: dev,
wgLogger: wgLogger, wgLogger: wgLogger,
netMon: netMon, netMon: netMon,
metrics: &reg,
} }
} }
@ -397,6 +402,7 @@ func TestNewConn(t *testing.T) {
EndpointsFunc: epFunc, EndpointsFunc: epFunc,
Logf: t.Logf, Logf: t.Logf,
NetMon: netMon, NetMon: netMon,
Metrics: new(usermetric.Registry),
}) })
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -523,6 +529,7 @@ func TestDeviceStartStop(t *testing.T) {
EndpointsFunc: func(eps []tailcfg.Endpoint) {}, EndpointsFunc: func(eps []tailcfg.Endpoint) {},
Logf: t.Logf, Logf: t.Logf,
NetMon: netMon, NetMon: netMon,
Metrics: new(usermetric.Registry),
}) })
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -1275,6 +1282,7 @@ func newTestConn(t testing.TB) *Conn {
conn, err := NewConn(Options{ conn, err := NewConn(Options{
NetMon: netMon, NetMon: netMon,
HealthTracker: new(health.Tracker), HealthTracker: new(health.Tracker),
Metrics: new(usermetric.Registry),
DisablePortMapper: true, DisablePortMapper: true,
Logf: t.Logf, Logf: t.Logf,
Port: port, Port: port,

View File

@ -50,6 +50,7 @@ func TestInjectInboundLeak(t *testing.T) {
Dialer: dialer, Dialer: dialer,
SetSubsystem: sys.Set, SetSubsystem: sys.Set,
HealthTracker: sys.HealthTracker(), HealthTracker: sys.HealthTracker(),
Metrics: sys.UserMetricsRegistry(),
}) })
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -107,6 +108,7 @@ func makeNetstack(tb testing.TB, config func(*Impl)) *Impl {
Dialer: dialer, Dialer: dialer,
SetSubsystem: sys.Set, SetSubsystem: sys.Set,
HealthTracker: sys.HealthTracker(), HealthTracker: sys.HealthTracker(),
Metrics: sys.UserMetricsRegistry(),
}) })
if err != nil { if err != nil {
tb.Fatal(err) tb.Fatal(err)

View File

@ -49,6 +49,7 @@
"tailscale.com/util/mak" "tailscale.com/util/mak"
"tailscale.com/util/set" "tailscale.com/util/set"
"tailscale.com/util/testenv" "tailscale.com/util/testenv"
"tailscale.com/util/usermetric"
"tailscale.com/version" "tailscale.com/version"
"tailscale.com/wgengine/capture" "tailscale.com/wgengine/capture"
"tailscale.com/wgengine/filter" "tailscale.com/wgengine/filter"
@ -195,6 +196,9 @@ type Config struct {
// HealthTracker, if non-nil, is the health tracker to use. // HealthTracker, if non-nil, is the health tracker to use.
HealthTracker *health.Tracker HealthTracker *health.Tracker
// Metrics, if non-nil, is the usermetrics registry to use.
Metrics *usermetric.Registry
// Dialer is the dialer to use for outbound connections. // Dialer is the dialer to use for outbound connections.
// If nil, a new Dialer is created. // If nil, a new Dialer is created.
Dialer *tsdial.Dialer Dialer *tsdial.Dialer
@ -249,6 +253,8 @@ func NewFakeUserspaceEngine(logf logger.Logf, opts ...any) (Engine, error) {
conf.ControlKnobs = v conf.ControlKnobs = v
case *health.Tracker: case *health.Tracker:
conf.HealthTracker = v conf.HealthTracker = v
case *usermetric.Registry:
conf.Metrics = v
default: default:
return nil, fmt.Errorf("unknown option type %T", v) return nil, fmt.Errorf("unknown option type %T", v)
} }
@ -289,9 +295,9 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
var tsTUNDev *tstun.Wrapper var tsTUNDev *tstun.Wrapper
if conf.IsTAP { if conf.IsTAP {
tsTUNDev = tstun.WrapTAP(logf, conf.Tun) tsTUNDev = tstun.WrapTAP(logf, conf.Tun, conf.Metrics)
} else { } else {
tsTUNDev = tstun.Wrap(logf, conf.Tun) tsTUNDev = tstun.Wrap(logf, conf.Tun, conf.Metrics)
} }
closePool.add(tsTUNDev) closePool.add(tsTUNDev)
@ -387,6 +393,7 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
NoteRecvActivity: e.noteRecvActivity, NoteRecvActivity: e.noteRecvActivity,
NetMon: e.netMon, NetMon: e.netMon,
HealthTracker: e.health, HealthTracker: e.health,
Metrics: conf.Metrics,
ControlKnobs: conf.ControlKnobs, ControlKnobs: conf.ControlKnobs,
OnPortUpdate: onPortUpdate, OnPortUpdate: onPortUpdate,
PeerByKeyFunc: e.PeerByKey, PeerByKeyFunc: e.PeerByKey,

View File

@ -22,6 +22,7 @@ func TestIsNetstack(t *testing.T) {
wgengine.Config{ wgengine.Config{
SetSubsystem: sys.Set, SetSubsystem: sys.Set,
HealthTracker: sys.HealthTracker(), HealthTracker: sys.HealthTracker(),
Metrics: sys.UserMetricsRegistry(),
}, },
) )
if err != nil { if err != nil {
@ -72,6 +73,7 @@ func TestIsNetstackRouter(t *testing.T) {
conf := tt.conf conf := tt.conf
conf.SetSubsystem = sys.Set conf.SetSubsystem = sys.Set
conf.HealthTracker = sys.HealthTracker() conf.HealthTracker = sys.HealthTracker()
conf.Metrics = sys.UserMetricsRegistry()
e, err := wgengine.NewUserspaceEngine(logger.Discard, conf) e, err := wgengine.NewUserspaceEngine(logger.Discard, conf)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)

View File

@ -25,6 +25,7 @@
"tailscale.com/types/key" "tailscale.com/types/key"
"tailscale.com/types/netmap" "tailscale.com/types/netmap"
"tailscale.com/types/opt" "tailscale.com/types/opt"
"tailscale.com/util/usermetric"
"tailscale.com/wgengine/router" "tailscale.com/wgengine/router"
"tailscale.com/wgengine/wgcfg" "tailscale.com/wgengine/wgcfg"
) )
@ -100,7 +101,8 @@ func nodeViews(v []*tailcfg.Node) []tailcfg.NodeView {
func TestUserspaceEngineReconfig(t *testing.T) { func TestUserspaceEngineReconfig(t *testing.T) {
ht := new(health.Tracker) ht := new(health.Tracker)
e, err := NewFakeUserspaceEngine(t.Logf, 0, ht) reg := new(usermetric.Registry)
e, err := NewFakeUserspaceEngine(t.Logf, 0, ht, reg)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -167,9 +169,10 @@ func TestUserspaceEnginePortReconfig(t *testing.T) {
// Keep making a wgengine until we find an unused port // Keep making a wgengine until we find an unused port
var ue *userspaceEngine var ue *userspaceEngine
ht := new(health.Tracker) ht := new(health.Tracker)
reg := new(usermetric.Registry)
for i := range 100 { for i := range 100 {
attempt := uint16(defaultPort + i) attempt := uint16(defaultPort + i)
e, err := NewFakeUserspaceEngine(t.Logf, attempt, &knobs, ht) e, err := NewFakeUserspaceEngine(t.Logf, attempt, &knobs, ht, reg)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -249,7 +252,8 @@ func TestUserspaceEnginePeerMTUReconfig(t *testing.T) {
var knobs controlknobs.Knobs var knobs controlknobs.Knobs
ht := new(health.Tracker) ht := new(health.Tracker)
e, err := NewFakeUserspaceEngine(t.Logf, 0, &knobs, ht) reg := new(usermetric.Registry)
e, err := NewFakeUserspaceEngine(t.Logf, 0, &knobs, ht, reg)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -9,6 +9,7 @@
"time" "time"
"tailscale.com/health" "tailscale.com/health"
"tailscale.com/util/usermetric"
) )
func TestWatchdog(t *testing.T) { func TestWatchdog(t *testing.T) {
@ -24,7 +25,8 @@ func TestWatchdog(t *testing.T) {
t.Run("default watchdog does not fire", func(t *testing.T) { t.Run("default watchdog does not fire", func(t *testing.T) {
t.Parallel() t.Parallel()
ht := new(health.Tracker) ht := new(health.Tracker)
e, err := NewFakeUserspaceEngine(t.Logf, 0, ht) reg := new(usermetric.Registry)
e, err := NewFakeUserspaceEngine(t.Logf, 0, ht, reg)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }