github.com/wfusion/gofusion@v1.1.14/common/infra/rotatelog/rotatelog.go (about) 1 // Package rotatelog provides a rolling logger. 2 // 3 // Note that this is v2.0 of lumberjack, and should be imported using gopkg.in 4 // 5 // Lumberjack is intended to be one part of a logging infrastructure. 6 // It is not an all-in-one solution, but instead is a pluggable 7 // component at the bottom of the logging stack that simply controls the files 8 // to which logs are written. 9 // 10 // Lumberjack plays well with any logging package that can write to an 11 // io.Writer, including the standard library's log package. 12 // 13 // Lumberjack assumes that only one process is writing to the output files. 14 // Using the same lumberjack configuration from multiple processes on the same 15 // machine will result in improper behavior. 16 package rotatelog 17 18 import ( 19 "compress/gzip" 20 "errors" 21 "fmt" 22 "io" 23 "io/ioutil" 24 "os" 25 "path/filepath" 26 "sort" 27 "strings" 28 "sync" 29 "time" 30 31 "github.com/dustin/go-humanize" 32 "go.uber.org/atomic" 33 34 "github.com/wfusion/gofusion/common/utils" 35 ) 36 37 const ( 38 backupTimeFormat = "2006-01-02T15-04-05.000" 39 compressSuffix = ".gz" 40 defaultMaxSize = 100 41 ) 42 43 // ensure we always implement io.WriteCloser 44 var _ io.WriteCloser = (*Logger)(nil) 45 46 // Logger is an io.WriteCloser that writes to the specified filename. 47 // 48 // Logger opens or creates the logfile on first Write. If the file exists and 49 // is less than MaxSize megabytes, lumberjack will open and append to that file. 50 // If the file exists and its size is >= MaxSize megabytes, the file is renamed 51 // by putting the current time in a timestamp in the name immediately before the 52 // file's extension (or the end of the filename if there's no extension). A new 53 // log file is then created using original filename. 54 // 55 // Whenever a write would cause the current log file exceed MaxSize megabytes, 56 // the current file is closed, renamed, and a new log file created with the 57 // original name. Thus, the filename you give Logger is always the "current" log 58 // file. 59 // 60 // Backups use the log file name given to Logger, in the form 61 // `name-timestamp.ext` where name is the filename without the extension, 62 // timestamp is the time at which the log was rotated formatted with the 63 // time.Time format of `2006-01-02T15-04-05.000` and the extension is the 64 // original extension. For example, if your Logger.Filename is 65 // `/var/log/foo/server.log`, a backup created at 6:30pm on Nov 11 2016 would 66 // use the filename `/var/log/foo/server-2016-11-04T18-30-00.000.log` 67 // 68 // Cleaning Up Old Log Files 69 // 70 // Whenever a new logfile gets created, old log files may be deleted. The most 71 // recent files according to the encoded timestamp will be retained, up to a 72 // number equal to MaxBackups (or all of them if MaxBackups is 0). Any files 73 // with an encoded timestamp older than MaxAge days are deleted, regardless of 74 // MaxBackups. Note that the time encoded in the timestamp is the rotation 75 // time, which may differ from the last time that file was written to. 76 // 77 // If MaxBackups and MaxAge are both 0, no old log files will be deleted. 78 type Logger struct { 79 // Filename is the file to write logs to. Backup log files will be retained 80 // in the same directory. It uses <processname>-rotate.log in 81 // os.TempDir() if empty. 82 Filename string `json:"filename" yaml:"filename" toml:"filename"` 83 84 // MaxSize is the maximum size in megabytes of the log file before it gets 85 // rotated. It defaults to 100 megabytes. 86 MaxSize int64 `json:"maxsize" yaml:"maxsize" toml:"maxsize"` 87 88 // MaxAge is the maximum number of days to retain old log files based on the 89 // timestamp encoded in their filename. Note that may not exactly correspond 90 // to calendar days due to daylight savings, leap seconds, etc. 91 // The default is not to remove old log files based on age. 92 MaxAge time.Duration `json:"maxage" yaml:"maxage" toml:"maxage"` 93 94 // MaxBackups is the maximum number of old log files to retain. The default 95 // is to retain all old log files (though MaxAge may still cause them to get 96 // deleted.) 97 MaxBackups int `json:"maxbackups" yaml:"maxbackups" toml:"maxbackups"` 98 99 // LocalTime determines if the time used for formatting the timestamps in 100 // backup files is the computer's local time. The default is to use UTC 101 // time. 102 LocalTime bool `json:"localtime" yaml:"localtime" toml:"localtime"` 103 104 // Compress determines if the rotated log files should be compressed 105 // using gzip. The default is not to perform compression. 106 Compress bool `json:"compress" yaml:"compress" toml:"compress"` 107 108 size atomic.Int64 109 file *os.File 110 mu sync.Mutex 111 112 millCh chan bool 113 startMill sync.Once 114 } 115 116 var ( 117 // currentTime exists so it can be mocked out by tests. 118 currentTime = time.Now 119 120 // os_Stat exists so it can be mocked out by tests. 121 osStat = os.Stat 122 ) 123 124 // Write implements io.Writer. If a write would cause the log file to be larger 125 // than MaxSize, the file is closed, renamed to include a timestamp of the 126 // current time, and a new log file is created using the original log file name. 127 // If the length of the write is greater than MaxSize, an error is returned. 128 func (l *Logger) Write(p []byte) (n int, err error) { 129 l.mu.Lock() 130 defer l.mu.Unlock() 131 132 writeLen := int64(len(p)) 133 if writeLen > l.max() { 134 return 0, fmt.Errorf( 135 "write length %d exceeds maximum file size %d", writeLen, l.max(), 136 ) 137 } 138 139 if l.file == nil { 140 if err = l.openExistingOrNew(len(p)); err != nil { 141 return 0, err 142 } 143 } 144 145 if l.size.Load()+writeLen > l.max() { 146 if err := l.rotate(); err != nil { 147 return 0, err 148 } 149 } 150 151 n, err = l.file.Write(p) 152 l.size.Add(int64(n)) 153 154 return n, err 155 } 156 157 // Close implements io.Closer, and closes the current logfile. 158 func (l *Logger) Close() error { 159 l.mu.Lock() 160 defer l.mu.Unlock() 161 return l.close() 162 } 163 164 // close closes the file if it is open. 165 func (l *Logger) close() error { 166 if l.file == nil { 167 return nil 168 } 169 err := l.file.Close() 170 l.file = nil 171 return err 172 } 173 174 // Rotate causes Logger to close the existing log file and immediately create a 175 // new one. This is a helper function for applications that want to initiate 176 // rotations outside of the normal rotation rules, such as in response to 177 // SIGHUP. After rotating, this initiates compression and removal of old log 178 // files according to the configuration. 179 func (l *Logger) Rotate() error { 180 l.mu.Lock() 181 defer l.mu.Unlock() 182 return l.rotate() 183 } 184 185 // rotate closes the current file, moves it aside with a timestamp in the name, 186 // (if it exists), opens a new file with the original filename, and then runs 187 // post-rotation processing and removal. 188 func (l *Logger) rotate() error { 189 if err := l.close(); err != nil { 190 return err 191 } 192 if err := l.openNew(); err != nil { 193 return err 194 } 195 l.mill() 196 return nil 197 } 198 199 // openNew opens a new log file for writing, moving any old log file out of the 200 // way. This methods assumes the file has already been closed. 201 func (l *Logger) openNew() error { 202 err := os.MkdirAll(l.dir(), 0755) 203 if err != nil { 204 return fmt.Errorf("can't make directories for new logfile: %s", err) 205 } 206 207 name := l.filename() 208 mode := os.FileMode(0600) 209 info, err := osStat(name) 210 if err == nil { 211 // Copy the mode off the old logfile. 212 mode = info.Mode() 213 // move the existing file 214 newname := backupName(name, l.LocalTime) 215 if err := os.Rename(name, newname); err != nil { 216 return fmt.Errorf("can't rename log file: %s", err) 217 } 218 219 // this is a no-op anywhere but linux 220 if err := chown(name, info); err != nil { 221 return err 222 } 223 } 224 225 // we use truncate here because this should only get called when we've moved 226 // the file ourselves. if someone else creates the file in the meantime, 227 // just wipe out the contents. 228 f, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode) 229 if err != nil { 230 return fmt.Errorf("can't open new logfile: %s", err) 231 } 232 l.file = f 233 l.size.Store(0) 234 return nil 235 } 236 237 // backupName creates a new filename from the given name, inserting a timestamp 238 // between the filename and the extension, using the local time if requested 239 // (otherwise UTC). 240 func backupName(name string, local bool) string { 241 dir := filepath.Dir(name) 242 filename := filepath.Base(name) 243 ext := filepath.Ext(filename) 244 prefix := filename[:len(filename)-len(ext)] 245 t := currentTime() 246 if !local { 247 t = t.UTC() 248 } 249 250 timestamp := t.Format(backupTimeFormat) 251 return filepath.Join(dir, fmt.Sprintf("%s-%s%s", prefix, timestamp, ext)) 252 } 253 254 // openExistingOrNew opens the logfile if it exists and if the current write 255 // would not put it over MaxSize. If there is no such file or the write would 256 // put it over the MaxSize, a new file is created. 257 func (l *Logger) openExistingOrNew(writeLen int) error { 258 l.mill() 259 260 filename := l.filename() 261 info, err := osStat(filename) 262 if os.IsNotExist(err) { 263 return l.openNew() 264 } 265 if err != nil { 266 return fmt.Errorf("error getting log file info: %s", err) 267 } 268 269 if info.Size()+int64(writeLen) >= l.max() { 270 return l.rotate() 271 } 272 273 file, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0644) 274 if err != nil { 275 // if we fail to open the old log file for some reason, just ignore 276 // it and open a new log file. 277 return l.openNew() 278 } 279 l.file = file 280 l.size.Store(0) 281 return nil 282 } 283 284 // filename generates the name of the logfile from the current time. 285 func (l *Logger) filename() string { 286 if l.Filename != "" { 287 return l.Filename 288 } 289 name := filepath.Base(os.Args[0]) + "-rotate.log" 290 return filepath.Join(os.TempDir(), name) 291 } 292 293 // millRunOnce performs compression and removal of stale log files. 294 // Log files are compressed if enabled via configuration and old log 295 // files are removed, keeping at most l.MaxBackups files, as long as 296 // none of them are older than MaxAge. 297 func (l *Logger) millRunOnce() error { 298 if l.MaxBackups == 0 && l.MaxAge == 0 && !l.Compress { 299 return nil 300 } 301 302 files, err := l.oldLogFiles() 303 if err != nil { 304 return err 305 } 306 307 var compress, remove []logInfo 308 309 if l.MaxBackups > 0 && l.MaxBackups < len(files) { 310 preserved := make(map[string]bool) 311 var remaining []logInfo 312 for _, f := range files { 313 // Only count the uncompressed log file or the 314 // compressed log file, not both. 315 fn := f.Name() 316 fn = strings.TrimSuffix(fn, compressSuffix) 317 preserved[fn] = true 318 319 if len(preserved) > l.MaxBackups { 320 remove = append(remove, f) 321 } else { 322 remaining = append(remaining, f) 323 } 324 } 325 files = remaining 326 } 327 if l.MaxAge > 0 { 328 cutoff := currentTime().Add(-1 * l.MaxAge) 329 330 var remaining []logInfo 331 for _, f := range files { 332 if f.timestamp.Before(cutoff) { 333 remove = append(remove, f) 334 } else { 335 remaining = append(remaining, f) 336 } 337 } 338 files = remaining 339 } 340 341 if l.Compress { 342 for _, f := range files { 343 if !strings.HasSuffix(f.Name(), compressSuffix) { 344 compress = append(compress, f) 345 } 346 } 347 } 348 349 for _, f := range remove { 350 errRemove := os.Remove(filepath.Join(l.dir(), f.Name())) 351 if err == nil && errRemove != nil { 352 err = errRemove 353 } 354 } 355 for _, f := range compress { 356 fn := filepath.Join(l.dir(), f.Name()) 357 errCompress := compressLogFile(fn, fn+compressSuffix) 358 if err == nil && errCompress != nil { 359 err = errCompress 360 } 361 } 362 363 return err 364 } 365 366 // millRun runs in a goroutine to manage post-rotation compression and removal 367 // of old log files. 368 func (l *Logger) millRun() { 369 for range l.millCh { 370 // what am I going to do, log this? 371 _ = l.millRunOnce() 372 } 373 } 374 375 // mill performs post-rotation compression and removal of stale log files, 376 // starting the mill goroutine if necessary. 377 func (l *Logger) mill() { 378 l.startMill.Do(func() { 379 l.millCh = make(chan bool, 1) 380 go utils.Catch(func() { l.millRun() }) 381 }) 382 select { 383 case l.millCh <- true: 384 default: 385 } 386 } 387 388 // oldLogFiles returns the list of backup log files stored in the same 389 // directory as the current log file, sorted by ModTime 390 func (l *Logger) oldLogFiles() ([]logInfo, error) { 391 files, err := ioutil.ReadDir(l.dir()) 392 if err != nil { 393 return nil, fmt.Errorf("can't read log file directory: %s", err) 394 } 395 logFiles := make([]logInfo, 0, l.MaxBackups+1) 396 397 prefix, ext := l.prefixAndExt() 398 399 for _, f := range files { 400 if f.IsDir() { 401 continue 402 } 403 if t, err := l.timeFromName(f.Name(), prefix, ext); err == nil { 404 logFiles = append(logFiles, logInfo{t, f}) 405 continue 406 } 407 if t, err := l.timeFromName(f.Name(), prefix, ext+compressSuffix); err == nil { 408 logFiles = append(logFiles, logInfo{t, f}) 409 continue 410 } 411 // error parsing means that the suffix at the end was not generated 412 // by lumberjack, and therefore it's not a backup file. 413 } 414 415 sort.Sort(byFormatTime(logFiles)) 416 417 return logFiles, nil 418 } 419 420 // timeFromName extracts the formatted time from the filename by stripping off 421 // the filename's prefix and extension. This prevents someone's filename from 422 // confusing time.parse. 423 func (l *Logger) timeFromName(filename, prefix, ext string) (time.Time, error) { 424 if !strings.HasPrefix(filename, prefix) { 425 return time.Time{}, errors.New("mismatched prefix") 426 } 427 if !strings.HasSuffix(filename, ext) { 428 return time.Time{}, errors.New("mismatched extension") 429 } 430 ts := filename[len(prefix) : len(filename)-len(ext)] 431 return time.Parse(backupTimeFormat, ts) 432 } 433 434 // max returns the maximum size in bytes of log files before rolling. 435 func (l *Logger) max() int64 { 436 if l.MaxSize == 0 { 437 return int64(defaultMaxSize * humanize.MiByte) 438 } 439 return l.MaxSize 440 } 441 442 // dir returns the directory for the current filename. 443 func (l *Logger) dir() string { 444 return filepath.Dir(l.filename()) 445 } 446 447 // prefixAndExt returns the filename part and extension part from the Logger's 448 // filename. 449 func (l *Logger) prefixAndExt() (prefix, ext string) { 450 filename := filepath.Base(l.filename()) 451 ext = filepath.Ext(filename) 452 prefix = filename[:len(filename)-len(ext)] + "-" 453 return prefix, ext 454 } 455 456 // compressLogFile compresses the given log file, removing the 457 // uncompressed log file if successful. 458 func compressLogFile(src, dst string) (err error) { 459 f, err := os.Open(src) 460 if err != nil { 461 return fmt.Errorf("failed to open log file: %v", err) 462 } 463 defer f.Close() 464 465 fi, err := osStat(src) 466 if err != nil { 467 return fmt.Errorf("failed to stat log file: %v", err) 468 } 469 470 if err := chown(dst, fi); err != nil { 471 return fmt.Errorf("failed to chown compressed log file: %v", err) 472 } 473 474 // If this file already exists, we presume it was created by 475 // a previous attempt to compress the log file. 476 gzf, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, fi.Mode()) 477 if err != nil { 478 return fmt.Errorf("failed to open compressed log file: %v", err) 479 } 480 defer gzf.Close() 481 482 gz := gzip.NewWriter(gzf) 483 484 defer func() { 485 if err != nil { 486 _ = os.Remove(dst) 487 err = fmt.Errorf("failed to compress log file: %v", err) 488 } 489 }() 490 491 if _, err := io.Copy(gz, f); err != nil { 492 return err 493 } 494 if err := gz.Close(); err != nil { 495 return err 496 } 497 if err := gzf.Close(); err != nil { 498 return err 499 } 500 501 if err := f.Close(); err != nil { 502 return err 503 } 504 if err := os.Remove(src); err != nil { 505 return err 506 } 507 508 return nil 509 } 510 511 // logInfo is a convenience struct to return the filename and its embedded 512 // timestamp. 513 type logInfo struct { 514 timestamp time.Time 515 os.FileInfo 516 } 517 518 // byFormatTime sorts by newest time formatted in the name. 519 type byFormatTime []logInfo 520 521 func (b byFormatTime) Less(i, j int) bool { 522 return b[i].timestamp.After(b[j].timestamp) 523 } 524 525 func (b byFormatTime) Swap(i, j int) { 526 b[i], b[j] = b[j], b[i] 527 } 528 529 func (b byFormatTime) Len() int { 530 return len(b) 531 }