Procházet zdrojové kódy

Merge pull request #58 from stof/chromephp

Added a ChromePhpHandler.
Jordi Boggiano před 14 roky
rodič
revize
1c717c3a6a

+ 1 - 0
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)
 

+ 2 - 0
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
 ----------

+ 77 - 0
src/Monolog/Formatter/ChromePhpFormatter.php

@@ -0,0 +1,77 @@
+<?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\Formatter;
+
+use Monolog\Logger;
+
+/**
+ * Formats a log message according to the ChromePhp array format
+ *
+ * @author Christophe Coevoet <stof@notk.org>
+ */
+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;
+    }
+}

+ 127 - 0
src/Monolog/Handler/ChromePhpHandler.php

@@ -0,0 +1,127 @@
+<?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\ChromePhpFormatter;
+
+/**
+ * Handler sending logs to the ChromePHP extension (http://www.chromephp.com/)
+ *
+ * @author Christophe Coevoet <stof@notk.org>
+ */
+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']);
+    }
+}

+ 158 - 0
tests/Monolog/Formatter/ChromePhpFormatterTest.php

@@ -0,0 +1,158 @@
+<?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\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)
+        );
+    }
+}

+ 3 - 1
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');
+$logger->addError('Error');

+ 98 - 0
tests/Monolog/Handler/ChromePhpHandlerTest.php

@@ -0,0 +1,98 @@
+<?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\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;
+    }
+}