trpc.group/trpc-go/trpc-go@v1.0.2/log/rollwriter/roll_writer_test.go (about)

     1  //
     2  //
     3  // Tencent is pleased to support the open source community by making tRPC available.
     4  //
     5  // Copyright (C) 2023 THL A29 Limited, a Tencent company.
     6  // All rights reserved.
     7  //
     8  // If you have downloaded a copy of the tRPC source code from Tencent,
     9  // please note that tRPC source code is licensed under the  Apache 2.0 License,
    10  // A copy of the Apache 2.0 License is included in this file.
    11  //
    12  //
    13  
    14  package rollwriter
    15  
    16  import (
    17  	"errors"
    18  	"fmt"
    19  	"io/fs"
    20  	"log"
    21  	"os"
    22  	"os/exec"
    23  	"path"
    24  	"path/filepath"
    25  	"runtime"
    26  	"strings"
    27  	"sync"
    28  	"testing"
    29  	"time"
    30  
    31  	"github.com/stretchr/testify/assert"
    32  	"github.com/stretchr/testify/require"
    33  	"go.uber.org/zap"
    34  	"go.uber.org/zap/zapcore"
    35  )
    36  
    37  // functional test
    38  // go test -v -cover
    39  // benchmark test
    40  // go test -bench=. -benchtime=20s -run=Bench
    41  
    42  const (
    43  	testTimes    = 100000
    44  	testRoutines = 256
    45  )
    46  
    47  func TestRollWriter(t *testing.T) {
    48  
    49  	// empty file name.
    50  	t.Run("empty_log_name", func(t *testing.T) {
    51  		logDir := t.TempDir()
    52  		_, err := NewRollWriter("")
    53  		assert.Error(t, err, "NewRollWriter: invalid log path")
    54  
    55  		// print log file list.
    56  		printLogFiles(logDir)
    57  	})
    58  
    59  	// no rolling.
    60  	t.Run("roll_by_default", func(t *testing.T) {
    61  		logDir := t.TempDir()
    62  		logName := "test.log"
    63  		w, err := NewRollWriter(filepath.Join(logDir, logName))
    64  		assert.NoError(t, err, "NewRollWriter: create logger ok")
    65  		log.SetOutput(w)
    66  		for i := 0; i < testTimes; i++ {
    67  			log.Printf("this is a test log: %d\n", i)
    68  		}
    69  
    70  		// check number of rolling log files(current files + backup files).
    71  		time.Sleep(20 * time.Millisecond)
    72  		logFiles := getLogBackups(logDir, logName)
    73  		if len(logFiles) != 1 {
    74  			t.Errorf("Number of log backup files should be 1")
    75  		}
    76  		require.Nil(t, w.Close())
    77  
    78  		// print log file list.
    79  		printLogFiles(logDir)
    80  	})
    81  
    82  	// roll by size.
    83  	t.Run("roll_by_size", func(t *testing.T) {
    84  		logDir := t.TempDir()
    85  		logName := "test_size.log"
    86  		const (
    87  			maxBackup = 2
    88  			maxSize   = 1
    89  			maxAge    = 1
    90  		)
    91  		w, err := NewRollWriter(filepath.Join(logDir, logName),
    92  			WithMaxSize(maxSize),
    93  			WithMaxAge(maxAge),
    94  			WithMaxBackups(maxBackup),
    95  		)
    96  		assert.NoError(t, err, "NewRollWriter: create logger ok")
    97  		log.SetOutput(w)
    98  		for i := 0; i < testTimes; i++ {
    99  			log.Printf("this is a test log: %d\n", i)
   100  		}
   101  
   102  		w.notify()
   103  		// check number of rolling log files.
   104  		var logFiles []os.FileInfo
   105  		require.Eventuallyf(t,
   106  			func() bool {
   107  				logFiles = getLogBackups(logDir, logName)
   108  				return len(logFiles) == maxBackup+1
   109  			},
   110  			5*time.Second,
   111  			time.Second,
   112  			"Number of log files should be %d, current: %d, %+v",
   113  			maxBackup+1, len(logFiles), func() []string {
   114  				names := make([]string, 0, len(logFiles))
   115  				for _, f := range logFiles {
   116  					names = append(names, f.Name())
   117  				}
   118  				return names
   119  			}(),
   120  		)
   121  
   122  		// check rolling log file size(allow to exceed a little).
   123  		for _, file := range logFiles {
   124  			if file.Size() > 1*1024*1024+1024 {
   125  				t.Errorf("Log file size exceeds max_size")
   126  			}
   127  		}
   128  		require.Nil(t, w.Close())
   129  
   130  		// print log file list.
   131  		printLogFiles(logDir)
   132  	})
   133  
   134  	// rolling by time.
   135  	t.Run("roll_by_time", func(t *testing.T) {
   136  		logDir := t.TempDir()
   137  		logName := "test_time.log"
   138  		const (
   139  			maxBackup = 3
   140  			maxSize   = 1
   141  			maxAge    = 1
   142  		)
   143  		w, err := NewRollWriter(filepath.Join(logDir, logName),
   144  			WithRotationTime(".%Y%m%d"),
   145  			WithMaxSize(maxSize),
   146  			WithMaxAge(maxAge),
   147  			WithMaxBackups(maxBackup),
   148  			WithCompress(true),
   149  		)
   150  		assert.NoError(t, err, "NewRollWriter: create logger ok")
   151  		log.SetOutput(w)
   152  		for i := 0; i < testTimes; i++ {
   153  			log.Printf("this is a test log: %d\n", i)
   154  		}
   155  
   156  		w.notify()
   157  		// check number of rolling log files.
   158  		var logFiles []os.FileInfo
   159  		require.Eventuallyf(t,
   160  			func() bool {
   161  				logFiles = getLogBackups(logDir, logName)
   162  				return len(logFiles) == maxBackup+1
   163  			},
   164  			5*time.Second,
   165  			time.Second,
   166  			"Number of log files should be %d, current: %d, %+v",
   167  			maxBackup+1, len(logFiles), func() []string {
   168  				names := make([]string, 0, len(logFiles))
   169  				for _, f := range logFiles {
   170  					names = append(names, f.Name())
   171  				}
   172  				return names
   173  			}(),
   174  		)
   175  
   176  		// check rolling log file size(allow to exceed a little).
   177  		for _, file := range logFiles {
   178  			if file.Size() > 1*1024*1024+1024 {
   179  				t.Errorf("Log file size exceeds max_size")
   180  			}
   181  		}
   182  
   183  		// check number of compressed files.
   184  		compressFileNum := 0
   185  		for _, file := range logFiles {
   186  			if strings.HasSuffix(file.Name(), compressSuffix) {
   187  				compressFileNum++
   188  			}
   189  		}
   190  		if compressFileNum != 3 {
   191  			t.Errorf("Number of compress log files should be 3")
   192  		}
   193  		require.Nil(t, w.Close())
   194  
   195  		// print log file list.
   196  		printLogFiles(logDir)
   197  	})
   198  }
   199  
   200  func TestAsyncRollWriter(t *testing.T) {
   201  	logDir := t.TempDir()
   202  	const flushThreshold = 4 * 1024
   203  
   204  	// no rolling(asynchronous mod)
   205  	t.Run("roll_by_default_async", func(t *testing.T) {
   206  		logName := "test.log"
   207  		w, err := NewRollWriter(filepath.Join(logDir, logName))
   208  		assert.NoError(t, err, "NewRollWriter: create logger ok")
   209  
   210  		asyncWriter := NewAsyncRollWriter(w, WithLogQueueSize(10), WithWriteLogSize(1024),
   211  			WithWriteLogInterval(100), WithDropLog(true))
   212  		log.SetOutput(asyncWriter)
   213  		for i := 0; i < testTimes; i++ {
   214  			log.Printf("this is a test log: %d\n", i)
   215  		}
   216  
   217  		// check number of rolling log files.
   218  		time.Sleep(20 * time.Millisecond)
   219  		logFiles := getLogBackups(logDir, logName)
   220  		if len(logFiles) != 1 {
   221  			t.Errorf("Number of log backup files should be 1")
   222  		}
   223  		require.Nil(t, asyncWriter.Close())
   224  	})
   225  
   226  	// rolling by size(asynchronous mod)
   227  	t.Run("roll_by_size_async", func(t *testing.T) {
   228  		logName := "test_size.log"
   229  		w, err := NewRollWriter(filepath.Join(logDir, logName),
   230  			WithMaxSize(1),
   231  			WithMaxAge(1),
   232  		)
   233  		assert.NoError(t, err, "NewRollWriter: create logger ok")
   234  
   235  		asyncWriter := NewAsyncRollWriter(w, WithWriteLogSize(flushThreshold))
   236  		log.SetOutput(asyncWriter)
   237  		for i := 0; i < testTimes; i++ {
   238  			log.Printf("this is a test log: %d\n", i)
   239  		}
   240  
   241  		// check number of rolling log files.
   242  		time.Sleep(200 * time.Millisecond)
   243  		logFiles := getLogBackups(logDir, logName)
   244  		if len(logFiles) != 5 {
   245  			t.Errorf("Number of log backup files should be 5")
   246  		}
   247  
   248  		// check rolling log file size(asynchronous mod, may exceed 4K at most)
   249  		for _, file := range logFiles {
   250  			if file.Size() > 1*1024*1024+flushThreshold*2 {
   251  				t.Errorf("Log file size exceeds max_size")
   252  			}
   253  		}
   254  		require.Nil(t, asyncWriter.Close())
   255  	})
   256  
   257  	// rolling by time(asynchronous mod)
   258  	t.Run("roll_by_time_async", func(t *testing.T) {
   259  		logName := "test_time.log"
   260  		w, err := NewRollWriter(filepath.Join(logDir, logName),
   261  			WithRotationTime(".%Y%m%d"),
   262  			WithMaxSize(1),
   263  			WithMaxAge(1),
   264  			WithCompress(true),
   265  		)
   266  		assert.NoError(t, err, "NewRollWriter: create logger ok")
   267  
   268  		asyncWriter := NewAsyncRollWriter(w, WithWriteLogSize(flushThreshold))
   269  		log.SetOutput(asyncWriter)
   270  		for i := 0; i < testTimes; i++ {
   271  			log.Printf("this is a test log: %d\n", i)
   272  		}
   273  		require.Nil(t, asyncWriter.Sync())
   274  
   275  		// check number of rolling log files.
   276  		time.Sleep(200 * time.Millisecond)
   277  		logFiles := getLogBackups(logDir, logName)
   278  		if len(logFiles) != 5 {
   279  			t.Errorf("Number of log files should be 5, current: %d", len(logFiles))
   280  		}
   281  
   282  		// check rolling log file size(asynchronous, may exceed 4K at most)
   283  		for _, file := range logFiles {
   284  			if file.Size() > 1*1024*1024+flushThreshold*2 {
   285  				t.Errorf("Log file size exceeds max_size")
   286  			}
   287  		}
   288  
   289  		// number of compressed files.
   290  		compressFileNum := 0
   291  		for _, file := range logFiles {
   292  			if strings.HasSuffix(file.Name(), compressSuffix) {
   293  				compressFileNum++
   294  			}
   295  		}
   296  		if compressFileNum != 4 {
   297  			t.Errorf("Number of compress log files should be 4")
   298  		}
   299  		require.Nil(t, asyncWriter.Close())
   300  	})
   301  
   302  	// wait 1 second.
   303  	time.Sleep(1 * time.Second)
   304  
   305  	// print log file list.
   306  	printLogFiles(logDir)
   307  }
   308  
   309  func TestRollWriterRace(t *testing.T) {
   310  	logDir := t.TempDir()
   311  
   312  	writer, err := NewRollWriter(
   313  		filepath.Join(logDir, "test.log"),
   314  		WithRotationTime(".%Y%m%d"),
   315  	)
   316  	require.Nil(t, err)
   317  	writer.opts.MaxSize = 1
   318  
   319  	wg := sync.WaitGroup{}
   320  	wg.Add(2)
   321  	go func() {
   322  		defer wg.Done()
   323  		for i := 0; i < 100; i++ {
   324  			writer.Write([]byte(fmt.Sprintf("this is a test log: 1-%d\n", i)))
   325  		}
   326  	}()
   327  
   328  	go func() {
   329  		defer wg.Done()
   330  		for i := 0; i < 100; i++ {
   331  			writer.Write([]byte(fmt.Sprintf("this is a test log: 2-%d\n", i)))
   332  		}
   333  	}()
   334  	wg.Wait()
   335  
   336  	time.Sleep(time.Second) // Wait till all the files are closed.
   337  	require.Nil(t, writer.Close())
   338  }
   339  
   340  func TestAsyncRollWriterRace(t *testing.T) {
   341  	logDir := t.TempDir()
   342  
   343  	writer, _ := NewRollWriter(
   344  		filepath.Join(logDir, "test.log"),
   345  		WithRotationTime(".%Y%m%d"),
   346  	)
   347  	writer.opts.MaxSize = 1
   348  	w := NewAsyncRollWriter(writer)
   349  
   350  	wg := sync.WaitGroup{}
   351  	wg.Add(2)
   352  	go func() {
   353  		defer wg.Done()
   354  		for i := 0; i < 100; i++ {
   355  			w.Write([]byte(fmt.Sprintf("this is a test log: 1-%d\n", i)))
   356  		}
   357  	}()
   358  
   359  	go func() {
   360  		defer wg.Done()
   361  		for i := 0; i < 100; i++ {
   362  			w.Write([]byte(fmt.Sprintf("this is a test log: 2-%d\n", i)))
   363  		}
   364  	}()
   365  	wg.Wait()
   366  
   367  	time.Sleep(20 * time.Millisecond)
   368  	require.Nil(t, w.Close())
   369  }
   370  
   371  func TestAsyncRollWriterSyncTwice(t *testing.T) {
   372  	w := NewAsyncRollWriter(&noopWriteCloser{})
   373  	w.Write([]byte("hello"))
   374  	require.Nil(t, w.Sync())
   375  	require.Nil(t, w.Sync())
   376  	require.Nil(t, w.Close())
   377  }
   378  
   379  func TestRollWriterError(t *testing.T) {
   380  	logDir := t.TempDir()
   381  	t.Run("reopen file", func(t *testing.T) {
   382  		r, err := NewRollWriter(path.Join(logDir, "trpc.log"))
   383  		require.Nil(t, err)
   384  		r.os = errOS{openFileErr: errAlwaysFail}
   385  		r.reopenFile()
   386  		require.Nil(t, r.Close())
   387  	})
   388  	t.Run("delay close and rename file", func(t *testing.T) {
   389  		r, err := NewRollWriter(path.Join(logDir, "trpc.log"))
   390  		require.Nil(t, err)
   391  		r.os = errOS{renameErr: errAlwaysFail}
   392  		f, err := os.CreateTemp(logDir, "trpc.log")
   393  		require.Nil(t, err)
   394  		r.delayCloseAndRenameFile(&closeAndRenameFile{file: f, rename: path.Join(logDir, "tmp.log")})
   395  		time.Sleep(30 * time.Millisecond)
   396  		require.Nil(t, r.Close())
   397  	})
   398  	t.Run("match log file", func(t *testing.T) {
   399  		r, err := NewRollWriter(path.Join(logDir, "trpc.log"))
   400  		require.Nil(t, err)
   401  		r.os = errOS{statErr: errAlwaysFail}
   402  		_, err = r.matchLogFile("trpc.log.20230130", "trpc.log")
   403  		require.NotNil(t, err)
   404  		require.Nil(t, r.Close())
   405  	})
   406  	t.Run("remove files", func(t *testing.T) {
   407  		r, err := NewRollWriter(path.Join(logDir, "trpc.log"))
   408  		require.Nil(t, err)
   409  		r.os = errOS{removeErr: errAlwaysFail}
   410  		r.removeFiles([]logInfo{{time.Time{}, &noopFileInfo{}}})
   411  		require.Nil(t, r.Close())
   412  	})
   413  	t.Run("compress file", func(t *testing.T) {
   414  		file := path.Join(logDir, "trpc.log")
   415  		r, err := NewRollWriter(file)
   416  		require.Nil(t, err)
   417  		r.os = errOS{openErr: errAlwaysFail}
   418  		require.NotNil(t, r.compressFile(file, ""))
   419  		r.os = errOS{openFileErr: errAlwaysFail}
   420  		f, err := os.Create(file)
   421  		require.Nil(t, err)
   422  		require.Nil(t, f.Close())
   423  		require.NotNil(t, r.compressFile(file, ""))
   424  		require.Nil(t, r.Close())
   425  	})
   426  }
   427  
   428  type noopFileInfo struct{}
   429  
   430  func (*noopFileInfo) Name() string {
   431  	return "trpc.log"
   432  }
   433  
   434  func (*noopFileInfo) IsDir() bool {
   435  	return false
   436  }
   437  
   438  func (*noopFileInfo) Type() os.FileMode {
   439  	return fs.ModeAppend
   440  }
   441  
   442  func (*noopFileInfo) Info() (os.FileInfo, error) {
   443  	return &noopFileInfo{}, nil
   444  }
   445  
   446  func (*noopFileInfo) Sys() interface{} {
   447  	return nil
   448  }
   449  
   450  func (*noopFileInfo) Size() int64 {
   451  	return 0
   452  }
   453  
   454  func (*noopFileInfo) Mode() os.FileMode {
   455  	return os.ModePerm
   456  }
   457  
   458  func (*noopFileInfo) ModTime() time.Time {
   459  	return time.Time{}
   460  }
   461  
   462  var errAlwaysFail = errors.New("always fail")
   463  
   464  type errOS struct {
   465  	openErr     error
   466  	openFileErr error
   467  	mkdirAllErr error
   468  	renameErr   error
   469  	statErr     error
   470  	removeErr   error
   471  }
   472  
   473  func (o errOS) Open(name string) (*os.File, error) {
   474  	if o.openErr != nil {
   475  		return nil, o.openErr
   476  	}
   477  	return os.Open(name)
   478  }
   479  
   480  func (o errOS) OpenFile(name string, flag int, perm fs.FileMode) (*os.File, error) {
   481  	if o.openFileErr != nil {
   482  		return nil, o.openFileErr
   483  	}
   484  	return os.OpenFile(name, flag, perm)
   485  }
   486  
   487  func (o errOS) MkdirAll(path string, perm fs.FileMode) error {
   488  	if o.mkdirAllErr != nil {
   489  		return o.mkdirAllErr
   490  	}
   491  	return os.MkdirAll(path, perm)
   492  }
   493  
   494  func (o errOS) Rename(oldpath string, newpath string) error {
   495  	if o.renameErr != nil {
   496  		return o.renameErr
   497  	}
   498  	return os.Rename(oldpath, newpath)
   499  }
   500  
   501  func (o errOS) Stat(name string) (fs.FileInfo, error) {
   502  	if o.statErr != nil {
   503  		return nil, o.statErr
   504  	}
   505  	return os.Stat(name)
   506  }
   507  
   508  func (o errOS) Remove(name string) error {
   509  	if o.removeErr != nil {
   510  		return o.removeErr
   511  	}
   512  	return os.Remove(name)
   513  }
   514  
   515  type noopWriteCloser struct{}
   516  
   517  func (*noopWriteCloser) Write(p []byte) (n int, err error) { return }
   518  
   519  func (*noopWriteCloser) Close() (err error) { return }
   520  
   521  // BenchmarkRollWriterBySize benchmarks RollWriter by size.
   522  func BenchmarkRollWriterBySize(b *testing.B) {
   523  	logDir := b.TempDir()
   524  
   525  	// init RollWriter.
   526  	writer, _ := NewRollWriter(filepath.Join(logDir, "test.log"))
   527  	core := zapcore.NewCore(
   528  		zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig()),
   529  		zapcore.AddSync(writer),
   530  		zapcore.DebugLevel,
   531  	)
   532  	logger := zap.New(
   533  		core,
   534  		zap.AddCaller(),
   535  	)
   536  
   537  	// warm up.
   538  	for i := 0; i < testTimes; i++ {
   539  		logger.Debug(fmt.Sprint("this is a test log: ", i))
   540  	}
   541  
   542  	b.SetParallelism(testRoutines / runtime.GOMAXPROCS(0))
   543  	b.ResetTimer()
   544  	b.RunParallel(func(pb *testing.PB) {
   545  		for pb.Next() {
   546  			logger.Debug("this is a test log")
   547  		}
   548  	})
   549  }
   550  
   551  // BenchmarkRollWriterByTime benchmarks RollWriter by time.
   552  func BenchmarkRollWriterByTime(b *testing.B) {
   553  	logDir := b.TempDir()
   554  
   555  	// init RollWriter.
   556  	writer, _ := NewRollWriter(
   557  		filepath.Join(logDir, "test.log"),
   558  		WithRotationTime(".%Y%m%d"),
   559  	)
   560  	core := zapcore.NewCore(
   561  		zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig()),
   562  		zapcore.AddSync(writer),
   563  		zapcore.DebugLevel,
   564  	)
   565  	logger := zap.New(
   566  		core,
   567  		zap.AddCaller(),
   568  	)
   569  
   570  	// warm up.
   571  	for i := 0; i < testTimes; i++ {
   572  		logger.Debug(fmt.Sprint("this is a test log: ", i))
   573  	}
   574  
   575  	b.SetParallelism(testRoutines / runtime.GOMAXPROCS(0))
   576  	b.ResetTimer()
   577  	b.RunParallel(func(pb *testing.PB) {
   578  		for pb.Next() {
   579  			logger.Debug("this is a test log")
   580  		}
   581  	})
   582  }
   583  
   584  // BenchmarkAsyncRollWriterBySize benchmarks asynchronous RollWriter.
   585  func BenchmarkAsyncRollWriterBySize(b *testing.B) {
   586  	logDir := b.TempDir()
   587  
   588  	// init RollWriter.
   589  	writer, _ := NewRollWriter(
   590  		filepath.Join(logDir, "test.log"),
   591  	)
   592  	asyncWriter := NewAsyncRollWriter(writer)
   593  	core := zapcore.NewCore(
   594  		zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig()),
   595  		asyncWriter,
   596  		zapcore.DebugLevel,
   597  	)
   598  	logger := zap.New(
   599  		core,
   600  		zap.AddCaller(),
   601  	)
   602  
   603  	// warm up.
   604  	for i := 0; i < testTimes; i++ {
   605  		logger.Debug(fmt.Sprint("this is a test log: ", i))
   606  	}
   607  
   608  	b.SetParallelism(testRoutines / runtime.GOMAXPROCS(0))
   609  	b.ResetTimer()
   610  	b.RunParallel(func(pb *testing.PB) {
   611  		for pb.Next() {
   612  			logger.Debug("this is a test log")
   613  		}
   614  	})
   615  }
   616  
   617  // BenchmarkAsyncRollWriterByTime benchmarks asynchronous RollWriter by time.
   618  func BenchmarkAsyncRollWriterByTime(b *testing.B) {
   619  	logDir := b.TempDir()
   620  
   621  	// init RollWriter.
   622  	writer, _ := NewRollWriter(
   623  		filepath.Join(logDir, "test.log"),
   624  		WithRotationTime(".%Y%m%d"),
   625  	)
   626  	asyncWriter := NewAsyncRollWriter(writer)
   627  	core := zapcore.NewCore(
   628  		zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig()),
   629  		asyncWriter,
   630  		zapcore.DebugLevel,
   631  	)
   632  	logger := zap.New(
   633  		core,
   634  		zap.AddCaller(),
   635  	)
   636  
   637  	// warm up.
   638  	for i := 0; i < testTimes; i++ {
   639  		logger.Debug(fmt.Sprint("this is a test log: ", i))
   640  	}
   641  
   642  	b.SetParallelism(testRoutines / runtime.GOMAXPROCS(0))
   643  	b.ResetTimer()
   644  	b.RunParallel(func(pb *testing.PB) {
   645  		for pb.Next() {
   646  			logger.Debug("this is a test log")
   647  		}
   648  	})
   649  }
   650  
   651  func printLogFiles(logDir string) {
   652  	fmt.Println("================================================")
   653  	fmt.Printf("[%s]:\n", logDir)
   654  
   655  	entries, err := os.ReadDir(logDir)
   656  	if err != nil {
   657  		fmt.Println("get entries failed ", err)
   658  	}
   659  	for _, entry := range entries {
   660  		info, err := entry.Info()
   661  		if err != nil {
   662  			fmt.Println("get info failed ", err)
   663  			continue
   664  		}
   665  		fmt.Println("\t", info.Name(), info.Size())
   666  	}
   667  }
   668  
   669  func execCommand(name string, args ...string) string {
   670  	fmt.Println(name, args)
   671  	cmd := exec.Command(name, args...)
   672  	output, err := cmd.Output()
   673  	if err != nil {
   674  		fmt.Printf("exec command failed, err: %v\n", err)
   675  	}
   676  	fmt.Println(string(output))
   677  
   678  	return string(output)
   679  }
   680  
   681  func getLogBackups(logDir, prefix string) []os.FileInfo {
   682  	entries, err := os.ReadDir(logDir)
   683  	if err != nil {
   684  		return nil
   685  	}
   686  
   687  	var logFiles []os.FileInfo
   688  	for _, file := range entries {
   689  		if !strings.HasPrefix(file.Name(), prefix) {
   690  			continue
   691  		}
   692  		if info, err := file.Info(); err == nil {
   693  			logFiles = append(logFiles, info)
   694  		}
   695  	}
   696  	return logFiles
   697  }