ChromePHPHandler.php 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. <?php declare(strict_types=1);
  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\Formatter\ChromePHPFormatter;
  12. use Monolog\Formatter\FormatterInterface;
  13. use Monolog\Logger;
  14. /**
  15. * Handler sending logs to the ChromePHP extension (http://www.chromephp.com/)
  16. *
  17. * This also works out of the box with Firefox 43+
  18. *
  19. * @author Christophe Coevoet <stof@notk.org>
  20. */
  21. class ChromePHPHandler extends AbstractProcessingHandler
  22. {
  23. /**
  24. * Version of the extension
  25. */
  26. const VERSION = '4.0';
  27. /**
  28. * Header name
  29. */
  30. const HEADER_NAME = 'X-ChromeLogger-Data';
  31. /**
  32. * Regular expression to detect supported browsers (matches any Chrome, or Firefox 43+)
  33. */
  34. const USER_AGENT_REGEX = '{\b(?:Chrome/\d+(?:\.\d+)*|Firefox/(?:4[3-9]|[5-9]\d|\d{3,})(?:\.\d)*)\b}';
  35. protected static $initialized = false;
  36. /**
  37. * Tracks whether we sent too much data
  38. *
  39. * Chrome limits the headers to 256KB, so when we sent 240KB we stop sending
  40. *
  41. * @var Boolean
  42. */
  43. protected static $overflowed = false;
  44. protected static $json = [
  45. 'version' => self::VERSION,
  46. 'columns' => ['label', 'log', 'backtrace', 'type'],
  47. 'rows' => [],
  48. ];
  49. protected static $sendHeaders = true;
  50. /**
  51. * @param int $level The minimum logging level at which this handler will be triggered
  52. * @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not
  53. */
  54. public function __construct($level = Logger::DEBUG, $bubble = true)
  55. {
  56. parent::__construct($level, $bubble);
  57. if (!function_exists('json_encode')) {
  58. throw new \RuntimeException('PHP\'s json extension is required to use Monolog\'s ChromePHPHandler');
  59. }
  60. }
  61. /**
  62. * {@inheritdoc}
  63. */
  64. public function handleBatch(array $records)
  65. {
  66. $messages = [];
  67. foreach ($records as $record) {
  68. if ($record['level'] < $this->level) {
  69. continue;
  70. }
  71. $messages[] = $this->processRecord($record);
  72. }
  73. if (!empty($messages)) {
  74. $messages = $this->getFormatter()->formatBatch($messages);
  75. self::$json['rows'] = array_merge(self::$json['rows'], $messages);
  76. $this->send();
  77. }
  78. }
  79. /**
  80. * {@inheritDoc}
  81. */
  82. protected function getDefaultFormatter(): FormatterInterface
  83. {
  84. return new ChromePHPFormatter();
  85. }
  86. /**
  87. * Creates & sends header for a record
  88. *
  89. * @see sendHeader()
  90. * @see send()
  91. * @param array $record
  92. */
  93. protected function write(array $record)
  94. {
  95. self::$json['rows'][] = $record['formatted'];
  96. $this->send();
  97. }
  98. /**
  99. * Sends the log header
  100. *
  101. * @see sendHeader()
  102. */
  103. protected function send()
  104. {
  105. if (self::$overflowed || !self::$sendHeaders) {
  106. return;
  107. }
  108. if (!self::$initialized) {
  109. self::$initialized = true;
  110. self::$sendHeaders = $this->headersAccepted();
  111. if (!self::$sendHeaders) {
  112. return;
  113. }
  114. self::$json['request_uri'] = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
  115. }
  116. $json = @json_encode(self::$json);
  117. $data = base64_encode(utf8_encode($json));
  118. if (strlen($data) > 240 * 1024) {
  119. self::$overflowed = true;
  120. $record = [
  121. 'message' => 'Incomplete logs, chrome header size limit reached',
  122. 'context' => [],
  123. 'level' => Logger::WARNING,
  124. 'level_name' => Logger::getLevelName(Logger::WARNING),
  125. 'channel' => 'monolog',
  126. 'datetime' => new \DateTimeImmutable(),
  127. 'extra' => [],
  128. ];
  129. self::$json['rows'][count(self::$json['rows']) - 1] = $this->getFormatter()->format($record);
  130. $json = @json_encode(self::$json);
  131. $data = base64_encode(utf8_encode($json));
  132. }
  133. if (trim($data) !== '') {
  134. $this->sendHeader(self::HEADER_NAME, $data);
  135. }
  136. }
  137. /**
  138. * Send header string to the client
  139. *
  140. * @param string $header
  141. * @param string $content
  142. */
  143. protected function sendHeader($header, $content)
  144. {
  145. if (!headers_sent() && self::$sendHeaders) {
  146. header(sprintf('%s: %s', $header, $content));
  147. }
  148. }
  149. /**
  150. * Verifies if the headers are accepted by the current user agent
  151. *
  152. * @return Boolean
  153. */
  154. protected function headersAccepted()
  155. {
  156. if (empty($_SERVER['HTTP_USER_AGENT'])) {
  157. return false;
  158. }
  159. return preg_match(self::USER_AGENT_REGEX, $_SERVER['HTTP_USER_AGENT']);
  160. }
  161. /**
  162. * BC getter for the sendHeaders property that has been made static
  163. */
  164. public function __get($property)
  165. {
  166. if ('sendHeaders' !== $property) {
  167. throw new \InvalidArgumentException('Undefined property '.$property);
  168. }
  169. return static::$sendHeaders;
  170. }
  171. /**
  172. * BC setter for the sendHeaders property that has been made static
  173. */
  174. public function __set($property, $value)
  175. {
  176. if ('sendHeaders' !== $property) {
  177. throw new \InvalidArgumentException('Undefined property '.$property);
  178. }
  179. static::$sendHeaders = $value;
  180. }
  181. }