github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/logger/file.go (about) 1 // Copyright 2017 Keybase, Inc. All rights reserved. Use of 2 // this source code is governed by the included BSD license. 3 4 package logger 5 6 import ( 7 "errors" 8 "fmt" 9 "os" 10 "path/filepath" 11 "regexp" 12 "sort" 13 "sync" 14 "time" 15 16 logging "github.com/keybase/go-logging" 17 ) 18 19 // LogFileConfig is the config structure for new style log files with rotation. 20 type LogFileConfig struct { 21 // Path is the path of the log file to use 22 Path string 23 // MaxSize is the size of log file (in bytes) before rotation, 0 for infinite. 24 MaxSize int64 25 // MaxAge is the duration before log rotation, zero value for infinite. 26 MaxAge time.Duration 27 // MaxKeepFiles is maximum number of log files for this service, older 28 // files are deleted. 29 MaxKeepFiles int 30 // RedirectStdErr indicates if the current stderr redirected to the given 31 // Path. 32 SkipRedirectStdErr bool 33 } 34 35 // SetLogFileConfig sets the log file config to be used globally. 36 func SetLogFileConfig(lfc *LogFileConfig, blc *BufferedLoggerConfig) error { 37 globalLock.Lock() 38 defer globalLock.Unlock() 39 40 first := true 41 var w = currentLogFileWriter 42 if w != nil { 43 first = false 44 w.lock.Lock() 45 defer w.lock.Unlock() 46 w.Close() 47 w.config = *lfc 48 } else { 49 w = NewLogFileWriter(*lfc) 50 51 // Clean up the default logger, if it is in use 52 select { 53 case stdErrLoggingShutdown <- struct{}{}: 54 default: 55 } 56 } 57 58 if err := w.Open(time.Now()); err != nil { 59 return err 60 } 61 62 if first { 63 buf, shutdown, _ := NewAutoFlushingBufferedWriter(w, blc) 64 w.stopFlushing = shutdown 65 fileBackend := logging.NewLogBackend(buf, "", 0) 66 logging.SetBackend(fileBackend) 67 68 stderrIsTerminal = false 69 currentLogFileWriter = w 70 } 71 return nil 72 } 73 74 type LogFileWriter struct { 75 lock sync.Mutex 76 config LogFileConfig 77 file *os.File 78 currentSize int64 79 currentStart time.Time 80 stopFlushing chan<- struct{} 81 } 82 83 func NewLogFileWriter(config LogFileConfig) *LogFileWriter { 84 return &LogFileWriter{ 85 config: config, 86 } 87 } 88 89 func (lfw *LogFileWriter) Open(at time.Time) error { 90 var err error 91 _, lfw.file, err = OpenLogFile(lfw.config.Path) 92 if err != nil { 93 return err 94 } 95 lfw.currentStart = at 96 lfw.currentSize = 0 97 fi, err := lfw.file.Stat() 98 if err != nil { 99 return err 100 } 101 lfw.currentSize = fi.Size() 102 if !lfw.config.SkipRedirectStdErr { 103 _ = tryRedirectStderrTo(lfw.file) 104 } 105 return nil 106 } 107 108 func (lfw *LogFileWriter) Close() error { 109 if lfw == nil { 110 return nil 111 } 112 lfw.lock.Lock() 113 defer lfw.lock.Unlock() 114 if lfw.file == nil { 115 return nil 116 } 117 if lfw.stopFlushing != nil { 118 lfw.stopFlushing <- struct{}{} 119 } 120 121 return lfw.file.Close() 122 } 123 124 const zeroDuration time.Duration = 0 125 const oldLogFileTimeRangeTimeLayout = "20060102T150405Z0700" 126 const oldLogFileTimeRangeTimeLayoutLegacy = "20060102T150405" 127 128 func (lfw *LogFileWriter) Write(bs []byte) (int, error) { 129 lfw.lock.Lock() 130 defer lfw.lock.Unlock() 131 n, err := lfw.file.Write(bs) 132 if err != nil { 133 return n, err 134 } 135 needRotation := false 136 if lfw.config.MaxSize > 0 { 137 lfw.currentSize += int64(n) 138 needRotation = needRotation || lfw.currentSize > lfw.config.MaxSize 139 } 140 if lfw.config.MaxAge != zeroDuration { 141 elapsed := time.Since(lfw.currentStart) 142 needRotation = needRotation || elapsed > lfw.config.MaxAge 143 } 144 if !needRotation { 145 return n, nil 146 } 147 // Close first because some systems don't like to rename otherwise. 148 lfw.file.Close() 149 lfw.file = nil 150 now := time.Now() 151 start := lfw.currentStart.Format(oldLogFileTimeRangeTimeLayout) 152 end := now.Format(oldLogFileTimeRangeTimeLayout) 153 tgt := fmt.Sprintf("%s-%s-%s", lfw.config.Path, start, end) 154 // Handle the error further down 155 err = os.Rename(lfw.config.Path, tgt) 156 if err != nil { 157 return n, err 158 } 159 // Spawn old log deletion worker if we have a max-amount of log-files. 160 if lfw.config.MaxKeepFiles > 0 { 161 go deleteOldLogFilesIfNeeded(lfw.config) 162 } 163 err = lfw.Open(now) 164 return n, err 165 } 166 167 func deleteOldLogFilesIfNeeded(config LogFileConfig) { 168 err := deleteOldLogFilesIfNeededWorker(config) 169 if err != nil { 170 log := New("logger") 171 log.Warning("Deletion of old log files failed: %v", err) 172 } 173 } 174 175 func deleteOldLogFilesIfNeededWorker(config LogFileConfig) error { 176 // Returns list of old log files (not the current one) sorted. 177 // The oldest one is first in the list. 178 entries, err := scanOldLogFiles(config.Path) 179 if err != nil { 180 return err 181 } 182 // entries has only the old renamed log files, not the current 183 // log file. E.g. if MaxKeepFiles is 2 then we keep the current 184 // file and one archived log file. If there are 3 archived files 185 // then removeN = 1 + 3 - 2 = 2. 186 removeN := 1 + len(entries) - config.MaxKeepFiles 187 if config.MaxKeepFiles <= 0 || removeN <= 0 { 188 return nil 189 } 190 // Try to remove all old log files that we want to remove, and 191 // don't stop on the first error. 192 for i := 0; i < removeN; i++ { 193 err2 := os.Remove(entries[i]) 194 if err == nil { 195 err = err2 196 } 197 } 198 return err 199 } 200 201 type logFilename struct { 202 fName string 203 start time.Time 204 } 205 206 type logFilenamesByTime []logFilename 207 208 func (a logFilenamesByTime) Len() int { return len(a) } 209 func (a logFilenamesByTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 210 func (a logFilenamesByTime) Less(i, j int) bool { 211 return a[i].start.Before(a[j].start) 212 } 213 214 // getLogFilenamesOrderByTime filters fNames to return only old log files 215 // starting with baseName, followed by a timestamp-range suffix. It also sorts 216 // them by start time, in increasing order. 217 // 218 // Both baseName and fNames are base names not including dir names. 219 // 220 // This function supports both old (no timezone) and current (with timezone) 221 // format of log file names. TODO: simplify this when we don't care about old 222 // format any more. 223 func getLogFilenamesOrderByTime( 224 baseName string, fNames []string) (names []string, err error) { 225 re, err := regexp.Compile(`^` + regexp.QuoteMeta(baseName) + 226 `-(\d{8}T\d{6}(?:(?:[Z\+-]\d{4})|(?:Z))?)-\d{8}T\d{6}(?:(?:[Z\+-]\d{4})|(?:Z))?$`) 227 if err != nil { 228 return nil, err 229 } 230 231 var logFilenames []logFilename 232 for _, fName := range fNames { 233 match := re.FindStringSubmatch(fName) 234 if len(match) != 2 { 235 continue 236 } 237 t, err1 := time.ParseInLocation(oldLogFileTimeRangeTimeLayout, match[1], time.Local) 238 if err1 != nil { 239 var err2 error 240 t, err2 = time.ParseInLocation(oldLogFileTimeRangeTimeLayoutLegacy, match[1], time.Local) 241 if err2 != nil { 242 return nil, errors.New(err1.Error() + " | " + err2.Error()) 243 } 244 } 245 logFilenames = append(logFilenames, logFilename{fName: fName, start: t}) 246 } 247 248 sort.Sort(logFilenamesByTime(logFilenames)) 249 250 names = make([]string, 0, len(logFilenames)) 251 for _, f := range logFilenames { 252 names = append(names, f.fName) 253 } 254 255 return names, nil 256 } 257 258 // scanOldLogFiles finds old archived log files corresponding to the log file path. 259 // Returns the list of such log files sorted with the eldest one first. 260 func scanOldLogFiles(path string) ([]string, error) { 261 dname, fname := filepath.Split(path) 262 if dname == "" { 263 dname = "." 264 } 265 dir, err := os.Open(dname) 266 if err != nil { 267 return nil, err 268 } 269 defer dir.Close() 270 ns, err := dir.Readdirnames(-1) 271 if err != nil { 272 return nil, err 273 } 274 names, err := getLogFilenamesOrderByTime(fname, ns) 275 if err != nil { 276 return nil, err 277 } 278 var res []string 279 for _, name := range names { 280 res = append(res, filepath.Join(dname, name)) 281 } 282 return res, nil 283 }