mirror of
https://github.com/Seldaek/monolog.git
synced 2025-02-24 06:52:34 +01:00
Merge remote-tracking branch 'rs/browser-console-handler'
This commit is contained in:
commit
aaeb238fe9
@ -142,6 +142,8 @@ Handlers
|
|||||||
inline `console` messages within [FireBug](http://getfirebug.com/).
|
inline `console` messages within [FireBug](http://getfirebug.com/).
|
||||||
- _ChromePHPHandler_: Handler for [ChromePHP](http://www.chromephp.com/), providing
|
- _ChromePHPHandler_: Handler for [ChromePHP](http://www.chromephp.com/), providing
|
||||||
inline `console` messages within Chrome.
|
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
|
### Log to databases
|
||||||
|
|
||||||
|
188
src/Monolog/Handler/BrowserConsoleHandler.php
Normal file
188
src/Monolog/Handler/BrowserConsoleHandler.php
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
<?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;
|
||||||
|
static protected $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 (c && c.groupCollapsed) {\n" . implode("\n", $script) . "\n}})(console);";
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function handleStyles($formatted)
|
||||||
|
{
|
||||||
|
$args = array(self::quote('font-weight: normal'));
|
||||||
|
$format = '%c' . $formatted;
|
||||||
|
$self = 'Monolog\Handler\BrowserConsoleHandler';
|
||||||
|
$format = preg_replace_callback('/\[\[(.*?)\]\]\{([^}]*)\}/s', function($m) use(&$args, $self) {
|
||||||
|
$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 public function handleCustomStyles($style, $string)
|
||||||
|
{
|
||||||
|
$self = 'Monolog\Handler\BrowserConsoleHandler';
|
||||||
|
return preg_replace_callback('/macro\s*:(.*?)(?:;|$)/', function($m) use($string, $self) {
|
||||||
|
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 public 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 public 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 public function quote($arg)
|
||||||
|
{
|
||||||
|
return '"' . addcslashes($arg, "\"\n") . '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function call()
|
||||||
|
{
|
||||||
|
$args = func_get_args();
|
||||||
|
$method = array_shift($args);
|
||||||
|
return self::call_array($method, $args);
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function call_array($method, array $args)
|
||||||
|
{
|
||||||
|
return 'c.' . $method . '(' . implode(', ', $args) . ');';
|
||||||
|
}
|
||||||
|
}
|
123
tests/Monolog/Handler/BrowserConsoleHandlerTest.php
Normal file
123
tests/Monolog/Handler/BrowserConsoleHandlerTest.php
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
<?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 (c && c.groupCollapsed) {
|
||||||
|
c.log("%cfoo%cbar%c", "font-weight: normal", "color: red", "font-weight: normal");
|
||||||
|
}})(console);
|
||||||
|
EOF;
|
||||||
|
|
||||||
|
$this->assertEquals($expected, BrowserConsoleHandler::generateScript());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEscaping()
|
||||||
|
{
|
||||||
|
$handler = new BrowserConsoleHandler();
|
||||||
|
$handler->setFormatter($this->getIdentityFormatter());
|
||||||
|
|
||||||
|
$handler->handle($this->getRecord(Logger::DEBUG, "[foo] [[\"bar\n[baz]\"]]{color: red}"));
|
||||||
|
|
||||||
|
$expected = <<<EOF
|
||||||
|
(function(c){if (c && c.groupCollapsed) {
|
||||||
|
c.log("%c[foo] %c\"bar\\n[baz]\"%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 (c && c.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 (c && c.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 (c && c.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());
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user