github.com/lingyao2333/mo-zero@v1.4.1/core/logx/rotatelogger.go (about) 1 package logx 2 3 import ( 4 "compress/gzip" 5 "errors" 6 "fmt" 7 "io" 8 "log" 9 "os" 10 "path" 11 "path/filepath" 12 "sort" 13 "strings" 14 "sync" 15 "time" 16 17 "github.com/lingyao2333/mo-zero/core/fs" 18 "github.com/lingyao2333/mo-zero/core/lang" 19 ) 20 21 const ( 22 dateFormat = "2006-01-02" 23 fileTimeFormat = time.RFC3339 24 hoursPerDay = 24 25 bufferSize = 100 26 defaultDirMode = 0o755 27 defaultFileMode = 0o600 28 gzipExt = ".gz" 29 megaBytes = 1 << 20 30 ) 31 32 // ErrLogFileClosed is an error that indicates the log file is already closed. 33 var ErrLogFileClosed = errors.New("error: log file closed") 34 35 type ( 36 // A RotateRule interface is used to define the log rotating rules. 37 RotateRule interface { 38 BackupFileName() string 39 MarkRotated() 40 OutdatedFiles() []string 41 ShallRotate(size int64) bool 42 } 43 44 // A RotateLogger is a Logger that can rotate log files with given rules. 45 RotateLogger struct { 46 filename string 47 backup string 48 fp *os.File 49 channel chan []byte 50 done chan lang.PlaceholderType 51 rule RotateRule 52 compress bool 53 // can't use threading.RoutineGroup because of cycle import 54 waitGroup sync.WaitGroup 55 closeOnce sync.Once 56 currentSize int64 57 } 58 59 // A DailyRotateRule is a rule to daily rotate the log files. 60 DailyRotateRule struct { 61 rotatedTime string 62 filename string 63 delimiter string 64 days int 65 gzip bool 66 } 67 68 // SizeLimitRotateRule a rotation rule that make the log file rotated base on size 69 SizeLimitRotateRule struct { 70 DailyRotateRule 71 maxSize int64 72 maxBackups int 73 } 74 ) 75 76 // DefaultRotateRule is a default log rotating rule, currently DailyRotateRule. 77 func DefaultRotateRule(filename, delimiter string, days int, gzip bool) RotateRule { 78 return &DailyRotateRule{ 79 rotatedTime: getNowDate(), 80 filename: filename, 81 delimiter: delimiter, 82 days: days, 83 gzip: gzip, 84 } 85 } 86 87 // BackupFileName returns the backup filename on rotating. 88 func (r *DailyRotateRule) BackupFileName() string { 89 return fmt.Sprintf("%s%s%s", r.filename, r.delimiter, getNowDate()) 90 } 91 92 // MarkRotated marks the rotated time of r to be the current time. 93 func (r *DailyRotateRule) MarkRotated() { 94 r.rotatedTime = getNowDate() 95 } 96 97 // OutdatedFiles returns the files that exceeded the keeping days. 98 func (r *DailyRotateRule) OutdatedFiles() []string { 99 if r.days <= 0 { 100 return nil 101 } 102 103 var pattern string 104 if r.gzip { 105 pattern = fmt.Sprintf("%s%s*%s", r.filename, r.delimiter, gzipExt) 106 } else { 107 pattern = fmt.Sprintf("%s%s*", r.filename, r.delimiter) 108 } 109 110 files, err := filepath.Glob(pattern) 111 if err != nil { 112 Errorf("failed to delete outdated log files, error: %s", err) 113 return nil 114 } 115 116 var buf strings.Builder 117 boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay*r.days)).Format(dateFormat) 118 buf.WriteString(r.filename) 119 buf.WriteString(r.delimiter) 120 buf.WriteString(boundary) 121 if r.gzip { 122 buf.WriteString(gzipExt) 123 } 124 boundaryFile := buf.String() 125 126 var outdates []string 127 for _, file := range files { 128 if file < boundaryFile { 129 outdates = append(outdates, file) 130 } 131 } 132 133 return outdates 134 } 135 136 // ShallRotate checks if the file should be rotated. 137 func (r *DailyRotateRule) ShallRotate(_ int64) bool { 138 return len(r.rotatedTime) > 0 && getNowDate() != r.rotatedTime 139 } 140 141 // NewSizeLimitRotateRule returns the rotation rule with size limit 142 func NewSizeLimitRotateRule(filename, delimiter string, days, maxSize, maxBackups int, gzip bool) RotateRule { 143 return &SizeLimitRotateRule{ 144 DailyRotateRule: DailyRotateRule{ 145 rotatedTime: getNowDateInRFC3339Format(), 146 filename: filename, 147 delimiter: delimiter, 148 days: days, 149 gzip: gzip, 150 }, 151 maxSize: int64(maxSize) * megaBytes, 152 maxBackups: maxBackups, 153 } 154 } 155 156 func (r *SizeLimitRotateRule) BackupFileName() string { 157 dir := filepath.Dir(r.filename) 158 prefix, ext := r.parseFilename() 159 timestamp := getNowDateInRFC3339Format() 160 return filepath.Join(dir, fmt.Sprintf("%s%s%s%s", prefix, r.delimiter, timestamp, ext)) 161 } 162 163 func (r *SizeLimitRotateRule) MarkRotated() { 164 r.rotatedTime = getNowDateInRFC3339Format() 165 } 166 167 func (r *SizeLimitRotateRule) OutdatedFiles() []string { 168 dir := filepath.Dir(r.filename) 169 prefix, ext := r.parseFilename() 170 171 var pattern string 172 if r.gzip { 173 pattern = fmt.Sprintf("%s%s%s%s*%s%s", dir, string(filepath.Separator), 174 prefix, r.delimiter, ext, gzipExt) 175 } else { 176 pattern = fmt.Sprintf("%s%s%s%s*%s", dir, string(filepath.Separator), 177 prefix, r.delimiter, ext) 178 } 179 180 files, err := filepath.Glob(pattern) 181 if err != nil { 182 Errorf("failed to delete outdated log files, error: %s", err) 183 return nil 184 } 185 186 sort.Strings(files) 187 188 outdated := make(map[string]lang.PlaceholderType) 189 190 // test if too many backups 191 if r.maxBackups > 0 && len(files) > r.maxBackups { 192 for _, f := range files[:len(files)-r.maxBackups] { 193 outdated[f] = lang.Placeholder 194 } 195 files = files[len(files)-r.maxBackups:] 196 } 197 198 // test if any too old backups 199 if r.days > 0 { 200 boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay*r.days)).Format(fileTimeFormat) 201 boundaryFile := filepath.Join(dir, fmt.Sprintf("%s%s%s%s", prefix, r.delimiter, boundary, ext)) 202 if r.gzip { 203 boundaryFile += gzipExt 204 } 205 for _, f := range files { 206 if f >= boundaryFile { 207 break 208 } 209 outdated[f] = lang.Placeholder 210 } 211 } 212 213 var result []string 214 for k := range outdated { 215 result = append(result, k) 216 } 217 return result 218 } 219 220 func (r *SizeLimitRotateRule) ShallRotate(size int64) bool { 221 return r.maxSize > 0 && r.maxSize < size 222 } 223 224 func (r *SizeLimitRotateRule) parseFilename() (prefix, ext string) { 225 logName := filepath.Base(r.filename) 226 ext = filepath.Ext(r.filename) 227 prefix = logName[:len(logName)-len(ext)] 228 return 229 } 230 231 // NewLogger returns a RotateLogger with given filename and rule, etc. 232 func NewLogger(filename string, rule RotateRule, compress bool) (*RotateLogger, error) { 233 l := &RotateLogger{ 234 filename: filename, 235 channel: make(chan []byte, bufferSize), 236 done: make(chan lang.PlaceholderType), 237 rule: rule, 238 compress: compress, 239 } 240 if err := l.init(); err != nil { 241 return nil, err 242 } 243 244 l.startWorker() 245 return l, nil 246 } 247 248 // Close closes l. 249 func (l *RotateLogger) Close() error { 250 var err error 251 252 l.closeOnce.Do(func() { 253 close(l.done) 254 l.waitGroup.Wait() 255 256 if err = l.fp.Sync(); err != nil { 257 return 258 } 259 260 err = l.fp.Close() 261 }) 262 263 return err 264 } 265 266 func (l *RotateLogger) Write(data []byte) (int, error) { 267 select { 268 case l.channel <- data: 269 return len(data), nil 270 case <-l.done: 271 log.Println(string(data)) 272 return 0, ErrLogFileClosed 273 } 274 } 275 276 func (l *RotateLogger) getBackupFilename() string { 277 if len(l.backup) == 0 { 278 return l.rule.BackupFileName() 279 } 280 281 return l.backup 282 } 283 284 func (l *RotateLogger) init() error { 285 l.backup = l.rule.BackupFileName() 286 287 if _, err := os.Stat(l.filename); err != nil { 288 basePath := path.Dir(l.filename) 289 if _, err = os.Stat(basePath); err != nil { 290 if err = os.MkdirAll(basePath, defaultDirMode); err != nil { 291 return err 292 } 293 } 294 295 if l.fp, err = os.Create(l.filename); err != nil { 296 return err 297 } 298 } else if l.fp, err = os.OpenFile(l.filename, os.O_APPEND|os.O_WRONLY, defaultFileMode); err != nil { 299 return err 300 } 301 302 fs.CloseOnExec(l.fp) 303 304 return nil 305 } 306 307 func (l *RotateLogger) maybeCompressFile(file string) { 308 if !l.compress { 309 return 310 } 311 312 defer func() { 313 if r := recover(); r != nil { 314 ErrorStack(r) 315 } 316 }() 317 318 if _, err := os.Stat(file); err != nil { 319 // file not exists or other error, ignore compression 320 return 321 } 322 323 compressLogFile(file) 324 } 325 326 func (l *RotateLogger) maybeDeleteOutdatedFiles() { 327 files := l.rule.OutdatedFiles() 328 for _, file := range files { 329 if err := os.Remove(file); err != nil { 330 Errorf("failed to remove outdated file: %s", file) 331 } 332 } 333 } 334 335 func (l *RotateLogger) postRotate(file string) { 336 go func() { 337 // we cannot use threading.GoSafe here, because of import cycle. 338 l.maybeCompressFile(file) 339 l.maybeDeleteOutdatedFiles() 340 }() 341 } 342 343 func (l *RotateLogger) rotate() error { 344 if l.fp != nil { 345 err := l.fp.Close() 346 l.fp = nil 347 if err != nil { 348 return err 349 } 350 } 351 352 _, err := os.Stat(l.filename) 353 if err == nil && len(l.backup) > 0 { 354 backupFilename := l.getBackupFilename() 355 err = os.Rename(l.filename, backupFilename) 356 if err != nil { 357 return err 358 } 359 360 l.postRotate(backupFilename) 361 } 362 363 l.backup = l.rule.BackupFileName() 364 if l.fp, err = os.Create(l.filename); err == nil { 365 fs.CloseOnExec(l.fp) 366 } 367 368 return err 369 } 370 371 func (l *RotateLogger) startWorker() { 372 l.waitGroup.Add(1) 373 374 go func() { 375 defer l.waitGroup.Done() 376 377 for { 378 select { 379 case event := <-l.channel: 380 l.write(event) 381 case <-l.done: 382 return 383 } 384 } 385 }() 386 } 387 388 func (l *RotateLogger) write(v []byte) { 389 if l.rule.ShallRotate(l.currentSize + int64(len(v))) { 390 if err := l.rotate(); err != nil { 391 log.Println(err) 392 } else { 393 l.rule.MarkRotated() 394 l.currentSize = 0 395 } 396 } 397 if l.fp != nil { 398 l.fp.Write(v) 399 l.currentSize += int64(len(v)) 400 } 401 } 402 403 func compressLogFile(file string) { 404 start := time.Now() 405 Infof("compressing log file: %s", file) 406 if err := gzipFile(file); err != nil { 407 Errorf("compress error: %s", err) 408 } else { 409 Infof("compressed log file: %s, took %s", file, time.Since(start)) 410 } 411 } 412 413 func getNowDate() string { 414 return time.Now().Format(dateFormat) 415 } 416 417 func getNowDateInRFC3339Format() string { 418 return time.Now().Format(fileTimeFormat) 419 } 420 421 func gzipFile(file string) error { 422 in, err := os.Open(file) 423 if err != nil { 424 return err 425 } 426 defer in.Close() 427 428 out, err := os.Create(fmt.Sprintf("%s%s", file, gzipExt)) 429 if err != nil { 430 return err 431 } 432 defer out.Close() 433 434 w := gzip.NewWriter(out) 435 if _, err = io.Copy(w, in); err != nil { 436 return err 437 } else if err = w.Close(); err != nil { 438 return err 439 } 440 441 return os.Remove(file) 442 }