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 }