rotatelogs.go 10 KB

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