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 <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2025-09-28 10:57:22 -07:00 committed by Brad Fitzpatrick
parent e466488a2a
commit 11b770fbc9
19 changed files with 240 additions and 77 deletions

View File

@ -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+

View File

@ -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())

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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()

View File

@ -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 {

View File

@ -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 {

View File

@ -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
}

View File

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

65
logtail/config.go Normal file
View File

@ -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
}

View File

@ -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
logtail/logtail_omit.go Normal file
View File

@ -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) {}

View File

@ -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

View File

@ -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) {}

View File

@ -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