diff --git a/composer.json b/composer.json index c8c28b04..c78f0456 100644 --- a/composer.json +++ b/composer.json @@ -16,9 +16,11 @@ "php": ">=5.3.0" }, "require-dev": { + "raven/raven": ">=0.2.0", "mlehner/gelf-php": "1.0.*" }, "suggest": { + "raven/raven": "Allow sending log messages to a Sentry server", "mlehner/gelf-php": "Allow sending log messages to a GrayLog2 server" }, "autoload": { diff --git a/src/Monolog/Formatter/RavenFormatter.php b/src/Monolog/Formatter/RavenFormatter.php new file mode 100644 index 00000000..d7eb917d --- /dev/null +++ b/src/Monolog/Formatter/RavenFormatter.php @@ -0,0 +1,51 @@ + + * + * 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; +use \Raven_Client; + +/** + * Serializes a log message for Raven (https://github.com/getsentry/raven-php) + * + * @author Marc Abramowitz + */ +class RavenFormatter extends NormalizerFormatter +{ + /** + * Translates Monolog log levels to Raven log levels. + */ + private $logLevels = array( + Logger::DEBUG => Raven_Client::DEBUG, + Logger::INFO => Raven_Client::INFO, + Logger::WARNING => Raven_Client::WARNING, + Logger::ERROR => Raven_Client::ERROR, + ); + + /** + * {@inheritdoc} + */ + public function format(array $record) + { + $record = parent::format($record); + + $record['level'] = $this->logLevels[$record['level']]; + $record['message'] = $record['channel'] . ': ' . $record['message']; + + if (isset($record['context']['context'])) + { + $record['context'] = $record['context']['context']; + } + + return $record; + } +} diff --git a/src/Monolog/Handler/RavenHandler.php b/src/Monolog/Handler/RavenHandler.php new file mode 100644 index 00000000..8faa1b86 --- /dev/null +++ b/src/Monolog/Handler/RavenHandler.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\Handler; + +use Monolog\Logger; +use Monolog\Handler\AbstractProcessingHandler; +use Monolog\Formatter\RavenFormatter; +use \Raven_Client; + +/** + * Handler to send messages to a Sentry (https://github.com/dcramer/sentry) server + * using raven-php (https://github.com/getsentry/raven-php) + * + * @author Marc Abramowitz + */ +class RavenHandler extends AbstractProcessingHandler +{ + /** + * @var Raven_Client the client object that sends the message to the server + */ + protected $ravenClient; + + /** + * @param Raven_Client $ravenClient + * @param integer $level The minimum logging level at which this handler will be triggered + * @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not + */ + public function __construct(Raven_Client $ravenClient, $level = Logger::DEBUG, $bubble = true) + { + parent::__construct($level, $bubble); + + $this->ravenClient = $ravenClient; + } + + /** + * {@inheritdoc} + */ + public function close() + { + $this->ravenClient = null; + } + + /** + * {@inheritdoc} + */ + protected function write(array $record) + { + if ($record['level'] == Logger::ERROR) + { + $this->ravenClient->captureException($record['context']['context']); + } + else + { + $this->ravenClient->captureMessage( + $record['formatted']['message'], $params = $record, + $record['formatted']['level'], $stack = true + ); + } + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter() + { + return new RavenFormatter(); + } +} diff --git a/tests/Monolog/Handler/RavenHandlerTest.php b/tests/Monolog/Handler/RavenHandlerTest.php new file mode 100644 index 00000000..e6040f28 --- /dev/null +++ b/tests/Monolog/Handler/RavenHandlerTest.php @@ -0,0 +1,107 @@ + + * + * 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; +use Monolog\Handler\RavenHandler; +use \Raven_Client; + +class MockRavenClient extends Raven_Client +{ + public function capture($data, $stack) + { + $this->lastData = $data; + $this->lastStack = $stack; + } + + public $lastData; + public $lastStack; +} + +class RavenHandlerTest extends TestCase +{ + public function setUp() + { + if (!class_exists("Raven_Client")) { + $this->markTestSkipped("raven/raven not installed"); + } + } + + /** + * @covers Monolog\Handler\RavenHandler::__construct + */ + public function testConstruct() + { + $handler = new RavenHandler($this->getRavenClient()); + $this->assertInstanceOf('Monolog\Handler\RavenHandler', $handler); + } + + protected function getHandler($ravenClient) + { + $handler = new RavenHandler($ravenClient); + return $handler; + } + + protected function getRavenClient() + { + $dsn = 'http://43f6017361224d098402974103bfc53d:a6a0538fc2934ba2bed32e08741b2cd3@marca.python.live.cheggnet.com:9000/1'; + return new MockRavenClient($dsn); + } + + public function testDebug() + { + $ravenClient = $this->getRavenClient(); + $handler = $this->getHandler($ravenClient); + + $record = $this->getRecord(Logger::DEBUG, "A test debug message"); + $handler->handle($record); + + $this->assertEquals(Raven_Client::DEBUG, $ravenClient->lastData['level']); + $this->assertContains($record['message'], $ravenClient->lastData['message']); + } + + public function testWarning() + { + $ravenClient = $this->getRavenClient(); + $handler = $this->getHandler($ravenClient); + + $record = $this->getRecord(Logger::WARNING, "A test warning message"); + $handler->handle($record); + + $this->assertEquals(Raven_Client::WARNING, $ravenClient->lastData['level']); + $this->assertContains($record['message'], $ravenClient->lastData['message']); + } + + public function testException() + { + $ravenClient = $this->getRavenClient(); + $handler = $this->getHandler($ravenClient); + + try + { + $this->methodThatThrowsAnException(); + } + catch (\Exception $e) + { + $record = $this->getRecord(Logger::ERROR, $e->getMessage(), array('context' => $e)); + $handler->handle($record); + } + + $this->assertEquals($record['message'], $ravenClient->lastData['message']); + } + + private function methodThatThrowsAnException() + { + throw new \Exception('This is an exception'); + } +}