diff --git a/doc/02-handlers-formatters-processors.md b/doc/02-handlers-formatters-processors.md index 808b4997..b849d09d 100644 --- a/doc/02-handlers-formatters-processors.md +++ b/doc/02-handlers-formatters-processors.md @@ -31,7 +31,6 @@ [`mail()`](http://php.net/manual/en/function.mail.php) function. - [_SwiftMailerHandler_](../src/Monolog/Handler/SwiftMailerHandler.php): Sends emails using a [`Swift_Mailer`](http://swiftmailer.org/) instance. - [_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. - [_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). @@ -40,6 +39,7 @@ - [_FleepHookHandler_](../src/Monolog/Handler/FleepHookHandler.php): Logs records to a [Fleep](https://fleep.io/) conversation using Webhooks. - [_IFTTTHandler_](../src/Monolog/Handler/IFTTTHandler.php): Notifies an [IFTTT](https://ifttt.com/maker) trigger with the log channel, level name and message. - [_TelegramBotHandler_](../src/Monolog/Handler/TelegramBotHandler.php): Logs records to a [Telegram](https://core.telegram.org/bots/api) bot account. +- [_HipChatHandler_](../src/Monolog/Handler/HipChatHandler.php: Logs records to a [HipChat](http://hipchat.com) chat room using its API. **Deprecated** and removed in Monolog 2.0, use Slack handlers instead, see [Atlassian's announcement](https://www.atlassian.com/partnerships/slack) ### Log specific servers and networked logging diff --git a/src/Monolog/Handler/HipChatHandler.php b/src/Monolog/Handler/HipChatHandler.php deleted file mode 100644 index 65b698f6..00000000 --- a/src/Monolog/Handler/HipChatHandler.php +++ /dev/null @@ -1,306 +0,0 @@ - - * - * 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 the hipchat api to a hipchat room - * - * This handler only supports the API v2 - * - * Notes: - * API token - HipChat API token - * Room - HipChat Room Id or name, where messages are sent - * Name - Name used to send the message (from) - * notify - Should the message trigger a notification in the clients - * - * @author Rafael Dohms - * @see https://www.hipchat.com/docs/api - */ -class HipChatHandler extends SocketHandler -{ - /** - * The maximum allowed length for the message. - */ - protected const MAXIMUM_MESSAGE_LENGTH = 9500; - - /** - * @var string - */ - private $token; - - /** - * @var string - */ - private $room; - - /** - * @var string - */ - private $name; - - /** - * @var bool - */ - private $notify; - - /** - * @var string - */ - private $format; - - /** - * @var string - */ - private $host; - - /** - * @param string $token HipChat API Token - * @param string $room The room that should be alerted of the message (Id or Name) - * @param string|null $name Name used in the "from" field. - * @param bool $notify Trigger a notification in clients or not - * @param string|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 $useSSL Whether to connect via SSL. - * @param string $format The format of the messages (default to text, can be set to html if you have html in the messages) - * @param string $host The HipChat server hostname. - */ - public function __construct( - string $token, - string $room, - ?string $name = 'Monolog', - bool $notify = false, - $level = Logger::CRITICAL, - bool $bubble = true, - bool $useSSL = true, - string $format = 'text', - string $host = 'api.hipchat.com' - ) { - $connectionString = $useSSL ? 'ssl://'.$host.':443' : $host.':80'; - parent::__construct($connectionString, $level, $bubble); - - $this->token = $token; - $this->name = $name; - $this->notify = $notify; - $this->room = $room; - $this->format = $format; - $this->host = $host; - } - - /** - * {@inheritdoc} - */ - protected function generateDataStream(array $record): string - { - $content = $this->buildContent($record); - - return $this->buildHeader($content) . $content; - } - - /** - * Builds the body of API call - */ - private function buildContent(array $record): string - { - $dataArray = [ - 'notify' => $this->notify ? 'true' : 'false', - 'message' => $record['formatted'], - 'message_format' => $this->format, - 'color' => $this->getAlertColor($record['level']), - ]; - - if (!$this->validateStringLength($dataArray['message'], static::MAXIMUM_MESSAGE_LENGTH)) { - if (function_exists('mb_substr')) { - $dataArray['message'] = mb_substr($dataArray['message'], 0, static::MAXIMUM_MESSAGE_LENGTH).' [truncated]'; - } else { - $dataArray['message'] = substr($dataArray['message'], 0, static::MAXIMUM_MESSAGE_LENGTH).' [truncated]'; - } - } - - // append the sender name if it is set - // always append it if we use the v1 api (it is required in v1) - if ($this->name !== null) { - $dataArray['from'] = (string) $this->name; - } - - return http_build_query($dataArray); - } - - /** - * Builds the header of the API Call - */ - private function buildHeader(string $content): string - { - // needed for rooms with special (spaces, etc) characters in the name - $room = rawurlencode($this->room); - $header = "POST /v2/room/{$room}/notification?auth_token={$this->token} HTTP/1.1\r\n"; - - $header .= "Host: {$this->host}\r\n"; - $header .= "Content-Type: application/x-www-form-urlencoded\r\n"; - $header .= "Content-Length: " . strlen($content) . "\r\n"; - $header .= "\r\n"; - - return $header; - } - - /** - * Assigns a color to each level of log records. - */ - protected function getAlertColor(int $level): string - { - switch (true) { - case $level >= Logger::ERROR: - return 'red'; - case $level >= Logger::WARNING: - return 'yellow'; - case $level >= Logger::INFO: - return 'green'; - case $level == Logger::DEBUG: - return 'gray'; - default: - return 'yellow'; - } - } - - /** - * {@inheritdoc} - */ - protected function write(array $record): void - { - parent::write($record); - $this->finalizeWrite(); - } - - /** - * Finalizes the request by reading some bytes and then closing the socket - * - * If we do not read some but close the socket too early, hipchat sometimes - * drops the request entirely. - */ - protected function finalizeWrite(): void - { - $res = $this->getResource(); - if (is_resource($res)) { - @fread($res, 2048); - } - $this->closeSocket(); - } - - /** - * {@inheritdoc} - */ - public function handleBatch(array $records): void - { - if (count($records) == 0) { - return; - } - - $batchRecords = $this->combineRecords($records); - - foreach ($batchRecords as $batchRecord) { - $this->handle($batchRecord); - } - } - - /** - * Combines multiple records into one. Error level of the combined record - * will be the highest level from the given records. Datetime will be taken - * from the first record. - */ - private function combineRecords(array $records): array - { - $batchRecord = null; - $batchRecords = []; - $messages = []; - $formattedMessages = []; - $level = 0; - $levelName = null; - $datetime = null; - - foreach ($records as $record) { - $record = $this->processRecord($record); - - if ($record['level'] > $level) { - $level = $record['level']; - $levelName = $record['level_name']; - } - - if (null === $datetime) { - $datetime = $record['datetime']; - } - - $messages[] = $record['message']; - $messageStr = implode(PHP_EOL, $messages); - $formattedMessages[] = $this->getFormatter()->format($record); - $formattedMessageStr = implode('', $formattedMessages); - - $batchRecord = [ - 'message' => $messageStr, - 'formatted' => $formattedMessageStr, - 'context' => [], - 'extra' => [], - ]; - - if (!$this->validateStringLength($batchRecord['formatted'], static::MAXIMUM_MESSAGE_LENGTH)) { - // Pop the last message and implode the remaining messages - $lastMessage = array_pop($messages); - $lastFormattedMessage = array_pop($formattedMessages); - $batchRecord['message'] = implode(PHP_EOL, $messages); - $batchRecord['formatted'] = implode('', $formattedMessages); - - $batchRecords[] = $batchRecord; - $messages = [$lastMessage]; - $formattedMessages = [$lastFormattedMessage]; - - $batchRecord = null; - } - } - - if (null !== $batchRecord) { - $batchRecords[] = $batchRecord; - } - - // Set the max level and datetime for all records - foreach ($batchRecords as &$batchRecord) { - $batchRecord = array_merge( - $batchRecord, - [ - 'level' => $level, - 'level_name' => $levelName, - 'datetime' => $datetime, - ] - ); - } - - return $batchRecords; - } - - /** - * Validates the length of a string. - * - * If the `mb_strlen()` function is available, it will use that, as HipChat - * allows UTF-8 characters. Otherwise, it will fall back to `strlen()`. - * - * Note that this might cause false failures in the specific case of using - * a valid name with less than 16 characters, but 16 or more bytes, on a - * system where `mb_strlen()` is unavailable. - */ - private function validateStringLength(string $str, int $length): bool - { - if (function_exists('mb_strlen')) { - return (mb_strlen($str) <= $length); - } - - return (strlen($str) <= $length); - } -} diff --git a/tests/Monolog/Handler/HipChatHandlerTest.php b/tests/Monolog/Handler/HipChatHandlerTest.php deleted file mode 100644 index 4df4de47..00000000 --- a/tests/Monolog/Handler/HipChatHandlerTest.php +++ /dev/null @@ -1,234 +0,0 @@ - - * - * 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 Rafael Dohms - * @see https://www.hipchat.com/docs/api - */ -class HipChatHandlerTest extends TestCase -{ - private $res; - /** @var HipChatHandler */ - private $handler; - - public function testWriteV2() - { - $this->createHandler('myToken', 'room1', 'Monolog', false, 'hipchat.foo.bar', 'v2'); - $this->handler->handle($this->getRecord(Logger::CRITICAL, 'test1')); - fseek($this->res, 0); - $content = fread($this->res, 1024); - - $this->assertRegexp('{POST /v2/room/room1/notification\?auth_token=.* HTTP/1.1\\r\\nHost: hipchat.foo.bar\\r\\nContent-Type: application/x-www-form-urlencoded\\r\\nContent-Length: \d{2,4}\\r\\n\\r\\n}', $content); - - return $content; - } - - public function testWriteV2Notify() - { - $this->createHandler('myToken', 'room1', 'Monolog', true, 'hipchat.foo.bar', 'v2'); - $this->handler->handle($this->getRecord(Logger::CRITICAL, 'test1')); - fseek($this->res, 0); - $content = fread($this->res, 1024); - - $this->assertRegexp('{POST /v2/room/room1/notification\?auth_token=.* HTTP/1.1\\r\\nHost: hipchat.foo.bar\\r\\nContent-Type: application/x-www-form-urlencoded\\r\\nContent-Length: \d{2,4}\\r\\n\\r\\n}', $content); - - return $content; - } - - public function testRoomSpaces() - { - $this->createHandler('myToken', 'room name', 'Monolog', false, 'hipchat.foo.bar', 'v2'); - $this->handler->handle($this->getRecord(Logger::CRITICAL, 'test1')); - fseek($this->res, 0); - $content = fread($this->res, 1024); - - $this->assertRegexp('{POST /v2/room/room%20name/notification\?auth_token=.* HTTP/1.1\\r\\nHost: hipchat.foo.bar\\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('/notify=0&message=test1&message_format=text&color=red&room_id=room1&from=Monolog$/', $content); - } - - /** - * @depends testWriteCustomHostHeader - */ - public function testWriteContentNotify($content) - { - $this->assertRegexp('/notify=1&message=test1&message_format=text&color=red&room_id=room1&from=Monolog$/', $content); - } - - /** - * @depends testWriteV2 - */ - public function testWriteContentV2($content) - { - $this->assertRegexp('/notify=false&message=test1&message_format=text&color=red&from=Monolog$/', $content); - } - - /** - * @depends testWriteV2Notify - */ - public function testWriteContentV2Notify($content) - { - $this->assertRegexp('/notify=true&message=test1&message_format=text&color=red&from=Monolog$/', $content); - } - - public function testWriteContentV2WithoutName() - { - $this->createHandler('myToken', 'room1', null, false, 'hipchat.foo.bar', 'v2'); - $this->handler->handle($this->getRecord(Logger::CRITICAL, 'test1')); - fseek($this->res, 0); - $content = fread($this->res, 1024); - - $this->assertRegexp('/notify=false&message=test1&message_format=text&color=red$/', $content); - - return $content; - } - - public function testWriteWithComplexMessage() - { - $this->createHandler(); - $this->handler->handle($this->getRecord(Logger::CRITICAL, 'Backup of database "example" finished in 16 minutes.')); - fseek($this->res, 0); - $content = fread($this->res, 1024); - - $this->assertRegexp('/message=Backup\+of\+database\+%22example%22\+finished\+in\+16\+minutes\./', $content); - } - - public function testWriteTruncatesLongMessage() - { - $this->createHandler(); - $this->handler->handle($this->getRecord(Logger::CRITICAL, str_repeat('abcde', 2000))); - fseek($this->res, 0); - $content = fread($this->res, 12000); - - $this->assertRegexp('/message='.str_repeat('abcde', 1900).'\+%5Btruncated%5D/', $content); - } - - /** - * @dataProvider provideLevelColors - */ - public function testWriteWithErrorLevelsAndColors($level, $expectedColor) - { - $this->createHandler(); - $this->handler->handle($this->getRecord($level, 'Backup of database "example" finished in 16 minutes.')); - fseek($this->res, 0); - $content = fread($this->res, 1024); - - $this->assertRegexp('/color='.$expectedColor.'/', $content); - } - - public function provideLevelColors() - { - return [ - [Logger::DEBUG, 'gray'], - [Logger::INFO, 'green'], - [Logger::WARNING, 'yellow'], - [Logger::ERROR, 'red'], - [Logger::CRITICAL, 'red'], - [Logger::ALERT, 'red'], - [Logger::EMERGENCY,'red'], - [Logger::NOTICE, 'green'], - ]; - } - - /** - * @dataProvider provideBatchRecords - */ - public function testHandleBatch($records, $expectedColor) - { - $this->createHandler(); - - $this->handler->handleBatch($records); - - fseek($this->res, 0); - $content = fread($this->res, 1024); - - $this->assertRegexp('/color='.$expectedColor.'/', $content); - } - - public function provideBatchRecords() - { - return [ - [ - [ - ['level' => Logger::WARNING, 'message' => 'Oh bugger!', 'level_name' => 'warning', 'datetime' => new \DateTimeImmutable()], - ['level' => Logger::NOTICE, 'message' => 'Something noticeable happened.', 'level_name' => 'notice', 'datetime' => new \DateTimeImmutable()], - ['level' => Logger::CRITICAL, 'message' => 'Everything is broken!', 'level_name' => 'critical', 'datetime' => new \DateTimeImmutable()], - ], - 'red', - ], - [ - [ - ['level' => Logger::WARNING, 'message' => 'Oh bugger!', 'level_name' => 'warning', 'datetime' => new \DateTimeImmutable()], - ['level' => Logger::NOTICE, 'message' => 'Something noticeable happened.', 'level_name' => 'notice', 'datetime' => new \DateTimeImmutable()], - ], - 'yellow', - ], - [ - [ - ['level' => Logger::DEBUG, 'message' => 'Just debugging.', 'level_name' => 'debug', 'datetime' => new \DateTimeImmutable()], - ['level' => Logger::NOTICE, 'message' => 'Something noticeable happened.', 'level_name' => 'notice', 'datetime' => new \DateTimeImmutable()], - ], - 'green', - ], - [ - [ - ['level' => Logger::DEBUG, 'message' => 'Just debugging.', 'level_name' => 'debug', 'datetime' => new \DateTimeImmutable()], - ], - 'gray', - ], - ]; - } - - private function createHandler($token = 'myToken', $room = 'room1', $name = 'Monolog', $notify = false, $host = 'api.hipchat.com', $version = 'v1') - { - $constructorArgs = [$token, $room, $name, $notify, Logger::DEBUG, true, true, 'text', $host, $version]; - $this->res = fopen('php://memory', 'a'); - $this->handler = $this->getMockBuilder('Monolog\Handler\HipChatHandler') - ->setConstructorArgs($constructorArgs) - ->setMethods(['fsockopen', 'streamSetTimeout', 'closeSocket']) - ->getMock(); - - $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()); - } - - public function testCreateWithTooLongNameV2() - { - // creating a handler with too long of a name but using the v2 api doesn't matter. - $hipChatHandler = new HipChatHandler('token', 'room', 'SixteenCharsHere', false, Logger::CRITICAL, true, true, 'test', 'api.hipchat.com', 'v2'); - } -}