LogViewer.php 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. <?php
  2. /**
  3. * @package Grav\Common\Helpers
  4. *
  5. * @copyright Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common\Helpers;
  9. use DateTime;
  10. use function array_slice;
  11. use function is_array;
  12. use function is_string;
  13. /**
  14. * Class LogViewer
  15. * @package Grav\Common\Helpers
  16. */
  17. class LogViewer
  18. {
  19. /** @var string */
  20. protected $pattern = '/\[(?P<date>.*?)\] (?P<logger>\w+)\.(?P<level>\w+): (?P<message>.*[^ ]+) (?P<context>[^ ]+) (?P<extra>[^ ]+)/';
  21. /**
  22. * Get the objects of a tailed file
  23. *
  24. * @param string $filepath
  25. * @param int $lines
  26. * @param bool $desc
  27. * @return array
  28. */
  29. public function objectTail($filepath, $lines = 1, $desc = true)
  30. {
  31. $data = $this->tail($filepath, $lines);
  32. $tailed_log = $data ? explode(PHP_EOL, $data) : [];
  33. $line_objects = [];
  34. foreach ($tailed_log as $line) {
  35. $line_objects[] = $this->parse($line);
  36. }
  37. return $desc ? $line_objects : array_reverse($line_objects);
  38. }
  39. /**
  40. * Optimized way to get just the last few entries of a log file
  41. *
  42. * @param string $filepath
  43. * @param int $lines
  44. * @return string|false
  45. */
  46. public function tail($filepath, $lines = 1)
  47. {
  48. $f = $filepath ? @fopen($filepath, 'rb') : false;
  49. if ($f === false) {
  50. return false;
  51. }
  52. $buffer = ($lines < 2 ? 64 : ($lines < 10 ? 512 : 4096));
  53. fseek($f, -1, SEEK_END);
  54. if (fread($f, 1) !== "\n") {
  55. --$lines;
  56. }
  57. // Start reading
  58. $output = '';
  59. // While we would like more
  60. while (ftell($f) > 0 && $lines >= 0) {
  61. // Figure out how far back we should jump
  62. $seek = min(ftell($f), $buffer);
  63. // Do the jump (backwards, relative to where we are)
  64. fseek($f, -$seek, SEEK_CUR);
  65. // Read a chunk and prepend it to our output
  66. $chunk = fread($f, $seek);
  67. if ($chunk === false) {
  68. throw new \RuntimeException('Cannot read file');
  69. }
  70. $output = $chunk . $output;
  71. // Jump back to where we started reading
  72. fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR);
  73. // Decrease our line counter
  74. $lines -= substr_count($chunk, "\n");
  75. }
  76. // While we have too many lines
  77. // (Because of buffer size we might have read too many)
  78. while ($lines++ < 0) {
  79. // Find first newline and remove all text before that
  80. $output = substr($output, strpos($output, "\n") + 1);
  81. }
  82. // Close file and return
  83. fclose($f);
  84. return trim($output);
  85. }
  86. /**
  87. * Helper class to get level color
  88. *
  89. * @param string $level
  90. * @return string
  91. */
  92. public static function levelColor($level)
  93. {
  94. $colors = [
  95. 'DEBUG' => 'green',
  96. 'INFO' => 'cyan',
  97. 'NOTICE' => 'yellow',
  98. 'WARNING' => 'yellow',
  99. 'ERROR' => 'red',
  100. 'CRITICAL' => 'red',
  101. 'ALERT' => 'red',
  102. 'EMERGENCY' => 'magenta'
  103. ];
  104. return $colors[$level] ?? 'white';
  105. }
  106. /**
  107. * Parse a monolog row into array bits
  108. *
  109. * @param string $line
  110. * @return array
  111. */
  112. public function parse($line)
  113. {
  114. if (!is_string($line) || $line === '') {
  115. return [];
  116. }
  117. preg_match($this->pattern, $line, $data);
  118. if (!isset($data['date'])) {
  119. return [];
  120. }
  121. preg_match('/(.*)- Trace:(.*)/', $data['message'], $matches);
  122. if (is_array($matches) && isset($matches[1])) {
  123. $data['message'] = trim($matches[1]);
  124. $data['trace'] = trim($matches[2]);
  125. }
  126. return [
  127. 'date' => DateTime::createFromFormat('Y-m-d H:i:s', $data['date']),
  128. 'logger' => $data['logger'],
  129. 'level' => $data['level'],
  130. 'message' => $data['message'],
  131. 'trace' => isset($data['trace']) ? self::parseTrace($data['trace']) : null,
  132. 'context' => json_decode($data['context'], true),
  133. 'extra' => json_decode($data['extra'], true)
  134. ];
  135. }
  136. /**
  137. * Parse text of trace into an array of lines
  138. *
  139. * @param string $trace
  140. * @param int $rows
  141. * @return array
  142. */
  143. public static function parseTrace($trace, $rows = 10)
  144. {
  145. $lines = array_filter(preg_split('/#\d*/m', $trace));
  146. return array_slice($lines, 0, $rows);
  147. }
  148. }