LogViewer.php 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. <?php
  2. /**
  3. * @package Grav\Common\Helpers
  4. *
  5. * @copyright Copyright (c) 2015 - 2021 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 -= 1;
  56. }
  57. // Start reading
  58. $output = '';
  59. $chunk = '';
  60. // While we would like more
  61. while (ftell($f) > 0 && $lines >= 0) {
  62. // Figure out how far back we should jump
  63. $seek = min(ftell($f), $buffer);
  64. // Do the jump (backwards, relative to where we are)
  65. fseek($f, -$seek, SEEK_CUR);
  66. // Read a chunk and prepend it to our output
  67. $output = ($chunk = fread($f, $seek)) . $output;
  68. // Jump back to where we started reading
  69. fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR);
  70. // Decrease our line counter
  71. $lines -= substr_count($chunk, "\n");
  72. }
  73. // While we have too many lines
  74. // (Because of buffer size we might have read too many)
  75. while ($lines++ < 0) {
  76. // Find first newline and remove all text before that
  77. $output = substr($output, strpos($output, "\n") + 1);
  78. }
  79. // Close file and return
  80. fclose($f);
  81. return trim($output);
  82. }
  83. /**
  84. * Helper class to get level color
  85. *
  86. * @param string $level
  87. * @return string
  88. */
  89. public static function levelColor($level)
  90. {
  91. $colors = [
  92. 'DEBUG' => 'green',
  93. 'INFO' => 'cyan',
  94. 'NOTICE' => 'yellow',
  95. 'WARNING' => 'yellow',
  96. 'ERROR' => 'red',
  97. 'CRITICAL' => 'red',
  98. 'ALERT' => 'red',
  99. 'EMERGENCY' => 'magenta'
  100. ];
  101. return $colors[$level] ?? 'white';
  102. }
  103. /**
  104. * Parse a monolog row into array bits
  105. *
  106. * @param string $line
  107. * @return array
  108. */
  109. public function parse($line)
  110. {
  111. if (!is_string($line) || strlen($line) === 0) {
  112. return array();
  113. }
  114. preg_match($this->pattern, $line, $data);
  115. if (!isset($data['date'])) {
  116. return array();
  117. }
  118. preg_match('/(.*)- Trace:(.*)/', $data['message'], $matches);
  119. if (is_array($matches) && isset($matches[1])) {
  120. $data['message'] = trim($matches[1]);
  121. $data['trace'] = trim($matches[2]);
  122. }
  123. return array(
  124. 'date' => DateTime::createFromFormat('Y-m-d H:i:s', $data['date']),
  125. 'logger' => $data['logger'],
  126. 'level' => $data['level'],
  127. 'message' => $data['message'],
  128. 'trace' => isset($data['trace']) ? $this->parseTrace($data['trace']) : null,
  129. 'context' => json_decode($data['context'], true),
  130. 'extra' => json_decode($data['extra'], true)
  131. );
  132. }
  133. /**
  134. * Parse text of trace into an array of lines
  135. *
  136. * @param string $trace
  137. * @param int $rows
  138. * @return array
  139. */
  140. public static function parseTrace($trace, $rows = 10)
  141. {
  142. $lines = array_filter(preg_split('/#\d*/m', $trace));
  143. return array_slice($lines, 0, $rows);
  144. }
  145. }