1
0
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:
Jordi Boggiano 2014-03-09 16:38:08 +01:00
commit aaeb238fe9
3 changed files with 313 additions and 0 deletions

View File

@ -142,6 +142,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

View 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) . ');';
}
}

View 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());
}
}