github.com/searKing/golang/go@v1.2.117/os/file_rotate.go (about) 1 // Copyright 2021 The searKing Author. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package os 6 7 import ( 8 "errors" 9 "fmt" 10 "io" 11 "os" 12 "regexp" 13 "sort" 14 "strings" 15 "sync" 16 "time" 17 18 filepath_ "github.com/searKing/golang/go/path/filepath" 19 "github.com/searKing/golang/go/sync/atomic" 20 time_ "github.com/searKing/golang/go/time" 21 ) 22 23 // RotateMode represents a way to rotate file. 24 // see details: https://man7.org/linux/man-pages/man8/logrotate.8.html 25 type RotateMode int 26 27 const ( 28 // RotateModeNew create new rotate file directly 29 // Immediately after rotation (before the postrotate script is run) the log file is created (with 30 // the same name as the log file just rotated). 31 // see option: `create` in https://man7.org/linux/man-pages/man8/logrotate.8.html 32 RotateModeNew RotateMode = iota 33 34 // RotateModeCopyRename Make a copy of the log file, but don't change the original at all. This option can be 35 // used, for instance, to make a snapshot of the current log file, or when some other 36 // utility needs to truncate or parse the file. When this option is used, the create 37 // option will have no effect, as the old log file stays in place. 38 // see option: `copy` in https://man7.org/linux/man-pages/man8/logrotate.8.html 39 RotateModeCopyRename RotateMode = iota 40 41 // RotateModeCopyTruncate Truncate the original log file in place after creating a copy, instead of moving the 42 // old log file and optionally creating a new one. It can be used when some program can‐ 43 // not be told to close its rotatefile and thus might continue writing (appending) to the 44 // previous log file forever. Note that there is a very small time slice between copying 45 // the file and truncating it, so some logging data might be lost. When this option is 46 // used, the create option will have no effect, as the old log file stays in place. 47 // see option: `copytruncate` in https://man7.org/linux/man-pages/man8/logrotate.8.html 48 RotateModeCopyTruncate RotateMode = iota 49 ) 50 51 // RotateFile logrotate reads everything about the log files it should be handling from the series of con‐ 52 // figuration files specified on the command line. Each configuration file can set global 53 // options (local definitions override global ones, and later definitions override earlier ones) 54 // and specify rotatefiles to rotate. A simple configuration file looks like this: 55 type RotateFile struct { 56 RotateMode RotateMode 57 FilePathPrefix string // FilePath = FilePathPrefix + now.Format(filePathRotateLayout) 58 FilePathRotateLayout string // Time layout to format rotate file 59 60 RotateFileGlob string // file glob to clean 61 62 // sets the symbolic link name that gets linked to the current file name being used. 63 FileLinkPath string 64 65 // Rotate files are rotated until RotateInterval expired before being removed 66 // take effects if only RotateInterval is bigger than 0. 67 RotateInterval time.Duration 68 69 // Rotate files are rotated if they grow bigger then size bytes. 70 // take effects if only RotateSize is bigger than 0. 71 RotateSize int64 72 73 // max age of a log file before it gets purged from the file system. 74 // Remove rotated logs older than duration. The age is only checked if the file is 75 // to be rotated. 76 // take effects if only MaxAge is bigger than 0. 77 MaxAge time.Duration 78 79 // Rotate files are rotated MaxCount times before being removed 80 // take effects if only MaxCount is bigger than 0. 81 MaxCount int 82 83 // Force File Rotate when start up 84 ForceNewFileOnStartup bool 85 86 // PreRotateHandler called before file rotate 87 // name means file path rotated 88 PreRotateHandler func(name string) 89 90 // PostRotateHandler called after file rotate 91 // name means file path rotated 92 PostRotateHandler func(name string) 93 94 cleaning atomic.Bool 95 mu sync.Mutex 96 usingSeq int // file rotated by size limit meet 97 usingFilePath string 98 usingFile *os.File 99 } 100 101 func NewRotateFile(layout string) *RotateFile { 102 return NewRotateFileWithStrftime(time_.LayoutTimeToSimilarStrftime(layout)) 103 } 104 105 func NewRotateFileWithStrftime(strftimeLayout string) *RotateFile { 106 return &RotateFile{ 107 FilePathRotateLayout: time_.LayoutStrftimeToSimilarTime(strftimeLayout), 108 RotateFileGlob: fileGlobFromStrftimeLayout(strftimeLayout), 109 RotateInterval: 24 * time.Hour, 110 } 111 } 112 113 func fileGlobFromStrftimeLayout(strftimeLayout string) string { 114 var regexps = []*regexp.Regexp{ 115 regexp.MustCompile(`%[%+A-Za-z]`), 116 regexp.MustCompile(`\*+`), 117 } 118 globPattern := strftimeLayout 119 for _, re := range regexps { 120 globPattern = re.ReplaceAllString(globPattern, "*") 121 } 122 return globPattern + `*` 123 } 124 125 func (f *RotateFile) Write(b []byte) (n int, err error) { 126 if err := f.checkValid("write"); err != nil { 127 return 0, err 128 } 129 // Guard against concurrent writes 130 f.mu.Lock() 131 defer f.mu.Unlock() 132 133 out, err := f.getWriterLocked(false, false) 134 if err != nil { 135 return 0, fmt.Errorf("acquite rotated file :%w", err) 136 } 137 138 return out.Write(b) 139 } 140 141 // WriteString is like Write, but writes the contents of string s rather than 142 // a slice of bytes. 143 func (f *RotateFile) WriteString(s string) (n int, err error) { 144 if err := f.checkValid("close"); err != nil { 145 return 0, err 146 } 147 return f.Write([]byte(s)) 148 } 149 150 // WriteAt writes len(b) bytes to the File starting at byte offset off. 151 // It returns the number of bytes written and an error, if any. 152 // WriteAt returns a non-nil error when n != len(b). 153 // 154 // If file was opened with the O_APPEND flag, WriteAt returns an error. 155 func (f *RotateFile) WriteAt(b []byte, off int64) (n int, err error) { 156 if err := f.checkValid("write"); err != nil { 157 return 0, err 158 } 159 // Guard against concurrent writes 160 f.mu.Lock() 161 defer f.mu.Unlock() 162 163 out, err := f.getWriterLocked(false, false) 164 if err != nil { 165 return 0, fmt.Errorf("acquite rotated file :%w", err) 166 } 167 if w, ok := out.(io.WriterAt); ok { 168 return w.WriteAt(b, off) 169 } 170 return 0, fmt.Errorf("WriteAt is not supported") 171 } 172 173 // Close satisfies the io.Closer interface. You must 174 // call this method if you performed any writes to 175 // the object. 176 func (f *RotateFile) Close() error { 177 if err := f.checkValid("close"); err != nil { 178 return err 179 } 180 f.mu.Lock() 181 defer f.mu.Unlock() 182 183 if f.usingFile == nil { // maybe file is closed or not open 184 return nil 185 } 186 defer f.serializedClean() 187 188 defer func() { f.usingFile = nil }() 189 return f.usingFile.Close() 190 } 191 192 // Rotate forcefully rotates the file. If the generated file name 193 // clash because file already exists, a numeric suffix of the form 194 // ".1", ".2", ".3" and so forth are appended to the end of the log file 195 // 196 // This method can be used in conjunction with a signal handler so to 197 // emulate servers that generate new log files when they receive a SIGHUP 198 func (f *RotateFile) Rotate(forceRotate bool) error { 199 if err := f.checkValid("rotate"); err != nil { 200 return err 201 } 202 f.mu.Lock() 203 defer f.mu.Unlock() 204 if _, err := f.getWriterLocked(true, forceRotate); err != nil { 205 return err 206 } 207 return nil 208 } 209 210 func (f *RotateFile) filePathByRotateTime() string { 211 // create a new file name using the regular time layout 212 return f.FilePathPrefix + time_.TruncateByLocation(time.Now(), f.RotateInterval).Format(f.FilePathRotateLayout) 213 } 214 215 func (f *RotateFile) filePathByRotateSize() (name string, seq int) { 216 // instead of just using the regular time layout, 217 // we create a new file name using names such as "foo.1", "foo.2", "foo.3", etc 218 return nextSeqFileName(f.filePathByRotateTime(), f.usingSeq) 219 } 220 221 func (f *RotateFile) filePathByRotate(forceRotate bool) (name string, seq int, byTime, bySize bool) { 222 // name using the regular time layout, without seq 223 name = f.filePathByRotateTime() 224 // startup 225 if f.usingFilePath == "" { 226 if f.ForceNewFileOnStartup { 227 // instead of just using the regular time layout, 228 // we create a new file name using names such as "foo", "foo.1", "foo.2", "foo.3", etc 229 name, seq = nextSeqFileName(name, f.usingSeq) 230 return name, seq, false, true 231 } 232 name, seq = maxSeqFileName(name) 233 return name, seq, true, false 234 } 235 236 // rotate by time 237 // compare expect time with current using file 238 if name != trimSeqFromNextFileName(f.usingFilePath, f.usingSeq) { 239 if forceRotate { 240 // instead of just using the regular time layout, 241 // we create a new file name using names such as "foo", "foo.1", "foo.2", "foo.3", etc 242 name, seq = nextSeqFileName(name, 0) 243 return name, seq, true, false 244 } 245 name, seq = maxSeqFileName(name) 246 return name, seq, true, false 247 } 248 249 // determine if rotate by size 250 251 // recreate file if file not exist, removed by other process for example 252 usingFileInfo, err := os.Stat(f.usingFilePath) 253 if os.IsNotExist(err) { 254 name = f.usingFilePath 255 seq = f.usingSeq 256 return name, seq, false, false 257 } 258 259 // rotate by size 260 // compare rotate size with current using file 261 if forceRotate || (err == nil && (f.RotateSize > 0 && usingFileInfo.Size() > f.RotateSize)) { 262 // instead of just using the regular time layout, 263 // we create a new file name using names such as "foo", "foo.1", "foo.2", "foo.3", etc 264 name, seq = nextSeqFileName(name, f.usingSeq) 265 return name, seq, false, true 266 } 267 name = f.usingFilePath 268 seq = f.usingSeq 269 return name, seq, false, false 270 } 271 272 func (f *RotateFile) makeUsingFileReadyLocked() error { 273 // using file exist, close this file if not ready to use 274 if f.usingFile != nil { 275 _, err := os.Stat(f.usingFile.Name()) 276 if err == nil { 277 return nil 278 } 279 _ = f.usingFile.Close() 280 f.usingFile = nil 281 } 282 283 // using file not exist, recreate the file and file link 284 file, err := AppendAllIfNotExist(f.usingFilePath) 285 if err != nil { 286 return err 287 } 288 289 // link -> filename 290 if f.FileLinkPath != "" { 291 if err := ReSymlink(f.usingFilePath, f.FileLinkPath); err != nil { 292 return err 293 } 294 } 295 f.usingFile = file 296 return nil 297 298 } 299 300 func (f *RotateFile) getWriterLocked(bailOnRotateFail, forceRotate bool) (out io.Writer, err error) { 301 newName, newSeq, byTime, bySize := f.filePathByRotate(forceRotate) 302 if !byTime && !bySize { // no need to rotate by time and size 303 err = f.makeUsingFileReadyLocked() 304 if err != nil { 305 return nil, err 306 } 307 return f.usingFile, nil 308 } 309 310 // rotate the file 311 if f.PreRotateHandler != nil { 312 f.PreRotateHandler(f.usingFilePath) 313 } 314 newFile, err := f.rotateLocked(newName) 315 if err != nil { 316 if bailOnRotateFail { 317 // Failure to rotate is a problem, but it's really not a great idea 318 // to stop your application just because you couldn't rename your log. 319 // 320 // We only return this error when explicitly needed (as specified by bailOnRotateFail) 321 return nil, err 322 } 323 // no file can be written, it's an error explicitly 324 if f.usingFile == nil { 325 return nil, err 326 } 327 return f.usingFile, nil 328 } 329 330 // swap file 331 if f.usingFile != nil { 332 _ = f.usingFile.Close() 333 f.usingFile = nil 334 } 335 f.usingFile = newFile 336 f.usingFilePath = newName 337 f.usingSeq = newSeq 338 if f.PostRotateHandler != nil { 339 f.PostRotateHandler(f.usingFilePath) 340 } 341 342 return f.usingFile, nil 343 } 344 345 // file may not be nil if err is nil 346 func (f *RotateFile) rotateLocked(newName string) (*os.File, error) { 347 // if we got here, then we need to create a file 348 if newName != f.usingFilePath { 349 var err error 350 var mode string 351 switch f.RotateMode { 352 case RotateModeCopyRename: 353 // for which open the file, and write file not by RotateFile 354 // CopyRenameFileAll = RenameFileAll(src->dst) + OpenFile(src) 355 // usingFilePath -> newName + recreate usingFilePath 356 err = CopyRenameAll(newName, f.usingFilePath) 357 mode = "copy_rename" 358 case RotateModeCopyTruncate: 359 // for which open the file, and write file not by RotateFile 360 // CopyTruncateFile = CopyFile(src->dst) + Truncate(src) 361 // usingFilePath -> newName + truncate usingFilePath 362 err = CopyTruncateAll(newName, f.usingFilePath) 363 mode = "copy_truncate" 364 case RotateModeNew: 365 // for which open the file, and write file by RotateFile 366 mode = "new" 367 fallthrough 368 default: 369 } 370 if err != nil { 371 return nil, fmt.Errorf("failed to %s file %s to %s: %w", mode, f.usingFilePath, newName, err) 372 } 373 } 374 375 // make sure new file exists 376 file, err := AppendAllIfNotExist(newName) 377 if err != nil { 378 return nil, err 379 } 380 381 // link -> filename 382 if f.FileLinkPath != "" { 383 if err := ReSymlink(newName, f.FileLinkPath); err != nil { 384 return nil, err 385 } 386 } 387 // unlink files on a separate goroutine 388 go f.serializedClean() 389 390 return file, nil 391 } 392 393 // unlink files 394 // expect run on a separate goroutine 395 func (f *RotateFile) serializedClean() error { 396 // running already, ignore duplicate clean 397 if !f.cleaning.CAS(false, true) { 398 return nil 399 } 400 defer f.cleaning.Store(false) 401 402 now := time.Now() 403 404 // find old files 405 var filesNotExpired []string 406 filesExpired, err := filepath_.GlobFunc(f.FilePathPrefix+f.RotateFileGlob, func(name string) bool { 407 fi, err := os.Stat(name) 408 if err != nil { 409 return false 410 } 411 412 fl, err := os.Lstat(name) 413 if err != nil { 414 return false 415 } 416 if f.MaxAge <= 0 { 417 filesNotExpired = append(filesNotExpired, name) 418 return false 419 } 420 421 if now.Sub(fi.ModTime()) < f.MaxAge { 422 filesNotExpired = append(filesNotExpired, name) 423 return false 424 } 425 426 if fl.Mode()&os.ModeSymlink == os.ModeSymlink { 427 return false 428 } 429 return true 430 }) 431 if err != nil { 432 return err 433 } 434 435 var filesExceedMaxCount []string 436 if f.MaxCount > 0 && len(filesNotExpired) > 0 { 437 removeCount := len(filesNotExpired) - f.MaxCount 438 if removeCount < 0 { 439 removeCount = 0 440 } 441 sort.Sort(rotateFileSlice(filesNotExpired)) 442 filesExceedMaxCount = filesNotExpired[:removeCount] 443 } 444 var errs []error 445 for _, path := range filesExpired { 446 err = os.Remove(path) 447 if err != nil { 448 errs = append(errs, err) 449 } 450 } 451 for _, path := range filesExceedMaxCount { 452 err = os.Remove(path) 453 if err != nil { 454 errs = append(errs, err) 455 } 456 } 457 return errors.Join(errs...) 458 } 459 460 // checkValid checks whether f is valid for use. 461 // If not, it returns an appropriate error, perhaps incorporating the operation name op. 462 func (f *RotateFile) checkValid(op string) error { 463 if f == nil { 464 return os.ErrInvalid 465 } 466 return nil 467 } 468 469 // foo.txt, 0 -> foo.txt 470 // foo.txt, 1 -> foo.txt.[1,2,...], which is not exist and seq is max 471 func nextSeqFileName(name string, seq int) (string, int) { 472 // Special case: if seq is 0, we don't want to append 0 to the file name. 473 if seq == 0 { 474 nf, err := LockAll(name) 475 if !os.IsExist(err) { 476 return name, seq 477 } 478 _ = nf.Close() 479 seq++ 480 } 481 482 // A new file has been requested. Instead of just using the 483 // regular strftime pattern, we create a new file name using 484 // generational names such as "foo.1", "foo.2", "foo.3", etc 485 nf, seqUsed, err := NextFile(name+".*", seq) 486 if err != nil { 487 return name, seq 488 } 489 defer nf.Close() 490 if seqUsed == 0 { 491 return name, seqUsed 492 } 493 return nf.Name(), seqUsed 494 } 495 496 // foo.txt -> foo.txt 497 // foo.txt.1 -> foo.txt 498 // foo.txt.1.1 -> foo.txt.1 499 func trimSeqFromNextFileName(name string, seq int) string { 500 if seq == 0 { 501 return name 502 } 503 return strings.TrimSuffix(name, fmt.Sprintf(".%d", seq)) 504 } 505 506 // foo.txt.* -> foo.txt.[1,2,...], which exists and seq is max 507 func maxSeqFileName(name string) (string, int) { 508 prefix, seq, suffix := MaxSeq(name + ".*") 509 if seq == 0 { 510 return name, seq 511 } 512 return fmt.Sprintf("%s%d%s", prefix, seq, suffix), seq 513 } 514 515 // sort filename by mode time and ascii in increase order 516 type rotateFileSlice []string 517 518 func (s rotateFileSlice) Len() int { 519 return len(s) 520 } 521 func (s rotateFileSlice) Swap(i, j int) { 522 s[i], s[j] = s[j], s[i] 523 } 524 525 func (s rotateFileSlice) Less(i, j int) bool { 526 fi, err := os.Stat(s[i]) 527 if err != nil { 528 return false 529 } 530 fj, err := os.Stat(s[j]) 531 if err != nil { 532 return false 533 } 534 if fi.ModTime().Equal(fj.ModTime()) { 535 if len(s[i]) == len(s[j]) { 536 return s[i] < s[j] 537 } 538 return len(s[i]) > len(s[j]) // foo.1, foo.2, ..., foo 539 } 540 return fi.ModTime().Before(fj.ModTime()) 541 }