Przeglądaj źródła

Add BrowserConsoleHandler

BrowserConsoleHandler is an handler sending logs to browser's javascript console
with no browser extension required
Olivier Poitrey 12 lat temu
rodzic
commit
577b7fe96c

+ 2 - 0
README.mdown

@@ -141,6 +141,8 @@ Handlers
   inline `console` messages within [FireBug](http://getfirebug.com/).
 - _ChromePHPHandler_: Handler for [ChromePHP](http://www.chromephp.com/), providing
   inline `console` messages within Chrome.
+- _BrowserConsoleHandler_: Handler to send logs to browser's Javascript `console` with
+  no browser extension required. Most browsers supporting `console` API are supported.
 
 ### Log to databases
 

+ 187 - 0
src/Monolog/Handler/BrowserConsoleHandler.php

@@ -0,0 +1,187 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Monolog\Handler;
+
+use Monolog\Logger;
+use Monolog\Formatter\LineFormatter;
+
+/**
+ * Handler sending logs to browser's javascript console with no browser extension required
+ *
+ * @author Olivier Poitrey <rs@dailymotion.com>
+ */
+class BrowserConsoleHandler extends AbstractProcessingHandler
+{
+    static protected
+        $initialized = false,
+        $records = array();
+
+    /**
+     * {@inheritDoc}
+     *
+     * Formatted output may contain some formatting markers to be transfered to `console.log` using the %c format.
+     *
+     * Example of formatted string:
+     *
+     *     You can do [blue text]{color: blue} or [green background]{background-color: green; color: white}
+     *
+     */
+    protected function getDefaultFormatter()
+    {
+        return new LineFormatter('[%channel%]{macro: autolabel} [%level_name%]{font-weight: bold} %message%');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function write(array $record)
+    {
+        // Accumulate records
+        self::$records[] = $record;
+
+        // Register shutdown handler if not already done
+        if (PHP_SAPI !== 'cli' && !self::$initialized) {
+            self::$initialized = true;
+            register_shutdown_function(array('Monolog\Handler\BrowserConsoleHandler', 'send'));
+        }
+    }
+
+    /**
+     * Convert records to javascript console commands and send it to the browser.
+     * This method is automatically called on PHP shutdown if output is HTML.
+     */
+    static public function send()
+    {
+        // Check content type
+        foreach (headers_list() as $header) {
+            if (strpos(strtolower($header), 'content-type:') === 0) {
+                if (strpos(strtolower($header), 'text/html') === false) {
+                    // This handler does only work with HTML outputs
+                    return;
+                }
+                break;
+            }
+        }
+
+        if (count(self::$records)) {
+            print '<script>' . self::generateScript() . '</script>';
+            self::reset();
+        }
+    }
+
+    /**
+     * Forget all logged records
+     */
+    static public function reset()
+    {
+        self::$records = array();
+    }
+
+    static public function generateScript()
+    {
+        $script = array();
+        foreach (self::$records as $record) {
+            $context = self::dump('Context', $record['context']);
+            $extra = self::dump('Extra', $record['extra']);
+
+            if (empty($context) && empty($extra)) {
+                $script[] = self::call_array('log', self::handleStyles($record['formatted']));
+            } else {
+                $script = array_merge($script,
+                    array(self::call_array('groupCollapsed', self::handleStyles($record['formatted']))),
+                    $context,
+                    $extra,
+                    array(self::call('groupEnd'))
+                );
+            }
+        }
+        return "(function(c){if (console && console.groupCollapsed) {\n" . implode("\n", $script) . "\n}})(console);";
+    }
+
+    static protected function handleStyles($formatted)
+    {
+        $args = array(self::quote('font-weight: normal'));
+        $format = '%c' . $formatted;
+        $format = preg_replace_callback('/\[(.*?)\]\{(.*?)\}/', function($m) use(&$args) {
+            $args[] = self::quote(self::handleCustomStyles($m[2], $m[1]));
+            $args[] = self::quote('font-weight: normal');
+            return '%c' . $m[1] . '%c';
+        }, $format);
+
+        array_unshift($args, self::quote($format));
+        return $args;
+    }
+
+    static protected function handleCustomStyles($style, $string)
+    {
+        return preg_replace_callback('/macro\s*:(.*?)(?:;|$)/', function($m) use($string) {
+            switch (trim($m[1])) {
+                case 'autolabel':
+                    return self::macroAutolabel($string);
+                    break;
+                default:
+                    return $m[1];
+            }
+        }, $style);
+    }
+
+    /**
+     * Format the string as a label with consistent auto assigned background color
+     */
+    static protected function macroAutolabel($string)
+    {
+        static $colors = array('blue', 'green', 'red', 'magenta', 'orange', 'black', 'grey');
+        static $labels = array();
+
+        if (!isset($labels[$string])) {
+            $labels[$string] = $colors[count($labels) % count($colors)];
+        }
+        $color = $labels[$string];
+
+        return "background-color: $color; color: white; border-radius: 3px; padding: 0 2px 0 2px";
+    }
+
+    static protected function dump($title, array $dict)
+    {
+        $script = array();
+        $dict = array_filter($dict);
+        if (empty($dict)) {
+            return $script;
+        }
+        $script[] = self::call('log', self::quote('%c%s'), self::quote('font-weight: bold'), self::quote($title));
+        foreach ($dict as $key => $value) {
+            $value = json_encode($value);
+            if (empty($value)) {
+                $value = self::quote('');
+            }
+            $script[] = self::call('log', self::quote('%s: %o'), self::quote($key), $value);
+        }
+        return $script;
+    }
+
+    static protected function quote($arg)
+    {
+        return '"' . addcslashes($arg, '"') . '"';
+    }
+
+    static protected function call()
+    {
+        $args = func_get_args();
+        $method = array_shift($args);
+        return self::call_array($method, $args);
+    }
+
+    static protected function call_array($method, array $args)
+    {
+        return 'c.' . $method . '(' . implode(', ', $args) . ');';
+    }
+}

+ 106 - 0
tests/Monolog/Handler/BrowserConsoleHandlerTest.php

@@ -0,0 +1,106 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Monolog\Handler;
+
+use Monolog\TestCase;
+use Monolog\Logger;
+
+/**
+ * @covers Monolog\Handler\BrowserConsoleHandlerTest
+ */
+class BrowserConsoleHandlerTest extends TestCase
+{
+    protected function setUp()
+    {
+        BrowserConsoleHandler::reset();
+    }
+
+    public function testStyling()
+    {
+        $handler = new BrowserConsoleHandler();
+        $handler->setFormatter($this->getIdentityFormatter());
+
+        $handler->handle($this->getRecord(Logger::DEBUG, 'foo[bar]{color: red}'));
+
+        $expected = <<<EOF
+(function(c){if (console && console.groupCollapsed) {
+c.log("%cfoo%cbar%c", "font-weight: normal", "color: red", "font-weight: normal");
+}})(console);
+EOF;
+
+        $this->assertEquals($expected, BrowserConsoleHandler::generateScript());
+    }
+
+    public function testAutolabel()
+    {
+        $handler = new BrowserConsoleHandler();
+        $handler->setFormatter($this->getIdentityFormatter());
+
+        $handler->handle($this->getRecord(Logger::DEBUG, '[foo]{macro: autolabel}'));
+        $handler->handle($this->getRecord(Logger::DEBUG, '[bar]{macro: autolabel}'));
+        $handler->handle($this->getRecord(Logger::DEBUG, '[foo]{macro: autolabel}'));
+
+        $expected = <<<EOF
+(function(c){if (console && console.groupCollapsed) {
+c.log("%c%cfoo%c", "font-weight: normal", "background-color: blue; color: white; border-radius: 3px; padding: 0 2px 0 2px", "font-weight: normal");
+c.log("%c%cbar%c", "font-weight: normal", "background-color: green; color: white; border-radius: 3px; padding: 0 2px 0 2px", "font-weight: normal");
+c.log("%c%cfoo%c", "font-weight: normal", "background-color: blue; color: white; border-radius: 3px; padding: 0 2px 0 2px", "font-weight: normal");
+}})(console);
+EOF;
+
+        $this->assertEquals($expected, BrowserConsoleHandler::generateScript());
+    }
+
+    public function testContext()
+    {
+        $handler = new BrowserConsoleHandler();
+        $handler->setFormatter($this->getIdentityFormatter());
+
+        $handler->handle($this->getRecord(Logger::DEBUG, 'test', array('foo' => 'bar')));
+
+        $expected = <<<EOF
+(function(c){if (console && console.groupCollapsed) {
+c.groupCollapsed("%ctest", "font-weight: normal");
+c.log("%c%s", "font-weight: bold", "Context");
+c.log("%s: %o", "foo", "bar");
+c.groupEnd();
+}})(console);
+EOF;
+
+        $this->assertEquals($expected, BrowserConsoleHandler::generateScript());
+    }
+
+    public function testConcurrentHandlers()
+    {
+        $handler1 = new BrowserConsoleHandler();
+        $handler1->setFormatter($this->getIdentityFormatter());
+
+        $handler2 = new BrowserConsoleHandler();
+        $handler2->setFormatter($this->getIdentityFormatter());
+
+        $handler1->handle($this->getRecord(Logger::DEBUG, 'test1'));
+        $handler2->handle($this->getRecord(Logger::DEBUG, 'test2'));
+        $handler1->handle($this->getRecord(Logger::DEBUG, 'test3'));
+        $handler2->handle($this->getRecord(Logger::DEBUG, 'test4'));
+
+        $expected = <<<EOF
+(function(c){if (console && console.groupCollapsed) {
+c.log("%ctest1", "font-weight: normal");
+c.log("%ctest2", "font-weight: normal");
+c.log("%ctest3", "font-weight: normal");
+c.log("%ctest4", "font-weight: normal");
+}})(console);
+EOF;
+
+        $this->assertEquals($expected, BrowserConsoleHandler::generateScript());
+    }
+}