ChromePHPHandler.php 5.5 KB

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