diff --git a/composer.json b/composer.json index 8c9e3b8a..060dd9cb 100644 --- a/composer.json +++ b/composer.json @@ -35,8 +35,8 @@ "ruflin/elastica": "Allow sending log messages to an Elastic Search server", "videlalvaro/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", - "ext-mongo": "Allow sending log messages to a MongoDB server", - "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", "rollbar/rollbar": "Allow sending log messages to Rollbar", "php-console/php-console": "Allow sending log messages to Google Chrome" diff --git a/src/Monolog/Formatter/MongoDBFormatter.php b/src/Monolog/Formatter/MongoDBFormatter.php index eb067bb7..d40da1f8 100644 --- a/src/Monolog/Formatter/MongoDBFormatter.php +++ b/src/Monolog/Formatter/MongoDBFormatter.php @@ -11,6 +11,8 @@ namespace Monolog\Formatter; +use MongoDB\BSON\UTCDateTime; + /** * Formats a record for use with the MongoDBHandler. * @@ -55,9 +57,9 @@ class MongoDBFormatter implements FormatterInterface { if ($this->maxNestingLevel == 0 || $nestingLevel <= $this->maxNestingLevel) { foreach ($record as $name => $value) { - if ($value instanceof \DateTime) { + if ($value instanceof \DateTimeInterface) { $record[$name] = $this->formatDate($value, $nestingLevel + 1); - } elseif ($value instanceof \Exception) { + } elseif ($value instanceof \Throwable) { $record[$name] = $this->formatException($value, $nestingLevel + 1); } elseif (is_array($value)) { $record[$name] = $this->formatArray($value, $nestingLevel + 1); @@ -80,7 +82,7 @@ class MongoDBFormatter implements FormatterInterface return $this->formatArray($objectVars, $nestingLevel); } - protected function formatException(\Exception $exception, $nestingLevel) + protected function formatException(\Throwable $exception, $nestingLevel) { $formattedException = array( 'class' => get_class($exception), @@ -98,8 +100,15 @@ class MongoDBFormatter implements FormatterInterface return $this->formatArray($formattedException, $nestingLevel); } - protected function formatDate(\DateTime $value, $nestingLevel) + protected function formatDate(\DateTimeInterface $value, $nestingLevel) { - return new \MongoDate($value->getTimestamp()); + $seconds = (int) $value->format('U'); + $milliseconds = (int) $value->format('u') / 1000; + + if ($seconds < 0) { + return new UTCDateTime($seconds * 1000 - $milliseconds); + } else { + return new UTCDateTime($seconds * 1000 + $milliseconds); + } } } diff --git a/src/Monolog/Handler/MongoDBHandler.php b/src/Monolog/Handler/MongoDBHandler.php index 56fe755b..15f6c882 100644 --- a/src/Monolog/Handler/MongoDBHandler.php +++ b/src/Monolog/Handler/MongoDBHandler.php @@ -11,41 +11,66 @@ namespace Monolog\Handler; +use MongoDB\Driver\BulkWrite; +use MongoDB\Driver\Manager; +use MongoDB\Client; use Monolog\Logger; use Monolog\Formatter\NormalizerFormatter; /** * Logs to a MongoDB database. * - * usage example: + * Usage example: * - * $log = new Logger('application'); - * $mongodb = new MongoDBHandler(new \Mongo("mongodb://localhost:27017"), "logs", "prod"); + * $log = new \Monolog\Logger('application'); + * $client = new \MongoDB\Client('mongodb://localhost:27017'); + * $mongodb = new \Monolog\Handler\MongoDBHandler($client, 'logs', 'prod'); * $log->pushHandler($mongodb); * - * @author Thomas Tourlourat + * The above examples uses the MongoDB PHP library's client class; however, the + * MongoDB\Driver\Manager class from ext-mongodb is also supported. */ class MongoDBHandler extends AbstractProcessingHandler { - protected $mongoCollection; + private $collection; + private $manager; + private $namespace; - public function __construct($mongo, $database, $collection, $level = Logger::DEBUG, $bubble = true) + /** + * Constructor. + * + * @param Client|Manager $mongodb MongoDB library or driver client + * @param string $database Database name + * @param string $collection Collection name + * @param int $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($mongodb, $database, $collection, $level = Logger::DEBUG, $bubble = true) { - if (!($mongo instanceof \MongoClient || $mongo instanceof \Mongo || $mongo instanceof \MongoDB\Client)) { - throw new \InvalidArgumentException('MongoClient, Mongo or MongoDB\Client instance required'); + if (!($mongodb instanceof Client || $mongodb instanceof Manager)) { + throw new \InvalidArgumentException('MongoDB\Client or MongoDB\Driver\Manager instance required'); } - $this->mongoCollection = $mongo->selectCollection($database, $collection); + if ($mongodb instanceof Client) { + $this->collection = $mongodb->selectCollection($database, $collection); + } else { + $this->manager = $mongodb; + $this->namespace = $database . '.' . $collection; + } parent::__construct($level, $bubble); } protected function write(array $record) { - if ($this->mongoCollection instanceof \MongoDB\Collection) { - $this->mongoCollection->insertOne($record["formatted"]); - } else { - $this->mongoCollection->save($record["formatted"]); + if (isset($this->collection)) { + $this->collection->insertOne($record['formatted']); + } + + if (isset($this->manager, $this->namespace)) { + $bulk = new BulkWrite; + $bulk->insert($record["formatted"]); + $this->manager->executeBulkWrite($this->namespace, $bulk); } } @@ -54,6 +79,6 @@ class MongoDBHandler extends AbstractProcessingHandler */ protected function getDefaultFormatter() { - return new NormalizerFormatter(); + return new NormalizerFormatter; } } diff --git a/tests/Monolog/Formatter/MongoDBFormatterTest.php b/tests/Monolog/Formatter/MongoDBFormatterTest.php index 52e699e0..273e877c 100644 --- a/tests/Monolog/Formatter/MongoDBFormatterTest.php +++ b/tests/Monolog/Formatter/MongoDBFormatterTest.php @@ -20,8 +20,8 @@ class MongoDBFormatterTest extends \PHPUnit_Framework_TestCase { public function setUp() { - if (!class_exists('MongoDate')) { - $this->markTestSkipped('mongo extension not installed'); + if (!class_exists('MongoDB\BSON\UTCDateTime')) { + $this->markTestSkipped('ext-mongodb not installed'); } } @@ -62,7 +62,7 @@ class MongoDBFormatterTest extends \PHPUnit_Framework_TestCase 'level' => Logger::WARNING, 'level_name' => Logger::getLevelName(Logger::WARNING), 'channel' => 'test', - 'datetime' => new \DateTime('2014-02-01 00:00:00'), + 'datetime' => new \DateTime('2016-01-21T21:11:30.123456+00:00'), 'extra' => array(), ); @@ -75,8 +75,8 @@ class MongoDBFormatterTest extends \PHPUnit_Framework_TestCase $this->assertEquals(Logger::WARNING, $formattedRecord['level']); $this->assertEquals(Logger::getLevelName(Logger::WARNING), $formattedRecord['level_name']); $this->assertEquals('test', $formattedRecord['channel']); - $this->assertInstanceOf('\MongoDate', $formattedRecord['datetime']); - $this->assertEquals('0.00000000 1391212800', $formattedRecord['datetime']->__toString()); + $this->assertInstanceOf('MongoDB\BSON\UTCDateTime', $formattedRecord['datetime']); + $this->assertEquals('1453410690123', $formattedRecord['datetime']->__toString()); $this->assertEquals(array(), $formattedRecord['extra']); } @@ -89,7 +89,7 @@ class MongoDBFormatterTest extends \PHPUnit_Framework_TestCase $record = array( 'message' => 'some log message', 'context' => array( - 'stuff' => new \DateTime('2014-02-01 02:31:33'), + 'stuff' => new \DateTime('1969-01-21T21:11:30.123456+00:00'), 'some_object' => $someObject, 'context_string' => 'some string', 'context_int' => 123456, @@ -98,7 +98,7 @@ class MongoDBFormatterTest extends \PHPUnit_Framework_TestCase 'level' => Logger::WARNING, 'level_name' => Logger::getLevelName(Logger::WARNING), 'channel' => 'test', - 'datetime' => new \DateTime('2014-02-01 00:00:00'), + 'datetime' => new \DateTime('2016-01-21T21:11:30.123456+00:00'), 'extra' => array(), ); @@ -106,8 +106,9 @@ class MongoDBFormatterTest extends \PHPUnit_Framework_TestCase $formattedRecord = $formatter->format($record); $this->assertCount(5, $formattedRecord['context']); - $this->assertInstanceOf('\MongoDate', $formattedRecord['context']['stuff']); - $this->assertEquals('0.00000000 1391221893', $formattedRecord['context']['stuff']->__toString()); + $this->assertInstanceOf('MongoDB\BSON\UTCDateTime', $formattedRecord['context']['stuff']); + $this->assertEquals('-29731710123', $formattedRecord['context']['stuff']->__toString()); + $this->assertEquals( array( 'foo' => 'something', @@ -144,7 +145,7 @@ class MongoDBFormatterTest extends \PHPUnit_Framework_TestCase 'level' => Logger::WARNING, 'level_name' => Logger::getLevelName(Logger::WARNING), 'channel' => 'test', - 'datetime' => new \DateTime('2014-02-01 00:00:00'), + 'datetime' => new \DateTime('2016-01-21T21:11:30.123456+00:00'), 'extra' => array(), ); @@ -180,7 +181,7 @@ class MongoDBFormatterTest extends \PHPUnit_Framework_TestCase 'level' => Logger::WARNING, 'level_name' => Logger::getLevelName(Logger::WARNING), 'channel' => 'test', - 'datetime' => new \DateTime('2014-02-01 00:00:00'), + 'datetime' => new \DateTime('2016-01-21T21:11:30.123456+00:00'), 'extra' => array(), ); @@ -219,7 +220,7 @@ class MongoDBFormatterTest extends \PHPUnit_Framework_TestCase 'level' => Logger::WARNING, 'level_name' => Logger::getLevelName(Logger::WARNING), 'channel' => 'test', - 'datetime' => new \DateTime('2014-02-01 00:00:00'), + 'datetime' => new \DateTime('2016-01-21T21:11:30.123456+00:00'), 'extra' => array(), ); @@ -248,7 +249,7 @@ class MongoDBFormatterTest extends \PHPUnit_Framework_TestCase 'level' => Logger::WARNING, 'level_name' => Logger::getLevelName(Logger::WARNING), 'channel' => 'test', - 'datetime' => new \DateTime('2014-02-01 00:00:00'), + 'datetime' => new \DateTime('2016-01-21T21:11:30.123456+00:00'), 'extra' => array(), ); diff --git a/tests/Monolog/Handler/MongoDBHandlerTest.php b/tests/Monolog/Handler/MongoDBHandlerTest.php index 0fdef63a..5d6d59dc 100644 --- a/tests/Monolog/Handler/MongoDBHandlerTest.php +++ b/tests/Monolog/Handler/MongoDBHandlerTest.php @@ -11,8 +11,9 @@ namespace Monolog\Handler; +use MongoDB\Driver\Manager; use Monolog\TestCase; -use Monolog\Logger; +use Monolog\Formatter\NormalizerFormatter; class MongoDBHandlerTest extends TestCase { @@ -21,45 +22,57 @@ class MongoDBHandlerTest extends TestCase */ public function testConstructorShouldThrowExceptionForInvalidMongo() { - new MongoDBHandler(new \stdClass(), 'DB', 'Collection'); + new MongoDBHandler(new \stdClass, 'db', 'collection'); } - public function testHandle() + public function testHandleWithLibraryClient() { - $mongo = $this->getMock('Mongo', array('selectCollection'), array(), '', false); - $collection = $this->getMock('stdClass', array('save')); + if (!(class_exists('MongoDB\Client'))) { + $this->markTestSkipped('mongodb/mongodb not installed'); + } - $mongo->expects($this->once()) + $mongodb = $this->getMockBuilder('MongoDB\Client') + ->disableOriginalConstructor() + ->getMock(); + + $collection = $this->getMockBuilder('MongoDB\Collection') + ->disableOriginalConstructor() + ->getMock(); + + $mongodb->expects($this->once()) ->method('selectCollection') - ->with('DB', 'Collection') + ->with('db', 'collection') ->will($this->returnValue($collection)); - $record = $this->getRecord(Logger::WARNING, 'test', array('data' => new \stdClass, 'foo' => 34)); - - $expected = array( - 'message' => 'test', - 'context' => array('data' => '[object] (stdClass: {})', 'foo' => 34), - 'level' => Logger::WARNING, - 'level_name' => 'WARNING', - 'channel' => 'test', - 'datetime' => $record['datetime']->format('Y-m-d H:i:s'), - 'extra' => array(), - ); + $record = $this->getRecord(); + $expected = $record; + $expected['datetime'] = $record['datetime']->format(NormalizerFormatter::SIMPLE_DATE); $collection->expects($this->once()) - ->method('save') + ->method('insertOne') ->with($expected); - $handler = new MongoDBHandler($mongo, 'DB', 'Collection'); + $handler = new MongoDBHandler($mongodb, 'db', 'collection'); $handler->handle($record); } -} -if (!class_exists('Mongo')) { - class Mongo + public function testHandleWithDriverManager() { - public function selectCollection() - { + if (!(class_exists('MongoDB\Driver\Manager'))) { + $this->markTestSkipped('ext-mongodb not installed'); + } + + /* This can become a unit test once ManagerInterface can be mocked. + * See: https://jira.mongodb.org/browse/PHPC-378 + */ + $mongodb = new Manager('mongodb://localhost:27017'); + $handler = new MongoDBHandler($mongodb, 'test', 'monolog'); + $record = $this->getRecord(); + + try { + $handler->handle($record); + } catch (\RuntimeException $e) { + $this->markTestSkipped('Could not connect to MongoDB server on mongodb://localhost:27017'); } } }