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