From 702d45b7e5619530c5908e22fec18aab328c632d Mon Sep 17 00:00:00 2001 From: "Theodor S. Midtlien" Date: Thu, 21 May 2026 19:05:16 +0200 Subject: [PATCH] Add logging tests --- util/log_test.go | 96 ++++++++++++++++++++++++++++++++++++ util/logrotate_linux.go | 14 ++++-- util/logrotate_linux_test.go | 95 +++++++++++++++++++++++++++++++++++ 3 files changed, 200 insertions(+), 5 deletions(-) create mode 100644 util/log_test.go create mode 100644 util/logrotate_linux_test.go diff --git a/util/log_test.go b/util/log_test.go new file mode 100644 index 000000000..e9933e479 --- /dev/null +++ b/util/log_test.go @@ -0,0 +1,96 @@ +package util + +import ( + "io" + "os" + "path/filepath" + "strings" + "testing" + "time" + + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" +) + +// TestSetupLogFile_RotatesOnSize drives >MaxSize bytes through the writer +// returned by setupLogFile and asserts a backup file appears. +func TestSetupLogFile_RotatesOnSize(t *testing.T) { + t.Setenv("NB_LOG_MAX_SIZE_MB", "1") + + dir := t.TempDir() + logPath := filepath.Join(dir, "netbird.log") + + w, err := setupLogFile(logPath, false) + require.NoError(t, err) + t.Cleanup(func() { + if c, ok := w.(io.Closer); ok { + _ = c.Close() + } + }) + + chunk := []byte(strings.Repeat("x", 64*1024) + "\n") + for range 20 { + _, err := w.Write(chunk) + require.NoError(t, err) + } + + info, err := os.Stat(logPath) + require.NoError(t, err) + require.Less(t, info.Size(), int64(1<<20), + "active log should be < 1 MB after rotation, got %d", info.Size()) + + require.Eventually(t, func() bool { + entries, _ := os.ReadDir(dir) + for _, e := range entries { + name := e.Name() + if name == filepath.Base(logPath) { + continue + } + if strings.HasPrefix(name, "netbird-") && strings.HasSuffix(name, ".log.gz") { + return true + } + } + return false + }, 5*time.Second, 50*time.Millisecond, "expected a rotated backup file in %s", dir) +} + +// TestSetupLogFile_RotationDisabled verifies that with rotation off, the file +// grows past MaxSize and no backups are created. +func TestSetupLogFile_RotationDisabled(t *testing.T) { + t.Setenv("NB_LOG_MAX_SIZE_MB", "1") + + dir := t.TempDir() + logPath := filepath.Join(dir, "netbird.log") + + w, err := setupLogFile(logPath, true) + require.NoError(t, err) + + f, ok := w.(*os.File) + require.True(t, ok, "expected plain *os.File when rotation is disabled, got %T", w) + t.Cleanup(func() { _ = f.Close() }) + + chunk := []byte(strings.Repeat("y", 64*1024) + "\n") + for range 20 { + _, err := w.Write(chunk) + require.NoError(t, err) + } + + info, err := os.Stat(logPath) + require.NoError(t, err) + require.GreaterOrEqual(t, info.Size(), int64(1<<20), + "file should exceed MaxSize when rotation is disabled, got %d", info.Size()) + + entries, err := os.ReadDir(dir) + require.NoError(t, err) + require.Len(t, entries, 1, "no backup files should exist when rotation is disabled, got %v", entries) +} + +// TestIsRotationDisabled_EnvFlag covers the NB_LOG_DISABLE_ROTATION env path. +// The logrotate-conflict branch is exercised separately on linux. +func TestIsRotationDisabled_EnvFlag(t *testing.T) { + logger := log.New() + logger.SetOutput(io.Discard) + + t.Setenv("NB_LOG_DISABLE_ROTATION", "true") + require.True(t, isRotationDisabled(logger)) +} diff --git a/util/logrotate_linux.go b/util/logrotate_linux.go index 17858db7b..7d2173ea8 100644 --- a/util/logrotate_linux.go +++ b/util/logrotate_linux.go @@ -23,7 +23,11 @@ const ( // indications of conflict with netbird. It returns true and the config file // path if a conflict was found. func FindFirstLogrotateConflict() (bool, string) { - for _, f := range listLogrotateConfigs() { + return findFirstLogrotateConflictIn(defaultLogrotateConfPath, defaultLogrotateConfDir) +} + +func findFirstLogrotateConflictIn(confPath, confDir string) (bool, string) { + for _, f := range listLogrotateConfigs(confPath, confDir) { present, err := scanLogrotateFile(f, netbirdString) if err != nil { if !errors.Is(err, fs.ErrNotExist) { @@ -39,9 +43,9 @@ func FindFirstLogrotateConflict() (bool, string) { } // listLogrotateConfigs returns all config files for logrotate. -func listLogrotateConfigs() []string { - files := []string{defaultLogrotateConfPath} - entries, err := os.ReadDir(defaultLogrotateConfDir) +func listLogrotateConfigs(confPath, confDir string) []string { + files := []string{confPath} + entries, err := os.ReadDir(confDir) if err != nil { return files } @@ -49,7 +53,7 @@ func listLogrotateConfigs() []string { if e.IsDir() { continue } - files = append(files, filepath.Join(defaultLogrotateConfDir, e.Name())) + files = append(files, filepath.Join(confDir, e.Name())) } return files } diff --git a/util/logrotate_linux_test.go b/util/logrotate_linux_test.go new file mode 100644 index 000000000..92d7e410b --- /dev/null +++ b/util/logrotate_linux_test.go @@ -0,0 +1,95 @@ +//go:build linux + +package util + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestFindFirstLogrotateConflict(t *testing.T) { + t.Run("conflict in confDir", func(t *testing.T) { + confPath, confDir := newLogrotateLayout(t) + conflictPath := filepath.Join(confDir, "netbird") + writeLogrotateConfig(t, conflictPath, `/var/log/netbird/*.log { + daily + rotate 7 +}`) + writeLogrotateConfig(t, filepath.Join(confDir, "nginx"), `/var/log/nginx/*.log { daily }`) + + got, path := findFirstLogrotateConflictIn(confPath, confDir) + require.True(t, got) + require.Equal(t, conflictPath, path) + }) + + t.Run("conflict in main conf file", func(t *testing.T) { + confPath, confDir := newLogrotateLayout(t) + writeLogrotateConfig(t, confPath, `weekly +rotate 4 +include /etc/logrotate.d +/var/log/netbird/client.log { rotate 5 }`) + + got, path := findFirstLogrotateConflictIn(confPath, confDir) + require.True(t, got) + require.Equal(t, confPath, path) + }) + + t.Run("no conflict when netbird is absent", func(t *testing.T) { + confPath, confDir := newLogrotateLayout(t) + writeLogrotateConfig(t, filepath.Join(confDir, "nginx"), `/var/log/nginx/*.log { daily }`) + writeLogrotateConfig(t, filepath.Join(confDir, "syslog"), `/var/log/syslog { weekly }`) + + got, path := findFirstLogrotateConflictIn(confPath, confDir) + require.False(t, got) + require.Empty(t, path) + }) + + t.Run("commented-out netbird line is ignored", func(t *testing.T) { + confPath, confDir := newLogrotateLayout(t) + writeLogrotateConfig(t, filepath.Join(confDir, "misc"), `# /var/log/netbird/*.log { daily } +/var/log/other.log { weekly }`) + + got, path := findFirstLogrotateConflictIn(confPath, confDir) + require.False(t, got) + require.Empty(t, path) + }) + + t.Run("subdirectories in confDir are ignored", func(t *testing.T) { + confPath, confDir := newLogrotateLayout(t) + sub := filepath.Join(confDir, "nested") + require.NoError(t, os.MkdirAll(sub, 0o755)) + writeLogrotateConfig(t, filepath.Join(sub, "netbird"), `/var/log/netbird/*.log { daily }`) + + got, path := findFirstLogrotateConflictIn(confPath, confDir) + require.False(t, got) + require.Empty(t, path) + }) + + t.Run("missing paths return no conflict", func(t *testing.T) { + dir := t.TempDir() + got, path := findFirstLogrotateConflictIn( + filepath.Join(dir, "does-not-exist.conf"), + filepath.Join(dir, "does-not-exist.d"), + ) + require.False(t, got) + require.Empty(t, path) + }) +} + +// newLogrotateLayout creates a temp logrotate.conf path and logrotate.d dir, +// returning their paths. The conf file itself is not created. +func newLogrotateLayout(t *testing.T) (confPath, confDir string) { + t.Helper() + root := t.TempDir() + confDir = filepath.Join(root, "logrotate.d") + require.NoError(t, os.MkdirAll(confDir, 0o755)) + return filepath.Join(root, "logrotate.conf"), confDir +} + +func writeLogrotateConfig(t *testing.T, path, body string) { + t.Helper() + require.NoError(t, os.WriteFile(path, []byte(body), 0o644)) +}