// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package logmon import ( "crypto/rand" "fmt" "os" "path/filepath" "runtime" "testing" "github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/client/lib/fifo" "github.com/hashicorp/nomad/helper/testlog" "github.com/hashicorp/nomad/helper/uuid" "github.com/hashicorp/nomad/testutil" "github.com/stretchr/testify/require" ) func TestLogmon_Start_rotate(t *testing.T) { ci.Parallel(t) require := require.New(t) var stdoutFifoPath, stderrFifoPath string dir := t.TempDir() if runtime.GOOS == "windows" { stdoutFifoPath = "//./pipe/test-rotate.stdout" stderrFifoPath = "//./pipe/test-rotate.stderr" } else { stdoutFifoPath = filepath.Join(dir, "stdout.fifo") stderrFifoPath = filepath.Join(dir, "stderr.fifo") } cfg := &LogConfig{ LogDir: dir, StdoutLogFile: "stdout", StdoutFifo: stdoutFifoPath, StderrLogFile: "stderr", StderrFifo: stderrFifoPath, MaxFiles: 2, MaxFileSizeMB: 1, } lm := NewLogMon(testlog.HCLogger(t)) require.NoError(lm.Start(cfg)) stdout, err := fifo.OpenWriter(stdoutFifoPath) require.NoError(err) // Write enough bytes such that the log is rotated bytes1MB := make([]byte, 1024*1024) _, err = rand.Read(bytes1MB) require.NoError(err) _, err = stdout.Write(bytes1MB) require.NoError(err) testutil.WaitForResult(func() (bool, error) { _, err = os.Stat(filepath.Join(dir, "stdout.0")) return err == nil, err }, func(err error) { require.NoError(err) }) testutil.WaitForResult(func() (bool, error) { _, err = os.Stat(filepath.Join(dir, "stdout.1")) return err == nil, err }, func(err error) { require.NoError(err) }) _, err = os.Stat(filepath.Join(dir, "stdout.2")) require.Error(err) require.NoError(lm.Stop()) require.NoError(lm.Stop()) } // asserts that calling Start twice restarts the log rotator and that any logs // published while the listener was unavailable are received. func TestLogmon_Start_restart_flusheslogs(t *testing.T) { ci.Parallel(t) if runtime.GOOS == "windows" { t.Skip("windows does not support pushing data to a pipe with no servers") } require := require.New(t) var stdoutFifoPath, stderrFifoPath string dir := t.TempDir() if runtime.GOOS == "windows" { stdoutFifoPath = "//./pipe/test-restart.stdout" stderrFifoPath = "//./pipe/test-restart.stderr" } else { stdoutFifoPath = filepath.Join(dir, "stdout.fifo") stderrFifoPath = filepath.Join(dir, "stderr.fifo") } cfg := &LogConfig{ LogDir: dir, StdoutLogFile: "stdout", StdoutFifo: stdoutFifoPath, StderrLogFile: "stderr", StderrFifo: stderrFifoPath, MaxFiles: 2, MaxFileSizeMB: 1, } lm := NewLogMon(testlog.HCLogger(t)) impl, ok := lm.(*logmonImpl) require.True(ok) require.NoError(lm.Start(cfg)) stdout, err := fifo.OpenWriter(stdoutFifoPath) require.NoError(err) stderr, err := fifo.OpenWriter(stderrFifoPath) require.NoError(err) // Write a string and assert it was written to the file _, err = stdout.Write([]byte("test\n")) require.NoError(err) testutil.WaitForResult(func() (bool, error) { raw, err := os.ReadFile(filepath.Join(dir, "stdout.0")) if err != nil { return false, err } return "test\n" == string(raw), fmt.Errorf("unexpected stdout %q", string(raw)) }, func(err error) { require.NoError(err) }) require.True(impl.tl.IsRunning()) // Close stdout and assert that logmon no longer writes to the file require.NoError(stdout.Close()) require.NoError(stderr.Close()) testutil.WaitForResult(func() (bool, error) { return !impl.tl.IsRunning(), fmt.Errorf("logmon is still running") }, func(err error) { require.NoError(err) }) stdout, err = fifo.OpenWriter(stdoutFifoPath) require.NoError(err) stderr, err = fifo.OpenWriter(stderrFifoPath) require.NoError(err) _, err = stdout.Write([]byte("te")) require.NoError(err) testutil.WaitForResult(func() (bool, error) { raw, err := os.ReadFile(filepath.Join(dir, "stdout.0")) if err != nil { return false, err } return "test\n" == string(raw), fmt.Errorf("unexpected stdout %q", string(raw)) }, func(err error) { require.NoError(err) }) // Start logmon again and assert that it appended to the file require.NoError(lm.Start(cfg)) stdout, err = fifo.OpenWriter(stdoutFifoPath) require.NoError(err) stderr, err = fifo.OpenWriter(stderrFifoPath) require.NoError(err) _, err = stdout.Write([]byte("st\n")) require.NoError(err) testutil.WaitForResult(func() (bool, error) { raw, err := os.ReadFile(filepath.Join(dir, "stdout.0")) if err != nil { return false, err } expected := "test\ntest\n" == string(raw) return expected, fmt.Errorf("unexpected stdout %q", string(raw)) }, func(err error) { require.NoError(err) }) } // asserts that calling Start twice restarts the log rotator func TestLogmon_Start_restart(t *testing.T) { ci.Parallel(t) require := require.New(t) var stdoutFifoPath, stderrFifoPath string dir := t.TempDir() if runtime.GOOS == "windows" { stdoutFifoPath = "//./pipe/test-restart.stdout" stderrFifoPath = "//./pipe/test-restart.stderr" } else { stdoutFifoPath = filepath.Join(dir, "stdout.fifo") stderrFifoPath = filepath.Join(dir, "stderr.fifo") } cfg := &LogConfig{ LogDir: dir, StdoutLogFile: "stdout", StdoutFifo: stdoutFifoPath, StderrLogFile: "stderr", StderrFifo: stderrFifoPath, MaxFiles: 2, MaxFileSizeMB: 1, } lm := NewLogMon(testlog.HCLogger(t)) impl, ok := lm.(*logmonImpl) require.True(ok) require.NoError(lm.Start(cfg)) t.Cleanup(func() { require.NoError(lm.Stop()) }) stdout, err := fifo.OpenWriter(stdoutFifoPath) require.NoError(err) stderr, err := fifo.OpenWriter(stderrFifoPath) require.NoError(err) // Write a string and assert it was written to the file _, err = stdout.Write([]byte("test\n")) require.NoError(err) testutil.WaitForResult(func() (bool, error) { raw, err := os.ReadFile(filepath.Join(dir, "stdout.0")) if err != nil { return false, err } return "test\n" == string(raw), fmt.Errorf("unexpected stdout %q", string(raw)) }, func(err error) { require.NoError(err) }) require.True(impl.tl.IsRunning()) // Close stderr and assert that logmon no longer writes to the file // Keep stdout open to ensure that IsRunning requires both require.NoError(stderr.Close()) testutil.WaitForResult(func() (bool, error) { return !impl.tl.IsRunning(), fmt.Errorf("logmon is still running") }, func(err error) { require.NoError(err) }) // Start logmon again and assert that it can receive logs again require.NoError(lm.Start(cfg)) stdout, err = fifo.OpenWriter(stdoutFifoPath) require.NoError(err) t.Cleanup(func() { require.NoError(stdout.Close()) }) stderr, err = fifo.OpenWriter(stderrFifoPath) require.NoError(err) t.Cleanup(func() { require.NoError(stderr.Close()) }) _, err = stdout.Write([]byte("test\n")) require.NoError(err) testutil.WaitForResult(func() (bool, error) { raw, err := os.ReadFile(filepath.Join(dir, "stdout.0")) if err != nil { return false, err } expected := "test\ntest\n" == string(raw) return expected, fmt.Errorf("unexpected stdout %q", string(raw)) }, func(err error) { require.NoError(err) }) } // panicWriter panics on use type panicWriter struct{} func (panicWriter) Write([]byte) (int, error) { panic("should not be called") } func (panicWriter) Close() error { panic("should not be called") } // TestLogmon_NewError asserts that newLogRotatorWrapper will return an error // if its unable to create the necessray files. func TestLogmon_NewError(t *testing.T) { ci.Parallel(t) // Pick a path that does not exist path := filepath.Join(uuid.Generate(), uuid.Generate(), uuid.Generate()) logger := testlog.HCLogger(t) // No code that uses the writer should get hit rotator := panicWriter{} w, err := newLogRotatorWrapper(path, logger, rotator) require.Error(t, err) require.Nil(t, w) }