BrowserConsoleHandler.php 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  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\LineFormatter;
  12. /**
  13. * Handler sending logs to browser's javascript console with no browser extension required
  14. *
  15. * @author Olivier Poitrey <rs@dailymotion.com>
  16. */
  17. class BrowserConsoleHandler extends AbstractProcessingHandler
  18. {
  19. protected static $initialized = false;
  20. protected static $records = array();
  21. /**
  22. * {@inheritDoc}
  23. *
  24. * Formatted output may contain some formatting markers to be transferred to `console.log` using the %c format.
  25. *
  26. * Example of formatted string:
  27. *
  28. * You can do [[blue text]]{color: blue} or [[green background]]{background-color: green; color: white}
  29. *
  30. */
  31. protected function getDefaultFormatter()
  32. {
  33. return new LineFormatter('[[%channel%]]{macro: autolabel} [[%level_name%]]{font-weight: bold} %message%');
  34. }
  35. /**
  36. * {@inheritDoc}
  37. */
  38. protected function write(array $record)
  39. {
  40. // Accumulate records
  41. self::$records[] = $record;
  42. // Register shutdown handler if not already done
  43. if (PHP_SAPI !== 'cli' && !self::$initialized) {
  44. self::$initialized = true;
  45. register_shutdown_function(array('Monolog\Handler\BrowserConsoleHandler', 'send'));
  46. }
  47. }
  48. /**
  49. * Convert records to javascript console commands and send it to the browser.
  50. * This method is automatically called on PHP shutdown if output is HTML.
  51. */
  52. public static function send()
  53. {
  54. // Check content type
  55. foreach (headers_list() as $header) {
  56. if (stripos($header, 'content-type:') === 0) {
  57. if (stripos($header, 'text/html') === false) {
  58. // This handler only works with HTML outputs
  59. return;
  60. }
  61. break;
  62. }
  63. }
  64. if (count(self::$records)) {
  65. echo '<script>' , self::generateScript() , '</script>';
  66. self::reset();
  67. }
  68. }
  69. /**
  70. * Forget all logged records
  71. */
  72. public static function reset()
  73. {
  74. self::$records = array();
  75. }
  76. private static function generateScript()
  77. {
  78. $script = array();
  79. foreach (self::$records as $record) {
  80. $context = self::dump('Context', $record['context']);
  81. $extra = self::dump('Extra', $record['extra']);
  82. if (empty($context) && empty($extra)) {
  83. $script[] = self::call_array('log', self::handleStyles($record['formatted']));
  84. } else {
  85. $script = array_merge($script,
  86. array(self::call_array('groupCollapsed', self::handleStyles($record['formatted']))),
  87. $context,
  88. $extra,
  89. array(self::call('groupEnd'))
  90. );
  91. }
  92. }
  93. return "(function (c) {if (c && c.groupCollapsed) {\n" . implode("\n", $script) . "\n}})(console);";
  94. }
  95. private static function handleStyles($formatted)
  96. {
  97. $args = array(self::quote('font-weight: normal'));
  98. $format = '%c' . $formatted;
  99. preg_match_all('/\[\[(.*?)\]\]\{([^}]*)\}/s', $format, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
  100. foreach (array_reverse($matches) as $match) {
  101. $args[] = self::quote(self::handleCustomStyles($match[2][0], $match[1][0]));
  102. $args[] = '"font-weight: normal"';
  103. $pos = $match[0][1];
  104. $format = substr($format, 0, $pos) . '%c' . $match[1][0] . '%c' . substr($format, $pos + strlen($match[0][0]));
  105. }
  106. array_unshift($args, self::quote($format));
  107. return $args;
  108. }
  109. private static function handleCustomStyles($style, $string)
  110. {
  111. static $colors = array('blue', 'green', 'red', 'magenta', 'orange', 'black', 'grey');
  112. static $labels = array();
  113. return preg_replace_callback('/macro\s*:(.*?)(?:;|$)/', function ($m) use ($string, &$colors, &$labels) {
  114. if (trim($m[1]) === 'autolabel') {
  115. // Format the string as a label with consistent auto assigned background color
  116. if (!isset($labels[$string])) {
  117. $labels[$string] = $colors[count($labels) % count($colors)];
  118. }
  119. $color = $labels[$string];
  120. return "background-color: $color; color: white; border-radius: 3px; padding: 0 2px 0 2px";
  121. }
  122. return $m[1];
  123. }, $style);
  124. }
  125. private static function dump($title, array $dict)
  126. {
  127. $script = array();
  128. $dict = array_filter($dict);
  129. if (empty($dict)) {
  130. return $script;
  131. }
  132. $script[] = self::call('log', self::quote('%c%s'), self::quote('font-weight: bold'), self::quote($title));
  133. foreach ($dict as $key => $value) {
  134. $value = json_encode($value);
  135. if (empty($value)) {
  136. $value = self::quote('');
  137. }
  138. $script[] = self::call('log', self::quote('%s: %o'), self::quote($key), $value);
  139. }
  140. return $script;
  141. }
  142. private static function quote($arg)
  143. {
  144. return '"' . addcslashes($arg, "\"\n") . '"';
  145. }
  146. private static function call()
  147. {
  148. $args = func_get_args();
  149. $method = array_shift($args);
  150. return self::call_array($method, $args);
  151. }
  152. private static function call_array($method, array $args)
  153. {
  154. return 'c.' . $method . '(' . implode(', ', $args) . ');';
  155. }
  156. }