SizeRotatingDailyHandler.php 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. <?php
  2. namespace UCore\Logging;
  3. use Monolog\Handler\StreamHandler;
  4. use Monolog\LogRecord;
  5. use Monolog\Level;
  6. /**
  7. * 支持文件大小限制的每日轮转日志处理器
  8. *
  9. * 功能特点:
  10. * 1. 按日期创建日志文件(如:laravel-2025-05-26.log)
  11. * 2. 当文件大小超过限制时,自动分割为备份文件(如:laravel-2025-05-26-1.log)
  12. * 3. 支持设置最大文件数量,自动清理旧文件
  13. */
  14. class SizeRotatingDailyHandler extends StreamHandler
  15. {
  16. /**
  17. * 最大文件大小(字节)
  18. *
  19. * @var int
  20. */
  21. protected int $maxFileSize;
  22. /**
  23. * 基础文件名
  24. *
  25. * @var string
  26. */
  27. protected string $baseFilename;
  28. /**
  29. * 最大文件数量
  30. *
  31. * @var int
  32. */
  33. protected int $maxFiles;
  34. /**
  35. * 构造函数
  36. *
  37. * @param string $filename 日志文件路径
  38. * @param int $maxFiles 最大文件数量
  39. * @param int|string|Level $level 日志级别
  40. * @param bool $bubble 是否冒泡
  41. * @param int|null $filePermission 文件权限
  42. * @param bool $useLocking 是否使用文件锁
  43. * @param int $maxFileSize 最大文件大小(字节),默认100MB
  44. */
  45. public function __construct(
  46. string $filename,
  47. int $maxFiles = 0,
  48. int|string|Level $level = Level::Debug,
  49. bool $bubble = true,
  50. ?int $filePermission = null,
  51. bool $useLocking = false,
  52. int $maxFileSize = 104857600 // 100MB
  53. ) {
  54. $this->maxFileSize = $maxFileSize;
  55. $this->baseFilename = $filename;
  56. $this->maxFiles = $maxFiles;
  57. // 获取当前应该使用的文件名
  58. $currentFilename = $this->getCurrentFilename();
  59. parent::__construct($currentFilename, $level, $bubble, $filePermission, $useLocking);
  60. }
  61. /**
  62. * 获取当前应该使用的文件名
  63. *
  64. * @return string
  65. */
  66. protected function getCurrentFilename(): string
  67. {
  68. // 始终返回基础的时间文件名
  69. // 文件大小检查和轮转在写入时处理
  70. return $this->getTimedFilename();
  71. }
  72. /**
  73. * 获取带时间戳的文件名
  74. *
  75. * @return string
  76. */
  77. protected function getTimedFilename(): string
  78. {
  79. $fileInfo = pathinfo($this->baseFilename);
  80. $timedFilename = $fileInfo['dirname'] . '/' . $fileInfo['filename'] . '-' . date('Y-m-d');
  81. if (isset($fileInfo['extension'])) {
  82. $timedFilename .= '.' . $fileInfo['extension'];
  83. }
  84. return $timedFilename;
  85. }
  86. /**
  87. * 获取下一个可用的备份文件名
  88. *
  89. * @param string $currentFilename 当前文件名
  90. * @return string
  91. */
  92. protected function getNextBackupFilename(string $currentFilename): string
  93. {
  94. $fileInfo = pathinfo($currentFilename);
  95. $baseName = $fileInfo['filename'];
  96. $extension = isset($fileInfo['extension']) ? '.' . $fileInfo['extension'] : '';
  97. $directory = $fileInfo['dirname'].'/size_rotating_daily';
  98. if(!is_dir($directory)){
  99. mkdir($directory);
  100. }
  101. // 检查是否已经是分割文件(包含日期和计数器)
  102. // 格式:filename-YYYY-MM-DD-N 或 filename-YYYY-MM-DD
  103. if (preg_match('/^(.+)-(\d{4}-\d{2}-\d{2})$/', $baseName, $matches)) {
  104. // 是日期文件但还没有计数器,从1开始
  105. $baseNameWithDate = $baseName;
  106. $startCounter = 1;
  107. } else {
  108. // 不是预期的格式,直接添加计数器
  109. $baseNameWithDate = $baseName;
  110. $startCounter = 1;
  111. }
  112. $counter = $startCounter;
  113. do {
  114. $backupFilename = $directory . '/' . $baseNameWithDate . '-' . $counter . $extension;
  115. $counter++;
  116. } while (file_exists($backupFilename));
  117. return $backupFilename;
  118. }
  119. /**
  120. * 重写写入方法,添加文件大小检查
  121. *
  122. * @param LogRecord $record
  123. * @return void
  124. */
  125. protected function write(LogRecord $record): void
  126. {
  127. // 先调用父类写入方法
  128. parent::write($record);
  129. // 在写入后检查文件大小,如果超过限制则进行分割
  130. if ($this->url && file_exists($this->url) && filesize($this->url) >= $this->maxFileSize) {
  131. // 进行文件分割
  132. $this->rotateDueToSize();
  133. }
  134. }
  135. /**
  136. * 由于文件大小超限而进行轮转
  137. *
  138. * @return void
  139. */
  140. protected function rotateDueToSize(): void
  141. {
  142. // 关闭当前文件句柄
  143. if ($this->stream) {
  144. fclose($this->stream);
  145. $this->stream = null;
  146. }
  147. // 获取备份文件名
  148. $backupFilename = $this->getNextBackupFilename($this->url);
  149. // 将当前文件重命名为备份文件
  150. if (file_exists($this->url)) {
  151. rename($this->url, $backupFilename);
  152. }
  153. // 继续使用原始文件名,下次写入时会创建新的空文件
  154. // 由于 $this->stream 已经设置为 null,父类的 write 方法会重新打开文件
  155. }
  156. }