From c362f9a07a98891ca18db4d5420c86acc17d85d9 Mon Sep 17 00:00:00 2001 From: Dominik Liebler Date: Fri, 18 Oct 2013 09:46:19 +0200 Subject: [PATCH 1/3] Flowdock handler --- src/Monolog/Handler/FlowdockHandler.php | 86 +++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/Monolog/Handler/FlowdockHandler.php diff --git a/src/Monolog/Handler/FlowdockHandler.php b/src/Monolog/Handler/FlowdockHandler.php new file mode 100644 index 00000000..08820adf --- /dev/null +++ b/src/Monolog/Handler/FlowdockHandler.php @@ -0,0 +1,86 @@ + + * + * 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 notifications through the Flowdock push API + * + * Notes: + * API token - Flowdock API token + * + * @author Dominik Liebler + * @see https://www.flowdock.com/api/push + */ +class FlowdockHandler extends AbstractProcessingHandler +{ + /** + * @var string + */ + protected $apiToken; + + /** + * @param string $apiToken + * @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 + */ + public function __construct($apiToken, $level = Logger::DEBUG, $bubble = true) + { + parent::__construct($level, $bubble); + $this->apiToken = $apiToken; + } + + /** + * @param array $record + */ + public function write(array $record) + { + $uri = 'https://api.flowdock.com/v1/messages/team_inbox/' . $this->apiToken; + + $tags = array( + '#logs', + '#' . strtolower($record['level_name']), + '#' . $record['channel'], + ); + + foreach ($record['extra'] as $key => $value) { + if ($key != 'requestIdent') { + $tags[] = '#' . $value; + } + } + + $data = array( + "subject" => sprintf("[%s] %s", $record['level_name'], $record['message']), + "content" => $record['message'], + "tags" => $tags + ); + + // add all extras + foreach ($record['extra'] as $key => $extra) { + $data[$key] = $extra; + } + + $streamContext = stream_context_create(array( + 'http' => array( + 'method' => 'POST', + 'headers' => "Content-type: application/json\r\n", + 'content' => json_encode($data) + ) + )); + + $streamHandle = fopen($uri, 'r', false, $streamContext); + stream_get_contents($streamHandle); + fclose($streamHandle); + } +} From 397658360647c798837e26fb32814758560eef51 Mon Sep 17 00:00:00 2001 From: Dominik Liebler Date: Sun, 16 Mar 2014 15:13:54 +0100 Subject: [PATCH 2/3] added FlowdockFormatter --- src/Monolog/Formatter/FlowdockFormatter.php | 104 ++++++++++++++++++ .../Formatter/FlowdockFormatterTest.php | 55 +++++++++ 2 files changed, 159 insertions(+) create mode 100644 src/Monolog/Formatter/FlowdockFormatter.php create mode 100644 tests/Monolog/Formatter/FlowdockFormatterTest.php diff --git a/src/Monolog/Formatter/FlowdockFormatter.php b/src/Monolog/Formatter/FlowdockFormatter.php new file mode 100644 index 00000000..af63d011 --- /dev/null +++ b/src/Monolog/Formatter/FlowdockFormatter.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Formatter; + +/** + * formats the record to be used in the FlowdockHandler + * + * @author Dominik Liebler + */ +class FlowdockFormatter implements FormatterInterface +{ + /** + * @var string + */ + private $source; + + /** + * @var string + */ + private $sourceEmail; + + /** + * @param string $source + * @param string $sourceEmail + */ + public function __construct($source, $sourceEmail) + { + $this->source = $source; + $this->sourceEmail = $sourceEmail; + } + + /** + * {@inheritdoc} + */ + public function format(array $record) + { + $tags = array( + '#logs', + '#' . strtolower($record['level_name']), + '#' . $record['channel'], + ); + + foreach ($record['extra'] as $value) { + $tags[] = '#' . $value; + } + + $subject = sprintf( + 'in %s: %s - %s', + $this->source, + $record['level_name'], + $this->getShortMessage($record['message']) + ); + + $record['flowdock'] = array( + 'source' => $this->source, + 'from_address' => $this->sourceEmail, + 'subject' => $subject, + 'content' => $record['message'], + 'tags' => $tags, + 'project' => $this->source, + ); + + return $record; + } + + /** + * {@inheritdoc} + */ + public function formatBatch(array $records) + { + $formatted = array(); + + foreach ($records as $record) { + $formatted[] = $this->format($record); + } + + return $formatted; + } + + /** + * @param string $message + * + * @return string + */ + public function getShortMessage($message) + { + $maxLength = 45; + + if (strlen($message) > $maxLength) { + $message = substr($message, 0, $maxLength - 4) . ' ...'; + } + + return $message; + } +} diff --git a/tests/Monolog/Formatter/FlowdockFormatterTest.php b/tests/Monolog/Formatter/FlowdockFormatterTest.php new file mode 100644 index 00000000..1b2fd97a --- /dev/null +++ b/tests/Monolog/Formatter/FlowdockFormatterTest.php @@ -0,0 +1,55 @@ + + * + * 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 Monolog\TestCase; + +class FlowdockFormatterTest extends TestCase +{ + /** + * @covers Monolog\Formatter\FlowdockFormatter::format + */ + public function testFormat() + { + $formatter = new FlowdockFormatter('test_source', 'source@test.com'); + $record = $this->getRecord(); + + $expected = array( + 'source' => 'test_source', + 'from_address' => 'source@test.com', + 'subject' => 'in test_source: WARNING - test', + 'content' => 'test', + 'tags' => array('#logs', '#warning', '#test'), + 'project' => 'test_source', + ); + $formatted = $formatter->format($record); + + $this->assertEquals($expected, $formatted['flowdock']); + } + + /** + * @ covers Monolog\Formatter\FlowdockFormatter::formatBatch + */ + public function testFormatBatch() + { + $formatter = new FlowdockFormatter('test_source', 'source@test.com'); + $records = array( + $this->getRecord(Logger::WARNING), + $this->getRecord(Logger::DEBUG), + ); + $formatted = $formatter->formatBatch($records); + + $this->assertArrayHasKey('flowdock', $formatted[0]); + $this->assertArrayHasKey('flowdock', $formatted[1]); + } +} From 7a8728279fca030c2e3f474dbf69dc4f1cd87a76 Mon Sep 17 00:00:00 2001 From: Dominik Liebler Date: Sun, 16 Mar 2014 15:24:58 +0100 Subject: [PATCH 3/3] refactored FlowdockHandler, removed formatting, using the FlowdockFormatter instead --- src/Monolog/Handler/FlowdockHandler.php | 87 +++++++++++-------- tests/Monolog/Handler/FlowdockHandlerTest.php | 81 +++++++++++++++++ 2 files changed, 132 insertions(+), 36 deletions(-) create mode 100644 tests/Monolog/Handler/FlowdockHandlerTest.php diff --git a/src/Monolog/Handler/FlowdockHandler.php b/src/Monolog/Handler/FlowdockHandler.php index 08820adf..2a52a9ff 100644 --- a/src/Monolog/Handler/FlowdockHandler.php +++ b/src/Monolog/Handler/FlowdockHandler.php @@ -11,7 +11,6 @@ namespace Monolog\Handler; -use Monolog\Formatter\LineFormatter; use Monolog\Logger; /** @@ -23,7 +22,7 @@ use Monolog\Logger; * @author Dominik Liebler * @see https://www.flowdock.com/api/push */ -class FlowdockHandler extends AbstractProcessingHandler +class FlowdockHandler extends SocketHandler { /** * @var string @@ -31,56 +30,72 @@ class FlowdockHandler extends AbstractProcessingHandler protected $apiToken; /** - * @param string $apiToken - * @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 + * @param string $apiToken + * @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 MissingExtensionException if OpenSSL is missing */ public function __construct($apiToken, $level = Logger::DEBUG, $bubble = true) { - parent::__construct($level, $bubble); + if (!extension_loaded('openssl')) { + throw new MissingExtensionException('The OpenSSL PHP extension is required to use the FlowdockHandler'); + } + + parent::__construct('ssl://api.flowdock.com:443', $level, $bubble); $this->apiToken = $apiToken; } /** + * {@inheritdoc} + * * @param array $record */ public function write(array $record) { - $uri = 'https://api.flowdock.com/v1/messages/team_inbox/' . $this->apiToken; + parent::write($record); - $tags = array( - '#logs', - '#' . strtolower($record['level_name']), - '#' . $record['channel'], - ); + $this->closeSocket(); + } - foreach ($record['extra'] as $key => $value) { - if ($key != 'requestIdent') { - $tags[] = '#' . $value; - } - } + /** + * {@inheritdoc} + * + * @param array $record + * @return string + */ + protected function generateDataStream($record) + { + $content = $this->buildContent($record); - $data = array( - "subject" => sprintf("[%s] %s", $record['level_name'], $record['message']), - "content" => $record['message'], - "tags" => $tags - ); + return $this->buildHeader($content) . $content; + } - // add all extras - foreach ($record['extra'] as $key => $extra) { - $data[$key] = $extra; - } + /** + * Builds the body of API call + * + * @param array $record + * @return string + */ + private function buildContent($record) + { + return json_encode($record['formatted']['flowdock']); + } - $streamContext = stream_context_create(array( - 'http' => array( - 'method' => 'POST', - 'headers' => "Content-type: application/json\r\n", - 'content' => json_encode($data) - ) - )); + /** + * Builds the header of the API Call + * + * @param string $content + * @return string + */ + private function buildHeader($content) + { + $header = "POST /v1/messages/team_inbox/" . $this->apiToken . " HTTP/1.1\r\n"; + $header .= "Host: api.flowdock.com\r\n"; + $header .= "Content-Type: application/json\r\n"; + $header .= "Content-Length: " . strlen($content) . "\r\n"; + $header .= "\r\n"; - $streamHandle = fopen($uri, 'r', false, $streamContext); - stream_get_contents($streamHandle); - fclose($streamHandle); + return $header; } } diff --git a/tests/Monolog/Handler/FlowdockHandlerTest.php b/tests/Monolog/Handler/FlowdockHandlerTest.php new file mode 100644 index 00000000..958de541 --- /dev/null +++ b/tests/Monolog/Handler/FlowdockHandlerTest.php @@ -0,0 +1,81 @@ + + * + * 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\FlowdockFormatter; +use Monolog\TestCase; +use Monolog\Logger; + +/** + * @author Dominik Liebler + * @see https://www.hipchat.com/docs/api + */ +class FlowdockHandlerTest extends TestCase +{ + /** + * @var resource + */ + private $res; + + /** + * @var FlowdockHandler + */ + private $handler; + + public function testWriteHeader() + { + $this->createHandler(); + $this->handler->handle($this->getRecord(Logger::CRITICAL, 'test1')); + fseek($this->res, 0); + $content = fread($this->res, 1024); + + $this->assertRegexp('/POST \/v1\/messages\/team_inbox\/.* HTTP\/1.1\\r\\nHost: api.flowdock.com\\r\\nContent-Type: application\/json\\r\\nContent-Length: \d{2,4}\\r\\n\\r\\n/', $content); + + return $content; + } + + /** + * @depends testWriteHeader + */ + public function testWriteContent($content) + { + $this->assertRegexp('/"source":"test_source"/', $content); + $this->assertRegexp('/"from_address":"source@test\.com"/', $content); + } + + private function createHandler($token = 'myToken') + { + $constructorArgs = array($token, Logger::DEBUG); + $this->res = fopen('php://memory', 'a'); + $this->handler = $this->getMock( + '\Monolog\Handler\FlowdockHandler', + array('fsockopen', 'streamSetTimeout', 'closeSocket'), + $constructorArgs + ); + + $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)); + + $this->handler->setFormatter(new FlowdockFormatter('test_source', 'source@test.com')); + } +}