From 20844b94ffe2d2e8c0f963ec3ae7dbd5fa8f2f11 Mon Sep 17 00:00:00 2001 From: gkedzierski Date: Fri, 13 Jun 2014 17:40:45 +0200 Subject: [PATCH 1/5] Add basic Slack logging functionality --- src/Monolog/Handler/SlackHandler.php | 138 +++++++++++++++++++++ tests/Monolog/Handler/SlackHandlerTest.php | 86 +++++++++++++ 2 files changed, 224 insertions(+) create mode 100644 src/Monolog/Handler/SlackHandler.php create mode 100644 tests/Monolog/Handler/SlackHandlerTest.php diff --git a/src/Monolog/Handler/SlackHandler.php b/src/Monolog/Handler/SlackHandler.php new file mode 100644 index 00000000..324bc020 --- /dev/null +++ b/src/Monolog/Handler/SlackHandler.php @@ -0,0 +1,138 @@ + + * + * 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; + +/** + * Sends notifications through Slack API + * + * @author Greg Kedzierski + * @see https://api.slack.com/ + */ +class SlackHandler extends SocketHandler +{ + /** + * Slack API token + * @var string + */ + private $token; + + /** + * Slack channel (encoded ID or name) + * @var string + */ + private $channel; + + /** + * Name of a bot + * @var string + */ + private $username; + + /** + * @param string $token Slack API token + * @param string $channel Slack channel (encoded ID or name) + * @param string $username Name of a bot + * @param 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($token, $channel, $username = 'Monolog', $level = Logger::CRITICAL, $bubble = true) + { + if (!extension_loaded('openssl')) { + throw new MissingExtensionException('The OpenSSL PHP extension is required to use the SlackHandler'); + } + + parent::__construct('ssl://slack.com:443', $level, $bubble); + + $this->token = $token; + $this->channel = $channel; + $this->username = $username; + } + + /** + * {@inheritdoc} + * + * @param array $record + * @return string + */ + protected function generateDataStream($record) + { + $content = $this->buildContent($record); + + return $this->buildHeader($content) . $content; + } + + /** + * Builds the body of API call + * + * @param array $record + * @return string + */ + private function buildContent($record) + { + $dataArray = array( + 'token' => $this->token, + 'channel' => $this->channel, + 'username' => $this->username, + 'attachments' => json_encode( + array( + array( + 'fallback' => $record['message'], + 'fields' => array( + array( + 'title' => 'Message', + 'value' => $record['message'], + 'short' => false + ), + array( + 'title' => 'Level', + 'value' => $record['level_name'], + 'short' => true + ) + ) + ) + ) + ) + ); + + return http_build_query($dataArray); + } + + /** + * Builds the header of the API Call + * + * @param string $content + * @return string + */ + private function buildHeader($content) + { + $header = "POST /api/chat.postMessage HTTP/1.1\r\n"; + $header .= "Host: slack.com\r\n"; + $header .= "Content-Type: application/x-www-form-urlencoded\r\n"; + $header .= "Content-Length: " . strlen($content) . "\r\n"; + $header .= "\r\n"; + + return $header; + } + + /** + * {@inheritdoc} + * + * @param array $record + */ + public function write(array $record) + { + parent::write($record); + $this->closeSocket(); + } +} diff --git a/tests/Monolog/Handler/SlackHandlerTest.php b/tests/Monolog/Handler/SlackHandlerTest.php new file mode 100644 index 00000000..e8132103 --- /dev/null +++ b/tests/Monolog/Handler/SlackHandlerTest.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\TestCase; +use Monolog\Logger; + +/** + * @author Greg Kedzierski + * @see https://api.slack.com/ + */ +class SlackHandlerTest extends TestCase +{ + /** + * @var resource + */ + private $res; + + /** + * @var SlackHandler + */ + private $handler; + + public function setUp() + { + if (!extension_loaded('openssl')) { + $this->markTestSkipped('This test requires openssl to run'); + } + } + + 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 \/api\/chat.postMessage HTTP\/1.1\\r\\nHost: slack.com\\r\\nContent-Type: application\/x-www-form-urlencoded\\r\\nContent-Length: \d{2,4}\\r\\n\\r\\n/', $content); + + return $content; + } + + /** + * @depends testWriteHeader + */ + public function testWriteContent($content) + { + $this->assertRegexp('/token=myToken&channel=channel1&username=Monolog&attachments=.*$/', $content); + } + + private function createHandler($token = 'myToken', $channel = 'channel1', $username = 'Monolog') + { + $constructorArgs = array($token, $channel, $username, Logger::DEBUG); + $this->res = fopen('php://memory', 'a'); + $this->handler = $this->getMock( + '\Monolog\Handler\SlackHandler', + 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($this->getIdentityFormatter()); + } +} From 83ebdc1f32872ffa9179b902f9d2c266ca23ea00 Mon Sep 17 00:00:00 2001 From: gkedzierski Date: Sun, 15 Jun 2014 15:55:24 +0200 Subject: [PATCH 2/5] Add colors to attachments based on level --- src/Monolog/Handler/SlackHandler.php | 22 ++++++++++++++ tests/Monolog/Handler/SlackHandlerTest.php | 35 +++++++++++++++++++--- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/Monolog/Handler/SlackHandler.php b/src/Monolog/Handler/SlackHandler.php index 324bc020..016f5019 100644 --- a/src/Monolog/Handler/SlackHandler.php +++ b/src/Monolog/Handler/SlackHandler.php @@ -88,6 +88,7 @@ class SlackHandler extends SocketHandler array( array( 'fallback' => $record['message'], + 'color' => $this->getAttachmentColor($record['level']), 'fields' => array( array( 'title' => 'Message', @@ -135,4 +136,25 @@ class SlackHandler extends SocketHandler parent::write($record); $this->closeSocket(); } + + /** + * Returned a Slack message attachment color associated with + * provided level. + * + * @param int $level + * @return string + */ + protected function getAttachmentColor($level) + { + switch (true) { + case $level >= Logger::ERROR: + return 'danger'; + case $level >= Logger::WARNING: + return 'warning'; + case $level >= Logger::INFO: + return 'good'; + default: + return '#e3e4e6'; + } + } } diff --git a/tests/Monolog/Handler/SlackHandlerTest.php b/tests/Monolog/Handler/SlackHandlerTest.php index e8132103..58fb6248 100644 --- a/tests/Monolog/Handler/SlackHandlerTest.php +++ b/tests/Monolog/Handler/SlackHandlerTest.php @@ -45,16 +45,43 @@ class SlackHandlerTest extends TestCase $content = fread($this->res, 1024); $this->assertRegexp('/POST \/api\/chat.postMessage HTTP\/1.1\\r\\nHost: slack.com\\r\\nContent-Type: application\/x-www-form-urlencoded\\r\\nContent-Length: \d{2,4}\\r\\n\\r\\n/', $content); + } - return $content; + public function testWriteContent() + { + $this->createHandler(); + $this->handler->handle($this->getRecord(Logger::CRITICAL, 'test1')); + fseek($this->res, 0); + $content = fread($this->res, 1024); + + $this->assertRegexp('/token=myToken&channel=channel1&username=Monolog&attachments=.*$/', $content); } /** - * @depends testWriteHeader + * @dataProvider provideLevelColors */ - public function testWriteContent($content) + public function testWriteContentWithColors($level, $expectedColor) { - $this->assertRegexp('/token=myToken&channel=channel1&username=Monolog&attachments=.*$/', $content); + $this->createHandler(); + $this->handler->handle($this->getRecord($level, 'test1')); + fseek($this->res, 0); + $content = fread($this->res, 1024); + + $this->assertRegexp('/color%22%3A%22'.$expectedColor.'/', $content); + } + + public function provideLevelColors() + { + return array( + array(Logger::DEBUG, '%23e3e4e6'), // escaped #e3e4e6 + array(Logger::INFO, 'good'), + array(Logger::NOTICE, 'good'), + array(Logger::WARNING, 'warning'), + array(Logger::ERROR, 'danger'), + array(Logger::CRITICAL, 'danger'), + array(Logger::ALERT, 'danger'), + array(Logger::EMERGENCY,'danger'), + ); } private function createHandler($token = 'myToken', $channel = 'channel1', $username = 'Monolog') From efd9a2065c6268833ca4a4779dde666109f9caa2 Mon Sep 17 00:00:00 2001 From: gkedzierski Date: Sun, 15 Jun 2014 16:35:40 +0200 Subject: [PATCH 3/5] Add plain text message support --- src/Monolog/Handler/SlackHandler.php | 33 ++++++++++++++++------ tests/Monolog/Handler/SlackHandlerTest.php | 16 +++++++++-- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/Monolog/Handler/SlackHandler.php b/src/Monolog/Handler/SlackHandler.php index 016f5019..5a3cdea2 100644 --- a/src/Monolog/Handler/SlackHandler.php +++ b/src/Monolog/Handler/SlackHandler.php @@ -40,13 +40,20 @@ class SlackHandler extends SocketHandler private $username; /** - * @param string $token Slack API token - * @param string $channel Slack channel (encoded ID or name) - * @param string $username Name of a bot - * @param 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 + * Whether the message should be added to Slack as attachment (plain text otherwise) + * @var bool */ - public function __construct($token, $channel, $username = 'Monolog', $level = Logger::CRITICAL, $bubble = true) + private $useAttachment; + + /** + * @param string $token Slack API token + * @param string $channel Slack channel (encoded ID or name) + * @param string $username Name of a bot + * @param 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 bool $useAttachment Whether the message should be added to Slack as attachment (plain text otherwise) + */ + public function __construct($token, $channel, $username = 'Monolog', $level = Logger::CRITICAL, $bubble = true, $useAttachment = true) { if (!extension_loaded('openssl')) { throw new MissingExtensionException('The OpenSSL PHP extension is required to use the SlackHandler'); @@ -57,6 +64,7 @@ class SlackHandler extends SocketHandler $this->token = $token; $this->channel = $channel; $this->username = $username; + $this->useAttachment = $useAttachment; } /** @@ -84,7 +92,12 @@ class SlackHandler extends SocketHandler 'token' => $this->token, 'channel' => $this->channel, 'username' => $this->username, - 'attachments' => json_encode( + 'text' => '', + 'attachments' => array() + ); + + if ($this->useAttachment) { + $dataArray['attachments'] = json_encode( array( array( 'fallback' => $record['message'], @@ -103,8 +116,10 @@ class SlackHandler extends SocketHandler ) ) ) - ) - ); + ); + } else { + $dataArray['text'] = $record['message']; + } return http_build_query($dataArray); } diff --git a/tests/Monolog/Handler/SlackHandlerTest.php b/tests/Monolog/Handler/SlackHandlerTest.php index 58fb6248..90b7f664 100644 --- a/tests/Monolog/Handler/SlackHandlerTest.php +++ b/tests/Monolog/Handler/SlackHandlerTest.php @@ -54,7 +54,7 @@ class SlackHandlerTest extends TestCase fseek($this->res, 0); $content = fread($this->res, 1024); - $this->assertRegexp('/token=myToken&channel=channel1&username=Monolog&attachments=.*$/', $content); + $this->assertRegexp('/token=myToken&channel=channel1&username=Monolog&text=&attachments=.*$/', $content); } /** @@ -70,6 +70,16 @@ class SlackHandlerTest extends TestCase $this->assertRegexp('/color%22%3A%22'.$expectedColor.'/', $content); } + public function testWriteContentWithPlainTextMessage() + { + $this->createHandler('myToken', 'channel1', 'Monolog', false); + $this->handler->handle($this->getRecord(Logger::CRITICAL, 'test1')); + fseek($this->res, 0); + $content = fread($this->res, 1024); + + $this->assertRegexp('/text=test1/', $content); + } + public function provideLevelColors() { return array( @@ -84,9 +94,9 @@ class SlackHandlerTest extends TestCase ); } - private function createHandler($token = 'myToken', $channel = 'channel1', $username = 'Monolog') + private function createHandler($token = 'myToken', $channel = 'channel1', $username = 'Monolog', $useAttachment = true) { - $constructorArgs = array($token, $channel, $username, Logger::DEBUG); + $constructorArgs = array($token, $channel, $username, Logger::DEBUG, true, $useAttachment); $this->res = fopen('php://memory', 'a'); $this->handler = $this->getMock( '\Monolog\Handler\SlackHandler', From f630bdf7de8137e0fc01612f389672430ba2c4a9 Mon Sep 17 00:00:00 2001 From: gkedzierski Date: Sun, 15 Jun 2014 16:38:49 +0200 Subject: [PATCH 4/5] Add SlackHandler to README --- README.mdown | 1 + 1 file changed, 1 insertion(+) diff --git a/README.mdown b/README.mdown index fd28a473..cdc8b29b 100644 --- a/README.mdown +++ b/README.mdown @@ -120,6 +120,7 @@ Handlers - _PushoverHandler_: Sends mobile notifications via the [Pushover](https://www.pushover.net/) API. - _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. ### Log specific servers and networked logging From be5210537933d86a5c4b5c58f837031d25038f31 Mon Sep 17 00:00:00 2001 From: gkedzierski Date: Mon, 16 Jun 2014 17:56:20 +0200 Subject: [PATCH 5/5] Move constructor argument before --- src/Monolog/Handler/SlackHandler.php | 4 ++-- tests/Monolog/Handler/SlackHandlerTest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Monolog/Handler/SlackHandler.php b/src/Monolog/Handler/SlackHandler.php index 5a3cdea2..f38a6928 100644 --- a/src/Monolog/Handler/SlackHandler.php +++ b/src/Monolog/Handler/SlackHandler.php @@ -49,11 +49,11 @@ class SlackHandler extends SocketHandler * @param string $token Slack API token * @param string $channel Slack channel (encoded ID or name) * @param string $username Name of a bot + * @param bool $useAttachment Whether the message should be added to Slack as attachment (plain text otherwise) * @param 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 bool $useAttachment Whether the message should be added to Slack as attachment (plain text otherwise) */ - public function __construct($token, $channel, $username = 'Monolog', $level = Logger::CRITICAL, $bubble = true, $useAttachment = true) + public function __construct($token, $channel, $username = 'Monolog', $useAttachment = true, $level = Logger::CRITICAL, $bubble = true) { if (!extension_loaded('openssl')) { throw new MissingExtensionException('The OpenSSL PHP extension is required to use the SlackHandler'); diff --git a/tests/Monolog/Handler/SlackHandlerTest.php b/tests/Monolog/Handler/SlackHandlerTest.php index 90b7f664..943c466b 100644 --- a/tests/Monolog/Handler/SlackHandlerTest.php +++ b/tests/Monolog/Handler/SlackHandlerTest.php @@ -96,7 +96,7 @@ class SlackHandlerTest extends TestCase private function createHandler($token = 'myToken', $channel = 'channel1', $username = 'Monolog', $useAttachment = true) { - $constructorArgs = array($token, $channel, $username, Logger::DEBUG, true, $useAttachment); + $constructorArgs = array($token, $channel, $username, $useAttachment, Logger::DEBUG, true); $this->res = fopen('php://memory', 'a'); $this->handler = $this->getMock( '\Monolog\Handler\SlackHandler',