mirror of
https://github.com/Seldaek/monolog.git
synced 2025-08-01 19:00:20 +02:00
Merge branch '1.x'
This commit is contained in:
@@ -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
|
||||
|
||||
|
@@ -1,306 +0,0 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Monolog package.
|
||||
*
|
||||
* (c) Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* 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 <rafael@doh.ms>
|
||||
* @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);
|
||||
}
|
||||
}
|
@@ -1,234 +0,0 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Monolog package.
|
||||
*
|
||||
* (c) Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* 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 <rafael@doh.ms>
|
||||
* @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');
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user