github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/logging/rotate.go (about)

     1  package logging
     2  
     3  import (
     4  	"io/fs"
     5  	"os"
     6  	"path/filepath"
     7  	"regexp"
     8  	"sort"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/ActiveState/cli/internal/constants"
    14  )
    15  
    16  var LogPrefixRx = regexp.MustCompile(`^[a-zA-Z\-]+`)
    17  
    18  func rotateLogs(files []fs.FileInfo, timeCutoff time.Time, amountCutoff int) []fs.FileInfo {
    19  	rotate := []fs.FileInfo{}
    20  
    21  	sort.Slice(files, func(i, j int) bool { return files[i].ModTime().After(files[j].ModTime()) })
    22  
    23  	// Collect the possible file prefixes that we're going to want to run through
    24  	prefixes := map[string]struct{}{}
    25  	for _, file := range files {
    26  		prefix := LogPrefixRx.FindString(file.Name())
    27  		if _, exists := prefixes[prefix]; !exists {
    28  			prefixes[prefix] = struct{}{}
    29  		}
    30  	}
    31  
    32  	for prefix := range prefixes {
    33  		c := 0
    34  		for _, file := range files {
    35  			currentPrefix := LogPrefixRx.FindString(file.Name())
    36  			if currentPrefix == prefix && strings.HasSuffix(file.Name(), FileNameSuffix) {
    37  				c = c + 1
    38  				if c > amountCutoff && file.ModTime().Before(timeCutoff) {
    39  					rotate = append(rotate, file)
    40  				}
    41  			}
    42  		}
    43  	}
    44  
    45  	return rotate
    46  }
    47  
    48  func rotateLogsOnDisk() {
    49  	// Clean up old log files
    50  	logDir := filepath.Dir(FilePath())
    51  	files, err := os.ReadDir(logDir)
    52  	if err != nil && !os.IsNotExist(err) {
    53  		Error("Could not scan config dir to clean up stale logs: %v", err)
    54  		return
    55  	}
    56  
    57  	// Prevent running over this logic too often as it affects performance
    58  	// https://activestatef.atlassian.net/browse/DX-1516
    59  	if len(files) < 30 {
    60  		return
    61  	}
    62  
    63  	infos := make([]fs.FileInfo, len(files))
    64  	for i, file := range files {
    65  		infos[i], err = file.Info()
    66  		if err != nil {
    67  			Error("Could not get file info for %s: %v", file.Name(), err)
    68  			return
    69  		}
    70  	}
    71  
    72  	rotate := rotateLogs(infos, time.Now().Add(-time.Hour), 10)
    73  	for _, file := range rotate {
    74  		if err := os.Remove(filepath.Join(logDir, file.Name())); err != nil {
    75  			Error("Could not clean up old log: %s, error: %v", file.Name(), err)
    76  		}
    77  	}
    78  }
    79  
    80  var stopTimer chan bool
    81  
    82  // StartRotateLogTimer starts log rotation on a timer and returns a function that should be called to stop it.
    83  func StartRotateLogTimer() func() {
    84  	interval := 1 * time.Minute
    85  	if durationString := os.Getenv(constants.SvcLogRotateIntervalEnvVarName); durationString != "" {
    86  		if duration, err := strconv.Atoi(durationString); err == nil {
    87  			interval = time.Duration(duration) * time.Millisecond
    88  		}
    89  	}
    90  
    91  	stopTimer = make(chan bool)
    92  	go func() {
    93  		rotateLogsOnDisk()
    94  		for {
    95  			select {
    96  			case <-stopTimer:
    97  				return
    98  			case <-time.After(interval):
    99  				rotateLogsOnDisk()
   100  			}
   101  		}
   102  	}()
   103  
   104  	return func() {
   105  		stopTimer <- true
   106  	}
   107  }