RotatingLog.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. from __future__ import annotations
  2. import os
  3. import time
  4. from typing import Iterable
  5. class RotatingLog:
  6. """
  7. An `open()` replacement that will automatically open and write
  8. to a new file if the prior file is too large or after a time interval.
  9. """
  10. def __init__(
  11. self,
  12. path: str = "./log_file",
  13. hourInterval: int | None = 24,
  14. megabyteLimit: int | None = 1024,
  15. ) -> None:
  16. """
  17. Args:
  18. path: a full or partial path with file name.
  19. hourInterval: the number of hours at which to rotate the file.
  20. megabyteLimit: the number of megabytes of file size the log may
  21. grow to, after which the log is rotated. Note: The log file
  22. may get a bit larger than limit do to writing out whole lines
  23. (last line may exceed megabyteLimit or "megabyteGuidline").
  24. """
  25. self.path = path
  26. self.timeInterval = None
  27. self.timeLimit = None
  28. self.sizeLimit = None
  29. if hourInterval is not None:
  30. self.timeInterval = hourInterval*60*60
  31. self.timeLimit = time.time()+self.timeInterval
  32. if megabyteLimit is not None:
  33. self.sizeLimit = megabyteLimit*1024*1024
  34. def __del__(self) -> None:
  35. self.close()
  36. def close(self) -> None:
  37. if hasattr(self, "file"):
  38. self.file.flush()
  39. self.file.close()
  40. self.closed = self.file.closed
  41. del self.file
  42. else:
  43. self.closed = True
  44. def shouldRotate(self) -> bool:
  45. """
  46. Returns a bool about whether a new log file should
  47. be created and written to (while at the same time
  48. stopping output to the old log file and closing it).
  49. """
  50. if not hasattr(self, "file"):
  51. return True
  52. if self.timeLimit is not None and time.time() > self.timeLimit:
  53. return True
  54. if self.sizeLimit is not None and self.file.tell() > self.sizeLimit:
  55. return True
  56. return False
  57. def filePath(self) -> str:
  58. dateString = time.strftime("%Y_%m_%d_%H", time.localtime())
  59. for i in range(26):
  60. limit = self.sizeLimit
  61. path = "%s_%s_%s.log" % (self.path, dateString, chr(i+97))
  62. if limit is None or not os.path.exists(path) or os.stat(path)[6] < limit:
  63. return path
  64. # Hmm, 26 files are full? throw the rest in z:
  65. # Maybe we should clear the self.sizeLimit here... maybe.
  66. return path
  67. def rotate(self) -> None:
  68. """
  69. Rotate the log now. You normally shouldn't need to call this.
  70. See write().
  71. """
  72. path=self.filePath()
  73. file=open(path, "a")
  74. if file:
  75. self.close()
  76. # This should be redundant with "a" open() mode,
  77. # but on some platforms tell() will return 0
  78. # until the first write:
  79. file.seek(0, 2)
  80. self.file=file
  81. # Some of these data members may be expected by some of our clients:
  82. self.closed = self.file.closed
  83. self.mode = self.file.mode
  84. self.name = self.file.name
  85. #self.encoding = self.file.encoding # Python 2.3
  86. #self.newlines = self.file.newlines # Python 2.3, maybe
  87. if self.timeLimit is not None and time.time() > self.timeLimit:
  88. assert self.timeInterval is not None
  89. self.timeLimit=time.time()+self.timeInterval
  90. else:
  91. # We'll keep writing to the old file, if available.
  92. print("RotatingLog error: Unable to open new log file \"%s\"." % (path,))
  93. def write(self, data: str) -> int | None:
  94. """
  95. Write the data to either the current log or a new one,
  96. depending on the return of shouldRotate() and whether
  97. the new file can be opened.
  98. """
  99. if self.shouldRotate():
  100. self.rotate()
  101. if hasattr(self, "file"):
  102. r = self.file.write(data)
  103. self.file.flush()
  104. return r
  105. return None
  106. def flush(self) -> None:
  107. return self.file.flush()
  108. def fileno(self) -> int:
  109. return self.file.fileno()
  110. def isatty(self) -> bool:
  111. return self.file.isatty()
  112. def __next__(self):
  113. return next(self.file)
  114. next = __next__
  115. def read(self, size):
  116. return self.file.read(size)
  117. def readline(self, size):
  118. return self.file.readline(size)
  119. def readlines(self, sizehint):
  120. return self.file.readlines(sizehint)
  121. def xreadlines(self):
  122. return self.file.xreadlines()
  123. def seek(self, offset: int, whence: int = 0) -> int:
  124. return self.file.seek(offset, whence)
  125. def tell(self) -> int:
  126. return self.file.tell()
  127. def truncate(self, size: int | None) -> int:
  128. return self.file.truncate(size)
  129. def writelines(self, sequence: Iterable[str]) -> None:
  130. return self.file.writelines(sequence)