From 7944eecbd8eb294e29acabff625e3b66f4adbf26 Mon Sep 17 00:00:00 2001 From: Ando Roots Date: Thu, 28 Aug 2014 22:08:03 +0300 Subject: [PATCH] Add FleepHookHandler This commit adds a Handler for sending logs to fleep.io conversations using their Webhook integration (https://fleep.io/integrations/webhooks/). --- README.mdown | 1 + src/Monolog/Handler/FleepHookHandler.php | 170 +++++++++++++++ .../Monolog/Handler/FleepHookHandlerTest.php | 200 ++++++++++++++++++ 3 files changed, 371 insertions(+) create mode 100644 src/Monolog/Handler/FleepHookHandler.php create mode 100644 tests/Monolog/Handler/FleepHookHandlerTest.php diff --git a/README.mdown b/README.mdown index e14b5f82..5125bce4 100644 --- a/README.mdown +++ b/README.mdown @@ -122,6 +122,7 @@ Handlers - _HipChatHandler_: Logs records to a [HipChat](http://hipchat.com) chat room using its API. - _FlowdockHandler_: Logs records to a [Flowdock](https://www.flowdock.com/) account. - _SlackHandler_: Logs records to a [Slack](https://www.slack.com/) account. +- _FleepHookHandler_: Logs records to a [Fleep](https://fleep.io/) conversation using Webhooks. ### Log specific servers and networked logging diff --git a/src/Monolog/Handler/FleepHookHandler.php b/src/Monolog/Handler/FleepHookHandler.php new file mode 100644 index 00000000..a938c1f6 --- /dev/null +++ b/src/Monolog/Handler/FleepHookHandler.php @@ -0,0 +1,170 @@ + + * + * 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\LineFormatter; +use Monolog\Logger; + +/** + * Sends logs to Fleep.io using WebHook integrations + * + * @see https://fleep.io/integrations/webhooks/ Fleep Webhooks Documentation + * @author Ando Roots + */ +class FleepHookHandler extends AbstractProcessingHandler +{ + + /** + * Fleep.io webhooks URI + */ + const HOOK_ENDPOINT = 'https://fleep.io/hook/'; + + /** + * @var string Webhook token (specifies the conversation where logs are sent) + */ + protected $token; + + /** + * @var string Full URI to the webhook endpoint (HOOK_ENDPOINT + token) + */ + protected $url; + + /** + * @var array Default options to Curl + */ + protected $curlOptions = array( + CURLOPT_POST => true, + CURLOPT_HTTPHEADER => array( + 'Content-Type: application/x-www-form-urlencoded' + ), + CURLOPT_RETURNTRANSFER => true + ); + + /** + * Construct a new Fleep.io Handler. + * + * You'll need a Fleep.op account to use this handler. + * For instructions on how to create a new web hook in your conversations + * see https://fleep.io/integrations/webhooks/ + * + * @param string $token Webhook token (ex: mTZG6s-XRfKdNTJtpVyVaA) + * @param bool|int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * @throws \LogicException + */ + public function __construct($token, $level = Logger::DEBUG, $bubble = true) + { + if (!extension_loaded('curl')) { + throw new \LogicException('The curl extension is needed to use FleepHookHandler'); + } + + $this->token = $token; + $this->url = self::HOOK_ENDPOINT . $this->token; + + parent::__construct($level, $bubble); + } + + /** + * @return string + */ + public function getToken() + { + return $this->token; + } + + /** + * @return array + */ + public function getCurlOptions() + { + return $this->curlOptions; + } + + /** + * Returns the default formatter to use with this handler + * + * Overloaded to remove empty context and extra arrays from the end of the log message. + * + * @author Ando Roots + * @return LineFormatter + */ + public function getDefaultFormatter() + { + return new LineFormatter(null, null, true, true); + + } + + /** + * Handles a log record + * + * @author Ando Roots + * @param array $record + */ + protected function write(array $record) + { + $this->send($record['formatted']); + } + + + /** + * Prepares the record for sending to Fleep + * + * @author Ando Roots + * @param string $message The formatted log message to send + */ + protected function send($message) + { + $this->addCurlOptions( + array( + CURLOPT_POSTFIELDS => http_build_query(array('message' => $message)), + CURLOPT_URL => $this->url, + ) + ); + + $this->execCurl($this->curlOptions); + + } + + /** + * Sends a new Curl request + * + * @author Ando Roots + * @param array $options Curl parameters, including the endpoint URL and POST payload + */ + protected function execCurl(array $options) + { + $curl = curl_init(); + + curl_setopt_array($curl, $options); + + curl_exec($curl); + curl_close($curl); + } + + + /** + * Adds or overwrites a curl option + * + * @author Ando Roots + * @param array $options An assoc array of Curl options, indexed by CURL_* constants + * @return $this + */ + public function addCurlOptions(array $options) + { + $this->curlOptions = array_replace( + $this->curlOptions, + $options + ); + + return $this; + } +} diff --git a/tests/Monolog/Handler/FleepHookHandlerTest.php b/tests/Monolog/Handler/FleepHookHandlerTest.php new file mode 100644 index 00000000..61290182 --- /dev/null +++ b/tests/Monolog/Handler/FleepHookHandlerTest.php @@ -0,0 +1,200 @@ + + * + * 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\LineFormatter; +use Monolog\Logger; +use Monolog\TestCase; + +/** + * Unit tests for the FleepHookHandler + * + * @author Ando Roots + * @coversDefaultClass \Monolog\Handler\FleepHookHandler + */ +class FleepHookHandlerTest extends TestCase +{ + /** + * Default token to use in tests + */ + const TOKEN = '123abc'; + + /** + * @var FleepHookHandler + */ + private $handler; + + /** + * @var Logger + */ + private $logger; + + public function setUp() + { + parent::setUp(); + + if (!extension_loaded('curl')) { + $this->markTestSkipped('This test requires curl extension to run'); + } + + // Create instances of the handler and logger for convenience + $this->handler = new FleepHookHandler(self::TOKEN); + $this->logger = new Logger('test'); + $this->logger->pushHandler($this->handler); + + } + + /** + * @covers ::__construct + */ + public function testConstructorSetsExpectedDefaults() + { + $this->assertEquals(self::TOKEN, $this->handler->getToken()); + $this->assertEquals(Logger::DEBUG, $this->handler->getLevel()); + $this->assertEquals(true, $this->handler->getBubble()); + } + + /** + * @covers ::write + */ + public function testWriteSendsFormattedMessageToFleep() + { + $handler = $this->mockHandler(array('send')); + + $message = 'theCakeIsALie'; + $handler->expects($this->once()) + ->method('send') + ->with( + $this->callback( + function ($message) { + return strstr($message, 'theCakeIsALie') && strstr($message, 'channel.ALERT'); + } + ) + ); + + $this->sendLog($handler, $message); + } + + /** + * @covers ::getDefaultFormatter + */ + public function testHandlerUsesLineFormatterWhichIgnoresEmptyArrays() + { + $record = array( + 'message' => 'msg', + 'context' => array(), + 'level' => Logger::DEBUG, + 'level_name' => Logger::getLevelName(Logger::DEBUG), + 'channel' => 'channel', + 'datetime' => new \DateTime(), + 'extra' => array(), + ); + + $expectedFormatter = new LineFormatter(null, null, true, true); + $expected = $expectedFormatter->format($record); + + $handlerFormatter = $this->handler->getDefaultFormatter(); + $actual = $handlerFormatter->format($record); + + $this->assertEquals($expected, $actual, 'Empty context and extra arrays should not be rendered'); + + } + + /** + * Tests that the URL to which the message is posted is of correct format + * + * Example: https://fleep.io/hook/mTZG6s-XRfKdNTJtpVyVaV + * @covers ::__construct + */ + public function testFleepEndpointUrlIsConstructedCorrectly() + { + $handler = $this->mockHandler(array('execCurl')); + + $token = self::TOKEN; + + // Set up expectation to execCurl: receive curlOpts array where URL is correct + $handler->expects($this->once()) + ->method('execCurl') + ->with( + $this->callback( + function (array $curlOpts) use ($token) { + return $curlOpts[CURLOPT_URL] === FleepHookHandler::HOOK_ENDPOINT . $token; + } + ) + ); + $this->sendLog($handler); + } + + /** + * Tests that the log message is added to the POST content, under the 'message' key + * + * @covers ::send + */ + public function testSendAddsMessageToCurlOpts() + { + $handler = $this->mockHandler(array('execCurl')); + $handler->expects($this->once()) + ->method('execCurl') + ->with( + $this->callback( + function ($curlOpts) { + parse_str($curlOpts[CURLOPT_POSTFIELDS], $body); + return isset($body['message']) && strstr($body['message'], 'msg'); + } + ) + ); + + $this->sendLog($handler, 'msg'); + } + + /** + * @covers ::addCurlOptions + */ + public function testAddCurlOptionsLeavesUnspecifiedOptionsIntact() + { + $this->handler->addCurlOptions(array(CURLOPT_PROXY => 'http://localhost:3128')); + $this->assertArrayHasKey(CURLOPT_POST, $this->handler->getCurlOptions(), 'addCurlOpts deleted a key!'); + } + + public function testAddCurlOptionsAddsANewCurlOption() + { + $proxy = 'http://localhost:3128'; + $this->handler->addCurlOptions(array(CURLOPT_PROXY => $proxy)); + $options = $this->handler->getCurlOptions(); + $this->assertEquals($options[CURLOPT_PROXY], $proxy); + } + + /** + * Helper method for simulating a long sending event from the logger + * + * @param FleepHookHandler $handler + * @param string $message + */ + private function sendLog(FleepHookHandler $handler, $message = 'test') + { + $logger = new Logger('channel'); + $logger->pushHandler($handler); + $logger->addAlert($message); + } + + /** + * Helper method for constructing a new mock of FleepHookHandler + * + * @param array $methods + * @return \PHPUnit_Framework_MockObject_MockObject + */ + private function mockHandler(array $methods) + { + return $this->getMock('Monolog\Handler\FleepHookHandler', $methods, array(self::TOKEN)); + } + +}