Browse Source

feature/logtail: pull logtail + netlog out to modular features

Removes 434 KB from the minimal Linux binary, or ~3%.

Primarily this comes from not linking in the zstd encoding code.

Fixes #17323

Change-Id: I0a90de307dfa1ad7422db7aa8b1b46c782bfaaf7
Signed-off-by: Brad Fitzpatrick <[email protected]>
Brad Fitzpatrick 5 months ago
parent
commit
11b770fbc9

+ 2 - 3
cmd/tailscaled/depaware-minbox.txt

@@ -158,7 +158,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
         tailscale.com/types/logger                                   from tailscale.com/appc+
         tailscale.com/types/logid                                    from tailscale.com/cmd/tailscaled+
         tailscale.com/types/mapx                                     from tailscale.com/ipn/ipnext
-        tailscale.com/types/netlogtype                               from tailscale.com/net/connstats+
+        tailscale.com/types/netlogtype                               from tailscale.com/net/connstats
         tailscale.com/types/netmap                                   from tailscale.com/control/controlclient+
         tailscale.com/types/nettype                                  from tailscale.com/ipn/localapi+
         tailscale.com/types/opt                                      from tailscale.com/control/controlknobs+
@@ -205,11 +205,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
         tailscale.com/util/syspolicy/ptype                           from tailscale.com/ipn/ipnlocal+
         tailscale.com/util/systemd                                   from tailscale.com/control/controlclient+
         tailscale.com/util/testenv                                   from tailscale.com/control/controlclient+
-        tailscale.com/util/truncate                                  from tailscale.com/logtail
         tailscale.com/util/usermetric                                from tailscale.com/health+
         tailscale.com/util/vizerror                                  from tailscale.com/tailcfg+
         tailscale.com/util/winutil                                   from tailscale.com/ipn/ipnauth
-        tailscale.com/util/zstdframe                                 from tailscale.com/control/controlclient+
+        tailscale.com/util/zstdframe                                 from tailscale.com/control/controlclient
         tailscale.com/version                                        from tailscale.com/clientupdate+
         tailscale.com/version/distro                                 from tailscale.com/clientupdate+
         tailscale.com/wgengine                                       from tailscale.com/cmd/tailscaled+

+ 18 - 12
cmd/tailscaled/tailscaled.go

@@ -402,7 +402,7 @@ func ipnServerOpts() (o serverOptions) {
 	return o
 }
 
