diff --git a/CHANGELOG.mdown b/CHANGELOG.mdown index 007740fc..439944a7 100644 --- a/CHANGELOG.mdown +++ b/CHANGELOG.mdown @@ -4,6 +4,7 @@ * Added Monolog\Logger::isHandling() to check if a handler will handle the given log level + * Added ChromePhpHandler * 1.0.2 (2011-10-24) diff --git a/README.mdown b/README.mdown index b6da10f1..5af707eb 100644 --- a/README.mdown +++ b/README.mdown @@ -41,6 +41,7 @@ Handlers - _StreamHandler_: Logs records into any php stream, use this for log files. - _RotatingFileHandler_: Logs records to a file and creates one logfile per day. It will also delete files older than $maxFiles. You should use [logrotate](http://linuxcommand.org/man_pages/logrotate8.html) for high profile setups though, this is just meant as a quick and dirty solution. - _FirePHPHandler_: Handler for [FirePHP](http://www.firephp.org/), providing inline `console` messages within [FireBug](http://getfirebug.com/). +- _ChromePhpHandler_: Handler for [ChromePHP](http://www.chromephp.com/), providing inline `console` messages within Chrome. - _NativeMailHandler_: Sends emails using PHP's mail() function. - _SwiftMailerHandler_: Sends emails using a SwiftMailer instance. - _SyslogHandler_: Logs records to the syslog. @@ -60,6 +61,7 @@ Formatters - _LineFormatter_: Formats a log record into a one-line string. - _JsonFormatter_: Encodes a log record into json. - _WildfireFormatter_: Used to format log records into the Wildfire/FirePHP protocol, only useful for the FirePHPHandler. +- _ChromePhpFormatter_: Used to format log records into the ChromePhp format, only useful for the ChromePhpHandler. Processors ---------- diff --git a/src/Monolog/Formatter/ChromePhpFormatter.php b/src/Monolog/Formatter/ChromePhpFormatter.php new file mode 100644 index 00000000..ace594e7 --- /dev/null +++ b/src/Monolog/Formatter/ChromePhpFormatter.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Formatter; + +use Monolog\Logger; + +/** + * Formats a log message according to the ChromePhp array format + * + * @author Christophe Coevoet + */ +class ChromePhpFormatter implements FormatterInterface +{ + /** + * Translates Monolog log levels to Wildfire levels. + */ + private $logLevels = array( + Logger::DEBUG => 'log', + Logger::INFO => 'info', + Logger::WARNING => 'warn', + Logger::ERROR => 'error', + Logger::CRITICAL => 'error', + Logger::ALERT => 'error', + ); + + /** + * {@inheritdoc} + */ + public function format(array $record) + { + // Retrieve the line and file if set and remove them from the formatted extra + $backtrace = 'unknown'; + if (isset($record['extra']['file']) && isset($record['extra']['line'])) { + $backtrace = $record['extra']['file'].' : '.$record['extra']['line']; + unset($record['extra']['file']); + unset($record['extra']['line']); + } + + $message = array('message' => $record['message']); + if ($record['context']) { + $message['context'] = $record['context']; + } + if ($record['extra']) { + $message['extra'] = $record['extra']; + } + if (count($message) === 1) { + $message = reset($message); + } + + return array( + $record['channel'], + $message, + $backtrace, + $this->logLevels[$record['level']], + ); + } + + public function formatBatch(array $records) + { + $formatted = array(); + + foreach ($records as $record) { + $formatted[] = $this->format($record); + } + + return $formatted; + } +} \ No newline at end of file diff --git a/src/Monolog/Handler/ChromePhpHandler.php b/src/Monolog/Handler/ChromePhpHandler.php new file mode 100644 index 00000000..f76cf1ab --- /dev/null +++ b/src/Monolog/Handler/ChromePhpHandler.php @@ -0,0 +1,127 @@ + + * + * 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\ChromePhpFormatter; + +/** + * Handler sending logs to the ChromePHP extension (http://www.chromephp.com/) + * + * @author Christophe Coevoet + */ +class ChromePhpHandler extends AbstractProcessingHandler +{ + /** + * Version of the extension + */ + const VERSION = '3.0'; + + /** + * Header name + */ + const HEADER_NAME = 'X-ChromePhp-Data'; + + static protected $initialized = false; + + static protected $json = array( + 'version' => self::VERSION, + 'columns' => array('label', 'log', 'backtrace', 'type'), + 'rows' => array(), + ); + + protected $sendHeaders = true; + + /** + * {@inheritdoc} + */ + public function handleBatch(array $records) + { + $messages = array(); + + foreach ($records as $record) { + if ($record['level'] < $this->level) { + continue; + } + $messages[] = $this->processRecord($record); + } + + if (!empty($messages)) { + $messages = $this->getFormatter()->formatBatch($messages); + self::$json['rows'] = array_merge(self::$json['rows'], $messages); + $this->send(); + } + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter() + { + return new ChromePhpFormatter(); + } + + /** + * Creates & sends header for a record + * + * @see sendHeader() + * @see send() + * @param array $record + */ + protected function write(array $record) + { + self::$json['rows'][] = $record['formatted']; + + $this->send(); + } + + /** + * Sends the log header + * + * @see sendHeader() + */ + protected function send() + { + if (!self::$initialized) { + $this->sendHeaders = $this->headersAccepted(); + self::$json['request_uri'] = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''; + + self::$initialized = true; + } + + $this->sendHeader(self::HEADER_NAME, base64_encode(utf8_encode(json_encode(self::$json)))); + } + + /** + * Send header string to the client + * + * @param string $header + * @param string $content + */ + protected function sendHeader($header, $content) + { + if (!headers_sent() && $this->sendHeaders) { + header(sprintf('%s: %s', $header, $content)); + } + } + + /** + * Verifies if the headers are accepted by the current user agent + * + * @return Boolean + */ + protected function headersAccepted() + { + return !isset($_SERVER['HTTP_USER_AGENT']) + || preg_match('{\bChrome/\d+[\.\d+]*\b}', $_SERVER['HTTP_USER_AGENT']); + } +} \ No newline at end of file diff --git a/tests/Monolog/Formatter/ChromePhpFormatterTest.php b/tests/Monolog/Formatter/ChromePhpFormatterTest.php new file mode 100644 index 00000000..e3d58be2 --- /dev/null +++ b/tests/Monolog/Formatter/ChromePhpFormatterTest.php @@ -0,0 +1,158 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Formatter; + +use Monolog\Logger; + +class ChromePhpFormatterTest extends \PHPUnit_Framework_TestCase +{ + /** + * @covers Monolog\Formatter\ChromePhpFormatter::format + */ + public function testDefaultFormat() + { + $formatter = new ChromePhpFormatter(); + $record = array( + 'level' => Logger::ERROR, + 'level_name' => 'ERROR', + 'channel' => 'meh', + 'context' => array('from' => 'logger'), + 'datetime' => new \DateTime("@0"), + 'extra' => array('ip' => '127.0.0.1'), + 'message' => 'log', + ); + + $message = $formatter->format($record); + + $this->assertEquals( + array( + 'meh', + array( + 'message' => 'log', + 'context' => array('from' => 'logger'), + 'extra' => array('ip' => '127.0.0.1'), + ), + 'unknown', + 'error' + ), + $message + ); + } + + /** + * @covers Monolog\Formatter\ChromePhpFormatter::format + */ + public function testFormatWithFileAndLine() + { + $formatter = new ChromePhpFormatter(); + $record = array( + 'level' => Logger::CRITICAL, + 'level_name' => 'CRITICAL', + 'channel' => 'meh', + 'context' => array('from' => 'logger'), + 'datetime' => new \DateTime("@0"), + 'extra' => array('ip' => '127.0.0.1', 'file' => 'test', 'line' => 14), + 'message' => 'log', + ); + + $message = $formatter->format($record); + + $this->assertEquals( + array( + 'meh', + array( + 'message' => 'log', + 'context' => array('from' => 'logger'), + 'extra' => array('ip' => '127.0.0.1'), + ), + 'test : 14', + 'error' + ), + $message + ); + } + + /** + * @covers Monolog\Formatter\ChromePhpFormatter::format + */ + public function testFormatWithoutContext() + { + $formatter = new ChromePhpFormatter(); + $record = array( + 'level' => Logger::DEBUG, + 'level_name' => 'DEBUG', + 'channel' => 'meh', + 'context' => array(), + 'datetime' => new \DateTime("@0"), + 'extra' => array(), + 'message' => 'log', + ); + + $message = $formatter->format($record); + + $this->assertEquals( + array( + 'meh', + 'log', + 'unknown', + 'log' + ), + $message + ); + } + + /** + * @covers Monolog\Formatter\ChromePhpFormatter::formatBatch + */ + public function testBatchFormatThrowException() + { + $formatter = new ChromePhpFormatter(); + $records = array( + array( + 'level' => Logger::INFO, + 'level_name' => 'INFO', + 'channel' => 'meh', + 'context' => array(), + 'datetime' => new \DateTime("@0"), + 'extra' => array(), + 'message' => 'log', + ), + array( + 'level' => Logger::WARNING, + 'level_name' => 'WARNING', + 'channel' => 'foo', + 'context' => array(), + 'datetime' => new \DateTime("@0"), + 'extra' => array(), + 'message' => 'log2', + ), + ); + + $this->assertEquals( + array( + array( + 'meh', + 'log', + 'unknown', + 'info' + ), + array( + 'foo', + 'log2', + 'unknown', + 'warn' + ), + ), + $formatter->formatBatch($records) + ); + } +} diff --git a/tests/Monolog/Functional/Handler/FirePHPHandlerTest.php b/tests/Monolog/Functional/Handler/FirePHPHandlerTest.php index 2e4f8711..7d95547a 100644 --- a/tests/Monolog/Functional/Handler/FirePHPHandlerTest.php +++ b/tests/Monolog/Functional/Handler/FirePHPHandlerTest.php @@ -20,11 +20,13 @@ spl_autoload_register(function($class) use Monolog\Logger; use Monolog\Handler\FirePHPHandler; +use Monolog\Handler\ChromePhpHandler; $logger = new Logger('firephp'); $logger->pushHandler(new FirePHPHandler); +$logger->pushHandler(new ChromePhpHandler()); $logger->addDebug('Debug'); $logger->addInfo('Info'); $logger->addWarning('Warning'); -$logger->addError('Error'); \ No newline at end of file +$logger->addError('Error'); diff --git a/tests/Monolog/Handler/ChromePhpHandlerTest.php b/tests/Monolog/Handler/ChromePhpHandlerTest.php new file mode 100644 index 00000000..c6a2020f --- /dev/null +++ b/tests/Monolog/Handler/ChromePhpHandlerTest.php @@ -0,0 +1,98 @@ + + * + * 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\ChromePhpHandler + */ +class ChromePhpHandlerTest extends TestCase +{ + protected function setUp() + { + TestChromePhpHandler::reset(); + } + + public function testHeaders() + { + $handler = new TestChromePhpHandler(); + $handler->setFormatter($this->getIdentityFormatter()); + $handler->handle($this->getRecord(Logger::DEBUG)); + $handler->handle($this->getRecord(Logger::WARNING)); + + $expected = array( + 'X-ChromePhp-Data' => base64_encode(utf8_encode(json_encode(array( + 'version' => ChromePhpHandler::VERSION, + 'columns' => array('label', 'log', 'backtrace', 'type'), + 'rows' => array( + 'test', + 'test', + ), + 'request_uri' => '', + )))) + ); + + $this->assertEquals($expected, $handler->getHeaders()); + } + + public function testConcurrentHandlers() + { + $handler = new TestChromePhpHandler(); + $handler->setFormatter($this->getIdentityFormatter()); + $handler->handle($this->getRecord(Logger::DEBUG)); + $handler->handle($this->getRecord(Logger::WARNING)); + + $handler2 = new TestChromePhpHandler(); + $handler2->setFormatter($this->getIdentityFormatter()); + $handler2->handle($this->getRecord(Logger::DEBUG)); + $handler2->handle($this->getRecord(Logger::WARNING)); + + $expected = array( + 'X-ChromePhp-Data' => base64_encode(utf8_encode(json_encode(array( + 'version' => ChromePhpHandler::VERSION, + 'columns' => array('label', 'log', 'backtrace', 'type'), + 'rows' => array( + 'test', + 'test', + 'test', + 'test', + ), + 'request_uri' => '', + )))) + ); + + $this->assertEquals($expected, $handler2->getHeaders()); + } +} + +class TestChromePhpHandler extends ChromePhpHandler +{ + protected $headers = array(); + + public static function reset() + { + self::$initialized = false; + self::$json['rows'] = array(); + } + + protected function sendHeader($header, $content) + { + $this->headers[$header] = $content; + } + + public function getHeaders() + { + return $this->headers; + } +} \ No newline at end of file