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 }