diff --git a/doc/02-handlers-formatters-processors.md b/doc/02-handlers-formatters-processors.md index 02e727e9..29f1f55d 100644 --- a/doc/02-handlers-formatters-processors.md +++ b/doc/02-handlers-formatters-processors.md @@ -55,6 +55,7 @@ - [_RollbarHandler_](../src/Monolog/Handler/RollbarHandler.php): Logs records to a [Rollbar](https://rollbar.com/) account. - [_SyslogUdpHandler_](../src/Monolog/Handler/SyslogUdpHandler.php): Logs records to a remote [Syslogd](http://www.rsyslog.com/) server. - [_LogEntriesHandler_](../src/Monolog/Handler/LogEntriesHandler.php): Logs records to a [LogEntries](http://logentries.com/) account. +- [_LogmaticHandler_](../src/Monolog/Handler/LogmaticHandler.php): Logs records to a [Logmatic](http://logmatic.io/) account. ### Logging in development diff --git a/src/Monolog/Formatter/LogmaticFormatter.php b/src/Monolog/Formatter/LogmaticFormatter.php new file mode 100644 index 00000000..83b93019 --- /dev/null +++ b/src/Monolog/Formatter/LogmaticFormatter.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Formatter; + +/** + * Encodes message information into JSON in a format compatible with Logmatic. + * + * @author Julien Breux + */ +class LogmaticFormatter extends JsonFormatter +{ + /** + * @param string + */ + protected $hostname = ''; + + /** + * @param string + */ + protected $appname = ''; + + /** + * Set hostname + * + * @param string $hostname + */ + public function setHostname(string $hostname) { + $this->hostname = $hostname; + } + + /** + * Set appname + * + * @param string $appname + */ + public function setAppname(string $appname) { + $this->appname = $appname; + } + + /** + * Appends the 'hostname' and 'appname' parameter for indexing by Logmatic. + * + * @see http://doc.logmatic.io/docs/basics-to-send-data + * @see \Monolog\Formatter\JsonFormatter::format() + */ + public function format(array $record) + { + if (!empty($this->hostname)) { + $record['hostname'] = $this->hostname; + } + if (!empty($this->appname)) { + $record['appname'] = $this->appname; + } + + return parent::format($record); + } +} diff --git a/src/Monolog/Handler/LogmaticHandler.php b/src/Monolog/Handler/LogmaticHandler.php new file mode 100644 index 00000000..e78d3cf2 --- /dev/null +++ b/src/Monolog/Handler/LogmaticHandler.php @@ -0,0 +1,88 @@ + + * + * 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\FormatterInterface; +use Monolog\Formatter\LogmaticFormatter; + +/** + * @author Julien Breux + */ +class LogmaticHandler extends SocketHandler +{ + /** + * @var string + */ + protected $logToken; + + /** + * @var string + */ + protected $hostname; + + /** + * @var string + */ + protected $appname; + + /** + * @param string $hostname Host name supplied by Logmatic. + * @param string $appname Application name supplied by Logmatic. + * @param string $token Log token supplied by Logmatic. + * @param bool $useSSL Whether or not SSL encryption should be used. + * @param int $level The minimum logging level to trigger this handler. + * @param bool $bubble Whether or not messages that are handled should bubble up the stack. + * + * @throws MissingExtensionException If SSL encryption is set to true and OpenSSL is missing + */ + public function __construct($token, $hostname = '', $appname = '', $useSSL = true, $level = Logger::DEBUG, $bubble = true) + { + if ($useSSL && !extension_loaded('openssl')) { + throw new MissingExtensionException('The OpenSSL PHP plugin is required to use SSL encrypted connection for LogmaticHandler'); + } + + $endpoint = $useSSL ? 'ssl://api.logmatic.io:10515' : 'api.logmatic.io:10514'; + $endpoint .= '/v1/'; + + parent::__construct($endpoint, $level, $bubble); + + $this->logToken = $token; + $this->hostname = $hostname; + $this->appname = $appname; + } + + /** + * {@inheritdoc} + */ + protected function generateDataStream($record): string + { + return $this->logToken . ' ' . $record['formatted']; + } + + /** + * {@inheritdoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + $formatter = new LogmaticFormatter(); + + if (!empty($this->hostname)) { + $formatter->setHostname($this->hostname); + } + if (!empty($this->appname)) { + $formatter->setAppname($this->appname); + } + + return $formatter; + } +} diff --git a/tests/Monolog/Formatter/LogmaticFormatterTest.php b/tests/Monolog/Formatter/LogmaticFormatterTest.php new file mode 100644 index 00000000..d27670fa --- /dev/null +++ b/tests/Monolog/Formatter/LogmaticFormatterTest.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Formatter; + +use Monolog\Test\TestCase; + +/** + * @author Julien Breux + */ +class LogmaticFormatterTest extends TestCase +{ + /** + * @covers Monolog\Formatter\LogmaticFormatter::format + */ + public function testFormat() + { + $formatter = new LogmaticFormatter(); + $formatter->setHostname('testHostname'); + $formatter->setAppname('testAppname'); + $record = $this->getRecord(); + $formatted_decoded = json_decode($formatter->format($record), true); + $this->assertArrayHasKey('hostname', $formatted_decoded); + $this->assertArrayHasKey('appname', $formatted_decoded); + $this->assertEquals('testHostname', $formatted_decoded['hostname']); + $this->assertEquals('testAppname', $formatted_decoded['appname']); + } +} diff --git a/tests/Monolog/Handler/LogmaticTest.php b/tests/Monolog/Handler/LogmaticTest.php new file mode 100644 index 00000000..3948a412 --- /dev/null +++ b/tests/Monolog/Handler/LogmaticTest.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Test\TestCase; +use Monolog\Logger; + +/** + * @author Julien Breux + */ +class LogmaticHandlerTest extends TestCase +{ + /** + * @var resource + */ + private $res; + + /** + * @var LogmaticHandler + */ + private $handler; + + public function testWriteContent() + { + $this->createHandler(); + $this->handler->handle($this->getRecord(Logger::CRITICAL, 'Critical write test')); + + fseek($this->res, 0); + $content = fread($this->res, 1024); + + $this->assertRegexp('/testToken {"message":"Critical write test","context":\[\],"level":500,"level_name":"CRITICAL","channel":"test","datetime":"(.*)","extra":\[\],"hostname":"testHostname","appname":"testAppname"}/', $content); + } + + public function testWriteBatchContent() + { + $records = [ + $this->getRecord(), + $this->getRecord(), + $this->getRecord(), + ]; + $this->createHandler(); + $this->handler->handleBatch($records); + + fseek($this->res, 0); + $content = fread($this->res, 1024); + + $this->assertRegexp('/testToken {"message":"test","context":\[\],"level":300,"level_name":"WARNING","channel":"test","datetime":"(.*)","extra":\[\],"hostname":"testHostname","appname":"testAppname"}/', $content); + } + + private function createHandler() + { + $useSSL = extension_loaded('openssl'); + $args = ['testToken', 'testHostname', 'testAppname', $useSSL, Logger::DEBUG, true]; + $this->res = fopen('php://memory', 'a'); + $this->handler = $this->getMock( + '\Monolog\Handler\LogmaticHandler', + ['fsockopen', 'streamSetTimeout', 'closeSocket'], + $args + ); + + $reflectionProperty = new \ReflectionProperty('\Monolog\Handler\SocketHandler', 'connectionString'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($this->handler, 'localhost:1234'); + + $this->handler->expects($this->any()) + ->method('fsockopen') + ->will($this->returnValue($this->res)); + $this->handler->expects($this->any()) + ->method('streamSetTimeout') + ->will($this->returnValue(true)); + $this->handler->expects($this->any()) + ->method('closeSocket') + ->will($this->returnValue(true)); + } +}