github.com/telepresenceio/telepresence/v2@v2.20.0-pro.6.0.20240517030216-236ea954e789/pkg/client/logging/rotatingfile.go (about) 1 package logging 2 3 import ( 4 "context" 5 "fmt" 6 "io/fs" 7 "os" 8 "path/filepath" 9 "sort" 10 "strings" 11 "sync" 12 "time" 13 14 "github.com/datawire/dlib/dlog" 15 "github.com/datawire/dlib/dtime" 16 "github.com/telepresenceio/telepresence/v2/pkg/dos" 17 ) 18 19 // A RotationStrategy answers the question if it is time to rotate the file now. It is called prior to every write 20 // so it needs to be fairly quick. 21 type RotationStrategy interface { 22 RotateNow(file *RotatingFile, writeSize int) bool 23 } 24 25 type rotateNever int 26 27 // The RotateNever strategy will always answer false to the RotateNow question. 28 const RotateNever = rotateNever(0) 29 30 func (rotateNever) RotateNow(_ *RotatingFile, _ int) bool { 31 return false 32 } 33 34 // A rotateOnce ensures that the file is rotated exactly once if it is of non-zero size when the 35 // first call to Write() arrives. 36 type rotateOnce struct { 37 called bool 38 } 39 40 func NewRotateOnce() RotationStrategy { 41 return &rotateOnce{} 42 } 43 44 func (r *rotateOnce) RotateNow(rf *RotatingFile, _ int) bool { 45 if r.called { 46 return false 47 } 48 r.called = true 49 return rf.Size() > 0 50 } 51 52 type rotateDaily int 53 54 // The RotateDaily strategy will ensure that the file is rotated if it is of non-zero size when a call 55 // to Write() arrives on a day different from the day when the current file was created. 56 const RotateDaily = rotateDaily(0) 57 58 func (rotateDaily) RotateNow(rf *RotatingFile, _ int) bool { 59 if rf.Size() == 0 { 60 return false 61 } 62 bt := rf.BirthTime() 63 return dtime.Now().In(bt.Location()).Day() != rf.BirthTime().Day() 64 } 65 66 type RotatingFile struct { 67 ctx context.Context 68 fileMode fs.FileMode 69 dirName string 70 fileName string 71 timeFormat string 72 localTime bool 73 maxFiles uint16 74 strategy RotationStrategy 75 mutex sync.Mutex 76 removeMutex sync.Mutex 77 78 // file is the current file. It is never nil 79 file dos.File 80 81 // size is the number of bytes written to the current file. 82 size int64 83 84 // birthTime is the time when the current file was first created 85 birthTime time.Time 86 } 87 88 // OpenRotatingFile opens a file with the given name after first having created the directory that it 89 // resides in and all parent directories. The file is opened write only. 90 // 91 // Parameters: 92 // 93 // - dirName: full path to the directory of the log file and its backups 94 // 95 // - fileName: name of the file that should be opened (relative to dirName) 96 // 97 // - timeFormat: the format to use for the timestamp that is added to rotated files 98 // 99 // - localTime: if true, use local time in timestamps, if false, use UTC 100 // 101 // - stdLogger: if not nil, all writes to os.Stdout and os.Stderr will be redirected to this logger as INFO level 102 // messages prefixed with <stdout> or <stderr> 103 // 104 // - fileMode: the mode to use when creating new files the file 105 // 106 // - strategy: determines when a rotation should take place 107 // 108 // - maxFiles: maximum number of files in rotation, including the currently active logfile. A value of zero means 109 // unlimited. 110 func OpenRotatingFile( 111 ctx context.Context, 112 logfilePath string, 113 timeFormat string, 114 localTime bool, 115 fileMode fs.FileMode, 116 strategy RotationStrategy, 117 maxFiles uint16, 118 ) (*RotatingFile, error) { 119 logfileDir, logfileBase := filepath.Split(logfilePath) 120 121 var err error 122 if err = dos.MkdirAll(ctx, logfileDir, 0o755); err != nil { 123 return nil, err 124 } 125 126 rf := &RotatingFile{ 127 ctx: ctx, 128 dirName: logfileDir, 129 fileName: logfileBase, 130 fileMode: fileMode, 131 strategy: strategy, 132 localTime: localTime, 133 timeFormat: timeFormat, 134 maxFiles: maxFiles, 135 } 136 137 // Try to open existing file for append. 138 if rf.file, err = dos.OpenFile(ctx, logfilePath, os.O_WRONLY|os.O_APPEND, rf.fileMode); err != nil { 139 if os.IsNotExist(err) { 140 // There is no existing file, go ahead and create a new one. 141 if err = rf.openNew(nil, ""); err == nil { 142 return rf, nil 143 } 144 } 145 return nil, err 146 } 147 // We successfully opened the existing file, get it plugged in. 148 stat, err := FStat(rf.file) 149 if err != nil { 150 return nil, fmt.Errorf("failed to stat %s: %w", logfilePath, err) 151 } 152 rf.birthTime = stat.BirthTime() 153 rf.size = stat.Size() 154 rf.afterOpen() 155 return rf, nil 156 } 157 158 // BirthTime returns the time when the current file was created. The time will be local if 159 // the file was opened with localTime == true and UTC otherwise. 160 func (rf *RotatingFile) BirthTime() time.Time { 161 rf.mutex.Lock() 162 bt := rf.birthTime 163 rf.mutex.Unlock() 164 return bt 165 } 166 167 // Close implements io.Closer. 168 func (rf *RotatingFile) Close() error { 169 return rf.file.Close() 170 } 171 172 // Rotate closes the currently opened file and renames it by adding a timestamp between the file name 173 // and its extension. A new file empty file is then opened to receive subsequent data. 174 func (rf *RotatingFile) Rotate() (err error) { 175 rf.mutex.Lock() 176 defer rf.mutex.Unlock() 177 return rf.rotate() 178 } 179 180 // Size returns the size of the current file. 181 func (rf *RotatingFile) Size() int64 { 182 rf.mutex.Lock() 183 sz := rf.size 184 rf.mutex.Unlock() 185 return sz 186 } 187 188 // Write implements io.Writer. 189 func (rf *RotatingFile) Write(data []byte) (int, error) { 190 rotateNow := rf.strategy.RotateNow(rf, len(data)) 191 rf.mutex.Lock() 192 defer rf.mutex.Unlock() 193 194 if rotateNow { 195 if err := rf.rotate(); err != nil { 196 return 0, err 197 } 198 } 199 l, err := rf.file.Write(data) 200 if err != nil { 201 return 0, err 202 } 203 rf.size += int64(l) 204 return l, nil 205 } 206 207 func (rf *RotatingFile) afterOpen() { 208 go rf.removeOldFiles() 209 } 210 211 func (rf *RotatingFile) fileTime(t time.Time) time.Time { 212 if rf.localTime { 213 t = t.Local() 214 } else { 215 t = t.UTC() 216 } 217 return t 218 } 219 220 func (rf *RotatingFile) openNew(prevInfo SysInfo, backupName string) (err error) { 221 fullPath := filepath.Join(rf.dirName, rf.fileName) 222 var flag int 223 if rf.file == nil { 224 flag = os.O_CREATE | os.O_WRONLY | os.O_TRUNC 225 } else { 226 // Open file with a different name so that a tail -F on the original doesn't fail with a permission denied 227 tmp := fullPath + ".tmp" 228 var tmpFile dos.File 229 if tmpFile, err = dos.OpenFile(rf.ctx, tmp, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, rf.fileMode); err != nil { 230 return fmt.Errorf("failed to createFile %s: %w", tmp, err) 231 } 232 233 var si SysInfo 234 si, err = FStat(tmpFile) 235 _ = tmpFile.Close() 236 237 if err != nil { 238 return fmt.Errorf("failed to stat %s: %w", tmp, err) 239 } 240 241 if prevInfo != nil && !prevInfo.HaveSameOwnerAndGroup(si) { 242 if err = prevInfo.SetOwnerAndGroup(tmp); err != nil { 243 return fmt.Errorf("failed to SetOwnerAndGroup for %s: %w", tmp, err) 244 } 245 } 246 247 if err = rf.file.Close(); err != nil { 248 return fmt.Errorf("failed to close %s: %w", rf.file.Name(), err) 249 } 250 if err = dos.Rename(rf.ctx, fullPath, backupName); err != nil { 251 return fmt.Errorf("failed to rename %s to %s: %w", fullPath, backupName, err) 252 } 253 if err = dos.Rename(rf.ctx, tmp, fullPath); err != nil { 254 return fmt.Errorf("failed to rename %s to %s: %w", tmp, fullPath, err) 255 } 256 // Need to restore birth time on Windows since it retains the birt time of the 257 // overwritten target of the rename operation. 258 if err = restoreCTimeAfterRename(fullPath, si.BirthTime()); err != nil { 259 return fmt.Errorf("failed to restore creation time of %s to %s: %w", tmp, si.BirthTime(), err) 260 } 261 flag = os.O_WRONLY | os.O_APPEND 262 } 263 if rf.file, err = dos.OpenFile(rf.ctx, fullPath, flag, rf.fileMode); err != nil { 264 return fmt.Errorf("failed to open file %s: %w", fullPath, err) 265 } 266 rf.birthTime = rf.fileTime(dtime.Now()) 267 rf.size = 0 268 rf.afterOpen() 269 return nil 270 } 271 272 // removeOldFiles checks how many files that currently exists (backups + current log file) with the same 273 // name as this RotatingFile and then, as long as the number of files exceed the maxFiles given to the 274 // constructor, it will continuously remove the oldest file. 275 // 276 // This function should typically run in its own goroutine. 277 func (rf *RotatingFile) removeOldFiles() { 278 rf.removeMutex.Lock() 279 defer rf.removeMutex.Unlock() 280 281 files, err := dos.ReadDir(rf.ctx, rf.dirName) 282 if err != nil { 283 return 284 } 285 ext := filepath.Ext(rf.fileName) 286 pfx := rf.fileName[:len(rf.fileName)-len(ext)] + "-" 287 288 // Use a map with unix nanosecond timestamp as key 289 names := make(map[int64]string, rf.maxFiles+2) 290 291 // Slice of timestamps later to be ordered 292 keys := make([]int64, 0, rf.maxFiles+2) 293 294 for _, file := range files { 295 fn := file.Name() 296 297 // Skip files that don't start with the prefix and end with the suffix. 298 if !(strings.HasPrefix(fn, pfx) && strings.HasSuffix(fn, ext)) { 299 continue 300 } 301 // Parse the timestamp from the file name 302 var ts time.Time 303 if ts, err = time.Parse(rf.timeFormat, fn[len(pfx):len(fn)-len(ext)]); err != nil { 304 continue 305 } 306 key := ts.UnixNano() 307 keys = append(keys, key) 308 names[key] = fn 309 } 310 mx := int(rf.maxFiles) - 1 // -1 to account for the current log file 311 if len(keys) <= mx { 312 return 313 } 314 sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] }) 315 for _, key := range keys[:len(keys)-mx] { 316 _ = os.Remove(filepath.Join(rf.dirName, names[key])) 317 } 318 } 319 320 func (rf *RotatingFile) rotate() error { 321 var prevInfo SysInfo 322 var backupName string 323 if rf.maxFiles == 0 || rf.maxFiles > 1 { 324 var err error 325 prevInfo, err = FStat(rf.file) 326 if err != nil || prevInfo == nil { 327 err = fmt.Errorf("failed to stat %s: %w", rf.file.Name(), err) 328 dlog.Error(rf.ctx, err) 329 return err 330 } 331 332 fullPath := filepath.Join(rf.dirName, rf.fileName) 333 ex := filepath.Ext(rf.fileName) 334 sf := fullPath[:len(fullPath)-len(ex)] 335 ts := rf.fileTime(dtime.Now()).Format(rf.timeFormat) 336 backupName = fmt.Sprintf("%s-%s%s", sf, ts, ex) 337 } 338 err := rf.openNew(prevInfo, backupName) 339 if err != nil { 340 dlog.Error(rf.ctx, err) 341 } 342 return err 343 }