Nenhuma descrição

DeduplicationHandler.php 5.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. <?php
  2. /*
  3. * This file is part of the Monolog package.
  4. *
  5. * (c) Jordi Boggiano <j.boggiano@seld.be>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Monolog\Handler;
  11. use Monolog\Logger;
  12. /**
  13. * Simple handler wrapper that deduplicates log records across multiple requests
  14. *
  15. * It also includes the BufferHandler functionality and will buffer
  16. * all messages until the end of the request or flush() is called.
  17. *
  18. * This works by storing all log records' messages above $deduplicationLevel
  19. * to the file specified by $deduplicationStore. When further logs come in at the end of the
  20. * request (or when flush() is called), all those above $deduplicationLevel are checked
  21. * against the existing stored logs. If they match and the timestamps in the stored log is
  22. * not older than $time seconds, the new log record is discarded. If no log record is new, the
  23. * whole data set is discarded.
  24. *
  25. * This is mainly useful in combination with Mail handlers or things like Slack or HipChat handlers
  26. * that send messages to people, to avoid spamming with the same message over and over in case of
  27. * a major component failure like a database server being down which makes all requests fail in the
  28. * same way.
  29. *
  30. * @author Jordi Boggiano <j.boggiano@seld.be>
  31. */
  32. class DeduplicationHandler extends BufferHandler
  33. {
  34. /**
  35. * @var string
  36. */
  37. protected $deduplicationStore;
  38. /**
  39. * @var int
  40. */
  41. protected $deduplicationLevel;
  42. /**
  43. * @var int
  44. */
  45. protected $time;
  46. /**
  47. * @var bool
  48. */
  49. private $gc = false;
  50. /**
  51. * @param HandlerInterface $handler Handler.
  52. * @param string $deduplicationStore The file/path where the deduplication log should be kept
  53. * @param int $deduplicationLevel The minimum logging level for log records to be looked at for deduplication purposes
  54. * @param int $time The period (in seconds) during which duplicate entries should be suppressed after a given log is sent through
  55. * @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not
  56. */
  57. public function __construct(HandlerInterface $handler, $deduplicationStore = null, $deduplicationLevel = Logger::ERROR, $time = 60, $bubble = true)
  58. {
  59. parent::__construct($handler, 0, Logger::DEBUG, $bubble, false);
  60. $this->deduplicationStore = $deduplicationStore === null ? sys_get_temp_dir() . '/monolog-dedup-' . substr(md5(__FILE__), 0, 20) .'.log' : $deduplicationStore;
  61. $this->deduplicationLevel = Logger::toMonologLevel($deduplicationLevel);
  62. $this->time = $time;
  63. }
  64. public function flush()
  65. {
  66. if ($this->bufferSize === 0) {
  67. return;
  68. }
  69. $passthru = null;
  70. foreach ($this->buffer as $record) {
  71. if ($record['level'] >= $this->deduplicationLevel) {
  72. $passthru = $passthru || !$this->isDuplicate($record);
  73. if ($passthru) {
  74. $this->appendRecord($record);
  75. }
  76. }
  77. }
  78. // default of null is valid as well as if no record matches duplicationLevel we just pass through
  79. if ($passthru === true || $passthru === null) {
  80. $this->handler->handleBatch($this->buffer);
  81. }
  82. $this->clear();
  83. if ($this->gc) {
  84. $this->collectLogs();
  85. }
  86. }
  87. private function isDuplicate(array $record)
  88. {
  89. if (!file_exists($this->deduplicationStore)) {
  90. return false;
  91. }
  92. $store = file($this->deduplicationStore, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
  93. if (!is_array($store)) {
  94. return false;
  95. }
  96. $yesterday = time() - 86400;
  97. $timestampValidity = $record['datetime']->getTimestamp() - $this->time;
  98. $expectedMessage = preg_replace('{[\r\n].*}', '', $record['message']);
  99. for ($i = count($store) - 1; $i >= 0; $i--) {
  100. list($timestamp, $level, $message) = explode(':', $store[$i], 3);
  101. if ($level === $record['level_name'] && $message === $expectedMessage && $timestamp > $timestampValidity) {
  102. return true;
  103. }
  104. if ($timestamp < $yesterday) {
  105. $this->gc = true;
  106. }
  107. }
  108. return false;
  109. }
  110. private function collectLogs()
  111. {
  112. if (!file_exists($this->deduplicationStore)) {
  113. return false;
  114. }
  115. $handle = fopen($this->deduplicationStore, 'rw+');
  116. flock($handle, LOCK_EX);
  117. $validLogs = array();
  118. $timestampValidity = time() - $this->time;
  119. while (!feof($handle)) {
  120. $log = fgets($handle);
  121. if (substr($log, 0, 10) >= $timestampValidity) {
  122. $validLogs[] = $log;
  123. }
  124. }
  125. ftruncate($handle, 0);
  126. rewind($handle);
  127. foreach ($validLogs as $log) {
  128. fwrite($handle, $log);
  129. }
  130. flock($handle, LOCK_UN);
  131. fclose($handle);
  132. $this->gc = false;
  133. }
  134. private function appendRecord(array $record)
  135. {
  136. file_put_contents($this->deduplicationStore, $record['datetime']->getTimestamp() . ':' . $record['level_name'] . ':' . preg_replace('{[\r\n].*}', '', $record['message']) . "\n", FILE_APPEND);
  137. }
  138. }