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