gitee.com/lh-her-team/common@v1.5.1/log/file-rotatelogs/rotatelogs.go (about) 1 // package rotatelogs is a port of File-RotateLogs from Perl 2 // (https://metacpan.org/release/File-RotateLogs), and it allows 3 // you to automatically rotate output files when you write to them 4 // according to the filename pattern that you can specify. 5 package rotatelogs 6 7 import ( 8 "fmt" 9 "io" 10 "os" 11 "path/filepath" 12 "regexp" 13 "strings" 14 "sync" 15 "time" 16 17 strftime "github.com/lestrrat-go/strftime" 18 "github.com/pkg/errors" 19 ) 20 21 func (c clockFn) Now() time.Time { 22 return c() 23 } 24 25 // New creates a new RotateLogs object. A log filename pattern 26 // must be passed. Optional `Option` parameters may be passed 27 func New(p string, options ...Option) (*RotateLogs, error) { 28 globPattern := p 29 for _, re := range patternConversionRegexps { 30 globPattern = re.ReplaceAllString(globPattern, "*") 31 } 32 pattern, err := strftime.New(p) 33 if err != nil { 34 return nil, errors.Wrap(err, `invalid strftime pattern`) 35 } 36 rl := &RotateLogs{} 37 rl.apply(globPattern, pattern, options...) 38 if rl.maxAge > 0 && rl.rotationCount > 0 { 39 return nil, errors.New("options MaxAge and RotationCount cannot be both set") 40 } 41 if rl.maxAge == 0 && rl.rotationCount == 0 { 42 // if both are 0, give maxAge a sane default 43 rl.maxAge = 7 * 24 * time.Hour 44 } 45 return rl, nil 46 } 47 48 func (rl *RotateLogs) apply(globPattern string, pattern *strftime.Strftime, options ...Option) { 49 var ( 50 rotationSize int64 51 rotationCount uint 52 linkName string 53 maxAge time.Duration 54 handler Handler 55 forceNewFile bool 56 clock Clock = Local 57 rotationTime = 24 * time.Hour 58 ) 59 for _, o := range options { 60 switch o.Name() { 61 case optkeyClock: 62 clock, _ = o.Value().(Clock) 63 case optkeyLinkName: 64 linkName, _ = o.Value().(string) 65 case optkeyMaxAge: 66 maxAge, _ = o.Value().(time.Duration) 67 if maxAge < 0 { 68 maxAge = 0 69 } 70 case optkeyRotationTime: 71 rotationTime, _ = o.Value().(time.Duration) 72 if rotationTime < 0 { 73 rotationTime = 0 74 } 75 case optkeyRotationSize: 76 rotationSize, _ = o.Value().(int64) 77 if rotationSize < 0 { 78 rotationSize = 0 79 } 80 case optkeyRotationCount: 81 rotationCount, _ = o.Value().(uint) 82 case optkeyHandler: 83 handler, _ = o.Value().(Handler) 84 case optkeyForceNewFile: 85 forceNewFile = true 86 } 87 } 88 rl.clock = clock 89 rl.eventHandler = handler 90 rl.globPattern = globPattern 91 rl.linkName = linkName 92 rl.maxAge = maxAge 93 rl.pattern = pattern 94 rl.rotationTime = rotationTime 95 rl.rotationSize = rotationSize 96 rl.rotationCount = rotationCount 97 rl.forceNewFile = forceNewFile 98 } 99 100 func (rl *RotateLogs) genFilename() string { 101 now := rl.clock.Now() 102 // XXX HACK: Truncate only happens in UTC semantics, apparently. 103 // observed values for truncating given time with 86400 secs: 104 // 105 // before truncation: 2018/06/01 03:54:54 2018-06-01T03:18:00+09:00 106 // after truncation: 2018/06/01 03:54:54 2018-05-31T09:00:00+09:00 107 // 108 // This is really annoying when we want to truncate in local time 109 // so we hack: we take the apparent local time in the local zone, 110 // and pretend that it's in UTC. do our math, and put it back to 111 // the local zone 112 var base time.Time 113 if now.Location() != time.UTC { 114 base = time.Date( 115 now.Year(), 116 now.Month(), 117 now.Day(), 118 now.Hour(), 119 now.Minute(), 120 now.Second(), 121 now.Nanosecond(), 122 time.UTC, 123 ) 124 base = base.Truncate(time.Duration(rl.rotationTime)) 125 base = time.Date( 126 base.Year(), 127 base.Month(), 128 base.Day(), 129 base.Hour(), 130 base.Minute(), 131 base.Second(), 132 base.Nanosecond(), 133 base.Location(), 134 ) 135 } else { 136 base = now.Truncate(time.Duration(rl.rotationTime)) 137 } 138 return rl.pattern.FormatString(base) 139 } 140 141 // Write satisfies the io.Writer interface. It writes to the 142 // appropriate file handle that is currently being used. 143 // If we have reached rotation time, the target file gets 144 // automatically rotated, and also purged if necessary. 145 func (rl *RotateLogs) Write(p []byte) (n int, err error) { 146 // Guard against concurrent writes 147 rl.mutex.Lock() 148 defer rl.mutex.Unlock() 149 out, err := rl.getWriterNoLock(false, false) 150 if err != nil { 151 return 0, errors.Wrap(err, `failed to acquite target io.Writer`) 152 } 153 return out.Write(p) 154 } 155 156 // must be locked during this operation 157 func (rl *RotateLogs) getWriterNoLock(bailOnRotateFail, useGenerationalNames bool) (io.Writer, error) { 158 var ( 159 baseFn = rl.genFilename() 160 previousFn = rl.curFn 161 filename, generation = rl.findNextFile(baseFn, useGenerationalNames) 162 ) 163 if len(filename) == 0 { 164 return rl.outFh, nil 165 } 166 // make sure the dir is existed, eg: 167 // ./foo/bar/baz/hello.log must make sure ./foo/bar/baz is existed 168 dirname := filepath.Dir(filename) 169 if err := os.MkdirAll(dirname, 0755); err != nil { 170 return nil, errors.Wrapf(err, "failed to create directory %s", dirname) 171 } 172 // if we got here, then we need to create a file 173 fh, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) 174 if err != nil { 175 return nil, errors.Errorf("failed to open file %s: %s", rl.pattern, err) 176 } 177 if err := rl.rotateNoLock(filename); err != nil { 178 err = errors.Wrap(err, "failed to rotate") 179 if bailOnRotateFail { 180 // Failure to rotate is a problem, but it's really not a great 181 // idea to stop your application just because you couldn't rename 182 // your log. 183 // 184 // We only return this error when explicitly needed (as specified by bailOnRotateFail) 185 // 186 // However, we *NEED* to close `fh` here 187 if fh != nil { // probably can't happen, but being paranoid 188 fh.Close() 189 } 190 return nil, err 191 } 192 } 193 rl.outFh.Close() 194 rl.outFh = fh 195 rl.curBaseFn = baseFn 196 rl.curFn = filename 197 rl.generation = generation 198 if h := rl.eventHandler; h != nil { 199 go h.Handle(&FileRotatedEvent{ 200 prev: previousFn, 201 current: filename, 202 }) 203 } 204 return fh, nil 205 } 206 207 func (rl *RotateLogs) findNextFile(baseFn string, useGenerationalNames bool) (string, int) { 208 var ( 209 forceNewFile bool 210 generation = rl.generation 211 filename = baseFn 212 sizeRotation = false 213 ) 214 fi, err := os.Stat(rl.curFn) 215 if err == nil && rl.rotationSize > 0 && rl.rotationSize <= fi.Size() { 216 forceNewFile = true 217 sizeRotation = true 218 } 219 if baseFn != rl.curBaseFn { 220 generation = 0 221 // even though this is the first write after calling New(), 222 // check if a new file needs to be created 223 if rl.forceNewFile { 224 forceNewFile = true 225 } 226 } else { 227 if !useGenerationalNames && !sizeRotation { 228 // nothing to do 229 return "", -1 230 } 231 forceNewFile = true 232 generation++ 233 } 234 if forceNewFile { 235 // A new file has been requested. Instead of just using the 236 // regular strftime pattern, we create a new file name using 237 // generational names such as "foo.1", "foo.2", "foo.3", etc 238 var name string 239 for { 240 if generation == 0 { 241 name = filename 242 } else { 243 name = fmt.Sprintf("%s.%d", filename, generation) 244 } 245 if _, err := os.Stat(name); err != nil { 246 filename = name 247 break 248 } 249 generation++ 250 } 251 } 252 return filename, generation 253 } 254 255 // CurrentFileName returns the current file name that 256 // the RotateLogs object is writing to 257 func (rl *RotateLogs) CurrentFileName() string { 258 rl.mutex.RLock() 259 defer rl.mutex.RUnlock() 260 return rl.curFn 261 } 262 263 var patternConversionRegexps = []*regexp.Regexp{ 264 regexp.MustCompile(`%[%+A-Za-z]`), 265 regexp.MustCompile(`\*+`), 266 } 267 268 type cleanupGuard struct { 269 enable bool 270 fn func() 271 mutex sync.Mutex 272 } 273 274 func (g *cleanupGuard) Enable() { 275 g.mutex.Lock() 276 defer g.mutex.Unlock() 277 g.enable = true 278 } 279 func (g *cleanupGuard) Run() { 280 g.fn() 281 } 282 283 // Rotate forcefully rotates the log files. If the generated file name 284 // clash because file already exists, a numeric suffix of the form 285 // ".1", ".2", ".3" and so forth are appended to the end of the log file 286 // 287 // Thie method can be used in conjunction with a signal handler so to 288 // emulate servers that generate new log files when they receive a 289 // SIGHUP 290 func (rl *RotateLogs) Rotate() error { 291 rl.mutex.Lock() 292 defer rl.mutex.Unlock() 293 if _, err := rl.getWriterNoLock(true, true); err != nil { 294 return err 295 } 296 return nil 297 } 298 299 func (rl *RotateLogs) rotateNoLock(filename string) error { 300 lockfn := filename + `_lock` 301 fh, err := os.OpenFile(lockfn, os.O_CREATE|os.O_EXCL, 0644) 302 if err != nil { 303 // Can't lock, just return 304 return err 305 } 306 var guard = cleanupGuard{ 307 fn: func() { 308 fh.Close() 309 os.Remove(lockfn) 310 }, 311 } 312 defer guard.Run() 313 if err = rl.linkFile(filename); err != nil { 314 return err 315 } 316 if rl.maxAge <= 0 && rl.rotationCount <= 0 { 317 return errors.New("panic: maxAge and rotationCount are both set") 318 } 319 matches, err := filepath.Glob(rl.globPattern) 320 if err != nil { 321 return err 322 } 323 cutoff := rl.clock.Now().Add(-1 * rl.maxAge) 324 toUnlink := rl.getUnLinkFiles(matches, cutoff) 325 if len(toUnlink) <= 0 { 326 return nil 327 } 328 guard.Enable() 329 go func() { 330 // unlink files on a separate goroutine 331 for _, path := range toUnlink { 332 os.Remove(path) 333 } 334 }() 335 return nil 336 } 337 338 func (rl *RotateLogs) linkFile(filename string) error { 339 if rl.linkName != "" { 340 tmpLinkName := filename + `_symlink` 341 // Change how the link name is generated based on where the 342 // target location is. if the location is directly underneath 343 // the main filename's parent directory, then we create a 344 // symlink with a relative path 345 var ( 346 linkDest = filename 347 linkDir = filepath.Dir(rl.linkName) 348 baseDir = filepath.Dir(filename) 349 ) 350 if strings.Contains(rl.linkName, baseDir) { 351 tmp, err := filepath.Rel(linkDir, filename) 352 if err != nil { 353 return errors.Wrapf(err, `failed to evaluate relative path from %#v to %#v`, baseDir, rl.linkName) 354 } 355 linkDest = tmp 356 } 357 if err := os.Symlink(linkDest, tmpLinkName); err != nil { 358 return errors.Wrap(err, `failed to create new symlink`) 359 } 360 // the directory where rl.linkName should be created must exist 361 if _, err := os.Stat(linkDir); err != nil { // Assume err != nil means the directory doesn't exist 362 if err := os.MkdirAll(linkDir, 0755); err != nil { 363 return errors.Wrapf(err, `failed to create directory %s`, linkDir) 364 } 365 } 366 if err := os.Rename(tmpLinkName, rl.linkName); err != nil { 367 return errors.Wrap(err, `failed to rename new symlink`) 368 } 369 } 370 return nil 371 } 372 373 func (rl *RotateLogs) getUnLinkFiles(matches []string, cutoff time.Time) []string { 374 var toUnlink []string 375 for _, path := range matches { 376 // Ignore lock files 377 if strings.HasSuffix(path, "_lock") || strings.HasSuffix(path, "_symlink") { 378 continue 379 } 380 fi, err := os.Stat(path) 381 if err != nil { 382 continue 383 } 384 fl, err := os.Lstat(path) 385 if err != nil { 386 continue 387 } 388 if rl.maxAge > 0 && fi.ModTime().After(cutoff) { 389 continue 390 } 391 if rl.rotationCount > 0 && fl.Mode()&os.ModeSymlink == os.ModeSymlink { 392 continue 393 } 394 toUnlink = append(toUnlink, path) 395 } 396 if rl.rotationCount > 0 { 397 // Only delete if we have more than rotationCount 398 if rl.rotationCount >= uint(len(toUnlink)) { 399 return nil 400 } 401 402 toUnlink = toUnlink[:len(toUnlink)-int(rl.rotationCount)] 403 } 404 return toUnlink 405 } 406 407 // Close satisfies the io.Closer interface. You must 408 // call this method if you performed any writes to 409 // the object. 410 func (rl *RotateLogs) Close() error { 411 rl.mutex.Lock() 412 defer rl.mutex.Unlock() 413 if rl.outFh == nil { 414 return nil 415 } 416 rl.outFh.Close() 417 rl.outFh = nil 418 return nil 419 }