Jordi Boggiano 10 лет назад
Родитель
Сommit
ba995ffd1e

+ 3 - 0
doc/01-usage.md

@@ -189,6 +189,9 @@ $logger->pushHandler($firephp);
 $securityLogger = new Logger('security');
 $securityLogger->pushHandler($stream);
 $securityLogger->pushHandler($firephp);
+
+// Or clone the first one to only change the channel
+$securityLogger = $logger->withName('security');
 ```
 
 ## Customizing the log format

+ 2 - 0
doc/02-handlers-formatters-processors.md

@@ -105,6 +105,8 @@
 - _PsrHandler_: Can be used to forward log records to an existing PSR-3 logger
 - _TestHandler_: Used for testing, it records everything that is sent to it and
   has accessors to read out the information.
+- _HandlerWrapper_: A simple handler wrapper you can inherit from to create
+ your own wrappers easily.
 
 ## Formatters
 

+ 89 - 3
src/Monolog/Formatter/JsonFormatter.php

@@ -11,6 +11,8 @@
 
 namespace Monolog\Formatter;
 
+use Exception;
+
 /**
  * Encodes whatever record data is passed to it as json
  *
@@ -18,13 +20,17 @@ namespace Monolog\Formatter;
  *
  * @author Jordi Boggiano <j.boggiano@seld.be>
  */
-class JsonFormatter implements FormatterInterface
+class JsonFormatter extends NormalizerFormatter
 {
     const BATCH_MODE_JSON = 1;
     const BATCH_MODE_NEWLINES = 2;
 
     protected $batchMode;
     protected $appendNewline;
+    /**
+     * @var bool
+     */
+    protected $includeStacktraces = false;
 
     /**
      * @param int $batchMode
@@ -64,7 +70,7 @@ class JsonFormatter implements FormatterInterface
      */
     public function format(array $record)
     {
-        return json_encode($record) . ($this->appendNewline ? "\n" : '');
+        return $this->toJson($this->normalize($record), true) . ($this->appendNewline ? "\n" : '');
     }
 
     /**
@@ -82,6 +88,14 @@ class JsonFormatter implements FormatterInterface
         }
     }
 
+    /**
+     * @param bool $include
+     */
+    public function includeStacktraces($include = true)
+    {
+        $this->includeStacktraces = $include;
+    }
+
     /**
      * Return a JSON-encoded array of records.
      *
@@ -90,7 +104,7 @@ class JsonFormatter implements FormatterInterface
      */
     protected function formatBatchJson(array $records)
     {
-        return json_encode($records);
+        return $this->toJson($this->normalize($records), true);
     }
 
     /**
@@ -113,4 +127,76 @@ class JsonFormatter implements FormatterInterface
 
         return implode("\n", $records);
     }
+
+    /**
+     * Normalizes given $data.
+     *
+     * @param mixed $data
+     *
+     * @return mixed
+     */
+    protected function normalize($data)
+    {
+        if (is_array($data) || $data instanceof \Traversable) {
+            $normalized = array();
+
+            $count = 1;
+            foreach ($data as $key => $value) {
+                if ($count++ >= 1000) {
+                    $normalized['...'] = 'Over 1000 items, aborting normalization';
+                    break;
+                }
+                $normalized[$key] = $this->normalize($value);
+            }
+
+            return $normalized;
+        }
+
+        if ($data instanceof Exception) {
+            return $this->normalizeException($data);
+        }
+
+        return $data;
+    }
+
+    /**
+     * Normalizes given exception with or without its own stack trace based on
+     * `includeStacktraces` property.
+     *
+     * @param Exception|Throwable $e
+     *
+     * @return array
+     */
+    protected function normalizeException($e)
+    {
+        // TODO 2.0 only check for Throwable
+        if (!$e instanceof Exception && !$e instanceof \Throwable) {
+            throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.get_class($e));
+        }
+
+        $data = array(
+            'class' => get_class($e),
+            'message' => $e->getMessage(),
+            'code' => $e->getCode(),
+            'file' => $e->getFile().':'.$e->getLine(),
+        );
+
+        if ($this->includeStacktraces) {
+            $trace = $e->getTrace();
+            foreach ($trace as $frame) {
+                if (isset($frame['file'])) {
+                    $data['trace'][] = $frame['file'].':'.$frame['line'];
+                } else {
+                    // We should again normalize the frames, because it might contain invalid items
+                    $data['trace'][] = $this->normalize($frame);
+                }
+            }
+        }
+
+        if ($previous = $e->getPrevious()) {
+            $data['previous'] = $this->normalizeException($previous);
+        }
+
+        return $data;
+    }
 }

+ 13 - 3
src/Monolog/Formatter/LineFormatter.php

@@ -11,8 +11,6 @@
 
 namespace Monolog\Formatter;
 
-use Exception;
-
 /**
  * Formats incoming records into a one-line string
  *
@@ -78,6 +76,13 @@ class LineFormatter extends NormalizerFormatter
             }
         }
 
+        foreach ($vars['context'] as $var => $val) {
+            if (false !== strpos($output, '%context.'.$var.'%')) {
+                $output = str_replace('%context.'.$var.'%', $this->stringify($val), $output);
+                unset($vars['context'][$var]);
+            }
+        }
+
         if ($this->ignoreEmptyContextAndExtra) {
             if (empty($vars['context'])) {
                 unset($vars['context']);
@@ -114,8 +119,13 @@ class LineFormatter extends NormalizerFormatter
         return $this->replaceNewlines($this->convertToString($value));
     }
 
-    protected function normalizeException(Exception $e)
+    protected function normalizeException($e)
     {
+        // TODO 2.0 only check for Throwable
+        if (!$e instanceof \Exception && !$e instanceof \Throwable) {
+            throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.get_class($e));
+        }
+
         $previousText = '';
         if ($previous = $e->getPrevious()) {
             do {

+ 8 - 2
src/Monolog/Formatter/NormalizerFormatter.php

@@ -90,7 +90,8 @@ class NormalizerFormatter implements FormatterInterface
         }
 
         if (is_object($data)) {
-            if ($data instanceof Exception) {
+            // TODO 2.0 only check for Throwable
+            if ($data instanceof Exception || (PHP_VERSION_ID > 70000 && $data instanceof \Throwable)) {
                 return $this->normalizeException($data);
             }
 
@@ -112,8 +113,13 @@ class NormalizerFormatter implements FormatterInterface
         return '[unknown('.gettype($data).')]';
     }
 
-    protected function normalizeException(Exception $e)
+    protected function normalizeException($e)
     {
+        // TODO 2.0 only check for Throwable
+        if (!$e instanceof Exception && !$e instanceof \Throwable) {
+            throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.get_class($e));
+        }
+
         $data = array(
             'class' => get_class($e),
             'message' => $e->getMessage(),

+ 9 - 0
src/Monolog/Handler/AbstractSyslogHandler.php

@@ -70,6 +70,15 @@ abstract class AbstractSyslogHandler extends AbstractProcessingHandler
             $this->facilities['local5'] = LOG_LOCAL5;
             $this->facilities['local6'] = LOG_LOCAL6;
             $this->facilities['local7'] = LOG_LOCAL7;
+        } else {
+            $this->facilities['local0'] = 128; // LOG_LOCAL0
+            $this->facilities['local1'] = 136; // LOG_LOCAL1
+            $this->facilities['local2'] = 144; // LOG_LOCAL2
+            $this->facilities['local3'] = 152; // LOG_LOCAL3
+            $this->facilities['local4'] = 160; // LOG_LOCAL4
+            $this->facilities['local5'] = 168; // LOG_LOCAL5
+            $this->facilities['local6'] = 176; // LOG_LOCAL6
+            $this->facilities['local7'] = 184; // LOG_LOCAL7
         }
 
         // convert textual description of facility to syslog constant

+ 65 - 16
src/Monolog/Handler/AmqpHandler.php

@@ -55,18 +55,12 @@ class AmqpHandler extends AbstractProcessingHandler
     protected function write(array $record)
     {
         $data = $record["formatted"];
-
-        $routingKey = sprintf(
-            '%s.%s',
-            // TODO 2.0 remove substr call
-            substr($record['level_name'], 0, 4),
-            $record['channel']
-        );
+        $routingKey = $this->getRoutingKey($record);
 
         if ($this->exchange instanceof AMQPExchange) {
             $this->exchange->publish(
                 $data,
-                strtolower($routingKey),
+                $routingKey,
                 0,
                 array(
                     'delivery_mode' => 2,
@@ -75,17 +69,72 @@ class AmqpHandler extends AbstractProcessingHandler
             );
         } else {
             $this->exchange->basic_publish(
-                new AMQPMessage(
-                    (string) $data,
-                    array(
-                        'delivery_mode' => 2,
-                        'content_type' => 'application/json',
-                    )
-                ),
+                $this->createAmqpMessage($data),
+                $this->exchangeName,
+                $routingKey
+            );
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function handleBatch(array $records)
+    {
+        if ($this->exchange instanceof AMQPExchange) {
+            parent::handleBatch($records);
+            return;
+        }
+
+        foreach ($records as $record) {
+            if (!$this->isHandling($record)) {
+                continue;
+            }
+
+            $record = $this->processRecord($record);
+            $data = $this->getFormatter()->format($record);
+
+            $this->exchange->batch_basic_publish(
+                $this->createAmqpMessage($data),
                 $this->exchangeName,
-                strtolower($routingKey)
+                $this->getRoutingKey($record)
             );
         }
+
+        $this->exchange->publish_batch();
+    }
+
+    /**
+     * Gets the routing key for the AMQP exchange
+     *
+     * @param array $record
+     * @return string
+     */
+    private function getRoutingKey(array $record)
+    {
+        $routingKey = sprintf(
+            '%s.%s',
+            // TODO 2.0 remove substr call
+            substr($record['level_name'], 0, 4),
+            $record['channel']
+        );
+
+        return strtolower($routingKey);
+    }
+
+    /**
+     * @param string $data
+     * @return AMQPMessage
+     */
+    private function createAmqpMessage($data)
+    {
+        return new AMQPMessage(
+            (string) $data,
+            array(
+                'delivery_mode' => 2,
+                'content_type' => 'application/json',
+            )
+        );
     }
 
     /**

+ 53 - 19
src/Monolog/Handler/BrowserConsoleHandler.php

@@ -46,9 +46,9 @@ class BrowserConsoleHandler extends AbstractProcessingHandler
         self::$records[] = $record;
 
         // Register shutdown handler if not already done
-        if (PHP_SAPI !== 'cli' && !self::$initialized) {
+        if (!self::$initialized) {
             self::$initialized = true;
-            register_shutdown_function(array('Monolog\Handler\BrowserConsoleHandler', 'send'));
+            $this->registerShutdownFunction();
         }
     }
 
@@ -58,26 +58,16 @@ class BrowserConsoleHandler extends AbstractProcessingHandler
      */
     public static function send()
     {
-        $htmlTags = true;
-        // Check content type
-        foreach (headers_list() as $header) {
-            if (stripos($header, 'content-type:') === 0) {
-                // This handler only works with HTML and javascript outputs
-                // text/javascript is obsolete in favour of application/javascript, but still used
-                if (stripos($header, 'application/javascript') !== false || stripos($header, 'text/javascript') !== false) {
-                    $htmlTags = false;
-                } elseif (stripos($header, 'text/html') === false) {
-                    return;
-                }
-                break;
-            }
+        $format = self::getResponseFormat();
+        if ($format === 'unknown') {
+            return;
         }
 
         if (count(self::$records)) {
-            if ($htmlTags) {
-                echo '<script>' , self::generateScript() , '</script>';
-            } else {
-                echo self::generateScript();
+            if ($format === 'html') {
+                self::writeOutput('<script>' , self::generateScript() , '</script>');
+            } elseif ($format === 'js') {
+                self::writeOutput(self::generateScript());
             }
             self::reset();
         }
@@ -91,6 +81,50 @@ class BrowserConsoleHandler extends AbstractProcessingHandler
         self::$records = array();
     }
 
+    /**
+     * Wrapper for register_shutdown_function to allow overriding
+     */
+    protected function registerShutdownFunction()
+    {
+        if (PHP_SAPI !== 'cli') {
+            register_shutdown_function(array('Monolog\Handler\BrowserConsoleHandler', 'send'));
+        }
+    }
+
+    /**
+     * Wrapper for echo to allow overriding
+     *
+     * @param string $str
+     */
+    protected static function writeOutput($str)
+    {
+        echo $str;
+    }
+
+    /**
+     * Checks the format of the response
+     *
+     * @return string One of 'js', 'html' or 'unknown'
+     */
+    protected static function getResponseFormat()
+    {
+        // Check content type
+        foreach (headers_list() as $header) {
+            if (stripos($header, 'content-type:') === 0) {
+                // This handler only works with HTML and javascript outputs
+                // text/javascript is obsolete in favour of application/javascript, but still used
+                if (stripos($header, 'application/javascript') !== false || stripos($header, 'text/javascript') !== false) {
+                    return 'js';
+                } elseif (stripos($header, 'text/html') !== false) {
+                    return 'html';
+                }
+                break;
+            }
+        }
+
+        return 'unknown';
+    }
+
     private static function generateScript()
     {
         $script = array();

+ 4 - 1
src/Monolog/Handler/ChromePHPHandler.php

@@ -17,6 +17,8 @@ use Monolog\Logger;
 /**
  * Handler sending logs to the ChromePHP extension (http://www.chromephp.com/)
  *
+ * This also works out of the box with Firefox 43+
+ *
  * @author Christophe Coevoet <stof@notk.org>
  */
 class ChromePHPHandler extends AbstractProcessingHandler
@@ -175,7 +177,8 @@ class ChromePHPHandler extends AbstractProcessingHandler
             return false;
         }
 
-        return preg_match('{\bChrome/\d+[\.\d+]*\b}', $_SERVER['HTTP_USER_AGENT']);
+        // matches any Chrome, or Firefox 43+
+        return preg_match('{\b(?:Chrome/\d+(?:\.\d+)*|Firefox/(?:4[3-9]|[5-9]\d|\d{3,})(?:\.\d)*)\b}', $_SERVER['HTTP_USER_AGENT']);
     }
 
     /**

+ 14 - 0
src/Monolog/Handler/GroupHandler.php

@@ -11,6 +11,8 @@
 
 namespace Monolog\Handler;
 
+use Monolog\Formatter\FormatterInterface;
+
 /**
  * Forwards records to multiple handlers
  *
@@ -77,4 +79,16 @@ class GroupHandler extends Handler implements ProcessableHandlerInterface
             $handler->handleBatch($records);
         }
     }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function setFormatter(FormatterInterface $formatter)
+    {
+        foreach ($this->handlers as $handler) {
+            $handler->setFormatter($formatter);
+        }
+
+        return $this;
+    }
 }

+ 104 - 0
src/Monolog/Handler/HandlerWrapper.php

@@ -0,0 +1,104 @@
+<?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\Formatter\FormatterInterface;
+
+/**
+ * This simple wrapper class can be used to extend handlers functionality.
+ *
+ * Example: A filtering handle. Inherit from this class, override isHandling() like this
+ *
+ * public function isHandling(array $record)
+ * {
+ *      if ($record meets certain conditions) {
+ *          return false;
+ *      }
+ *      return $this->handler->isHandling($record);
+ * }
+ *
+ * @author Alexey Karapetov <alexey@karapetov.com>
+ */
+class HandlerWrapper implements HandlerInterface
+{
+    /**
+     * @var HandlerInterface
+     */
+    private $handler;
+
+    /**
+     * HandlerWrapper constructor.
+     * @param HandlerInterface $handler
+     */
+    public function __construct(HandlerInterface $handler)
+    {
+        $this->handler = $handler;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isHandling(array $record)
+    {
+        return $this->handler->isHandling($record);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function handle(array $record)
+    {
+        return $this->handler->handle($record);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function handleBatch(array $records)
+    {
+        return $this->handler->handleBatch($records);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function pushProcessor($callback)
+    {
+        $this->handler->pushProcessor($callback);
+        return $this;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function popProcessor()
+    {
+        return $this->handler->popProcessor();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function setFormatter(FormatterInterface $formatter)
+    {
+        $this->handler->setFormatter($formatter);
+        return $this;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getFormatter()
+    {
+        return $this->handler->getFormatter();
+    }
+}

+ 24 - 0
src/Monolog/Handler/RavenHandler.php

@@ -38,6 +38,12 @@ class RavenHandler extends AbstractProcessingHandler
         Logger::EMERGENCY => Raven_Client::FATAL,
     );
 
+    /**
+     * @var string should represent the current version of the calling
+     *             software. Can be any string (git commit, version number)
+     */
+    private $release;
+
     /**
      * @var Raven_Client the client object that sends the message to the server
      */
@@ -139,6 +145,10 @@ class RavenHandler extends AbstractProcessingHandler
             $options['tags'] = array_merge($options['tags'], $record['context']['tags']);
             unset($record['context']['tags']);
         }
+        if (!empty($record['context']['fingerprint'])) {
+            $options['fingerprint'] = $record['context']['fingerprint'];
+            unset($record['context']['fingerprint']);
+        }
         if (!empty($record['context']['logger'])) {
             $options['logger'] = $record['context']['logger'];
             unset($record['context']['logger']);
@@ -165,6 +175,10 @@ class RavenHandler extends AbstractProcessingHandler
             $options['extra']['extra'] = $record['extra'];
         }
 
+        if (!empty($this->release) && !isset($options['release'])) {
+            $options['release'] = $this->release;
+        }
+
         if (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Exception) {
             $options['extra']['message'] = $record['formatted'];
             $this->ravenClient->captureException($record['context']['exception'], $options);
@@ -204,4 +218,14 @@ class RavenHandler extends AbstractProcessingHandler
     {
         return array('checksum', 'release');
     }
+
+    /**
+     * @param string $value
+     */
+    public function setRelease($value)
+    {
+        $this->release = $value;
+
+        return $this;
+    }
 }

+ 4 - 0
src/Monolog/Handler/RotatingFileHandler.php

@@ -115,7 +115,11 @@ class RotatingFileHandler extends StreamHandler
 
         foreach (array_slice($logFiles, $this->maxFiles) as $file) {
             if (is_writable($file)) {
+                // suppress errors here as unlink() might fail if two processes
+                // are cleaning up/rotating at the same time
+                set_error_handler(function ($errno, $errstr, $errfile, $errline) {});
                 unlink($file);
+                restore_error_handler();
             }
         }
 

+ 13 - 0
src/Monolog/Logger.php

@@ -157,6 +157,19 @@ class Logger implements LoggerInterface
         return $this->name;
     }
 
+    /**
+     * Return a new cloned instance with the name changed
+     *
+     * @return static
+     */
+    public function withName($name)
+    {
+        $new = clone $this;
+        $new->name = $name;
+
+        return $new;
+    }
+
     /**
      * Pushes a handler on to the stack.
      *

+ 44 - 0
tests/Monolog/Formatter/JsonFormatterTest.php

@@ -75,4 +75,48 @@ class JsonFormatterTest extends TestCase
         });
         $this->assertEquals(implode("\n", $expected), $formatter->formatBatch($records));
     }
+
+    public function testDefFormatWithException()
+    {
+        $formatter = new JsonFormatter();
+        $exception = new \RuntimeException('Foo');
+        $message = $formatter->format(array(
+            'level_name' => 'CRITICAL',
+            'channel' => 'core',
+            'context' => array('exception' => $exception),
+            'datetime' => new \DateTime(),
+            'extra' => array(),
+            'message' => 'foobar',
+        ));
+
+        if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
+            $path = substr(json_encode($exception->getFile(), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), 1, -1);
+        } else {
+            $path = substr(json_encode($exception->getFile()), 1, -1);
+        }
+        $this->assertEquals('{"level_name":"CRITICAL","channel":"core","context":{"exception":{"class":"RuntimeException","message":"'.$exception->getMessage().'","code":'.$exception->getCode().',"file":"'.$path.':'.$exception->getLine().'"}},"datetime":'.json_encode(new \DateTime()).',"extra":[],"message":"foobar"}'."\n", $message);
+    }
+
+    public function testDefFormatWithPreviousException()
+    {
+        $formatter = new JsonFormatter();
+        $exception = new \RuntimeException('Foo', 0, new \LogicException('Wut?'));
+        $message = $formatter->format(array(
+            'level_name' => 'CRITICAL',
+            'channel' => 'core',
+            'context' => array('exception' => $exception),
+            'datetime' => new \DateTime(),
+            'extra' => array(),
+            'message' => 'foobar',
+        ));
+
+        if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
+            $pathPrevious = substr(json_encode($exception->getPrevious()->getFile(), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), 1, -1);
+            $pathException = substr(json_encode($exception->getFile(), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), 1, -1);
+        } else {
+            $pathPrevious = substr(json_encode($exception->getPrevious()->getFile()), 1, -1);
+            $pathException = substr(json_encode($exception->getFile()), 1, -1);
+        }
+        $this->assertEquals('{"level_name":"CRITICAL","channel":"core","context":{"exception":{"class":"RuntimeException","message":"'.$exception->getMessage().'","code":'.$exception->getCode().',"file":"'.$pathException.':'.$exception->getLine().'","previous":{"class":"LogicException","message":"'.$exception->getPrevious()->getMessage().'","code":'.$exception->getPrevious()->getCode().',"file":"'.$pathPrevious.':'.$exception->getPrevious()->getLine().'"}}},"datetime":'.json_encode(new \DateTime()).',"extra":[],"message":"foobar"}'."\n", $message);
+    }
 }

+ 14 - 0
tests/Monolog/Formatter/LineFormatterTest.php

@@ -91,6 +91,20 @@ class LineFormatterTest extends \PHPUnit_Framework_TestCase
         $this->assertEquals('['.date('Y-m-d').'] meh.ERROR: log  '."\n", $message);
     }
 
+    public function testContextAndExtraReplacement()
+    {
+        $formatter = new LineFormatter('%context.foo% => %extra.foo%');
+        $message = $formatter->format(array(
+            'level_name' => 'ERROR',
+            'channel' => 'meh',
+            'context' => array('foo' => 'bar'),
+            'datetime' => new \DateTime,
+            'extra' => array('foo' => 'xbar'),
+            'message' => 'log',
+        ));
+        $this->assertEquals('bar => xbar', $message);
+    }
+
     public function testDefFormatWithObject()
     {
         $formatter = new LineFormatter(null, 'Y-m-d');

+ 130 - 0
tests/Monolog/Handler/HandlerWrapperTest.php

@@ -0,0 +1,130 @@
+<?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;
+
+/**
+ * @author Alexey Karapetov <alexey@karapetov.com>
+ */
+class HandlerWrapperTest extends TestCase
+{
+    /**
+     * @var HandlerWrapper
+     */
+    private $wrapper;
+
+    private $handler;
+
+    public function setUp()
+    {
+        parent::setUp();
+        $this->handler = $this->getMock('Monolog\\Handler\\HandlerInterface');
+        $this->wrapper = new HandlerWrapper($this->handler);
+    }
+
+    /**
+     * @return array
+     */
+    public function trueFalseDataProvider()
+    {
+        return array(
+            array(true),
+            array(false),
+        );
+    }
+
+    /**
+     * @param $result
+     * @dataProvider trueFalseDataProvider
+     */
+    public function testIsHandling($result)
+    {
+        $record = $this->getRecord();
+        $this->handler->expects($this->once())
+            ->method('isHandling')
+            ->with($record)
+            ->willReturn($result);
+
+        $this->assertEquals($result, $this->wrapper->isHandling($record));
+    }
+
+    /**
+     * @param $result
+     * @dataProvider trueFalseDataProvider
+     */
+    public function testHandle($result)
+    {
+        $record = $this->getRecord();
+        $this->handler->expects($this->once())
+            ->method('handle')
+            ->with($record)
+            ->willReturn($result);
+
+        $this->assertEquals($result, $this->wrapper->handle($record));
+    }
+
+    /**
+     * @param $result
+     * @dataProvider trueFalseDataProvider
+     */
+    public function testHandleBatch($result)
+    {
+        $records = $this->getMultipleRecords();
+        $this->handler->expects($this->once())
+            ->method('handleBatch')
+            ->with($records)
+            ->willReturn($result);
+
+        $this->assertEquals($result, $this->wrapper->handleBatch($records));
+    }
+
+    public function testPushProcessor()
+    {
+        $processor = function () {};
+        $this->handler->expects($this->once())
+            ->method('pushProcessor')
+            ->with($processor);
+
+        $this->assertEquals($this->wrapper, $this->wrapper->pushProcessor($processor));
+    }
+
+    public function testPopProcessor()
+    {
+        $processor = function () {};
+        $this->handler->expects($this->once())
+            ->method('popProcessor')
+            ->willReturn($processor);
+
+        $this->assertEquals($processor, $this->wrapper->popProcessor());
+    }
+
+    public function testSetFormatter()
+    {
+        $formatter = $this->getMock('Monolog\\Formatter\\FormatterInterface');
+        $this->handler->expects($this->once())
+            ->method('setFormatter')
+            ->with($formatter);
+
+        $this->assertEquals($this->wrapper, $this->wrapper->setFormatter($formatter));
+    }
+
+    public function testGetFormatter()
+    {
+        $formatter = $this->getMock('Monolog\\Formatter\\FormatterInterface');
+        $this->handler->expects($this->once())
+            ->method('getFormatter')
+            ->willReturn($formatter);
+
+        $this->assertEquals($formatter, $this->wrapper->getFormatter());
+    }
+}

+ 28 - 0
tests/Monolog/Handler/RavenHandlerTest.php

@@ -99,6 +99,18 @@ class RavenHandlerTest extends TestCase
         $this->assertEquals($release, $ravenClient->lastData['release']);
     }
 
+    public function testFingerprint()
+    {
+        $ravenClient = $this->getRavenClient();
+        $handler = $this->getHandler($ravenClient);
+
+        $fingerprint = array('{{ default }}', 'other value');
+        $record = $this->getRecord(Logger::INFO, 'test', array('fingerprint' => $fingerprint));
+        $handler->handle($record);
+
+        $this->assertEquals($fingerprint, $ravenClient->lastData['fingerprint']);
+    }
+
     public function testUserContext()
     {
         $ravenClient = $this->getRavenClient();
@@ -192,6 +204,22 @@ class RavenHandlerTest extends TestCase
         $this->assertSame($formatter, $handler->getBatchFormatter());
     }
 
+    public function testRelease()
+    {
+        $ravenClient = $this->getRavenClient();
+        $handler = $this->getHandler($ravenClient);
+        $release = 'v42.42.42';
+        $handler->setRelease($release);
+        $record = $this->getRecord(Logger::INFO, 'test');
+        $handler->handle($record);
+        $this->assertEquals($release, $ravenClient->lastData['release']);
+
+        $localRelease = 'v41.41.41';
+        $record = $this->getRecord(Logger::INFO, 'test', array('release' => $localRelease));
+        $handler->handle($record);
+        $this->assertEquals($localRelease, $ravenClient->lastData['release']);
+    }
+
     private function methodThatThrowsAnException()
     {
         throw new \Exception('This is an exception');

+ 13 - 0
tests/Monolog/LoggerTest.php

@@ -33,6 +33,19 @@ class LoggerTest extends \PHPUnit_Framework_TestCase
         $this->assertEquals('ERROR', Logger::getLevelName(Logger::ERROR));
     }
 
+    /**
+     * @covers Monolog\Logger::withName
+     */
+    public function testWithName()
+    {
+        $first = new Logger('first', array($handler = new TestHandler()));
+        $second = $first->withName('second');
+
+        $this->assertSame('first', $first->getName());
+        $this->assertSame('second', $second->getName());
+        $this->assertSame($handler, $second->popHandler());
+    }
+
     /**
      * @covers Monolog\Logger::toMonologLevel
      */