github.com/telepresenceio/telepresence/v2@v2.20.0-pro.6.0.20240517030216-236ea954e789/pkg/client/logging/initcontext_test.go (about)

     1  package logging
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"log"
     7  	"os"
     8  	"path/filepath"
     9  	"strconv"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/sirupsen/logrus"
    14  	"github.com/stretchr/testify/require"
    15  
    16  	"github.com/datawire/dlib/dlog"
    17  	"github.com/datawire/dlib/dtime"
    18  	"github.com/telepresenceio/telepresence/v2/pkg/client"
    19  	"github.com/telepresenceio/telepresence/v2/pkg/filelocation"
    20  )
    21  
    22  type dtimeHook struct{}
    23  
    24  func (dtimeHook) Levels() []logrus.Level {
    25  	return logrus.AllLevels
    26  }
    27  
    28  func (dtimeHook) Fire(entry *logrus.Entry) error {
    29  	entry.Time = dtime.Now()
    30  	return nil
    31  }
    32  
    33  func TestInitContext(t *testing.T) {
    34  	const logName = "testing"
    35  
    36  	ft := dtime.NewFakeTime()
    37  
    38  	testSetup := func(t *testing.T) (ctx context.Context, logDir, logFile string) {
    39  		t.Helper()
    40  		ctx = dlog.NewTestContext(t, false)
    41  		env, err := client.LoadEnv()
    42  		if err != nil {
    43  			t.Fatal(err)
    44  		}
    45  		ctx = client.WithEnv(ctx, env)
    46  
    47  		// Ensure that we use a temporary log dir
    48  		logDir = t.TempDir()
    49  		ctx = filelocation.WithAppUserLogDir(ctx, logDir)
    50  
    51  		cfg, err := client.LoadConfig(ctx)
    52  		require.NoError(t, err)
    53  		ctx = client.WithConfig(ctx, cfg)
    54  
    55  		// Ensure that we never consider Stdout to be a terminal
    56  		saveIsTerminal := IsTerminal
    57  		IsTerminal = func(int) bool { return false }
    58  		t.Cleanup(func() { IsTerminal = saveIsTerminal })
    59  
    60  		// Use ft
    61  		dtime.SetNow(ft.Now)
    62  		t.Cleanup(func() { dtime.SetNow(time.Now) })
    63  
    64  		// InitContext overrides both file descriptors 1/2 and the variables
    65  		// os.Stdout/os.Stdin; so they need to be backed up and restored.
    66  		saveStdout := os.Stdout
    67  		saveStderr := os.Stderr
    68  		restoreStd, err := dupStd()
    69  		require.NoError(t, err)
    70  		t.Cleanup(func() {
    71  			os.Stdout = saveStdout
    72  			os.Stderr = saveStderr
    73  			restoreStd()
    74  		})
    75  
    76  		return ctx, logDir, filepath.Join(logDir, logName+".log")
    77  	}
    78  
    79  	closeLog := func(t *testing.T) {
    80  		t.Helper()
    81  		check := require.New(t)
    82  		check.IsType(&RotatingFile{}, loggerForTest.Out)
    83  		check.NoError(loggerForTest.Out.(*RotatingFile).Close())
    84  	}
    85  
    86  	t.Run("stdout and stderr", func(t *testing.T) {
    87  		ctx, _, logFile := testSetup(t)
    88  		check := require.New(t)
    89  
    90  		c, err := InitContext(ctx, logName, NewRotateOnce(), true)
    91  		loggerForTest.AddHook(&dtimeHook{})
    92  		check.NoError(err)
    93  		check.NotNil(c)
    94  		defer closeLog(t)
    95  
    96  		require.FileExists(t, logFile)
    97  
    98  		infoMsg := "info"
    99  		fmt.Fprintln(os.Stdout, infoMsg)
   100  		time.Sleep(10 * time.Millisecond) // Ensure that message is logged before the next is produced
   101  
   102  		ft.Step(time.Second)
   103  		errMsg := "error"
   104  		fmt.Fprintln(os.Stderr, errMsg)
   105  		time.Sleep(30 * time.Millisecond)
   106  
   107  		bs, err := os.ReadFile(logFile)
   108  		check.NoError(err)
   109  		s := string(bs)
   110  		check.Contains(s, infoMsg)
   111  		check.Contains(s, errMsg)
   112  	})
   113  
   114  	t.Run("captures output of builtin functions", func(t *testing.T) {
   115  		ctx, _, logFile := testSetup(t)
   116  		check := require.New(t)
   117  
   118  		c, err := InitContext(ctx, logName, NewRotateOnce(), true)
   119  		loggerForTest.AddHook(&dtimeHook{})
   120  		check.NoError(err)
   121  		check.NotNil(c)
   122  		defer closeLog(t)
   123  
   124  		msg := "some message"
   125  		println(msg)
   126  		check.FileExists(logFile)
   127  		time.Sleep(30 * time.Millisecond)
   128  		bs, err := os.ReadFile(logFile)
   129  		check.NoError(err)
   130  		check.Contains(string(bs), msg)
   131  	})
   132  
   133  	t.Run("captures output of standard logger", func(t *testing.T) {
   134  		ctx, _, logFile := testSetup(t)
   135  		check := require.New(t)
   136  
   137  		c, err := InitContext(ctx, logName, NewRotateOnce(), true)
   138  		loggerForTest.AddHook(&dtimeHook{})
   139  		check.NoError(err)
   140  		check.NotNil(c)
   141  		defer closeLog(t)
   142  
   143  		msg := "some message"
   144  		log.Print(msg)
   145  		time.Sleep(100 * time.Millisecond)
   146  		check.FileExists(logFile)
   147  
   148  		bs, err := os.ReadFile(logFile)
   149  		check.NoError(err)
   150  		check.Contains(string(bs), fmt.Sprintf("info    stdlog : %s\n", msg))
   151  	})
   152  
   153  	t.Run("next session rotates on write", func(t *testing.T) {
   154  		ctx, logDir, logFile := testSetup(t)
   155  		check := require.New(t)
   156  
   157  		c, err := InitContext(ctx, logName, NewRotateOnce(), true)
   158  		loggerForTest.AddHook(&dtimeHook{})
   159  		check.NoError(err)
   160  		check.NotNil(c)
   161  		infoMsg := "info message"
   162  		dlog.Info(c, infoMsg)
   163  		closeLog(t)
   164  		ft.Step(time.Second)
   165  
   166  		c, err = InitContext(ctx, logName, NewRotateOnce(), true)
   167  		loggerForTest.AddHook(&dtimeHook{})
   168  		check.NoError(err)
   169  		check.NotNil(c)
   170  		dlog.Info(c, infoMsg)
   171  		check.FileExists(logFile)
   172  		defer closeLog(t)
   173  
   174  		infoTs := dtime.Now().Format("2006-01-02 15:04:05.0000")
   175  		backupFile := filepath.Join(logDir, fmt.Sprintf("%s-%s.log", logName, dtime.Now().Format("20060102T150405")))
   176  		check.FileExists(backupFile)
   177  
   178  		bs, err := os.ReadFile(logFile)
   179  		check.NoError(err)
   180  		check.Contains(string(bs), fmt.Sprintf("%s info    %s\n", infoTs, infoMsg))
   181  	})
   182  
   183  	t.Run("birthtime updates after rotate", func(t *testing.T) {
   184  		ctx, _, _ := testSetup(t)
   185  		check := require.New(t)
   186  
   187  		c, err := InitContext(ctx, logName, NewRotateOnce(), true)
   188  		loggerForTest.AddHook(&dtimeHook{})
   189  		check.NoError(err)
   190  		check.NotNil(c)
   191  		dlog.Info(c, "info message")
   192  		bt1 := loggerForTest.Out.(*RotatingFile).birthTime
   193  		closeLog(t)
   194  
   195  		c, err = InitContext(ctx, logName, NewRotateOnce(), true)
   196  		loggerForTest.AddHook(&dtimeHook{})
   197  		check.NoError(err)
   198  		check.NotNil(c)
   199  		dlog.Info(c, "info message")
   200  		bt2 := loggerForTest.Out.(*RotatingFile).birthTime
   201  		closeLog(t)
   202  		check.Equal(bt1, bt2)
   203  	})
   204  
   205  	t.Run("next session appends when no rotate", func(t *testing.T) {
   206  		ctx, _, logFile := testSetup(t)
   207  		check := require.New(t)
   208  
   209  		c, err := InitContext(ctx, logName, RotateNever, true)
   210  		loggerForTest.AddHook(&dtimeHook{})
   211  		check.NoError(err)
   212  		check.NotNil(c)
   213  		infoMsg1 := "info message 1"
   214  		dlog.Info(c, infoMsg1)
   215  		closeLog(t)
   216  
   217  		c, err = InitContext(ctx, logName, RotateNever, true)
   218  		loggerForTest.AddHook(&dtimeHook{})
   219  		check.NoError(err)
   220  		check.NotNil(c)
   221  		infoMsg2 := "info message 2"
   222  		dlog.Info(c, infoMsg2)
   223  		closeLog(t)
   224  
   225  		bs, err := os.ReadFile(logFile)
   226  		check.NoError(err)
   227  		infoTs := dtime.Now().Format("2006-01-02 15:04:05.0000")
   228  		check.Contains(string(bs), fmt.Sprintf("%s info    %s\n", infoTs, infoMsg1))
   229  		check.Contains(string(bs), fmt.Sprintf("%s info    %s\n", infoTs, infoMsg2))
   230  	})
   231  
   232  	t.Run("old files are removed", func(t *testing.T) {
   233  		ctx, logDir, _ := testSetup(t)
   234  		check := require.New(t)
   235  
   236  		maxFiles := 5
   237  		if me := os.Getenv("TELEPRESENCE_MAX_LOGFILES"); me != "" {
   238  			if mx, err := strconv.Atoi(me); err == nil && mx >= 0 {
   239  				maxFiles = mx
   240  			}
   241  		}
   242  		for i := 0; i < maxFiles+2; i++ {
   243  			ft.Step(24 * time.Hour)
   244  			c, err := InitContext(ctx, logName, NewRotateOnce(), true)
   245  			loggerForTest.AddHook(&dtimeHook{})
   246  			check.NoError(err)
   247  			check.NotNil(c)
   248  			infoMsg := "info message"
   249  			dlog.Info(c, infoMsg)
   250  			closeLog(t)
   251  		}
   252  		// Give file remover some time to finish
   253  		time.Sleep(100 * time.Millisecond)
   254  
   255  		files, err := os.ReadDir(logDir)
   256  		check.NoError(err)
   257  		check.Equal(maxFiles, len(files))
   258  	})
   259  }