diff --git a/CHANGELOG.mdown b/CHANGELOG.mdown index 79bf6285..76ea0ead 100644 --- a/CHANGELOG.mdown +++ b/CHANGELOG.mdown @@ -1,12 +1,13 @@ -### 1.22.0 (2016-11-XX) +### 1.22.0 (2016-11-26) + * Added SlackbotHandler and SlackWebhookHandler to set up Slack integration more easily * Added MercurialProcessor to add mercurial revision and branch names to log records * Added support for AWS SDK v3 in DynamoDbHandler * Fixed fatal errors occuring when normalizing generators that have been fully consumed * Fixed RollbarHandler to include a level (rollbar level), monolog_level (original name), channel and datetime (unix) * Fixed RollbarHandler not flushing records automatically, calling close() explicitly is not necessary anymore * Fixed SyslogUdpHandler to avoid sending empty frames - * Fixed a few PHP7 compatibility issues + * Fixed a few PHP 7.0 and 7.1 compatibility issues ### 1.21.0 (2016-07-29) diff --git a/doc/02-handlers-formatters-processors.md b/doc/02-handlers-formatters-processors.md index d0bf3e9a..9987908c 100644 --- a/doc/02-handlers-formatters-processors.md +++ b/doc/02-handlers-formatters-processors.md @@ -33,7 +33,9 @@ - [_PushoverHandler_](../src/Monolog/Handler/PushoverHandler.php): Sends mobile notifications via the [Pushover](https://www.pushover.net/) API. - [_HipChatHandler_](../src/Monolog/Handler/HipChatHandler.php): Logs records to a [HipChat](http://hipchat.com) chat room using its API. - [_FlowdockHandler_](../src/Monolog/Handler/FlowdockHandler.php): Logs records to a [Flowdock](https://www.flowdock.com/) account. -- [_SlackHandler_](../src/Monolog/Handler/SlackHandler.php): Logs records to a [Slack](https://www.slack.com/) account. +- [_SlackbotHandler_](../src/Monolog/Handler/SlackbotHandler.php): Logs records to a [Slack](https://www.slack.com/) account using the Slackbot incoming hook. +- [_SlackWebhookHandler_](../src/Monolog/Handler/SlackWebhookHandler.php): Logs records to a [Slack](https://www.slack.com/) account using Slack Webhooks. +- [_SlackHandler_](../src/Monolog/Handler/SlackHandler.php): Logs records to a [Slack](https://www.slack.com/) account using the Slack API (complex setup). - [_SendGridHandler_](../src/Monolog/Handler/SendGridHandler.php): Sends emails via the SendGrid API. - [_MandrillHandler_](../src/Monolog/Handler/MandrillHandler.php): Sends emails via the Mandrill API using a [`Swift_Message`](http://swiftmailer.org/) instance. - [_FleepHookHandler_](../src/Monolog/Handler/FleepHookHandler.php): Logs records to a [Fleep](https://fleep.io/) conversation using Webhooks. diff --git a/src/Monolog/Formatter/NormalizerFormatter.php b/src/Monolog/Formatter/NormalizerFormatter.php index 592713f1..ce990bb8 100644 --- a/src/Monolog/Formatter/NormalizerFormatter.php +++ b/src/Monolog/Formatter/NormalizerFormatter.php @@ -79,17 +79,13 @@ class NormalizerFormatter implements FormatterInterface return $data; } - if (is_array($data) || $data instanceof \Traversable) { + if (is_array($data)) { $normalized = []; $count = 1; - if ($data instanceof \Generator && !$data->valid()) { - return array('...' => 'Generator is already consumed, aborting'); - } - foreach ($data as $key => $value) { if ($count++ >= 1000) { - $normalized['...'] = 'Over 1000 items ('.($data instanceof \Generator ? 'generator function' : count($data).' total').'), aborting normalization'; + $normalized['...'] = 'Over 1000 items ('.count($data).' total), aborting normalization'; break; } $normalized[$key] = $this->normalize($value, $depth + 1); diff --git a/src/Monolog/Handler/Slack/SlackRecord.php b/src/Monolog/Handler/Slack/SlackRecord.php new file mode 100644 index 00000000..cb0b8717 --- /dev/null +++ b/src/Monolog/Handler/Slack/SlackRecord.php @@ -0,0 +1,248 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler\Slack; + +use Monolog\Logger; +use Monolog\Formatter\LineFormatter; +use Monolog\Formatter\FormatterInterface; + +/** + * Slack record utility helping to log to Slack webhooks or API. + * + * @author Greg Kedzierski + * @author Haralan Dobrev + * @see https://api.slack.com/incoming-webhooks + * @see https://api.slack.com/docs/message-attachments + */ +class SlackRecord +{ + const COLOR_DANGER = 'danger'; + + const COLOR_WARNING = 'warning'; + + const COLOR_GOOD = 'good'; + + const COLOR_DEFAULT = '#e3e4e6'; + + /** + * Slack channel (encoded ID or name) + * @var string|null + */ + private $channel; + + /** + * Name of a bot + * @var string + */ + private $username; + + /** + * Emoji icon name + * @var string + */ + private $iconEmoji; + + /** + * Whether the message should be added to Slack as attachment (plain text otherwise) + * @var bool + */ + private $useAttachment; + + /** + * Whether the the context/extra messages added to Slack as attachments are in a short style + * @var bool + */ + private $useShortAttachment; + + /** + * Whether the attachment should include context and extra data + * @var bool + */ + private $includeContextAndExtra; + + /** + * @var FormatterInterface + */ + private $formatter; + + /** + * @var LineFormatter + */ + private $lineFormatter; + + public function __construct($channel = null, $username = 'Monolog', $useAttachment = true, $iconEmoji = null, $useShortAttachment = false, $includeContextAndExtra = false, FormatterInterface $formatter = null) + { + $this->channel = $channel; + $this->username = $username; + $this->iconEmoji = trim($iconEmoji, ':'); + $this->useAttachment = $useAttachment; + $this->useShortAttachment = $useShortAttachment; + $this->includeContextAndExtra = $includeContextAndExtra; + $this->formatter = $formatter; + + if ($this->includeContextAndExtra) { + $this->lineFormatter = new LineFormatter(); + } + } + + public function getSlackData(array $record) + { + $dataArray = array( + 'username' => $this->username, + 'text' => '', + ); + + if ($this->channel) { + $dataArray['channel'] = $this->channel; + } + + if ($this->formatter) { + $message = $this->formatter->format($record); + } else { + $message = $record['message']; + } + + if ($this->useAttachment) { + $attachment = array( + 'fallback' => $message, + 'text' => $message, + 'color' => $this->getAttachmentColor($record['level']), + 'fields' => array(), + ); + + if ($this->useShortAttachment) { + $attachment['title'] = $record['level_name']; + } else { + $attachment['title'] = 'Message'; + $attachment['fields'][] = $this->generateAttachmentField('Level', $record['level_name'], true); + } + + if ($this->includeContextAndExtra) { + foreach (array('extra', 'context') as $key) { + if (empty($record[$key])) { + continue; + } + + if ($this->useShortAttachment) { + $attachment['fields'][] = $this->generateAttachmentField( + ucfirst($key), + $this->stringify($record[$key]), + true + ); + } else { + // Add all extra fields as individual fields in attachment + $attachment['fields'] = array_merge( + $attachment['fields'], + $this->generateAttachmentFields($record[$key]) + ); + } + } + } + + $dataArray['attachments'] = array($attachment); + } else { + $dataArray['text'] = $message; + } + + if ($this->iconEmoji) { + $dataArray['icon_emoji'] = ":{$this->iconEmoji}:"; + } + + return $dataArray; + } + + /** + * Returned a Slack message attachment color associated with + * provided level. + * + * @param int $level + * @return string + */ + public function getAttachmentColor($level) + { + switch (true) { + case $level >= Logger::ERROR: + return self::COLOR_DANGER; + case $level >= Logger::WARNING: + return self::COLOR_WARNING; + case $level >= Logger::INFO: + return self::COLOR_GOOD; + default: + return self::COLOR_DEFAULT; + } + } + + /** + * Stringifies an array of key/value pairs to be used in attachment fields + * + * @param array $fields + * @return string|null + */ + public function stringify($fields) + { + if (!$this->lineFormatter) { + return null; + } + + $string = ''; + foreach ($fields as $var => $val) { + $string .= $var.': '.$this->lineFormatter->stringify($val)." | "; + } + + $string = rtrim($string, " |"); + + return $string; + } + + /** + * Sets the formatter + * + * @param FormatterInterface $formatter + */ + public function setFormatter(FormatterInterface $formatter) + { + $this->formatter = $formatter; + } + + /** + * Generates attachment field + * + * @param string $title + * @param string|array $value + * @param bool $short + * @return array + */ + private function generateAttachmentField($title, $value, $short) + { + return array( + 'title' => $title, + 'value' => is_array($value) ? $this->lineFormatter->stringify($value) : $value, + 'short' => $short + ); + } + + /** + * Generates a collection of attachment fields from array + * + * @param array $data + * @return array + */ + private function generateAttachmentFields(array $data) + { + $fields = array(); + foreach ($data as $key => $value) { + $fields[] = $this->generateAttachmentField($key, $value, false); + } + + return $fields; + } +} diff --git a/src/Monolog/Handler/SlackHandler.php b/src/Monolog/Handler/SlackHandler.php index d311e2df..4308b411 100644 --- a/src/Monolog/Handler/SlackHandler.php +++ b/src/Monolog/Handler/SlackHandler.php @@ -11,8 +11,9 @@ namespace Monolog\Handler; +use Monolog\Formatter\FormatterInterface; use Monolog\Logger; -use Monolog\Formatter\LineFormatter; +use Monolog\Handler\Slack\SlackRecord; /** * Sends notifications through Slack API @@ -29,45 +30,10 @@ class SlackHandler extends SocketHandler private $token; /** - * Slack channel (encoded ID or name) - * @var string + * Instance of the SlackRecord util class preparing data for Slack API. + * @var SlackRecord */ - private $channel; - - /** - * Name of a bot - * @var string - */ - private $username; - - /** - * Emoji icon name - * @var string - */ - private $iconEmoji; - - /** - * Whether the message should be added to Slack as attachment (plain text otherwise) - * @var bool - */ - private $useAttachment; - - /** - * Whether the the context/extra messages added to Slack as attachments are in a short style - * @var bool - */ - private $useShortAttachment; - - /** - * Whether the attachment should include context and extra data - * @var bool - */ - private $includeContextAndExtra; - - /** - * @var LineFormatter - */ - private $lineFormatter; + private $slackRecord; /** * @param string $token Slack API token @@ -79,10 +45,9 @@ class SlackHandler extends SocketHandler * @param bool $bubble Whether the messages that are handled can bubble up the stack or not * @param bool $useShortAttachment Whether the the context/extra messages added to Slack as attachments are in a short style * @param bool $includeContextAndExtra Whether the attachment should include context and extra data - * @param bool $printSimpleMessage Whether the attachment should include only the message without extradata * @throws MissingExtensionException If no OpenSSL PHP extension configured */ - public function __construct($token, $channel, $username = 'Monolog', $useAttachment = true, $iconEmoji = null, $level = Logger::CRITICAL, $bubble = true, $useShortAttachment = false, $includeContextAndExtra = false, $printSimpleMessage = false) + public function __construct($token, $channel, $username = 'Monolog', $useAttachment = true, $iconEmoji = null, $level = Logger::CRITICAL, $bubble = true, $useShortAttachment = false, $includeContextAndExtra = false) { if (!extension_loaded('openssl')) { throw new MissingExtensionException('The OpenSSL PHP extension is required to use the SlackHandler'); @@ -90,17 +55,22 @@ class SlackHandler extends SocketHandler parent::__construct('ssl://slack.com:443', $level, $bubble); - $this->token = $token; - $this->channel = $channel; - $this->username = $username; - $this->iconEmoji = is_string($iconEmoji) ? trim($iconEmoji, ':') : null; - $this->useAttachment = $useAttachment; - $this->useShortAttachment = $useShortAttachment; - $this->includeContextAndExtra = $includeContextAndExtra; + $this->slackRecord = new SlackRecord( + $channel, + $username, + $useAttachment, + $iconEmoji, + $useShortAttachment, + $includeContextAndExtra, + $this->formatter + ); - if ($this->includeContextAndExtra && $this->useShortAttachment) { - $this->lineFormatter = new LineFormatter; - } + $this->token = $token; + } + + public function getSlackRecord() + { + return $this->slackRecord; } /** @@ -137,87 +107,11 @@ class SlackHandler extends SocketHandler */ protected function prepareContentData($record) { - $dataArray = [ - 'token' => $this->token, - 'channel' => $this->channel, - 'username' => $this->username, - 'text' => '', - 'attachments' => [], - ]; + $dataArray = $this->slackRecord->getSlackData($record); + $dataArray['token'] = $this->token; - if ($this->formatter && !$printSimpleMessage) { - $message = $this->formatter->format($record); - } else { - $message = $record['message']; - } - - if ($this->useAttachment) { - $attachment = [ - 'fallback' => $message, - 'color' => $this->getAttachmentColor($record['level']), - 'fields' => [], - ]; - - if ($this->useShortAttachment) { - $attachment['title'] = $record['level_name']; - $attachment['text'] = $message; - } else { - $attachment['title'] = 'Message'; - $attachment['text'] = $message; - $attachment['fields'][] = [ - 'title' => 'Level', - 'value' => $record['level_name'], - 'short' => true, - ]; - } - - if ($this->includeContextAndExtra) { - if (!empty($record['extra'])) { - if ($this->useShortAttachment) { - $attachment['fields'][] = [ - 'title' => "Extra", - 'value' => $this->stringify($record['extra']), - 'short' => $this->useShortAttachment, - ]; - } else { - // Add all extra fields as individual fields in attachment - foreach ($record['extra'] as $var => $val) { - $attachment['fields'][] = [ - 'title' => $var, - 'value' => $val, - 'short' => $this->useShortAttachment, - ]; - } - } - } - - if (!empty($record['context'])) { - if ($this->useShortAttachment) { - $attachment['fields'][] = [ - 'title' => "Context", - 'value' => $this->stringify($record['context']), - 'short' => $this->useShortAttachment, - ]; - } else { - // Add all context fields as individual fields in attachment - foreach ($record['context'] as $var => $val) { - $attachment['fields'][] = [ - 'title' => $var, - 'value' => $val, - 'short' => $this->useShortAttachment, - ]; - } - } - } - } - - $dataArray['attachments'] = json_encode([$attachment]); - } else { - $dataArray['text'] = $message; - } - - if ($this->iconEmoji) { - $dataArray['icon_emoji'] = ":{$this->iconEmoji}:"; + if (!empty($dataArray['attachments'])) { + $dataArray['attachments'] = json_encode($dataArray['attachments']); } return $dataArray; @@ -261,19 +155,16 @@ class SlackHandler extends SocketHandler * * @param int $level * @return string + * @deprecated Use underlying SlackRecord instead */ 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'; - } + trigger_error( + 'SlackHandler::getAttachmentColor() is deprecated. Use underlying SlackRecord instead.', + E_USER_DEPRECATED + ); + + return $this->slackRecord->getAttachmentColor($level); } /** @@ -281,16 +172,31 @@ class SlackHandler extends SocketHandler * * @param array $fields * @return string + * @deprecated Use underlying SlackRecord instead */ protected function stringify($fields) { - $string = ''; - foreach ($fields as $var => $val) { - $string .= $var.': '.$this->lineFormatter->stringify($val)." | "; - } + trigger_error( + 'SlackHandler::stringify() is deprecated. Use underlying SlackRecord instead.', + E_USER_DEPRECATED + ); - $string = rtrim($string, " |"); + return $this->slackRecord->stringify($fields); + } - return $string; + public function setFormatter(FormatterInterface $formatter) + { + parent::setFormatter($formatter); + $this->slackRecord->setFormatter($formatter); + + return $this; + } + + public function getFormatter() + { + $formatter = parent::getFormatter(); + $this->slackRecord->setFormatter($formatter); + + return $formatter; } } diff --git a/src/Monolog/Handler/SlackWebhookHandler.php b/src/Monolog/Handler/SlackWebhookHandler.php new file mode 100644 index 00000000..a2df0f1a --- /dev/null +++ b/src/Monolog/Handler/SlackWebhookHandler.php @@ -0,0 +1,108 @@ + + * + * 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\FormatterInterface; +use Monolog\Logger; +use Monolog\Handler\Slack\SlackRecord; + +/** + * Sends notifications through Slack Webhooks + * + * @author Haralan Dobrev + * @see https://api.slack.com/incoming-webhooks + */ +class SlackWebhookHandler extends AbstractProcessingHandler +{ + /** + * Slack Webhook token + * @var string + */ + private $webhookUrl; + + /** + * Instance of the SlackRecord util class preparing data for Slack API. + * @var SlackRecord + */ + private $slackRecord; + + /** + * @param string $webhookUrl Slack Webhook URL + * @param string|null $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 string|null $iconEmoji The emoji name to use (or null) + * @param bool $useShortAttachment Whether the the context/extra messages added to Slack as attachments are in a short style + * @param bool $includeContextAndExtra Whether the attachment should include context and extra data + * @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($webhookUrl, $channel = null, $username = 'Monolog', $useAttachment = true, $iconEmoji = null, $useShortAttachment = false, $includeContextAndExtra = false, $level = Logger::CRITICAL, $bubble = true) + { + parent::__construct($level, $bubble); + + $this->webhookUrl = $webhookUrl; + + $this->slackRecord = new SlackRecord( + $channel, + $username, + $useAttachment, + $iconEmoji, + $useShortAttachment, + $includeContextAndExtra, + $this->formatter + ); + } + + public function getSlackRecord() + { + return $this->slackRecord; + } + + /** + * {@inheritdoc} + * + * @param array $record + */ + protected function write(array $record) + { + $postData = $this->slackRecord->getSlackData($record); + $postString = json_encode($postData); + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $this->webhookUrl); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + if (defined('CURLOPT_SAFE_UPLOAD')) { + curl_setopt($ch, CURLOPT_SAFE_UPLOAD, true); + } + curl_setopt($ch, CURLOPT_POSTFIELDS, array('payload' => $postString)); + + Curl\Util::execute($ch); + } + + public function setFormatter(FormatterInterface $formatter) + { + parent::setFormatter($formatter); + $this->slackRecord->setFormatter($formatter); + + return $this; + } + + public function getFormatter() + { + $formatter = parent::getFormatter(); + $this->slackRecord->setFormatter($formatter); + + return $formatter; + } +} diff --git a/src/Monolog/Handler/SlackbotHandler.php b/src/Monolog/Handler/SlackbotHandler.php new file mode 100644 index 00000000..baead525 --- /dev/null +++ b/src/Monolog/Handler/SlackbotHandler.php @@ -0,0 +1,80 @@ + + * + * 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's Slackbot + * + * @author Haralan Dobrev + * @see https://slack.com/apps/A0F81R8ET-slackbot + */ +class SlackbotHandler extends AbstractProcessingHandler +{ + /** + * The slug of the Slack team + * @var string + */ + private $slackTeam; + + /** + * Slackbot token + * @var string + */ + private $token; + + /** + * Slack channel name + * @var string + */ + private $channel; + + /** + * @param string $slackTeam Slack team slug + * @param string $token Slackbot token + * @param string $channel Slack channel (encoded ID or name) + * @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($slackTeam, $token, $channel, $level = Logger::CRITICAL, $bubble = true) + { + parent::__construct($level, $bubble); + + $this->slackTeam = $slackTeam; + $this->token = $token; + $this->channel = $channel; + } + + /** + * {@inheritdoc} + * + * @param array $record + */ + protected function write(array $record) + { + $slackbotUrl = sprintf( + 'https://%s.slack.com/services/hooks/slackbot?token=%s&channel=%s', + $this->slackTeam, + $this->token, + $this->channel + ); + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $slackbotUrl); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $record['message']); + + Curl\Util::execute($ch); + } +} diff --git a/tests/Monolog/Handler/Slack/SlackRecordTest.php b/tests/Monolog/Handler/Slack/SlackRecordTest.php new file mode 100644 index 00000000..d6c6c229 --- /dev/null +++ b/tests/Monolog/Handler/Slack/SlackRecordTest.php @@ -0,0 +1,341 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler\Slack; + +use Monolog\Logger; +use Monolog\TestCase; + +/** + * @coversDefaultClass Monolog\Handler\Slack\SlackRecord + */ +class SlackRecordTest extends TestCase +{ + private $channel; + + protected function setUp() + { + $this->channel = 'monolog_alerts'; + } + + public function dataGetAttachmentColor() + { + return array( + array(Logger::DEBUG, SlackRecord::COLOR_DEFAULT), + array(Logger::INFO, SlackRecord::COLOR_GOOD), + array(Logger::NOTICE, SlackRecord::COLOR_GOOD), + array(Logger::WARNING, SlackRecord::COLOR_WARNING), + array(Logger::ERROR, SlackRecord::COLOR_DANGER), + array(Logger::CRITICAL, SlackRecord::COLOR_DANGER), + array(Logger::ALERT, SlackRecord::COLOR_DANGER), + array(Logger::EMERGENCY, SlackRecord::COLOR_DANGER), + ); + } + /** + * @dataProvider dataGetAttachmentColor + * @param int $logLevel + * @param string $expectedColour RGB hex color or name of Slack color + * @covers ::getAttachmentColor + */ + public function testGetAttachmentColor($logLevel, $expectedColour) + { + $slackRecord = new SlackRecord('#test'); + $this->assertSame( + $expectedColour, + $slackRecord->getAttachmentColor($logLevel) + ); + } + + public function testAddsChannel() + { + $record = new SlackRecord($this->channel); + $data = $record->getSlackData($this->getRecord()); + + $this->assertArrayHasKey('channel', $data); + $this->assertSame($this->channel, $data['channel']); + } + + public function testStringifyReturnsNullWithNoLineFormatter() + { + $slackRecord = new SlackRecord('#test'); + $this->assertNull($slackRecord->stringify(array('foo' => 'bar'))); + } + + public function testAddsDefaultUsername() + { + $record = new SlackRecord($this->channel); + $data = $record->getSlackData($this->getRecord()); + + $this->assertArrayHasKey('username', $data); + $this->assertSame('Monolog', $data['username']); + } + + /** + * @return array + */ + public function dataStringify() + { + return array( + array(array(), ''), + array(array('foo' => 'bar'), 'foo: bar'), + array(array('Foo' => 'bAr'), 'Foo: bAr'), + ); + } + + /** + * @dataProvider dataStringify + */ + public function testStringifyWithLineFormatter($fields, $expectedResult) + { + $slackRecord = new SlackRecord( + '#test', + 'test', + true, + null, + true, + true + ); + + $this->assertSame($expectedResult, $slackRecord->stringify($fields)); + } + + public function testAddsCustomUsername() + { + $username = 'Monolog bot'; + $record = new SlackRecord($this->channel, $username); + $data = $record->getSlackData($this->getRecord()); + + $this->assertArrayHasKey('username', $data); + $this->assertSame($username, $data['username']); + } + + public function testNoIcon() + { + $record = new SlackRecord($this->channel); + $data = $record->getSlackData($this->getRecord()); + + $this->assertArrayNotHasKey('icon_emoji', $data); + } + + public function testAddsIcon() + { + $record = new SlackRecord($this->channel, 'Monolog', true, 'ghost'); + $data = $record->getSlackData($this->getRecord()); + + $this->assertArrayHasKey('icon_emoji', $data); + $this->assertSame(':ghost:', $data['icon_emoji']); + } + + public function testAddsEmptyTextIfUseAttachment() + { + $record = new SlackRecord($this->channel); + $data = $record->getSlackData($this->getRecord()); + + $this->assertArrayHasKey('text', $data); + $this->assertSame('', $data['text']); + } + + public function testAttachmentsNotPresentIfNoAttachment() + { + $record = new SlackRecord($this->channel, 'Monolog', false); + $data = $record->getSlackData($this->getRecord()); + + $this->assertArrayNotHasKey('attachments', $data); + } + + public function testAddsOneAttachment() + { + $record = new SlackRecord($this->channel); + $data = $record->getSlackData($this->getRecord()); + + $this->assertArrayHasKey('attachments', $data); + $this->assertArrayHasKey(0, $data['attachments']); + $this->assertInternalType('array', $data['attachments'][0]); + } + + public function testTextEqualsMessageIfNoFormatter() + { + $message = 'Test message'; + $record = new SlackRecord($this->channel, 'Monolog', false); + $data = $record->getSlackData($this->getRecord(Logger::WARNING, $message)); + + $this->assertArrayHasKey('text', $data); + $this->assertSame($message, $data['text']); + } + + public function testTextEqualsFormatterOutput() + { + $formatter = $this->getMock('Monolog\\Formatter\\FormatterInterface'); + $formatter + ->expects($this->any()) + ->method('format') + ->will($this->returnCallback(function ($record) { return $record['message'] . 'test'; })); + + $formatter2 = $this->getMock('Monolog\\Formatter\\FormatterInterface'); + $formatter2 + ->expects($this->any()) + ->method('format') + ->will($this->returnCallback(function ($record) { return $record['message'] . 'test1'; })); + + $message = 'Test message'; + $record = new SlackRecord($this->channel, 'Monolog', false, null, false, false, $formatter); + $data = $record->getSlackData($this->getRecord(Logger::WARNING, $message)); + + $this->assertArrayHasKey('text', $data); + $this->assertSame($message . 'test', $data['text']); + + $record->setFormatter($formatter2); + $data = $record->getSlackData($this->getRecord(Logger::WARNING, $message)); + + $this->assertArrayHasKey('text', $data); + $this->assertSame($message . 'test1', $data['text']); + } + + public function testAddsFallbackAndTextToAttachment() + { + $message = 'Test message'; + $record = new SlackRecord($this->channel); + $data = $record->getSlackData($this->getRecord(Logger::WARNING, $message)); + + $this->assertSame($message, $data['attachments'][0]['text']); + $this->assertSame($message, $data['attachments'][0]['fallback']); + } + + public function testMapsLevelToColorAttachmentColor() + { + $record = new SlackRecord($this->channel); + $errorLoggerRecord = $this->getRecord(Logger::ERROR); + $emergencyLoggerRecord = $this->getRecord(Logger::EMERGENCY); + $warningLoggerRecord = $this->getRecord(Logger::WARNING); + $infoLoggerRecord = $this->getRecord(Logger::INFO); + $debugLoggerRecord = $this->getRecord(Logger::DEBUG); + + $data = $record->getSlackData($errorLoggerRecord); + $this->assertSame(SlackRecord::COLOR_DANGER, $data['attachments'][0]['color']); + + $data = $record->getSlackData($emergencyLoggerRecord); + $this->assertSame(SlackRecord::COLOR_DANGER, $data['attachments'][0]['color']); + + $data = $record->getSlackData($warningLoggerRecord); + $this->assertSame(SlackRecord::COLOR_WARNING, $data['attachments'][0]['color']); + + $data = $record->getSlackData($infoLoggerRecord); + $this->assertSame(SlackRecord::COLOR_GOOD, $data['attachments'][0]['color']); + + $data = $record->getSlackData($debugLoggerRecord); + $this->assertSame(SlackRecord::COLOR_DEFAULT, $data['attachments'][0]['color']); + } + + public function testAddsShortAttachmentWithoutContextAndExtra() + { + $level = Logger::ERROR; + $levelName = Logger::getLevelName($level); + $record = new SlackRecord($this->channel, 'Monolog', true, null, true); + $data = $record->getSlackData($this->getRecord($level, 'test', array('test' => 1))); + + $attachment = $data['attachments'][0]; + $this->assertArrayHasKey('title', $attachment); + $this->assertArrayHasKey('fields', $attachment); + $this->assertSame($levelName, $attachment['title']); + $this->assertSame(array(), $attachment['fields']); + } + + public function testAddsShortAttachmentWithContextAndExtra() + { + $level = Logger::ERROR; + $levelName = Logger::getLevelName($level); + $record = new SlackRecord($this->channel, 'Monolog', true, null, true, true); + $loggerRecord = $this->getRecord($level, 'test', array('test' => 1)); + $loggerRecord['extra'] = array('tags' => array('web')); + $data = $record->getSlackData($loggerRecord); + + $attachment = $data['attachments'][0]; + $this->assertArrayHasKey('title', $attachment); + $this->assertArrayHasKey('fields', $attachment); + $this->assertCount(2, $attachment['fields']); + $this->assertSame($levelName, $attachment['title']); + $this->assertSame( + array( + array( + 'title' => 'Extra', + 'value' => 'tags: ["web"]', + 'short' => true + ), + array( + 'title' => 'Context', + 'value' => 'test: 1', + 'short' => true + ) + ), + $attachment['fields'] + ); + } + + public function testAddsLongAttachmentWithoutContextAndExtra() + { + $level = Logger::ERROR; + $levelName = Logger::getLevelName($level); + $record = new SlackRecord($this->channel, 'Monolog', true, null); + $data = $record->getSlackData($this->getRecord($level, 'test', array('test' => 1))); + + $attachment = $data['attachments'][0]; + $this->assertArrayHasKey('title', $attachment); + $this->assertArrayHasKey('fields', $attachment); + $this->assertCount(1, $attachment['fields']); + $this->assertSame('Message', $attachment['title']); + $this->assertSame( + array(array( + 'title' => 'Level', + 'value' => $levelName, + 'short' => true + )), + $attachment['fields'] + ); + } + + public function testAddsLongAttachmentWithContextAndExtra() + { + $level = Logger::ERROR; + $levelName = Logger::getLevelName($level); + $record = new SlackRecord($this->channel, 'Monolog', true, null, false, true); + $loggerRecord = $this->getRecord($level, 'test', array('test' => 1)); + $loggerRecord['extra'] = array('tags' => array('web')); + $data = $record->getSlackData($loggerRecord); + + $expectedFields = array( + array( + 'title' => 'Level', + 'value' => $levelName, + 'short' => true, + ), + array( + 'title' => 'tags', + 'value' => '["web"]', + 'short' => false + ), + array( + 'title' => 'test', + 'value' => 1, + 'short' => false + ) + ); + + $attachment = $data['attachments'][0]; + $this->assertArrayHasKey('title', $attachment); + $this->assertArrayHasKey('fields', $attachment); + $this->assertCount(3, $attachment['fields']); + $this->assertSame('Message', $attachment['title']); + $this->assertSame( + $expectedFields, + $attachment['fields'] + ); + } +} diff --git a/tests/Monolog/Handler/SlackHandlerTest.php b/tests/Monolog/Handler/SlackHandlerTest.php index c08284a5..e8abd15d 100644 --- a/tests/Monolog/Handler/SlackHandlerTest.php +++ b/tests/Monolog/Handler/SlackHandlerTest.php @@ -14,6 +14,7 @@ namespace Monolog\Handler; use Monolog\Test\TestCase; use Monolog\Logger; use Monolog\Formatter\LineFormatter; +use Monolog\Handler\Slack\SlackRecord; /** * @author Greg Kedzierski @@ -55,7 +56,10 @@ class SlackHandlerTest extends TestCase fseek($this->res, 0); $content = fread($this->res, 1024); - $this->assertRegexp('/token=myToken&channel=channel1&username=Monolog&text=&attachments=.*$/', $content); + $this->assertRegExp('/username=Monolog/', $content); + $this->assertRegExp('/channel=channel1/', $content); + $this->assertRegExp('/token=myToken/', $content); + $this->assertRegExp('/attachments/', $content); } public function testWriteContentUsesFormatterIfProvided() @@ -71,8 +75,8 @@ class SlackHandlerTest extends TestCase fseek($this->res, 0); $content2 = fread($this->res, 1024); - $this->assertRegexp('/token=myToken&channel=channel1&username=Monolog&text=test1.*$/', $content); - $this->assertRegexp('/token=myToken&channel=channel1&username=Monolog&text=foo--test2.*$/', $content2); + $this->assertRegexp('/text=test1/', $content); + $this->assertRegexp('/text=foo--test2/', $content2); } public function testWriteContentWithEmoji() @@ -82,7 +86,7 @@ class SlackHandlerTest extends TestCase fseek($this->res, 0); $content = fread($this->res, 1024); - $this->assertRegexp('/icon_emoji=%3Aalien%3A$/', $content); + $this->assertRegexp('/icon_emoji=%3Aalien%3A/', $content); } /** @@ -95,7 +99,7 @@ class SlackHandlerTest extends TestCase fseek($this->res, 0); $content = fread($this->res, 1024); - $this->assertRegexp('/color%22%3A%22'.$expectedColor.'/', $content); + $this->assertRegexp('/%22color%22%3A%22'.$expectedColor.'/', $content); } public function testWriteContentWithPlainTextMessage() @@ -110,16 +114,16 @@ class SlackHandlerTest extends TestCase public function provideLevelColors() { - return [ - [Logger::DEBUG, '%23e3e4e6'], // escaped #e3e4e6 - [Logger::INFO, 'good'], - [Logger::NOTICE, 'good'], - [Logger::WARNING, 'warning'], - [Logger::ERROR, 'danger'], - [Logger::CRITICAL, 'danger'], - [Logger::ALERT, 'danger'], - [Logger::EMERGENCY,'danger'], - ]; + return array( + array(Logger::DEBUG, urlencode(SlackRecord::COLOR_DEFAULT)), + array(Logger::INFO, SlackRecord::COLOR_GOOD), + array(Logger::NOTICE, SlackRecord::COLOR_GOOD), + array(Logger::WARNING, SlackRecord::COLOR_WARNING), + array(Logger::ERROR, SlackRecord::COLOR_DANGER), + array(Logger::CRITICAL, SlackRecord::COLOR_DANGER), + array(Logger::ALERT, SlackRecord::COLOR_DANGER), + array(Logger::EMERGENCY,SlackRecord::COLOR_DANGER), + ); } private function createHandler($token = 'myToken', $channel = 'channel1', $username = 'Monolog', $useAttachment = true, $iconEmoji = null, $useShortAttachment = false, $includeExtra = false) diff --git a/tests/Monolog/Handler/SlackWebhookHandlerTest.php b/tests/Monolog/Handler/SlackWebhookHandlerTest.php new file mode 100644 index 00000000..cc9b7bab --- /dev/null +++ b/tests/Monolog/Handler/SlackWebhookHandlerTest.php @@ -0,0 +1,106 @@ + + * + * 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; +use Monolog\Formatter\LineFormatter; +use Monolog\Handler\Slack\SlackRecord; + +/** + * @author Haralan Dobrev + * @see https://api.slack.com/incoming-webhooks + * @coversDefaultClass Monolog\Handler\SlackWebhookHandler + */ +class SlackWebhookHandlerTest extends TestCase +{ + const WEBHOOK_URL = 'https://hooks.slack.com/services/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E'; + + /** + * @covers ::__construct + * @covers ::getSlackRecord + */ + public function testConstructorMinimal() + { + $handler = new SlackWebhookHandler(self::WEBHOOK_URL); + $slackRecord = $handler->getSlackRecord(); + $this->assertInstanceOf('Monolog\Handler\Slack\SlackRecord', $slackRecord); + $this->assertEquals(array( + 'username' => 'Monolog', + 'text' => '', + 'attachments' => array( + array( + 'fallback' => 'test', + 'text' => 'test', + 'color' => SlackRecord::COLOR_WARNING, + 'fields' => array( + array( + 'title' => 'Level', + 'value' => 'WARNING', + 'short' => true, + ), + ), + 'title' => 'Message', + ), + ), + ), $slackRecord->getSlackData($this->getRecord())); + } + + /** + * @covers ::__construct + * @covers ::getSlackRecord + */ + public function testConstructorFull() + { + $handler = new SlackWebhookHandler( + self::WEBHOOK_URL, + 'test-channel', + 'test-username', + false, + ':ghost:', + false, + false, + Logger::DEBUG, + false + ); + + $slackRecord = $handler->getSlackRecord(); + $this->assertInstanceOf('Monolog\Handler\Slack\SlackRecord', $slackRecord); + $this->assertEquals(array( + 'username' => 'test-username', + 'text' => 'test', + 'channel' => 'test-channel', + 'icon_emoji' => ':ghost:', + ), $slackRecord->getSlackData($this->getRecord())); + } + + /** + * @covers ::getFormatter + */ + public function testGetFormatter() + { + $handler = new SlackWebhookHandler(self::WEBHOOK_URL); + $formatter = $handler->getFormatter(); + $this->assertInstanceOf('Monolog\Formatter\FormatterInterface', $formatter); + } + + /** + * @covers ::setFormatter + */ + public function testSetFormatter() + { + $handler = new SlackWebhookHandler(self::WEBHOOK_URL); + $formatter = new LineFormatter(); + $handler->setFormatter($formatter); + $this->assertSame($formatter, $handler->getFormatter()); + } +} diff --git a/tests/Monolog/Handler/SlackbotHandlerTest.php b/tests/Monolog/Handler/SlackbotHandlerTest.php new file mode 100644 index 00000000..b1b02bde --- /dev/null +++ b/tests/Monolog/Handler/SlackbotHandlerTest.php @@ -0,0 +1,47 @@ + + * + * 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 Haralan Dobrev + * @see https://slack.com/apps/A0F81R8ET-slackbot + * @coversDefaultClass Monolog\Handler\SlackbotHandler + */ +class SlackbotHandlerTest extends TestCase +{ + /** + * @covers ::__construct + */ + public function testConstructorMinimal() + { + $handler = new SlackbotHandler('test-team', 'test-token', 'test-channel'); + $this->assertInstanceOf('Monolog\Handler\AbstractProcessingHandler', $handler); + } + + /** + * @covers ::__construct + */ + public function testConstructorFull() + { + $handler = new SlackbotHandler( + 'test-team', + 'test-token', + 'test-channel', + Logger::DEBUG, + false + ); + $this->assertInstanceOf('Monolog\Handler\AbstractProcessingHandler', $handler); + } +}