-var logPol *logpolicy.Policy
+var logPol *logpolicy.Policy // or nil if not used
 var debugMux *http.ServeMux
 
 func run() (err error) {
@@ -432,15 +432,19 @@ func run() (err error) {
 		sys.Set(netMon)
 	}
 
-	pol := logpolicy.New(logtail.CollectionNode, netMon, sys.HealthTracker.Get(), nil /* use log.Printf */)
-	pol.SetVerbosityLevel(args.verbose)
-	logPol = pol
-	defer func() {
-		// Finish uploading logs after closing everything else.
-		ctx, cancel := context.WithTimeout(context.Background(), time.Second)
-		defer cancel()
-		pol.Shutdown(ctx)
-	}()
+	var publicLogID logid.PublicID
+	if buildfeatures.HasLogTail {
+		pol := logpolicy.New(logtail.CollectionNode, netMon, sys.HealthTracker.Get(), nil /* use log.Printf */)
+		pol.SetVerbosityLevel(args.verbose)
+		publicLogID = pol.PublicID
+		logPol = pol
+		defer func() {
+			// Finish uploading logs after closing everything else.
+			ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+			defer cancel()
+			pol.Shutdown(ctx)
+		}()
+	}
 
 	if err := envknob.ApplyDiskConfigError(); err != nil {
 		log.Printf("Error reading environment config: %v", err)
@@ -449,7 +453,7 @@ func run() (err error) {
 	if isWinSvc {
 		// Run the IPN server from the Windows service manager.
 		log.Printf("Running service...")
-		if err := runWindowsService(pol); err != nil {
+		if err := runWindowsService(logPol); err != nil {
 			log.Printf("runservice: %v", err)
 		}
 		log.Printf("Service ended.")
@@ -493,7 +497,7 @@ func run() (err error) {
 		hostinfo.SetApp(app)
 	}
 
-	return startIPNServer(context.Background(), logf, pol.PublicID, sys)
+	return startIPNServer(context.Background(), logf, publicLogID, sys)
 }
 
 var (
@@ -503,6 +507,7 @@ var (
 
 var sigPipe os.Signal // set by sigpipe.go
 
+// logID may be the zero value if logging is not in use.
 func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID, sys *tsd.System) error {
 	ln, err := safesocket.Listen(args.socketpath)
 	if err != nil {
@@ -600,6 +605,7 @@ var (
 	hookNewNetstack feature.Hook[func(_ logger.Logf, _ *tsd.System, onlyNetstack bool) (tsd.NetstackImpl, error)]
 )
 
+// logID may be the zero value if logging is not in use.
 func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID, sys *tsd.System) (_ *ipnlocal.LocalBackend, retErr error) {
 	if logPol != nil {
 		logPol.Logtail.SetNetMon(sys.NetMon.Get())

+ 8 - 2
cmd/tailscaled/tailscaled_windows.go

@@ -149,6 +149,8 @@ var syslogf logger.Logf = logger.Discard
 //
 // At this point we're still the parent process that
 // Windows started.
+//
+// pol may be nil.
 func runWindowsService(pol *logpolicy.Policy) error {
 	go func() {
 		logger.Logf(log.Printf).JSON(1, "SupportInfo", osdiag.SupportInfo(osdiag.LogSupportInfoReasonStartup))
@@ -169,7 +171,7 @@ func runWindowsService(pol *logpolicy.Policy) error {
 }
 
 type ipnService struct {
-	Policy *logpolicy.Policy
+	Policy *logpolicy.Policy // or nil if logging not in use
 }
 
 // Called by Windows to execute the windows service.
@@ -186,7 +188,11 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
 	doneCh := make(chan struct{})
 	go func() {
 		defer close(doneCh)
-		args := []string{"/subproc", service.Policy.PublicID.String()}
+		publicID := "none"
+		if service.Policy != nil {
+			publicID = service.Policy.PublicID.String()
+		}
+		args := []string{"/subproc", publicID}
 		// Make a logger without a date prefix, as filelogger
 		// and logtail both already add their own. All we really want
 		// from the log package is the automatic newline.

+ 13 - 0
feature/buildfeatures/feature_logtail_disabled.go

@@ -0,0 +1,13 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Code generated by gen.go; DO NOT EDIT.
+
+//go:build ts_omit_logtail
+
+package buildfeatures
+
+// HasLogTail is whether the binary was built with support for modular feature "upload logs to log.tailscale.com (debug logs for bug reports and also by network flow logs if enabled)".
+// Specifically, it's whether the binary was NOT built with the "ts_omit_logtail" build tag.
+// It's a const so it can be used for dead code elimination.
+const HasLogTail = false

+ 13 - 0
feature/buildfeatures/feature_logtail_enabled.go

@@ -0,0 +1,13 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Code generated by gen.go; DO NOT EDIT.
+
+//go:build !ts_omit_logtail
+
+package buildfeatures
+
+// HasLogTail is whether the binary was built with support for modular feature "upload logs to log.tailscale.com (debug logs for bug reports and also by network flow logs if enabled)".
+// Specifically, it's whether the binary was NOT built with the "ts_omit_logtail" build tag.
+// It's a const so it can be used for dead code elimination.
+const HasLogTail = true

+ 13 - 0
feature/buildfeatures/feature_netlog_disabled.go

@@ -0,0 +1,13 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Code generated by gen.go; DO NOT EDIT.
+
+//go:build ts_omit_netlog
+
+package buildfeatures
+
+// HasNetLog is whether the binary was built with support for modular feature "Network flow logging support".
+// Specifically, it's whether the binary was NOT built with the "ts_omit_netlog" build tag.
+// It's a const so it can be used for dead code elimination.
+const HasNetLog = false

+ 13 - 0
feature/buildfeatures/feature_netlog_enabled.go

@@ -0,0 +1,13 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Code generated by gen.go; DO NOT EDIT.
+
+//go:build !ts_omit_netlog
+
+package buildfeatures
+
+// HasNetLog is whether the binary was built with support for modular feature "Network flow logging support".
+// Specifically, it's whether the binary was NOT built with the "ts_omit_netlog" build tag.
+// It's a const so it can be used for dead code elimination.
+const HasNetLog = true

+ 11 - 2
feature/featuretags/featuretags.go

@@ -115,7 +115,11 @@ var Features = map[FeatureTag]FeatureMeta{
 	"iptables":      {"IPTables", "Linux iptables support", nil},
 	"kube":          {"Kube", "Kubernetes integration", nil},
 	"linuxdnsfight": {"LinuxDNSFight", "Linux support for detecting DNS fights (inotify watching of /etc/resolv.conf)", nil},
-	"oauthkey":      {"OAuthKey", "OAuth secret-to-authkey resolution support", nil},
+	"logtail": {
+		Sym:  "LogTail",
+		Desc: "upload logs to log.tailscale.com (debug logs for bug reports and also by network flow logs if enabled)",
+	},
+	"oauthkey": {"OAuthKey", "OAuth secret-to-authkey resolution support", nil},
 	"outboundproxy": {
 		Sym:  "OutboundProxy",
 		Desc: "Outbound localhost HTTP/SOCK5 proxy support",
@@ -123,7 +127,12 @@ var Features = map[FeatureTag]FeatureMeta{
 	},
 	"portlist":   {"PortList", "Optionally advertise listening service ports", nil},
 	"portmapper": {"PortMapper", "NAT-PMP/PCP/UPnP port mapping support", nil},
-	"netstack":   {"Netstack", "gVisor netstack (userspace networking) support", nil},
+	"netlog": {
+		Sym:  "NetLog",
+		Desc: "Network flow logging support",
+		Deps: []FeatureTag{"logtail"},
+	},
+	"netstack": {"Netstack", "gVisor netstack (userspace networking) support", nil},
 	"networkmanager": {
 		Sym:  "NetworkManager",
 		Desc: "Linux NetworkManager integration",

+ 3 - 1
ipn/ipnlocal/local.go

@@ -202,7 +202,7 @@ type LocalBackend struct {
 	store                    ipn.StateStore  // non-nil; TODO(bradfitz): remove; use sys
 	dialer                   *tsdial.Dialer  // non-nil; TODO(bradfitz): remove; use sys
 	pushDeviceToken          syncs.AtomicValue[string]
-	backendLogID             logid.PublicID
+	backendLogID             logid.PublicID // or zero value if logging not in use
 	unregisterSysPolicyWatch func()
 	varRoot                  string         // or empty if SetVarRoot never called
 	logFlushFunc             func()         // or nil if SetLogFlusher wasn't called
@@ -456,6 +456,8 @@ type clientGen func(controlclient.Options) (controlclient.Client, error)
 // but is not actually running.
 //
 // If dialer is nil, a new one is made.
+//
+// The logID may be the zero value if logging is not in use.
 func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, loginFlags controlclient.LoginFlags) (_ *LocalBackend, err error) {
 	e := sys.Engine.Get()
 	store := sys.StateStore.Get()

+ 10 - 0
ipn/localapi/localapi.go

@@ -28,6 +28,7 @@ import (
 	"tailscale.com/client/tailscale/apitype"
 	"tailscale.com/clientupdate"
 	"tailscale.com/envknob"
+	"tailscale.com/feature/buildfeatures"
 	"tailscale.com/health/healthmsg"
 	"tailscale.com/hostinfo"
 	"tailscale.com/ipn"
@@ -575,6 +576,15 @@ func (h *Handler) serveGoroutines(w http.ResponseWriter, r *http.Request) {
 func (h *Handler) serveLogTap(w http.ResponseWriter, r *http.Request) {
 	ctx := r.Context()
 
+	if !buildfeatures.HasLogTail {
+		// TODO(bradfitz): separate out logtail tap functionality from upload
+		// functionality to make this possible? But seems unlikely people would
+		// want just this. They could "tail -f" or "journalctl -f" their logs
+		// themselves.
+		http.Error(w, "logtap not supported in this build", http.StatusNotImplemented)
+		return
+	}
+
 	// Require write access (~root) as the logs could contain something
 	// sensitive.
 	if !h.PermitWrite {

+ 2 - 1
log/sockstatlog/logger.go

@@ -17,6 +17,7 @@ import (
 	"sync/atomic"
 	"time"
 
+	"tailscale.com/feature/buildfeatures"
 	"tailscale.com/health"
 	"tailscale.com/logpolicy"
 	"tailscale.com/logtail"
@@ -97,7 +98,7 @@ func SockstatLogID(logID logid.PublicID) logid.PrivateID {
 // The netMon parameter is optional. It should be specified in environments where
 // Tailscaled is manipulating the routing table.
 func NewLogger(logdir string, logf logger.Logf, logID logid.PublicID, netMon *netmon.Monitor, health *health.Tracker) (*Logger, error) {
-	if !sockstats.IsAvailable {
+	if !sockstats.IsAvailable || !buildfeatures.HasLogTail {
 		return nil, nil
 	}
 	if netMon == nil {

+ 3 - 1
logpolicy/logpolicy.go

@@ -31,6 +31,7 @@ import (
 	"golang.org/x/term"
 	"tailscale.com/atomicfile"
 	"tailscale.com/envknob"
+	"tailscale.com/feature/buildfeatures"
 	"tailscale.com/health"
 	"tailscale.com/hostinfo"
 	"tailscale.com/log/filelogger"
@@ -106,6 +107,7 @@ type Policy struct {
 	// Logtail is the logger.
 	Logtail *logtail.Logger
 	// PublicID is the logger's instance identifier.
+	// It may be the zero value if logging is not in use.
 	PublicID logid.PublicID
 	// Logf is where to write informational messages about this Logger.
 	Logf logger.Logf
@@ -682,7 +684,7 @@ func (opts Options) init(disableLogging bool) (*logtail.Config, *Policy) {
 
 // New returns a new log policy (a logger and its instance ID).
 func (opts Options) New() *Policy {
-	disableLogging := envknob.NoLogsNoSupport() || testenv.InTest() || runtime.GOOS == "plan9"
+	disableLogging := envknob.NoLogsNoSupport() || testenv.InTest() || runtime.GOOS == "plan9" || !buildfeatures.HasLogTail
 	_, policy := opts.init(disableLogging)
 	return policy
 }

+ 2 - 0
logtail/buffer.go

@@ -1,6 +1,8 @@
 // Copyright (c) Tailscale Inc & AUTHORS
 // SPDX-License-Identifier: BSD-3-Clause
 
+//go:build !ts_omit_logtail
+
 package logtail
 
 import (

+ 65 - 0
logtail/config.go

@@ -0,0 +1,65 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package logtail
+
+import (
+	"io"
+	"net/http"
+	"time"
+
+	"tailscale.com/tstime"
+	"tailscale.com/types/logid"
+)
+
+// DefaultHost is the default host name to upload logs to when
+// Config.BaseURL isn't provided.
+const DefaultHost = "log.tailscale.com"
+
+const defaultFlushDelay = 2 * time.Second
+
+const (
+	// CollectionNode is the name of a logtail Config.Collection
+	// for tailscaled (or equivalent: IPNExtension, Android app).
+	CollectionNode = "tailnode.log.tailscale.io"
+)
+
+type Config struct {
+	Collection     string          // collection name, a domain name
+	PrivateID      logid.PrivateID // private ID for the primary log stream
+	CopyPrivateID  logid.PrivateID // private ID for a log stream that is a superset of this log stream
+	BaseURL        string          // if empty defaults to "https://log.tailscale.com"
+	HTTPC          *http.Client    // if empty defaults to http.DefaultClient
+	SkipClientTime bool            // if true, client_time is not written to logs
+	LowMemory      bool            // if true, logtail minimizes memory use
+	Clock          tstime.Clock    // if set, Clock.Now substitutes uses of time.Now
+	Stderr         io.Writer       // if set, logs are sent here instead of os.Stderr
+	StderrLevel    int             // max verbosity level to write to stderr; 0 means the non-verbose messages only
+	Buffer         Buffer          // temp storage, if nil a MemoryBuffer
+	CompressLogs   bool            // whether to compress the log uploads
+	MaxUploadSize  int             // maximum upload size; 0 means using the default
+
+	// MetricsDelta, if non-nil, is a func that returns an encoding
+	// delta in clientmetrics to upload alongside existing logs.
+	// It can return either an empty string (for nothing) or a string
+	// that's safe to embed in a JSON string literal without further escaping.
+	MetricsDelta func() string
+
+	// FlushDelayFn, if non-nil is a func that returns how long to wait to
+	// accumulate logs before uploading them. 0 or negative means to upload
+	// immediately.
+	//
+	// If nil, a default value is used. (currently 2 seconds)
+	FlushDelayFn func() time.Duration
+
+	// IncludeProcID, if true, results in an ephemeral process identifier being
+	// included in logs. The ID is random and not guaranteed to be globally
+	// unique, but it can be used to distinguish between different instances
+	// running with same PrivateID.
+	IncludeProcID bool
+
+	// IncludeProcSequence, if true, results in an ephemeral sequence number
+	// being included in the logs. The sequence number is incremented for each
+	// log message sent, but is not persisted across process restarts.
+	IncludeProcSequence bool
+}

+ 2 - 52
logtail/logtail.go

@@ -1,6 +1,8 @@
 // Copyright (c) Tailscale Inc & AUTHORS
 // SPDX-License-Identifier: BSD-3-Clause
 
+//go:build !ts_omit_logtail
+
 // Package logtail sends logs to log.tailscale.com.
 package logtail
 
@@ -51,58 +53,6 @@ const lowMemRatio = 4
 // but not too large to be a notable waste of memory if retained forever.
 const bufferSize = 4 << 10
 
-// DefaultHost is the default host name to upload logs to when
-// Config.BaseURL isn't provided.
-const DefaultHost = "log.tailscale.com"
-
-const defaultFlushDelay = 2 * time.Second
-
-const (
-	// CollectionNode is the name of a logtail Config.Collection
-	// for tailscaled (or equivalent: IPNExtension, Android app).
-	CollectionNode = "tailnode.log.tailscale.io"
-)
-
-type Config struct {
-	Collection     string          // collection name, a domain name
-	PrivateID      logid.PrivateID // private ID for the primary log stream
-	CopyPrivateID  logid.PrivateID // private ID for a log stream that is a superset of this log stream
-	BaseURL        string          // if empty defaults to "https://log.tailscale.com"
-	HTTPC          *http.Client    // if empty defaults to http.DefaultClient
-	SkipClientTime bool            // if true, client_time is not written to logs
-	LowMemory      bool            // if true, logtail minimizes memory use
-	Clock          tstime.Clock    // if set, Clock.Now substitutes uses of time.Now
-	Stderr         io.Writer       // if set, logs are sent here instead of os.Stderr
-	StderrLevel    int             // max verbosity level to write to stderr; 0 means the non-verbose messages only
-	Buffer         Buffer          // temp storage, if nil a MemoryBuffer
-	CompressLogs   bool            // whether to compress the log uploads
-	MaxUploadSize  int             // maximum upload size; 0 means using the default
-
-	// MetricsDelta, if non-nil, is a func that returns an encoding
-	// delta in clientmetrics to upload alongside existing logs.
-	// It can return either an empty string (for nothing) or a string
-	// that's safe to embed in a JSON string literal without further escaping.
-	MetricsDelta func() string
-
-	// FlushDelayFn, if non-nil is a func that returns how long to wait to
-	// accumulate logs before uploading them. 0 or negative means to upload
-	// immediately.
-	//
-	// If nil, a default value is used. (currently 2 seconds)
-	FlushDelayFn func() time.Duration
-
-	// IncludeProcID, if true, results in an ephemeral process identifier being
-	// included in logs. The ID is random and not guaranteed to be globally
-	// unique, but it can be used to distinguish between different instances
-	// running with same PrivateID.
-	IncludeProcID bool
-
-	// IncludeProcSequence, if true, results in an ephemeral sequence number
-	// being included in the logs. The sequence number is incremented for each
-	// log message sent, but is not persisted across process restarts.
-	IncludeProcSequence bool
-}
-
 func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
 	if cfg.BaseURL == "" {
 		cfg.BaseURL = "https://" + DefaultHost

+ 44 - 0
logtail/logtail_omit.go

@@ -0,0 +1,44 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build ts_omit_logtail
+
+package logtail
+
+import (
+	"context"
+
+	tslogger "tailscale.com/types/logger"
+	"tailscale.com/types/logid"
+)
+
+// Noop implementations of everything when ts_omit_logtail is set.
+
+type Logger struct{}
+
+type Buffer any
+
+func Disable() {}
+
+func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
+	return &Logger{}
+}
+
+func (*Logger) Write(p []byte) (n int, err error) {
+	return len(p), nil
+}
+
+func (*Logger) Logf(format string, args ...any)    {}
+func (*Logger) Shutdown(ctx context.Context) error { return nil }
+func (*Logger) SetVerbosityLevel(level int)        {}
+
+func (l *Logger) SetSockstatsLabel(label any) {}
+
+func (l *Logger) PrivateID() logid.PrivateID { return logid.PrivateID{} }
+func (l *Logger) StartFlush()                {}
+
+func RegisterLogTap(dst chan<- string) (unregister func()) {
+	return func() {}
+}
+
+func (*Logger) SetNetMon(any) {}

+ 2 - 0
wgengine/netlog/logger.go → wgengine/netlog/netlog.go

@@ -1,6 +1,8 @@
 // Copyright (c) Tailscale Inc & AUTHORS
 // SPDX-License-Identifier: BSD-3-Clause
 
+//go:build !ts_omit_netlog && !ts_omit_logtail
+
 // Package netlog provides a logger that monitors a TUN device and
 // periodically records any traffic into a log stream.
 package netlog

+ 13 - 0
wgengine/netlog/netlog_omit.go

@@ -0,0 +1,13 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build ts_omit_netlog || ts_omit_logtail
+
+package netlog
+
+type Logger struct{}
+
+func (*Logger) Startup(...any) error { return nil }
+func (*Logger) Running() bool        { return false }
+func (*Logger) Shutdown(any) error   { return nil }
+func (*Logger) ReconfigRoutes(any)   {}

+ 3 - 3
wgengine/userspace.go

@@ -962,7 +962,7 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config,
 	netLogIDsWasValid := !oldLogIDs.NodeID.IsZero() && !oldLogIDs.DomainID.IsZero()
 	netLogIDsChanged := netLogIDsNowValid && netLogIDsWasValid && newLogIDs != oldLogIDs
 	netLogRunning := netLogIDsNowValid && !routerCfg.Equal(&router.Config{})
-	if envknob.NoLogsNoSupport() {
+	if !buildfeatures.HasNetLog || envknob.NoLogsNoSupport() {
 		netLogRunning = false
 	}
 
@@ -1017,7 +1017,7 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config,
 
 	// Shutdown the network logger because the IDs changed.
 	// Let it be started back up by subsequent logic.
-	if netLogIDsChanged && e.networkLogger.Running() {
+	if buildfeatures.HasNetLog && netLogIDsChanged && e.networkLogger.Running() {
 		e.logf("wgengine: Reconfig: shutting down network logger")
 		ctx, cancel := context.WithTimeout(context.Background(), networkLoggerUploadTimeout)
 		defer cancel()
@@ -1028,7 +1028,7 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config,
 
 	// Startup the network logger.
 	// Do this before configuring the router so that we capture initial packets.
-	if netLogRunning && !e.networkLogger.Running() {
+	if buildfeatures.HasNetLog && netLogRunning && !e.networkLogger.Running() {
 		nid := cfg.NetworkLogging.NodeID
 		tid := cfg.NetworkLogging.DomainID
 		logExitFlowEnabled := cfg.NetworkLogging.LogExitFlowEnabled