Log.php 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * MINZ - Copyright 2011 Marien Fressinaud
  5. * Licensed under AGPL3 <https://www.gnu.org/licenses/>
  6. */
  7. /**
  8. * The Minz_Log class is used to log errors and warnings
  9. */
  10. class Minz_Log {
  11. /**
  12. * Syslog priority corresponding to each value accepted by the `log_level` system setting,
  13. * from the most to the least severe.
  14. * @var array<string,int>
  15. */
  16. private const LOG_LEVELS = [
  17. 'error' => LOG_ERR,
  18. 'warning' => LOG_WARNING,
  19. 'notice' => LOG_NOTICE,
  20. 'info' => LOG_INFO,
  21. 'debug' => LOG_DEBUG,
  22. ];
  23. private static string $lastLog = '';
  24. /**
  25. * Records a message in a specific log file
  26. * The message is not logged if
  27. * - environment = SILENT
  28. * - level is less severe than the threshold determined by `log_level`,
  29. * or, by default, by `environment` (PRODUCTION keeps only warnings and errors)
  30. * @param string $information error message / information to record
  31. * @param int $level error level https://www.php.net/function.syslog
  32. * @param string $file_name log file
  33. * @throws Minz_PermissionDeniedException
  34. */
  35. public static function record(string $information, int $level, ?string $file_name = null): void {
  36. $env = getenv('FRESHRSS_ENV');
  37. $log_level = '';
  38. try {
  39. $conf = Minz_Configuration::get('system');
  40. $log_level = $conf->log_level;
  41. if ($env == '') {
  42. $env = $conf->environment;
  43. }
  44. } catch (Minz_ConfigurationException $e) {
  45. if ($env == '') {
  46. $env = 'production';
  47. }
  48. }
  49. if ($log_level === '' || !isset(self::LOG_LEVELS[$log_level])) {
  50. $log_level = match ($env) {
  51. 'silent' => 'error',
  52. 'production' => 'warning',
  53. default => 'debug',
  54. };
  55. }
  56. self::$lastLog = $information;
  57. if (! ($env === 'silent' || $level > self::LOG_LEVELS[$log_level])) {
  58. $username = Minz_User::name() ?? Minz_User::INTERNAL_USER;
  59. if ($file_name == null) {
  60. $file_name = join_path(USERS_PATH, $username, LOG_FILENAME);
  61. }
  62. $level_labels = array_flip(self::LOG_LEVELS);
  63. if (!isset($level_labels[$level])) {
  64. $level = LOG_INFO;
  65. }
  66. $level_label = $level_labels[$level];
  67. $log = '[' . date('r') . '] [' . $level_label . '] --- ' . str_replace(["\r", "\n"], ' ', $information) . "\n";
  68. if (defined('COPY_LOG_TO_SYSLOG') && COPY_LOG_TO_SYSLOG) {
  69. syslog($level, '[' . $username . '] ' . trim($log));
  70. }
  71. self::ensureMaxLogSize($file_name);
  72. if (file_put_contents($file_name, $log, FILE_APPEND | LOCK_EX) === false) {
  73. throw new Minz_PermissionDeniedException($file_name, Minz_Exception::ERROR);
  74. }
  75. }
  76. }
  77. /**
  78. * Returns the most recently logged message as a string value, and clears it.
  79. * Mainly for use in PHPUnit tests.
  80. *
  81. * @phpstan-impure The return value might change, even though input arguments are the same between calls.
  82. */
  83. public static function getLastLog(bool $clear_last_log = true): string {
  84. $lastLog = self::$lastLog;
  85. if ($clear_last_log) {
  86. self::$lastLog = '';
  87. }
  88. return $lastLog;
  89. }
  90. /**
  91. * Make sure we do not waste a huge amount of disk space with old log messages.
  92. *
  93. * This method can be called multiple times for one script execution, but its result will not change unless
  94. * you call clearstatcache() in between. We won’t do do that for performance reasons.
  95. *
  96. * @throws Minz_PermissionDeniedException
  97. */
  98. protected static function ensureMaxLogSize(string $file_name): void {
  99. $maxSize = defined('MAX_LOG_SIZE') ? MAX_LOG_SIZE : 1048576;
  100. if ($maxSize > 0 && @filesize($file_name) > $maxSize) {
  101. $fp = fopen($file_name, 'c+');
  102. if (is_resource($fp) && flock($fp, LOCK_EX)) {
  103. fseek($fp, -(int)($maxSize / 2), SEEK_END);
  104. $content = fread($fp, $maxSize);
  105. rewind($fp);
  106. ftruncate($fp, 0);
  107. fwrite($fp, $content ?: '');
  108. fwrite($fp, sprintf("[%s] [notice] --- Log rotate.\n", date('r')));
  109. fflush($fp);
  110. flock($fp, LOCK_UN);
  111. } else {
  112. throw new Minz_PermissionDeniedException($file_name, Minz_Exception::ERROR);
  113. }
  114. fclose($fp);
  115. }
  116. }
  117. /**
  118. * Some helpers to Minz_Log::record() method
  119. * Parameters are the same of those of the record() method.
  120. * @throws Minz_PermissionDeniedException
  121. */
  122. public static function debug(string $msg, ?string $file_name = null): void {
  123. self::record($msg, LOG_DEBUG, $file_name);
  124. }
  125. /** @throws Minz_PermissionDeniedException */
  126. public static function notice(string $msg, ?string $file_name = null): void {
  127. self::record($msg, LOG_NOTICE, $file_name);
  128. }
  129. /** @throws Minz_PermissionDeniedException */
  130. public static function warning(string $msg, ?string $file_name = null): void {
  131. self::record($msg, LOG_WARNING, $file_name);
  132. }
  133. /** @throws Minz_PermissionDeniedException */
  134. public static function error(string $msg, ?string $file_name = null): void {
  135. self::record($msg, LOG_ERR, $file_name);
  136. }
  137. }