MRBS 1.12.2 等保2.0二级整改完整提交

包含:登录失败锁定、90天密码有效期、30分钟会话超时、
强制改密、登录审计日志、屏幕水印、企业背景图、
备案信息固定底部、favicon、JS空集合保护、
会话过期体验优化(403 JSON)、display_errors 关闭、
固定 key 根治 Integrity check failed 等全部改动

注意:config.inc.php/.htaccess/.user.ini 含敏感信息,
通过 .gitignore 排除,勿推送到公开仓库。
This commit is contained in:
人事系统开发
2026-09-09 16:55:02 +08:00
commit 1ba6efd8ed
2151 changed files with 528780 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
<?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;
use Monolog\ResettableInterface;
use Psr\Log\LogLevel;
/**
* Base Handler class providing basic level/bubble support
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @phpstan-import-type Level from \Monolog\Logger
* @phpstan-import-type LevelName from \Monolog\Logger
*/
abstract class AbstractHandler extends Handler implements ResettableInterface
{
/**
* @var int
* @phpstan-var Level
*/
protected $level = Logger::DEBUG;
/** @var bool */
protected $bubble = true;
/**
* @param int|string $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
*
* @phpstan-param Level|LevelName|LogLevel::* $level
*/
public function __construct($level = Logger::DEBUG, bool $bubble = true)
{
$this->setLevel($level);
$this->bubble = $bubble;
}
/**
* {@inheritDoc}
*/
public function isHandling(array $record): bool
{
return $record['level'] >= $this->level;
}
/**
* Sets minimum logging level at which this handler will be triggered.
*
* @param Level|LevelName|LogLevel::* $level Level or level name
* @return self
*/
public function setLevel($level): self
{
$this->level = Logger::toMonologLevel($level);
return $this;
}
/**
* Gets minimum logging level at which this handler will be triggered.
*
* @return int
*
* @phpstan-return Level
*/
public function getLevel(): int
{
return $this->level;
}
/**
* Sets the bubbling behavior.
*
* @param bool $bubble true means that this handler allows bubbling.
* false means that bubbling is not permitted.
* @return self
*/
public function setBubble(bool $bubble): self
{
$this->bubble = $bubble;
return $this;
}
/**
* Gets the bubbling behavior.
*
* @return bool true means that this handler allows bubbling.
* false means that bubbling is not permitted.
*/
public function getBubble(): bool
{
return $this->bubble;
}
/**
* {@inheritDoc}
*/
public function reset()
{
}
}
@@ -0,0 +1,69 @@
<?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;
/**
* Base Handler class providing the Handler structure, including processors and formatters
*
* Classes extending it should (in most cases) only implement write($record)
*
* @author Jordi Boggiano <j.boggiano@seld.be>
* @author Christophe Coevoet <stof@notk.org>
*
* @phpstan-import-type LevelName from \Monolog\Logger
* @phpstan-import-type Level from \Monolog\Logger
* @phpstan-import-type Record from \Monolog\Logger
* @phpstan-type FormattedRecord array{message: string, context: mixed[], level: Level, level_name: LevelName, channel: string, datetime: \DateTimeImmutable, extra: mixed[], formatted: mixed}
*/
abstract class AbstractProcessingHandler extends AbstractHandler implements ProcessableHandlerInterface, FormattableHandlerInterface
{
use ProcessableHandlerTrait;
use FormattableHandlerTrait;
/**
* {@inheritDoc}
*/
public function handle(array $record): bool
{
if (!$this->isHandling($record)) {
return false;
}
if ($this->processors) {
/** @var Record $record */
$record = $this->processRecord($record);
}
$record['formatted'] = $this->getFormatter()->format($record);
$this->write($record);
return false === $this->bubble;
}
/**
* Writes the record down to the log of the implementing handler
*
* @phpstan-param FormattedRecord $record
*/
abstract protected function write(array $record): void;
/**
* @return void
*/
public function reset()
{
parent::reset();
$this->resetProcessors();
}
}
@@ -0,0 +1,106 @@
<?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;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\LineFormatter;
/**
* Common syslog functionality
*
* @phpstan-import-type Level from \Monolog\Logger
*/
abstract class AbstractSyslogHandler extends AbstractProcessingHandler
{
/** @var int */
protected $facility;
/**
* Translates Monolog log levels to syslog log priorities.
* @var array
* @phpstan-var array<Level, int>
*/
protected $logLevels = [
Logger::DEBUG => LOG_DEBUG,
Logger::INFO => LOG_INFO,
Logger::NOTICE => LOG_NOTICE,
Logger::WARNING => LOG_WARNING,
Logger::ERROR => LOG_ERR,
Logger::CRITICAL => LOG_CRIT,
Logger::ALERT => LOG_ALERT,
Logger::EMERGENCY => LOG_EMERG,
];
/**
* List of valid log facility names.
* @var array<string, int>
*/
protected $facilities = [
'auth' => LOG_AUTH,
'authpriv' => LOG_AUTHPRIV,
'cron' => LOG_CRON,
'daemon' => LOG_DAEMON,
'kern' => LOG_KERN,
'lpr' => LOG_LPR,
'mail' => LOG_MAIL,
'news' => LOG_NEWS,
'syslog' => LOG_SYSLOG,
'user' => LOG_USER,
'uucp' => LOG_UUCP,
];
/**
* @param string|int $facility Either one of the names of the keys in $this->facilities, or a LOG_* facility constant
*/
public function __construct($facility = LOG_USER, $level = Logger::DEBUG, bool $bubble = true)
{
parent::__construct($level, $bubble);
if (!defined('PHP_WINDOWS_VERSION_BUILD')) {
$this->facilities['local0'] = LOG_LOCAL0;
$this->facilities['local1'] = LOG_LOCAL1;
$this->facilities['local2'] = LOG_LOCAL2;
$this->facilities['local3'] = LOG_LOCAL3;
$this->facilities['local4'] = LOG_LOCAL4;
$this->facilities['local5'] = LOG_LOCAL5;
$this->facilities['local6'] = LOG_LOCAL6;
$this->facilities['local7'] = LOG_LOCAL7;
} else {
$this->facilities['local0'] = 128; // LOG_LOCAL0
$this->facilities['local1'] = 136; // LOG_LOCAL1
$this->facilities['local2'] = 144; // LOG_LOCAL2
$this->facilities['local3'] = 152; // LOG_LOCAL3
$this->facilities['local4'] = 160; // LOG_LOCAL4
$this->facilities['local5'] = 168; // LOG_LOCAL5
$this->facilities['local6'] = 176; // LOG_LOCAL6
$this->facilities['local7'] = 184; // LOG_LOCAL7
}
// convert textual description of facility to syslog constant
if (is_string($facility) && array_key_exists(strtolower($facility), $this->facilities)) {
$facility = $this->facilities[strtolower($facility)];
} elseif (!in_array($facility, array_values($this->facilities), true)) {
throw new \UnexpectedValueException('Unknown facility value "'.$facility.'" given');
}
$this->facility = $facility;
}
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter(): FormatterInterface
{
return new LineFormatter('%channel%.%level_name%: %message% %context% %extra%');
}
}
+171
View File
@@ -0,0 +1,171 @@
<?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;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\JsonFormatter;
use PhpAmqpLib\Message\AMQPMessage;
use PhpAmqpLib\Channel\AMQPChannel;
use AMQPExchange;
/**
* @phpstan-import-type Record from \Monolog\Logger
*/
class AmqpHandler extends AbstractProcessingHandler
{
/**
* @var AMQPExchange|AMQPChannel $exchange
*/
protected $exchange;
/** @var array<string, mixed> */
private $extraAttributes = [];
/**
* @return array<string, mixed>
*/
public function getExtraAttributes(): array
{
return $this->extraAttributes;
}
/**
* Configure extra attributes to pass to the AMQPExchange (if you are using the amqp extension)
*
* @param array<string, mixed> $extraAttributes One of content_type, content_encoding,
* message_id, user_id, app_id, delivery_mode,
* priority, timestamp, expiration, type
* or reply_to, headers.
* @return AmqpHandler
*/
public function setExtraAttributes(array $extraAttributes): self
{
$this->extraAttributes = $extraAttributes;
return $this;
}
/**
* @var string
*/
protected $exchangeName;
/**
* @param AMQPExchange|AMQPChannel $exchange AMQPExchange (php AMQP ext) or PHP AMQP lib channel, ready for use
* @param string|null $exchangeName Optional exchange name, for AMQPChannel (PhpAmqpLib) only
*/
public function __construct($exchange, ?string $exchangeName = null, $level = Logger::DEBUG, bool $bubble = true)
{
if ($exchange instanceof AMQPChannel) {
$this->exchangeName = (string) $exchangeName;
} elseif (!$exchange instanceof AMQPExchange) {
throw new \InvalidArgumentException('PhpAmqpLib\Channel\AMQPChannel or AMQPExchange instance required');
} elseif ($exchangeName) {
@trigger_error('The $exchangeName parameter can only be passed when using PhpAmqpLib, if using an AMQPExchange instance configure it beforehand', E_USER_DEPRECATED);
}
$this->exchange = $exchange;
parent::__construct($level, $bubble);
}
/**
* {@inheritDoc}
*/
protected function write(array $record): void
{
$data = $record["formatted"];
$routingKey = $this->getRoutingKey($record);
if ($this->exchange instanceof AMQPExchange) {
$attributes = [
'delivery_mode' => 2,
'content_type' => 'application/json',
];
if ($this->extraAttributes) {
$attributes = array_merge($attributes, $this->extraAttributes);
}
$this->exchange->publish(
$data,
$routingKey,
0,
$attributes
);
} else {
$this->exchange->basic_publish(
$this->createAmqpMessage($data),
$this->exchangeName,
$routingKey
);
}
}
/**
* {@inheritDoc}
*/
public function handleBatch(array $records): void
{
if ($this->exchange instanceof AMQPExchange) {
parent::handleBatch($records);
return;
}
foreach ($records as $record) {
if (!$this->isHandling($record)) {
continue;
}
/** @var Record $record */
$record = $this->processRecord($record);
$data = $this->getFormatter()->format($record);
$this->exchange->batch_basic_publish(
$this->createAmqpMessage($data),
$this->exchangeName,
$this->getRoutingKey($record)
);
}
$this->exchange->publish_batch();
}
/**
* Gets the routing key for the AMQP exchange
*
* @phpstan-param Record $record
*/
protected function getRoutingKey(array $record): string
{
$routingKey = sprintf('%s.%s', $record['level_name'], $record['channel']);
return strtolower($routingKey);
}
private function createAmqpMessage(string $data): AMQPMessage
{
$attributes = [
'delivery_mode' => 2,
'content_type' => 'application/json',
];
if ($this->extraAttributes) {
$attributes = array_merge($attributes, $this->extraAttributes);
}
return new AMQPMessage($data, $attributes);
}
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter(): FormatterInterface
{
return new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, false);
}
}
@@ -0,0 +1,308 @@
<?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\Formatter\FormatterInterface;
use Monolog\Formatter\LineFormatter;
use Monolog\Utils;
use Monolog\Logger;
use function count;
use function headers_list;
use function stripos;
use function trigger_error;
use const E_USER_DEPRECATED;
/**
* Handler sending logs to browser's javascript console with no browser extension required
*
* @author Olivier Poitrey <rs@dailymotion.com>
*
* @phpstan-import-type FormattedRecord from AbstractProcessingHandler
*/
class BrowserConsoleHandler extends AbstractProcessingHandler
{
/** @var bool */
protected static $initialized = false;
/** @var FormattedRecord[] */
protected static $records = [];
protected const FORMAT_HTML = 'html';
protected const FORMAT_JS = 'js';
protected const FORMAT_UNKNOWN = 'unknown';
/**
* {@inheritDoc}
*
* Formatted output may contain some formatting markers to be transferred to `console.log` using the %c format.
*
* Example of formatted string:
*
* You can do [[blue text]]{color: blue} or [[green background]]{background-color: green; color: white}
*/
protected function getDefaultFormatter(): FormatterInterface
{
return new LineFormatter('[[%channel%]]{macro: autolabel} [[%level_name%]]{font-weight: bold} %message%');
}
/**
* {@inheritDoc}
*/
protected function write(array $record): void
{
// Accumulate records
static::$records[] = $record;
// Register shutdown handler if not already done
if (!static::$initialized) {
static::$initialized = true;
$this->registerShutdownFunction();
}
}
/**
* Convert records to javascript console commands and send it to the browser.
* This method is automatically called on PHP shutdown if output is HTML or Javascript.
*/
public static function send(): void
{
$format = static::getResponseFormat();
if ($format === self::FORMAT_UNKNOWN) {
return;
}
if (count(static::$records)) {
if ($format === self::FORMAT_HTML) {
static::writeOutput('<script>' . static::generateScript() . '</script>');
} elseif ($format === self::FORMAT_JS) {
static::writeOutput(static::generateScript());
}
static::resetStatic();
}
}
public function close(): void
{
self::resetStatic();
}
public function reset()
{
parent::reset();
self::resetStatic();
}
/**
* Forget all logged records
*/
public static function resetStatic(): void
{
static::$records = [];
}
/**
* Wrapper for register_shutdown_function to allow overriding
*/
protected function registerShutdownFunction(): void
{
if (PHP_SAPI !== 'cli') {
register_shutdown_function(['Monolog\Handler\BrowserConsoleHandler', 'send']);
}
}
/**
* Wrapper for echo to allow overriding
*/
protected static function writeOutput(string $str): void
{
echo $str;
}
/**
* Checks the format of the response
*
* If Content-Type is set to application/javascript or text/javascript -> js
* If Content-Type is set to text/html, or is unset -> html
* If Content-Type is anything else -> unknown
*
* @return string One of 'js', 'html' or 'unknown'
* @phpstan-return self::FORMAT_*
*/
protected static function getResponseFormat(): string
{
// Check content type
foreach (headers_list() as $header) {
if (stripos($header, 'content-type:') === 0) {
return static::getResponseFormatFromContentType($header);
}
}
return self::FORMAT_HTML;
}
/**
* @return string One of 'js', 'html' or 'unknown'
* @phpstan-return self::FORMAT_*
*/
protected static function getResponseFormatFromContentType(string $contentType): string
{
// This handler only works with HTML and javascript outputs
// text/javascript is obsolete in favour of application/javascript, but still used
if (stripos($contentType, 'application/javascript') !== false || stripos($contentType, 'text/javascript') !== false) {
return self::FORMAT_JS;
}
if (stripos($contentType, 'text/html') !== false) {
return self::FORMAT_HTML;
}
return self::FORMAT_UNKNOWN;
}
private static function generateScript(): string
{
$script = [];
foreach (static::$records as $record) {
$context = static::dump('Context', $record['context']);
$extra = static::dump('Extra', $record['extra']);
if (empty($context) && empty($extra)) {
$script[] = static::call_array(static::getConsoleMethodForLevel($record['level']), static::handleStyles($record['formatted']));
} else {
$script = array_merge(
$script,
[static::call_array('groupCollapsed', static::handleStyles($record['formatted']))],
$context,
$extra,
[static::call('groupEnd')]
);
}
}
return "(function (c) {if (c && c.groupCollapsed) {\n" . implode("\n", $script) . "\n}})(console);";
}
private static function getConsoleMethodForLevel(int $level): string
{
return [
Logger::DEBUG => 'debug',
Logger::INFO => 'info',
Logger::NOTICE => 'info',
Logger::WARNING => 'warn',
Logger::ERROR => 'error',
Logger::CRITICAL => 'error',
Logger::ALERT => 'error',
Logger::EMERGENCY => 'error',
][$level] ?? 'log';
}
/**
* @return string[]
*/
private static function handleStyles(string $formatted): array
{
$args = [];
$format = '%c' . $formatted;
preg_match_all('/\[\[(.*?)\]\]\{([^}]*)\}/s', $format, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
foreach (array_reverse($matches) as $match) {
$args[] = '"font-weight: normal"';
$args[] = static::quote(static::handleCustomStyles($match[2][0], $match[1][0]));
$pos = $match[0][1];
$format = Utils::substr($format, 0, $pos) . '%c' . $match[1][0] . '%c' . Utils::substr($format, $pos + strlen($match[0][0]));
}
$args[] = static::quote('font-weight: normal');
$args[] = static::quote($format);
return array_reverse($args);
}
private static function handleCustomStyles(string $style, string $string): string
{
static $colors = ['blue', 'green', 'red', 'magenta', 'orange', 'black', 'grey'];
static $labels = [];
$style = preg_replace_callback('/macro\s*:(.*?)(?:;|$)/', function (array $m) use ($string, &$colors, &$labels) {
if (trim($m[1]) === 'autolabel') {
// Format the string as a label with consistent auto assigned background color
if (!isset($labels[$string])) {
$labels[$string] = $colors[count($labels) % count($colors)];
}
$color = $labels[$string];
return "background-color: $color; color: white; border-radius: 3px; padding: 0 2px 0 2px";
}
return $m[1];
}, $style);
if (null === $style) {
$pcreErrorCode = preg_last_error();
throw new \RuntimeException('Failed to run preg_replace_callback: ' . $pcreErrorCode . ' / ' . Utils::pcreLastErrorMessage($pcreErrorCode));
}
return $style;
}
/**
* @param mixed[] $dict
* @return mixed[]
*/
private static function dump(string $title, array $dict): array
{
$script = [];
$dict = array_filter($dict);
if (empty($dict)) {
return $script;
}
$script[] = static::call('log', static::quote('%c%s'), static::quote('font-weight: bold'), static::quote($title));
foreach ($dict as $key => $value) {
$value = json_encode($value);
if (empty($value)) {
$value = static::quote('');
}
$script[] = static::call('log', static::quote('%s: %o'), static::quote((string) $key), $value);
}
return $script;
}
private static function quote(string $arg): string
{
return '"' . addcslashes($arg, "\"\n\\") . '"';
}
/**
* @param mixed $args
*/
private static function call(...$args): string
{
$method = array_shift($args);
if (!is_string($method)) {
throw new \UnexpectedValueException('Expected the first arg to be a string, got: '.var_export($method, true));
}
return static::call_array($method, $args);
}
/**
* @param mixed[] $args
*/
private static function call_array(string $method, array $args): string
{
return 'c.' . $method . '(' . implode(', ', $args) . ');';
}
}
+167
View File
@@ -0,0 +1,167 @@
<?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;
use Monolog\ResettableInterface;
use Monolog\Formatter\FormatterInterface;
/**
* Buffers all records until closing the handler and then pass them as batch.
*
* This is useful for a MailHandler to send only one mail per request instead of
* sending one per log message.
*
* @author Christophe Coevoet <stof@notk.org>
*
* @phpstan-import-type Record from \Monolog\Logger
*/
class BufferHandler extends AbstractHandler implements ProcessableHandlerInterface, FormattableHandlerInterface
{
use ProcessableHandlerTrait;
/** @var HandlerInterface */
protected $handler;
/** @var int */
protected $bufferSize = 0;
/** @var int */
protected $bufferLimit;
/** @var bool */
protected $flushOnOverflow;
/** @var Record[] */
protected $buffer = [];
/** @var bool */
protected $initialized = false;
/**
* @param HandlerInterface $handler Handler.
* @param int $bufferLimit How many entries should be buffered at most, beyond that the oldest items are removed from the buffer.
* @param bool $flushOnOverflow If true, the buffer is flushed when the max size has been reached, by default oldest entries are discarded
*/
public function __construct(HandlerInterface $handler, int $bufferLimit = 0, $level = Logger::DEBUG, bool $bubble = true, bool $flushOnOverflow = false)
{
parent::__construct($level, $bubble);
$this->handler = $handler;
$this->bufferLimit = $bufferLimit;
$this->flushOnOverflow = $flushOnOverflow;
}
/**
* {@inheritDoc}
*/
public function handle(array $record): bool
{
if ($record['level'] < $this->level) {
return false;
}
if (!$this->initialized) {
// __destructor() doesn't get called on Fatal errors
register_shutdown_function([$this, 'close']);
$this->initialized = true;
}
if ($this->bufferLimit > 0 && $this->bufferSize === $this->bufferLimit) {
if ($this->flushOnOverflow) {
$this->flush();
} else {
array_shift($this->buffer);
$this->bufferSize--;
}
}
if ($this->processors) {
/** @var Record $record */
$record = $this->processRecord($record);
}
$this->buffer[] = $record;
$this->bufferSize++;
return false === $this->bubble;
}
public function flush(): void
{
if ($this->bufferSize === 0) {
return;
}
$this->handler->handleBatch($this->buffer);
$this->clear();
}
public function __destruct()
{
// suppress the parent behavior since we already have register_shutdown_function()
// to call close(), and the reference contained there will prevent this from being
// GC'd until the end of the request
}
/**
* {@inheritDoc}
*/
public function close(): void
{
$this->flush();
$this->handler->close();
}
/**
* Clears the buffer without flushing any messages down to the wrapped handler.
*/
public function clear(): void
{
$this->bufferSize = 0;
$this->buffer = [];
}
public function reset()
{
$this->flush();
parent::reset();
$this->resetProcessors();
if ($this->handler instanceof ResettableInterface) {
$this->handler->reset();
}
}
/**
* {@inheritDoc}
*/
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
if ($this->handler instanceof FormattableHandlerInterface) {
$this->handler->setFormatter($formatter);
return $this;
}
throw new \UnexpectedValueException('The nested handler of type '.get_class($this->handler).' does not support formatters.');
}
/**
* {@inheritDoc}
*/
public function getFormatter(): FormatterInterface
{
if ($this->handler instanceof FormattableHandlerInterface) {
return $this->handler->getFormatter();
}
throw new \UnexpectedValueException('The nested handler of type '.get_class($this->handler).' does not support formatters.');
}
}
+196
View File
@@ -0,0 +1,196 @@
<?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\Formatter\ChromePHPFormatter;
use Monolog\Formatter\FormatterInterface;
use Monolog\Logger;
use Monolog\Utils;
/**
* Handler sending logs to the ChromePHP extension (http://www.chromephp.com/)
*
* This also works out of the box with Firefox 43+
*
* @author Christophe Coevoet <stof@notk.org>
*
* @phpstan-import-type Record from \Monolog\Logger
*/
class ChromePHPHandler extends AbstractProcessingHandler
{
use WebRequestRecognizerTrait;
/**
* Version of the extension
*/
protected const VERSION = '4.0';
/**
* Header name
*/
protected const HEADER_NAME = 'X-ChromeLogger-Data';
/**
* Regular expression to detect supported browsers (matches any Chrome, or Firefox 43+)
*/
protected const USER_AGENT_REGEX = '{\b(?:Chrome/\d+(?:\.\d+)*|HeadlessChrome|Firefox/(?:4[3-9]|[5-9]\d|\d{3,})(?:\.\d)*)\b}';
/** @var bool */
protected static $initialized = false;
/**
* Tracks whether we sent too much data
*
* Chrome limits the headers to 4KB, so when we sent 3KB we stop sending
*
* @var bool
*/
protected static $overflowed = false;
/** @var mixed[] */
protected static $json = [
'version' => self::VERSION,
'columns' => ['label', 'log', 'backtrace', 'type'],
'rows' => [],
];
/** @var bool */
protected static $sendHeaders = true;
public function __construct($level = Logger::DEBUG, bool $bubble = true)
{
parent::__construct($level, $bubble);
if (!function_exists('json_encode')) {
throw new \RuntimeException('PHP\'s json extension is required to use Monolog\'s ChromePHPHandler');
}
}
/**
* {@inheritDoc}
*/
public function handleBatch(array $records): void
{
if (!$this->isWebRequest()) {
return;
}
$messages = [];
foreach ($records as $record) {
if ($record['level'] < $this->level) {
continue;
}
/** @var Record $message */
$message = $this->processRecord($record);
$messages[] = $message;
}
if (!empty($messages)) {
$messages = $this->getFormatter()->formatBatch($messages);
self::$json['rows'] = array_merge(self::$json['rows'], $messages);
$this->send();
}
}
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter(): FormatterInterface
{
return new ChromePHPFormatter();
}
/**
* Creates & sends header for a record
*
* @see sendHeader()
* @see send()
*/
protected function write(array $record): void
{
if (!$this->isWebRequest()) {
return;
}
self::$json['rows'][] = $record['formatted'];
$this->send();
}
/**
* Sends the log header
*
* @see sendHeader()
*/
protected function send(): void
{
if (self::$overflowed || !self::$sendHeaders) {
return;
}
if (!self::$initialized) {
self::$initialized = true;
self::$sendHeaders = $this->headersAccepted();
if (!self::$sendHeaders) {
return;
}
self::$json['request_uri'] = $_SERVER['REQUEST_URI'] ?? '';
}
$json = Utils::jsonEncode(self::$json, Utils::DEFAULT_JSON_FLAGS & ~JSON_UNESCAPED_UNICODE, true);
$data = base64_encode($json);
if (strlen($data) > 3 * 1024) {
self::$overflowed = true;
$record = [
'message' => 'Incomplete logs, chrome header size limit reached',
'context' => [],
'level' => Logger::WARNING,
'level_name' => Logger::getLevelName(Logger::WARNING),
'channel' => 'monolog',
'datetime' => new \DateTimeImmutable(),
'extra' => [],
];
self::$json['rows'][count(self::$json['rows']) - 1] = $this->getFormatter()->format($record);
$json = Utils::jsonEncode(self::$json, Utils::DEFAULT_JSON_FLAGS & ~JSON_UNESCAPED_UNICODE, true);
$data = base64_encode($json);
}
if (trim($data) !== '') {
$this->sendHeader(static::HEADER_NAME, $data);
}
}
/**
* Send header string to the client
*/
protected function sendHeader(string $header, string $content): void
{
if (!headers_sent() && self::$sendHeaders) {
header(sprintf('%s: %s', $header, $content));
}
}
/**
* Verifies if the headers are accepted by the current user agent
*/
protected function headersAccepted(): bool
{
if (empty($_SERVER['HTTP_USER_AGENT'])) {
return false;
}
return preg_match(static::USER_AGENT_REGEX, $_SERVER['HTTP_USER_AGENT']) === 1;
}
}
+77
View File
@@ -0,0 +1,77 @@
<?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\Formatter\FormatterInterface;
use Monolog\Formatter\JsonFormatter;
use Monolog\Logger;
/**
* CouchDB handler
*
* @author Markus Bachmann <markus.bachmann@bachi.biz>
*/
class CouchDBHandler extends AbstractProcessingHandler
{
/** @var mixed[] */
private $options;
/**
* @param mixed[] $options
*/
public function __construct(array $options = [], $level = Logger::DEBUG, bool $bubble = true)
{
$this->options = array_merge([
'host' => 'localhost',
'port' => 5984,
'dbname' => 'logger',
'username' => null,
'password' => null,
], $options);
parent::__construct($level, $bubble);
}
/**
* {@inheritDoc}
*/
protected function write(array $record): void
{
$basicAuth = null;
if ($this->options['username']) {
$basicAuth = sprintf('%s:%s@', $this->options['username'], $this->options['password']);
}
$url = 'http://'.$basicAuth.$this->options['host'].':'.$this->options['port'].'/'.$this->options['dbname'];
$context = stream_context_create([
'http' => [
'method' => 'POST',
'content' => $record['formatted'],
'ignore_errors' => true,
'max_redirects' => 0,
'header' => 'Content-type: application/json',
],
]);
if (false === @file_get_contents($url, false, $context)) {
throw new \RuntimeException(sprintf('Could not connect to %s', $url));
}
}
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter(): FormatterInterface
{
return new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, false);
}
}
+167
View File
@@ -0,0 +1,167 @@
<?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;
use Monolog\Utils;
/**
* Logs to Cube.
*
* @link https://github.com/square/cube/wiki
* @author Wan Chen <kami@kamisama.me>
* @deprecated Since 2.8.0 and 3.2.0, Cube appears abandoned and thus we will drop this handler in Monolog 4
*/
class CubeHandler extends AbstractProcessingHandler
{
/** @var resource|\Socket|null */
private $udpConnection = null;
/** @var resource|\CurlHandle|null */
private $httpConnection = null;
/** @var string */
private $scheme;
/** @var string */
private $host;
/** @var int */
private $port;
/** @var string[] */
private $acceptedSchemes = ['http', 'udp'];
/**
* Create a Cube handler
*
* @throws \UnexpectedValueException when given url is not a valid url.
* A valid url must consist of three parts : protocol://host:port
* Only valid protocols used by Cube are http and udp
*/
public function __construct(string $url, $level = Logger::DEBUG, bool $bubble = true)
{
$urlInfo = parse_url($url);
if ($urlInfo === false || !isset($urlInfo['scheme'], $urlInfo['host'], $urlInfo['port'])) {
throw new \UnexpectedValueException('URL "'.$url.'" is not valid');
}
if (!in_array($urlInfo['scheme'], $this->acceptedSchemes)) {
throw new \UnexpectedValueException(
'Invalid protocol (' . $urlInfo['scheme'] . ').'
. ' Valid options are ' . implode(', ', $this->acceptedSchemes)
);
}
$this->scheme = $urlInfo['scheme'];
$this->host = $urlInfo['host'];
$this->port = (int) $urlInfo['port'];
parent::__construct($level, $bubble);
}
/**
* Establish a connection to an UDP socket
*
* @throws \LogicException when unable to connect to the socket
* @throws MissingExtensionException when there is no socket extension
*/
protected function connectUdp(): void
{
if (!extension_loaded('sockets')) {
throw new MissingExtensionException('The sockets extension is required to use udp URLs with the CubeHandler');
}
$udpConnection = socket_create(AF_INET, SOCK_DGRAM, 0);
if (false === $udpConnection) {
throw new \LogicException('Unable to create a socket');
}
$this->udpConnection = $udpConnection;
if (!socket_connect($this->udpConnection, $this->host, $this->port)) {
throw new \LogicException('Unable to connect to the socket at ' . $this->host . ':' . $this->port);
}
}
/**
* Establish a connection to an http server
*
* @throws \LogicException when unable to connect to the socket
* @throws MissingExtensionException when no curl extension
*/
protected function connectHttp(): void
{
if (!extension_loaded('curl')) {
throw new MissingExtensionException('The curl extension is required to use http URLs with the CubeHandler');
}
$httpConnection = curl_init('http://'.$this->host.':'.$this->port.'/1.0/event/put');
if (false === $httpConnection) {
throw new \LogicException('Unable to connect to ' . $this->host . ':' . $this->port);
}
$this->httpConnection = $httpConnection;
curl_setopt($this->httpConnection, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($this->httpConnection, CURLOPT_RETURNTRANSFER, true);
}
/**
* {@inheritDoc}
*/
protected function write(array $record): void
{
$date = $record['datetime'];
$data = ['time' => $date->format('Y-m-d\TH:i:s.uO')];
unset($record['datetime']);
if (isset($record['context']['type'])) {
$data['type'] = $record['context']['type'];
unset($record['context']['type']);
} else {
$data['type'] = $record['channel'];
}
$data['data'] = $record['context'];
$data['data']['level'] = $record['level'];
if ($this->scheme === 'http') {
$this->writeHttp(Utils::jsonEncode($data));
} else {
$this->writeUdp(Utils::jsonEncode($data));
}
}
private function writeUdp(string $data): void
{
if (!$this->udpConnection) {
$this->connectUdp();
}
socket_send($this->udpConnection, $data, strlen($data), 0);
}
private function writeHttp(string $data): void
{
if (!$this->httpConnection) {
$this->connectHttp();
}
if (null === $this->httpConnection) {
throw new \LogicException('No connection could be established');
}
curl_setopt($this->httpConnection, CURLOPT_POSTFIELDS, '['.$data.']');
curl_setopt($this->httpConnection, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length: ' . strlen('['.$data.']'),
]);
Curl\Util::execute($this->httpConnection, 5, false);
}
}
+71
View File
@@ -0,0 +1,71 @@
<?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\Curl;
use CurlHandle;
/**
* This class is marked as internal and it is not under the BC promise of the package.
*
* @internal
*/
final class Util
{
/** @var array<int> */
private static $retriableErrorCodes = [
CURLE_COULDNT_RESOLVE_HOST,
CURLE_COULDNT_CONNECT,
CURLE_HTTP_NOT_FOUND,
CURLE_READ_ERROR,
CURLE_OPERATION_TIMEOUTED,
CURLE_HTTP_POST_ERROR,
CURLE_SSL_CONNECT_ERROR,
];
/**
* Executes a CURL request with optional retries and exception on failure
*
* @param resource|CurlHandle $ch curl handler
* @param int $retries
* @param bool $closeAfterDone
* @return bool|string @see curl_exec
*/
public static function execute($ch, int $retries = 5, bool $closeAfterDone = true)
{
while ($retries--) {
$curlResponse = curl_exec($ch);
if ($curlResponse === false) {
$curlErrno = curl_errno($ch);
if (false === in_array($curlErrno, self::$retriableErrorCodes, true) || !$retries) {
$curlError = curl_error($ch);
if ($closeAfterDone) {
curl_close($ch);
}
throw new \RuntimeException(sprintf('Curl error (code %d): %s', $curlErrno, $curlError));
}
continue;
}
if ($closeAfterDone) {
curl_close($ch);
}
return $curlResponse;
}
return false;
}
}
@@ -0,0 +1,186 @@
<?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;
use Psr\Log\LogLevel;
/**
* Simple handler wrapper that deduplicates log records across multiple requests
*
* It also includes the BufferHandler functionality and will buffer
* all messages until the end of the request or flush() is called.
*
* This works by storing all log records' messages above $deduplicationLevel
* to the file specified by $deduplicationStore. When further logs come in at the end of the
* request (or when flush() is called), all those above $deduplicationLevel are checked
* against the existing stored logs. If they match and the timestamps in the stored log is
* not older than $time seconds, the new log record is discarded. If no log record is new, the
* whole data set is discarded.
*
* This is mainly useful in combination with Mail handlers or things like Slack or HipChat handlers
* that send messages to people, to avoid spamming with the same message over and over in case of
* a major component failure like a database server being down which makes all requests fail in the
* same way.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @phpstan-import-type Record from \Monolog\Logger
* @phpstan-import-type LevelName from \Monolog\Logger
* @phpstan-import-type Level from \Monolog\Logger
*/
class DeduplicationHandler extends BufferHandler
{
/**
* @var string
*/
protected $deduplicationStore;
/**
* @var Level
*/
protected $deduplicationLevel;
/**
* @var int
*/
protected $time;
/**
* @var bool
*/
private $gc = false;
/**
* @param HandlerInterface $handler Handler.
* @param string $deduplicationStore The file/path where the deduplication log should be kept
* @param string|int $deduplicationLevel The minimum logging level for log records to be looked at for deduplication purposes
* @param int $time The period (in seconds) during which duplicate entries should be suppressed after a given log is sent through
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not
*
* @phpstan-param Level|LevelName|LogLevel::* $deduplicationLevel
*/
public function __construct(HandlerInterface $handler, ?string $deduplicationStore = null, $deduplicationLevel = Logger::ERROR, int $time = 60, bool $bubble = true)
{
parent::__construct($handler, 0, Logger::DEBUG, $bubble, false);
$this->deduplicationStore = $deduplicationStore === null ? sys_get_temp_dir() . '/monolog-dedup-' . substr(md5(__FILE__), 0, 20) .'.log' : $deduplicationStore;
$this->deduplicationLevel = Logger::toMonologLevel($deduplicationLevel);
$this->time = $time;
}
public function flush(): void
{
if ($this->bufferSize === 0) {
return;
}
$passthru = null;
foreach ($this->buffer as $record) {
if ($record['level'] >= $this->deduplicationLevel) {
$passthru = $passthru || !$this->isDuplicate($record);
if ($passthru) {
$this->appendRecord($record);
}
}
}
// default of null is valid as well as if no record matches duplicationLevel we just pass through
if ($passthru === true || $passthru === null) {
$this->handler->handleBatch($this->buffer);
}
$this->clear();
if ($this->gc) {
$this->collectLogs();
}
}
/**
* @phpstan-param Record $record
*/
private function isDuplicate(array $record): bool
{
if (!file_exists($this->deduplicationStore)) {
return false;
}
$store = file($this->deduplicationStore, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if (!is_array($store)) {
return false;
}
$yesterday = time() - 86400;
$timestampValidity = $record['datetime']->getTimestamp() - $this->time;
$expectedMessage = preg_replace('{[\r\n].*}', '', $record['message']);
for ($i = count($store) - 1; $i >= 0; $i--) {
list($timestamp, $level, $message) = explode(':', $store[$i], 3);
if ($level === $record['level_name'] && $message === $expectedMessage && $timestamp > $timestampValidity) {
return true;
}
if ($timestamp < $yesterday) {
$this->gc = true;
}
}
return false;
}
private function collectLogs(): void
{
if (!file_exists($this->deduplicationStore)) {
return;
}
$handle = fopen($this->deduplicationStore, 'rw+');
if (!$handle) {
throw new \RuntimeException('Failed to open file for reading and writing: ' . $this->deduplicationStore);
}
flock($handle, LOCK_EX);
$validLogs = [];
$timestampValidity = time() - $this->time;
while (!feof($handle)) {
$log = fgets($handle);
if ($log && substr($log, 0, 10) >= $timestampValidity) {
$validLogs[] = $log;
}
}
ftruncate($handle, 0);
rewind($handle);
foreach ($validLogs as $log) {
fwrite($handle, $log);
}
flock($handle, LOCK_UN);
fclose($handle);
$this->gc = false;
}
/**
* @phpstan-param Record $record
*/
private function appendRecord(array $record): void
{
file_put_contents($this->deduplicationStore, $record['datetime']->getTimestamp() . ':' . $record['level_name'] . ':' . preg_replace('{[\r\n].*}', '', $record['message']) . "\n", FILE_APPEND);
}
}
@@ -0,0 +1,47 @@
<?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;
use Monolog\Formatter\NormalizerFormatter;
use Monolog\Formatter\FormatterInterface;
use Doctrine\CouchDB\CouchDBClient;
/**
* CouchDB handler for Doctrine CouchDB ODM
*
* @author Markus Bachmann <markus.bachmann@bachi.biz>
*/
class DoctrineCouchDBHandler extends AbstractProcessingHandler
{
/** @var CouchDBClient */
private $client;
public function __construct(CouchDBClient $client, $level = Logger::DEBUG, bool $bubble = true)
{
$this->client = $client;
parent::__construct($level, $bubble);
}
/**
* {@inheritDoc}
*/
protected function write(array $record): void
{
$this->client->postDocument($record['formatted']);
}
protected function getDefaultFormatter(): FormatterInterface
{
return new NormalizerFormatter;
}
}
+104
View File
@@ -0,0 +1,104 @@
<?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 Aws\Sdk;
use Aws\DynamoDb\DynamoDbClient;
use Monolog\Formatter\FormatterInterface;
use Aws\DynamoDb\Marshaler;
use Monolog\Formatter\ScalarFormatter;
use Monolog\Logger;
/**
* Amazon DynamoDB handler (http://aws.amazon.com/dynamodb/)
*
* @link https://github.com/aws/aws-sdk-php/
* @author Andrew Lawson <adlawson@gmail.com>
*/
class DynamoDbHandler extends AbstractProcessingHandler
{
public const DATE_FORMAT = 'Y-m-d\TH:i:s.uO';
/**
* @var DynamoDbClient
*/
protected $client;
/**
* @var string
*/
protected $table;
/**
* @var int
*/
protected $version;
/**
* @var Marshaler
*/
protected $marshaler;
public function __construct(DynamoDbClient $client, string $table, $level = Logger::DEBUG, bool $bubble = true)
{
/** @phpstan-ignore-next-line */
if (defined('Aws\Sdk::VERSION') && version_compare(Sdk::VERSION, '3.0', '>=')) {
$this->version = 3;
$this->marshaler = new Marshaler;
} else {
$this->version = 2;
}
$this->client = $client;
$this->table = $table;
parent::__construct($level, $bubble);
}
/**
* {@inheritDoc}
*/
protected function write(array $record): void
{
$filtered = $this->filterEmptyFields($record['formatted']);
if ($this->version === 3) {
$formatted = $this->marshaler->marshalItem($filtered);
} else {
/** @phpstan-ignore-next-line */
$formatted = $this->client->formatAttributes($filtered);
}
$this->client->putItem([
'TableName' => $this->table,
'Item' => $formatted,
]);
}
/**
* @param mixed[] $record
* @return mixed[]
*/
protected function filterEmptyFields(array $record): array
{
return array_filter($record, function ($value) {
return !empty($value) || false === $value || 0 === $value;
});
}
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter(): FormatterInterface
{
return new ScalarFormatter(self::DATE_FORMAT);
}
}
+129
View File
@@ -0,0 +1,129 @@
<?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 Elastica\Document;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\ElasticaFormatter;
use Monolog\Logger;
use Elastica\Client;
use Elastica\Exception\ExceptionInterface;
/**
* Elastic Search handler
*
* Usage example:
*
* $client = new \Elastica\Client();
* $options = array(
* 'index' => 'elastic_index_name',
* 'type' => 'elastic_doc_type', Types have been removed in Elastica 7
* );
* $handler = new ElasticaHandler($client, $options);
* $log = new Logger('application');
* $log->pushHandler($handler);
*
* @author Jelle Vink <jelle.vink@gmail.com>
*/
class ElasticaHandler extends AbstractProcessingHandler
{
/**
* @var Client
*/
protected $client;
/**
* @var mixed[] Handler config options
*/
protected $options = [];
/**
* @param Client $client Elastica Client object
* @param mixed[] $options Handler configuration
*/
public function __construct(Client $client, array $options = [], $level = Logger::DEBUG, bool $bubble = true)
{
parent::__construct($level, $bubble);
$this->client = $client;
$this->options = array_merge(
[
'index' => 'monolog', // Elastic index name
'type' => 'record', // Elastic document type
'ignore_error' => false, // Suppress Elastica exceptions
],
$options
);
}
/**
* {@inheritDoc}
*/
protected function write(array $record): void
{
$this->bulkSend([$record['formatted']]);
}
/**
* {@inheritDoc}
*/
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
if ($formatter instanceof ElasticaFormatter) {
return parent::setFormatter($formatter);
}
throw new \InvalidArgumentException('ElasticaHandler is only compatible with ElasticaFormatter');
}
/**
* @return mixed[]
*/
public function getOptions(): array
{
return $this->options;
}
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter(): FormatterInterface
{
return new ElasticaFormatter($this->options['index'], $this->options['type']);
}
/**
* {@inheritDoc}
*/
public function handleBatch(array $records): void
{
$documents = $this->getFormatter()->formatBatch($records);
$this->bulkSend($documents);
}
/**
* Use Elasticsearch bulk API to send list of documents
*
* @param Document[] $documents
*
* @throws \RuntimeException
*/
protected function bulkSend(array $documents): void
{
try {
$this->client->addDocuments($documents);
} catch (ExceptionInterface $e) {
if (!$this->options['ignore_error']) {
throw new \RuntimeException("Error sending messages to Elasticsearch", 0, $e);
}
}
}
}
@@ -0,0 +1,219 @@
<?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 Elastic\Elasticsearch\Response\Elasticsearch;
use Throwable;
use RuntimeException;
use Monolog\Logger;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\ElasticsearchFormatter;
use InvalidArgumentException;
use Elasticsearch\Common\Exceptions\RuntimeException as ElasticsearchRuntimeException;
use Elasticsearch\Client;
use Elastic\Elasticsearch\Exception\InvalidArgumentException as ElasticInvalidArgumentException;
use Elastic\Elasticsearch\Client as Client8;
/**
* Elasticsearch handler
*
* @link https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/index.html
*
* Simple usage example:
*
* $client = \Elasticsearch\ClientBuilder::create()
* ->setHosts($hosts)
* ->build();
*
* $options = array(
* 'index' => 'elastic_index_name',
* 'type' => 'elastic_doc_type',
* );
* $handler = new ElasticsearchHandler($client, $options);
* $log = new Logger('application');
* $log->pushHandler($handler);
*
* @author Avtandil Kikabidze <akalongman@gmail.com>
*/
class ElasticsearchHandler extends AbstractProcessingHandler
{
/**
* @var Client|Client8
*/
protected $client;
/**
* @var mixed[] Handler config options
*/
protected $options = [];
/**
* @var bool
*/
private $needsType;
/**
* @param Client|Client8 $client Elasticsearch Client object
* @param mixed[] $options Handler configuration
*/
public function __construct($client, array $options = [], $level = Logger::DEBUG, bool $bubble = true)
{
if (!$client instanceof Client && !$client instanceof Client8) {
throw new \TypeError('Elasticsearch\Client or Elastic\Elasticsearch\Client instance required');
}
parent::__construct($level, $bubble);
$this->client = $client;
$this->options = array_merge(
[
'index' => 'monolog', // Elastic index name
'type' => '_doc', // Elastic document type
'ignore_error' => false, // Suppress Elasticsearch exceptions
],
$options
);
if ($client instanceof Client8 || $client::VERSION[0] === '7') {
$this->needsType = false;
// force the type to _doc for ES8/ES7
$this->options['type'] = '_doc';
} else {
$this->needsType = true;
}
}
/**
* {@inheritDoc}
*/
protected function write(array $record): void
{
$this->bulkSend([$record['formatted']]);
}
/**
* {@inheritDoc}
*/
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
if ($formatter instanceof ElasticsearchFormatter) {
return parent::setFormatter($formatter);
}
throw new InvalidArgumentException('ElasticsearchHandler is only compatible with ElasticsearchFormatter');
}
/**
* Getter options
*
* @return mixed[]
*/
public function getOptions(): array
{
return $this->options;
}
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter(): FormatterInterface
{
return new ElasticsearchFormatter($this->options['index'], $this->options['type']);
}
/**
* {@inheritDoc}
*/
public function handleBatch(array $records): void
{
$documents = $this->getFormatter()->formatBatch($records);
$this->bulkSend($documents);
}
/**
* Use Elasticsearch bulk API to send list of documents
*
* @param array[] $records Records + _index/_type keys
* @throws \RuntimeException
*/
protected function bulkSend(array $records): void
{
try {
$params = [
'body' => [],
];
foreach ($records as $record) {
$params['body'][] = [
'index' => $this->needsType ? [
'_index' => $record['_index'],
'_type' => $record['_type'],
] : [
'_index' => $record['_index'],
],
];
unset($record['_index'], $record['_type']);
$params['body'][] = $record;
}
/** @var Elasticsearch */
$responses = $this->client->bulk($params);
if ($responses['errors'] === true) {
throw $this->createExceptionFromResponses($responses);
}
} catch (Throwable $e) {
if (! $this->options['ignore_error']) {
throw new RuntimeException('Error sending messages to Elasticsearch', 0, $e);
}
}
}
/**
* Creates elasticsearch exception from responses array
*
* Only the first error is converted into an exception.
*
* @param mixed[]|Elasticsearch $responses returned by $this->client->bulk()
*/
protected function createExceptionFromResponses($responses): Throwable
{
// @phpstan-ignore offsetAccess.nonOffsetAccessible
foreach ($responses['items'] ?? [] as $item) {
if (isset($item['index']['error'])) {
return $this->createExceptionFromError($item['index']['error']);
}
}
if (class_exists(ElasticInvalidArgumentException::class)) {
return new ElasticInvalidArgumentException('Elasticsearch failed to index one or more records.');
}
return new ElasticsearchRuntimeException('Elasticsearch failed to index one or more records.');
}
/**
* Creates elasticsearch exception from error array
*
* @param mixed[] $error
*/
protected function createExceptionFromError(array $error): Throwable
{
$previous = isset($error['caused_by']) ? $this->createExceptionFromError($error['caused_by']) : null;
if (class_exists(ElasticInvalidArgumentException::class)) {
return new ElasticInvalidArgumentException($error['type'] . ': ' . $error['reason'], 0, $previous);
}
return new ElasticsearchRuntimeException($error['type'] . ': ' . $error['reason'], 0, $previous);
}
}
+91
View File
@@ -0,0 +1,91 @@
<?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\Formatter\LineFormatter;
use Monolog\Formatter\FormatterInterface;
use Monolog\Logger;
use Monolog\Utils;
/**
* Stores to PHP error_log() handler.
*
* @author Elan Ruusamäe <glen@delfi.ee>
*/
class ErrorLogHandler extends AbstractProcessingHandler
{
public const OPERATING_SYSTEM = 0;
public const SAPI = 4;
/** @var int */
protected $messageType;
/** @var bool */
protected $expandNewlines;
/**
* @param int $messageType Says where the error should go.
* @param bool $expandNewlines If set to true, newlines in the message will be expanded to be take multiple log entries
*/
public function __construct(int $messageType = self::OPERATING_SYSTEM, $level = Logger::DEBUG, bool $bubble = true, bool $expandNewlines = false)
{
parent::__construct($level, $bubble);
if (false === in_array($messageType, self::getAvailableTypes(), true)) {
$message = sprintf('The given message type "%s" is not supported', print_r($messageType, true));
throw new \InvalidArgumentException($message);
}
$this->messageType = $messageType;
$this->expandNewlines = $expandNewlines;
}
/**
* @return int[] With all available types
*/
public static function getAvailableTypes(): array
{
return [
self::OPERATING_SYSTEM,
self::SAPI,
];
}
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter(): FormatterInterface
{
return new LineFormatter('[%datetime%] %channel%.%level_name%: %message% %context% %extra%');
}
/**
* {@inheritDoc}
*/
protected function write(array $record): void
{
if (!$this->expandNewlines) {
error_log((string) $record['formatted'], $this->messageType);
return;
}
$lines = preg_split('{[\r\n]+}', (string) $record['formatted']);
if ($lines === false) {
$pcreErrorCode = preg_last_error();
throw new \RuntimeException('Failed to preg_split formatted string: ' . $pcreErrorCode . ' / '. Utils::pcreLastErrorMessage($pcreErrorCode));
}
foreach ($lines as $line) {
error_log($line, $this->messageType);
}
}
}
@@ -0,0 +1,71 @@
<?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 Throwable;
/**
* Forwards records to at most one handler
*
* If a handler fails, the exception is suppressed and the record is forwarded to the next handler.
*
* As soon as one handler handles a record successfully, the handling stops there.
*
* @phpstan-import-type Record from \Monolog\Logger
*/
class FallbackGroupHandler extends GroupHandler
{
/**
* {@inheritDoc}
*/
public function handle(array $record): bool
{
if ($this->processors) {
/** @var Record $record */
$record = $this->processRecord($record);
}
foreach ($this->handlers as $handler) {
try {
$handler->handle($record);
break;
} catch (Throwable $e) {
// What throwable?
}
}
return false === $this->bubble;
}
/**
* {@inheritDoc}
*/
public function handleBatch(array $records): void
{
if ($this->processors) {
$processed = [];
foreach ($records as $record) {
$processed[] = $this->processRecord($record);
}
/** @var Record[] $records */
$records = $processed;
}
foreach ($this->handlers as $handler) {
try {
$handler->handleBatch($records);
break;
} catch (Throwable $e) {
// What throwable?
}
}
}
}
+212
View File
@@ -0,0 +1,212 @@
<?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;
use Monolog\ResettableInterface;
use Monolog\Formatter\FormatterInterface;
use Psr\Log\LogLevel;
/**
* Simple handler wrapper that filters records based on a list of levels
*
* It can be configured with an exact list of levels to allow, or a min/max level.
*
* @author Hennadiy Verkh
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @phpstan-import-type Record from \Monolog\Logger
* @phpstan-import-type Level from \Monolog\Logger
* @phpstan-import-type LevelName from \Monolog\Logger
*/
class FilterHandler extends Handler implements ProcessableHandlerInterface, ResettableInterface, FormattableHandlerInterface
{
use ProcessableHandlerTrait;
/**
* Handler or factory callable($record, $this)
*
* @var callable|HandlerInterface
* @phpstan-var callable(?Record, HandlerInterface): HandlerInterface|HandlerInterface
*/
protected $handler;
/**
* Minimum level for logs that are passed to handler
*
* @var int[]
* @phpstan-var array<Level, int>
*/
protected $acceptedLevels;
/**
* Whether the messages that are handled can bubble up the stack or not
*
* @var bool
*/
protected $bubble;
/**
* @psalm-param HandlerInterface|callable(?Record, HandlerInterface): HandlerInterface $handler
*
* @param callable|HandlerInterface $handler Handler or factory callable($record|null, $filterHandler).
* @param int|array $minLevelOrList A list of levels to accept or a minimum level if maxLevel is provided
* @param int|string $maxLevel Maximum level to accept, only used if $minLevelOrList is not an array
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not
*
* @phpstan-param Level|LevelName|LogLevel::*|array<Level|LevelName|LogLevel::*> $minLevelOrList
* @phpstan-param Level|LevelName|LogLevel::* $maxLevel
*/
public function __construct($handler, $minLevelOrList = Logger::DEBUG, $maxLevel = Logger::EMERGENCY, bool $bubble = true)
{
$this->handler = $handler;
$this->bubble = $bubble;
$this->setAcceptedLevels($minLevelOrList, $maxLevel);
if (!$this->handler instanceof HandlerInterface && !is_callable($this->handler)) {
throw new \RuntimeException("The given handler (".json_encode($this->handler).") is not a callable nor a Monolog\Handler\HandlerInterface object");
}
}
/**
* @phpstan-return array<int, Level>
*/
public function getAcceptedLevels(): array
{
return array_flip($this->acceptedLevels);
}
/**
* @param int|string|array $minLevelOrList A list of levels to accept or a minimum level or level name if maxLevel is provided
* @param int|string $maxLevel Maximum level or level name to accept, only used if $minLevelOrList is not an array
*
* @phpstan-param Level|LevelName|LogLevel::*|array<Level|LevelName|LogLevel::*> $minLevelOrList
* @phpstan-param Level|LevelName|LogLevel::* $maxLevel
*/
public function setAcceptedLevels($minLevelOrList = Logger::DEBUG, $maxLevel = Logger::EMERGENCY): self
{
if (is_array($minLevelOrList)) {
$acceptedLevels = array_map('Monolog\Logger::toMonologLevel', $minLevelOrList);
} else {
$minLevelOrList = Logger::toMonologLevel($minLevelOrList);
$maxLevel = Logger::toMonologLevel($maxLevel);
$acceptedLevels = array_values(array_filter(Logger::getLevels(), function ($level) use ($minLevelOrList, $maxLevel) {
return $level >= $minLevelOrList && $level <= $maxLevel;
}));
}
$this->acceptedLevels = array_flip($acceptedLevels);
return $this;
}
/**
* {@inheritDoc}
*/
public function isHandling(array $record): bool
{
return isset($this->acceptedLevels[$record['level']]);
}
/**
* {@inheritDoc}
*/
public function handle(array $record): bool
{
if (!$this->isHandling($record)) {
return false;
}
if ($this->processors) {
/** @var Record $record */
$record = $this->processRecord($record);
}
$this->getHandler($record)->handle($record);
return false === $this->bubble;
}
/**
* {@inheritDoc}
*/
public function handleBatch(array $records): void
{
$filtered = [];
foreach ($records as $record) {
if ($this->isHandling($record)) {
$filtered[] = $record;
}
}
if (count($filtered) > 0) {
$this->getHandler($filtered[count($filtered) - 1])->handleBatch($filtered);
}
}
/**
* Return the nested handler
*
* If the handler was provided as a factory callable, this will trigger the handler's instantiation.
*
* @return HandlerInterface
*
* @phpstan-param Record $record
*/
public function getHandler(?array $record = null)
{
if (!$this->handler instanceof HandlerInterface) {
$this->handler = ($this->handler)($record, $this);
if (!$this->handler instanceof HandlerInterface) {
throw new \RuntimeException("The factory callable should return a HandlerInterface");
}
}
return $this->handler;
}
/**
* {@inheritDoc}
*/
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
$handler = $this->getHandler();
if ($handler instanceof FormattableHandlerInterface) {
$handler->setFormatter($formatter);
return $this;
}
throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.');
}
/**
* {@inheritDoc}
*/
public function getFormatter(): FormatterInterface
{
$handler = $this->getHandler();
if ($handler instanceof FormattableHandlerInterface) {
return $handler->getFormatter();
}
throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.');
}
public function reset()
{
$this->resetProcessors();
if ($this->getHandler() instanceof ResettableInterface) {
$this->getHandler()->reset();
}
}
}
@@ -0,0 +1,29 @@
<?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\FingersCrossed;
/**
* Interface for activation strategies for the FingersCrossedHandler.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*
* @phpstan-import-type Record from \Monolog\Logger
*/
interface ActivationStrategyInterface
{
/**
* Returns whether the given record activates the handler.
*
* @phpstan-param Record $record
*/
public function isHandlerActivated(array $record): bool;
}
@@ -0,0 +1,77 @@
<?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\FingersCrossed;
use Monolog\Logger;
use Psr\Log\LogLevel;
/**
* Channel and Error level based monolog activation strategy. Allows to trigger activation
* based on level per channel. e.g. trigger activation on level 'ERROR' by default, except
* for records of the 'sql' channel; those should trigger activation on level 'WARN'.
*
* Example:
*
* <code>
* $activationStrategy = new ChannelLevelActivationStrategy(
* Logger::CRITICAL,
* array(
* 'request' => Logger::ALERT,
* 'sensitive' => Logger::ERROR,
* )
* );
* $handler = new FingersCrossedHandler(new StreamHandler('php://stderr'), $activationStrategy);
* </code>
*
* @author Mike Meessen <netmikey@gmail.com>
*
* @phpstan-import-type Record from \Monolog\Logger
* @phpstan-import-type Level from \Monolog\Logger
* @phpstan-import-type LevelName from \Monolog\Logger
*/
class ChannelLevelActivationStrategy implements ActivationStrategyInterface
{
/**
* @var Level
*/
private $defaultActionLevel;
/**
* @var array<string, Level>
*/
private $channelToActionLevel;
/**
* @param int|string $defaultActionLevel The default action level to be used if the record's category doesn't match any
* @param array<string, int> $channelToActionLevel An array that maps channel names to action levels.
*
* @phpstan-param array<string, Level> $channelToActionLevel
* @phpstan-param Level|LevelName|LogLevel::* $defaultActionLevel
*/
public function __construct($defaultActionLevel, array $channelToActionLevel = [])
{
$this->defaultActionLevel = Logger::toMonologLevel($defaultActionLevel);
$this->channelToActionLevel = array_map('Monolog\Logger::toMonologLevel', $channelToActionLevel);
}
/**
* @phpstan-param Record $record
*/
public function isHandlerActivated(array $record): bool
{
if (isset($this->channelToActionLevel[$record['channel']])) {
return $record['level'] >= $this->channelToActionLevel[$record['channel']];
}
return $record['level'] >= $this->defaultActionLevel;
}
}
@@ -0,0 +1,46 @@
<?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\FingersCrossed;
use Monolog\Logger;
use Psr\Log\LogLevel;
/**
* Error level based activation strategy.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*
* @phpstan-import-type Level from \Monolog\Logger
* @phpstan-import-type LevelName from \Monolog\Logger
*/
class ErrorLevelActivationStrategy implements ActivationStrategyInterface
{
/**
* @var Level
*/
private $actionLevel;
/**
* @param int|string $actionLevel Level or name or value
*
* @phpstan-param Level|LevelName|LogLevel::* $actionLevel
*/
public function __construct($actionLevel)
{
$this->actionLevel = Logger::toMonologLevel($actionLevel);
}
public function isHandlerActivated(array $record): bool
{
return $record['level'] >= $this->actionLevel;
}
}
@@ -0,0 +1,252 @@
<?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\Handler\FingersCrossed\ErrorLevelActivationStrategy;
use Monolog\Handler\FingersCrossed\ActivationStrategyInterface;
use Monolog\Logger;
use Monolog\ResettableInterface;
use Monolog\Formatter\FormatterInterface;
use Psr\Log\LogLevel;
/**
* Buffers all records until a certain level is reached
*
* The advantage of this approach is that you don't get any clutter in your log files.
* Only requests which actually trigger an error (or whatever your actionLevel is) will be
* in the logs, but they will contain all records, not only those above the level threshold.
*
* You can then have a passthruLevel as well which means that at the end of the request,
* even if it did not get activated, it will still send through log records of e.g. at least a
* warning level.
*
* You can find the various activation strategies in the
* Monolog\Handler\FingersCrossed\ namespace.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @phpstan-import-type Record from \Monolog\Logger
* @phpstan-import-type Level from \Monolog\Logger
* @phpstan-import-type LevelName from \Monolog\Logger
*/
class FingersCrossedHandler extends Handler implements ProcessableHandlerInterface, ResettableInterface, FormattableHandlerInterface
{
use ProcessableHandlerTrait;
/**
* @var callable|HandlerInterface
* @phpstan-var callable(?Record, HandlerInterface): HandlerInterface|HandlerInterface
*/
protected $handler;
/** @var ActivationStrategyInterface */
protected $activationStrategy;
/** @var bool */
protected $buffering = true;
/** @var int */
protected $bufferSize;
/** @var Record[] */
protected $buffer = [];
/** @var bool */
protected $stopBuffering;
/**
* @var ?int
* @phpstan-var ?Level
*/
protected $passthruLevel;
/** @var bool */
protected $bubble;
/**
* @psalm-param HandlerInterface|callable(?Record, HandlerInterface): HandlerInterface $handler
*
* @param callable|HandlerInterface $handler Handler or factory callable($record|null, $fingersCrossedHandler).
* @param int|string|ActivationStrategyInterface $activationStrategy Strategy which determines when this handler takes action, or a level name/value at which the handler is activated
* @param int $bufferSize How many entries should be buffered at most, beyond that the oldest items are removed from the buffer.
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not
* @param bool $stopBuffering Whether the handler should stop buffering after being triggered (default true)
* @param int|string $passthruLevel Minimum level to always flush to handler on close, even if strategy not triggered
*
* @phpstan-param Level|LevelName|LogLevel::* $passthruLevel
* @phpstan-param Level|LevelName|LogLevel::*|ActivationStrategyInterface $activationStrategy
*/
public function __construct($handler, $activationStrategy = null, int $bufferSize = 0, bool $bubble = true, bool $stopBuffering = true, $passthruLevel = null)
{
if (null === $activationStrategy) {
$activationStrategy = new ErrorLevelActivationStrategy(Logger::WARNING);
}
// convert simple int activationStrategy to an object
if (!$activationStrategy instanceof ActivationStrategyInterface) {
$activationStrategy = new ErrorLevelActivationStrategy($activationStrategy);
}
$this->handler = $handler;
$this->activationStrategy = $activationStrategy;
$this->bufferSize = $bufferSize;
$this->bubble = $bubble;
$this->stopBuffering = $stopBuffering;
if ($passthruLevel !== null) {
$this->passthruLevel = Logger::toMonologLevel($passthruLevel);
}
if (!$this->handler instanceof HandlerInterface && !is_callable($this->handler)) {
throw new \RuntimeException("The given handler (".json_encode($this->handler).") is not a callable nor a Monolog\Handler\HandlerInterface object");
}
}
/**
* {@inheritDoc}
*/
public function isHandling(array $record): bool
{
return true;
}
/**
* Manually activate this logger regardless of the activation strategy
*/
public function activate(): void
{
if ($this->stopBuffering) {
$this->buffering = false;
}
$this->getHandler(end($this->buffer) ?: null)->handleBatch($this->buffer);
$this->buffer = [];
}
/**
* {@inheritDoc}
*/
public function handle(array $record): bool
{
if ($this->processors) {
/** @var Record $record */
$record = $this->processRecord($record);
}
if ($this->buffering) {
$this->buffer[] = $record;
if ($this->bufferSize > 0 && count($this->buffer) > $this->bufferSize) {
array_shift($this->buffer);
}
if ($this->activationStrategy->isHandlerActivated($record)) {
$this->activate();
}
} else {
$this->getHandler($record)->handle($record);
}
return false === $this->bubble;
}
/**
* {@inheritDoc}
*/
public function close(): void
{
$this->flushBuffer();
$this->getHandler()->close();
}
public function reset()
{
$this->flushBuffer();
$this->resetProcessors();
if ($this->getHandler() instanceof ResettableInterface) {
$this->getHandler()->reset();
}
}
/**
* Clears the buffer without flushing any messages down to the wrapped handler.
*
* It also resets the handler to its initial buffering state.
*/
public function clear(): void
{
$this->buffer = [];
$this->reset();
}
/**
* Resets the state of the handler. Stops forwarding records to the wrapped handler.
*/
private function flushBuffer(): void
{
if (null !== $this->passthruLevel) {
$level = $this->passthruLevel;
$this->buffer = array_filter($this->buffer, function ($record) use ($level) {
return $record['level'] >= $level;
});
if (count($this->buffer) > 0) {
$this->getHandler(end($this->buffer))->handleBatch($this->buffer);
}
}
$this->buffer = [];
$this->buffering = true;
}
/**
* Return the nested handler
*
* If the handler was provided as a factory callable, this will trigger the handler's instantiation.
*
* @return HandlerInterface
*
* @phpstan-param Record $record
*/
public function getHandler(?array $record = null)
{
if (!$this->handler instanceof HandlerInterface) {
$this->handler = ($this->handler)($record, $this);
if (!$this->handler instanceof HandlerInterface) {
throw new \RuntimeException("The factory callable should return a HandlerInterface");
}
}
return $this->handler;
}
/**
* {@inheritDoc}
*/
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
$handler = $this->getHandler();
if ($handler instanceof FormattableHandlerInterface) {
$handler->setFormatter($formatter);
return $this;
}
throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.');
}
/**
* {@inheritDoc}
*/
public function getFormatter(): FormatterInterface
{
$handler = $this->getHandler();
if ($handler instanceof FormattableHandlerInterface) {
return $handler->getFormatter();
}
throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.');
}
}
+180
View File
@@ -0,0 +1,180 @@
<?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\Formatter\WildfireFormatter;
use Monolog\Formatter\FormatterInterface;
/**
* Simple FirePHP Handler (http://www.firephp.org/), which uses the Wildfire protocol.
*
* @author Eric Clemmons (@ericclemmons) <eric@uxdriven.com>
*
* @phpstan-import-type FormattedRecord from AbstractProcessingHandler
*/
class FirePHPHandler extends AbstractProcessingHandler
{
use WebRequestRecognizerTrait;
/**
* WildFire JSON header message format
*/
protected const PROTOCOL_URI = 'http://meta.wildfirehq.org/Protocol/JsonStream/0.2';
/**
* FirePHP structure for parsing messages & their presentation
*/
protected const STRUCTURE_URI = 'http://meta.firephp.org/Wildfire/Structure/FirePHP/FirebugConsole/0.1';
/**
* Must reference a "known" plugin, otherwise headers won't display in FirePHP
*/
protected const PLUGIN_URI = 'http://meta.firephp.org/Wildfire/Plugin/FirePHP/Library-FirePHPCore/0.3';
/**
* Header prefix for Wildfire to recognize & parse headers
*/
protected const HEADER_PREFIX = 'X-Wf';
/**
* Whether or not Wildfire vendor-specific headers have been generated & sent yet
* @var bool
*/
protected static $initialized = false;
/**
* Shared static message index between potentially multiple handlers
* @var int
*/
protected static $messageIndex = 1;
/** @var bool */
protected static $sendHeaders = true;
/**
* Base header creation function used by init headers & record headers
*
* @param array<int|string> $meta Wildfire Plugin, Protocol & Structure Indexes
* @param string $message Log message
*
* @return array<string, string> Complete header string ready for the client as key and message as value
*
* @phpstan-return non-empty-array<string, string>
*/
protected function createHeader(array $meta, string $message): array
{
$header = sprintf('%s-%s', static::HEADER_PREFIX, join('-', $meta));
return [$header => $message];
}
/**
* Creates message header from record
*
* @return array<string, string>
*
* @phpstan-return non-empty-array<string, string>
*
* @see createHeader()
*
* @phpstan-param FormattedRecord $record
*/
protected function createRecordHeader(array $record): array
{
// Wildfire is extensible to support multiple protocols & plugins in a single request,
// but we're not taking advantage of that (yet), so we're using "1" for simplicity's sake.
return $this->createHeader(
[1, 1, 1, self::$messageIndex++],
$record['formatted']
);
}
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter(): FormatterInterface
{
return new WildfireFormatter();
}
/**
* Wildfire initialization headers to enable message parsing
*
* @see createHeader()
* @see sendHeader()
*
* @return array<string, string>
*/
protected function getInitHeaders(): array
{
// Initial payload consists of required headers for Wildfire
return array_merge(
$this->createHeader(['Protocol', 1], static::PROTOCOL_URI),
$this->createHeader([1, 'Structure', 1], static::STRUCTURE_URI),
$this->createHeader([1, 'Plugin', 1], static::PLUGIN_URI)
);
}
/**
* Send header string to the client
*/
protected function sendHeader(string $header, string $content): void
{
if (!headers_sent() && self::$sendHeaders) {
header(sprintf('%s: %s', $header, $content));
}
}
/**
* Creates & sends header for a record, ensuring init headers have been sent prior
*
* @see sendHeader()
* @see sendInitHeaders()
*/
protected function write(array $record): void
{
if (!self::$sendHeaders || !$this->isWebRequest()) {
return;
}
// WildFire-specific headers must be sent prior to any messages
if (!self::$initialized) {
self::$initialized = true;
self::$sendHeaders = $this->headersAccepted();
if (!self::$sendHeaders) {
return;
}
foreach ($this->getInitHeaders() as $header => $content) {
$this->sendHeader($header, $content);
}
}
$header = $this->createRecordHeader($record);
if (trim(current($header)) !== '') {
$this->sendHeader(key($header), current($header));
}
}
/**
* Verifies if the headers are accepted by the current user agent
*/
protected function headersAccepted(): bool
{
if (!empty($_SERVER['HTTP_USER_AGENT']) && preg_match('{\bFirePHP/\d+\.\d+\b}', $_SERVER['HTTP_USER_AGENT'])) {
return true;
}
return isset($_SERVER['HTTP_X_FIREPHP_VERSION']);
}
}
+135
View File
@@ -0,0 +1,135 @@
<?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\Formatter\FormatterInterface;
use Monolog\Formatter\LineFormatter;
use Monolog\Logger;
/**
* Sends logs to Fleep.io using Webhook integrations
*
* You'll need a Fleep.io account to use this handler.
*
* @see https://fleep.io/integrations/webhooks/ Fleep Webhooks Documentation
* @author Ando Roots <ando@sqroot.eu>
*
* @phpstan-import-type FormattedRecord from AbstractProcessingHandler
*/
class FleepHookHandler extends SocketHandler
{
protected const FLEEP_HOST = 'fleep.io';
protected const FLEEP_HOOK_URI = '/hook/';
/**
* @var string Webhook token (specifies the conversation where logs are sent)
*/
protected $token;
/**
* Construct a new Fleep.io Handler.
*
* For instructions on how to create a new web hook in your conversations
* see https://fleep.io/integrations/webhooks/
*
* @param string $token Webhook token
* @throws MissingExtensionException
*/
public function __construct(
string $token,
$level = Logger::DEBUG,
bool $bubble = true,
bool $persistent = false,
float $timeout = 0.0,
float $writingTimeout = 10.0,
?float $connectionTimeout = null,
?int $chunkSize = null
) {
if (!extension_loaded('openssl')) {
throw new MissingExtensionException('The OpenSSL PHP extension is required to use the FleepHookHandler');
}
$this->token = $token;
$connectionString = 'ssl://' . static::FLEEP_HOST . ':443';
parent::__construct(
$connectionString,
$level,
$bubble,
$persistent,
$timeout,
$writingTimeout,
$connectionTimeout,
$chunkSize
);
}
/**
* Returns the default formatter to use with this handler
*
* Overloaded to remove empty context and extra arrays from the end of the log message.
*
* @return LineFormatter
*/
protected function getDefaultFormatter(): FormatterInterface
{
return new LineFormatter(null, null, true, true);
}
/**
* Handles a log record
*/
public function write(array $record): void
{
parent::write($record);
$this->closeSocket();
}
/**
* {@inheritDoc}
*/
protected function generateDataStream(array $record): string
{
$content = $this->buildContent($record);
return $this->buildHeader($content) . $content;
}
/**
* Builds the header of the API Call
*/
private function buildHeader(string $content): string
{
$header = "POST " . static::FLEEP_HOOK_URI . $this->token . " HTTP/1.1\r\n";
$header .= "Host: " . static::FLEEP_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;
}
/**
* Builds the body of API call
*
* @phpstan-param FormattedRecord $record
*/
private function buildContent(array $record): string
{
$dataArray = [
'message' => $record['formatted'],
];
return http_build_query($dataArray);
}
}
+133
View File
@@ -0,0 +1,133 @@
<?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;
use Monolog\Utils;
use Monolog\Formatter\FlowdockFormatter;
use Monolog\Formatter\FormatterInterface;
/**
* Sends notifications through the Flowdock push API
*
* This must be configured with a FlowdockFormatter instance via setFormatter()
*
* Notes:
* API token - Flowdock API token
*
* @author Dominik Liebler <liebler.dominik@gmail.com>
* @see https://www.flowdock.com/api/push
*
* @phpstan-import-type FormattedRecord from AbstractProcessingHandler
* @deprecated Since 2.9.0 and 3.3.0, Flowdock was shutdown we will thus drop this handler in Monolog 4
*/
class FlowdockHandler extends SocketHandler
{
/**
* @var string
*/
protected $apiToken;
/**
* @throws MissingExtensionException if OpenSSL is missing
*/
public function __construct(
string $apiToken,
$level = Logger::DEBUG,
bool $bubble = true,
bool $persistent = false,
float $timeout = 0.0,
float $writingTimeout = 10.0,
?float $connectionTimeout = null,
?int $chunkSize = null
) {
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,
$persistent,
$timeout,
$writingTimeout,
$connectionTimeout,
$chunkSize
);
$this->apiToken = $apiToken;
}
/**
* {@inheritDoc}
*/
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
if (!$formatter instanceof FlowdockFormatter) {
throw new \InvalidArgumentException('The FlowdockHandler requires an instance of Monolog\Formatter\FlowdockFormatter to function correctly');
}
return parent::setFormatter($formatter);
}
/**
* Gets the default formatter.
*/
protected function getDefaultFormatter(): FormatterInterface
{
throw new \InvalidArgumentException('The FlowdockHandler must be configured (via setFormatter) with an instance of Monolog\Formatter\FlowdockFormatter to function correctly');
}
/**
* {@inheritDoc}
*/
protected function write(array $record): void
{
parent::write($record);
$this->closeSocket();
}
/**
* {@inheritDoc}
*/
protected function generateDataStream(array $record): string
{
$content = $this->buildContent($record);
return $this->buildHeader($content) . $content;
}
/**
* Builds the body of API call
*
* @phpstan-param FormattedRecord $record
*/
private function buildContent(array $record): string
{
return Utils::jsonEncode($record['formatted']['flowdock']);
}
/**
* Builds the header of the API Call
*/
private function buildHeader(string $content): string
{
$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";
return $header;
}
}
@@ -0,0 +1,37 @@
<?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\Formatter\FormatterInterface;
/**
* Interface to describe loggers that have a formatter
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
interface FormattableHandlerInterface
{
/**
* Sets the formatter.
*
* @param FormatterInterface $formatter
* @return HandlerInterface self
*/
public function setFormatter(FormatterInterface $formatter): HandlerInterface;
/**
* Gets the formatter.
*
* @return FormatterInterface
*/
public function getFormatter(): FormatterInterface;
}
@@ -0,0 +1,60 @@
<?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\Formatter\FormatterInterface;
use Monolog\Formatter\LineFormatter;
/**
* Helper trait for implementing FormattableInterface
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
trait FormattableHandlerTrait
{
/**
* @var ?FormatterInterface
*/
protected $formatter;
/**
* {@inheritDoc}
*/
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
$this->formatter = $formatter;
return $this;
}
/**
* {@inheritDoc}
*/
public function getFormatter(): FormatterInterface
{
if (!$this->formatter) {
$this->formatter = $this->getDefaultFormatter();
}
return $this->formatter;
}
/**
* Gets the default formatter.
*
* Overwrite this if the LineFormatter is not a good default for your handler.
*/
protected function getDefaultFormatter(): FormatterInterface
{
return new LineFormatter();
}
}
+57
View File
@@ -0,0 +1,57 @@
<?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 Gelf\PublisherInterface;
use Monolog\Logger;
use Monolog\Formatter\GelfMessageFormatter;
use Monolog\Formatter\FormatterInterface;
/**
* Handler to send messages to a Graylog2 (http://www.graylog2.org) server
*
* @author Matt Lehner <mlehner@gmail.com>
* @author Benjamin Zikarsky <benjamin@zikarsky.de>
*/
class GelfHandler extends AbstractProcessingHandler
{
/**
* @var PublisherInterface the publisher object that sends the message to the server
*/
protected $publisher;
/**
* @param PublisherInterface $publisher a gelf publisher object
*/
public function __construct(PublisherInterface $publisher, $level = Logger::DEBUG, bool $bubble = true)
{
parent::__construct($level, $bubble);
$this->publisher = $publisher;
}
/**
* {@inheritDoc}
*/
protected function write(array $record): void
{
$this->publisher->publish($record['formatted']);
}
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter(): FormatterInterface
{
return new GelfMessageFormatter();
}
}
+132
View File
@@ -0,0 +1,132 @@
<?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\Formatter\FormatterInterface;
use Monolog\ResettableInterface;
/**
* Forwards records to multiple handlers
*
* @author Lenar Lõhmus <lenar@city.ee>
*
* @phpstan-import-type Record from \Monolog\Logger
*/
class GroupHandler extends Handler implements ProcessableHandlerInterface, ResettableInterface
{
use ProcessableHandlerTrait;
/** @var HandlerInterface[] */
protected $handlers;
/** @var bool */
protected $bubble;
/**
* @param HandlerInterface[] $handlers Array of Handlers.
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not
*/
public function __construct(array $handlers, bool $bubble = true)
{
foreach ($handlers as $handler) {
if (!$handler instanceof HandlerInterface) {
throw new \InvalidArgumentException('The first argument of the GroupHandler must be an array of HandlerInterface instances.');
}
}
$this->handlers = $handlers;
$this->bubble = $bubble;
}
/**
* {@inheritDoc}
*/
public function isHandling(array $record): bool
{
foreach ($this->handlers as $handler) {
if ($handler->isHandling($record)) {
return true;
}
}
return false;
}
/**
* {@inheritDoc}
*/
public function handle(array $record): bool
{
if ($this->processors) {
/** @var Record $record */
$record = $this->processRecord($record);
}
foreach ($this->handlers as $handler) {
$handler->handle($record);
}
return false === $this->bubble;
}
/**
* {@inheritDoc}
*/
public function handleBatch(array $records): void
{
if ($this->processors) {
$processed = [];
foreach ($records as $record) {
$processed[] = $this->processRecord($record);
}
/** @var Record[] $records */
$records = $processed;
}
foreach ($this->handlers as $handler) {
$handler->handleBatch($records);
}
}
public function reset()
{
$this->resetProcessors();
foreach ($this->handlers as $handler) {
if ($handler instanceof ResettableInterface) {
$handler->reset();
}
}
}
public function close(): void
{
parent::close();
foreach ($this->handlers as $handler) {
$handler->close();
}
}
/**
* {@inheritDoc}
*/
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
foreach ($this->handlers as $handler) {
if ($handler instanceof FormattableHandlerInterface) {
$handler->setFormatter($formatter);
}
}
return $this;
}
}
+62
View File
@@ -0,0 +1,62 @@
<?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;
/**
* Base Handler class providing basic close() support as well as handleBatch
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
abstract class Handler implements HandlerInterface
{
/**
* {@inheritDoc}
*/
public function handleBatch(array $records): void
{
foreach ($records as $record) {
$this->handle($record);
}
}
/**
* {@inheritDoc}
*/
public function close(): void
{
}
public function __destruct()
{
try {
$this->close();
} catch (\Throwable $e) {
// do nothing
}
}
public function __sleep()
{
$this->close();
$reflClass = new \ReflectionClass($this);
$keys = [];
foreach ($reflClass->getProperties() as $reflProp) {
if (!$reflProp->isStatic()) {
$keys[] = $reflProp->getName();
}
}
return $keys;
}
}
+85
View File
@@ -0,0 +1,85 @@
<?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;
/**
* Interface that all Monolog Handlers must implement
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @phpstan-import-type Record from \Monolog\Logger
* @phpstan-import-type Level from \Monolog\Logger
*/
interface HandlerInterface
{
/**
* Checks whether the given record will be handled by this handler.
*
* This is mostly done for performance reasons, to avoid calling processors for nothing.
*
* Handlers should still check the record levels within handle(), returning false in isHandling()
* is no guarantee that handle() will not be called, and isHandling() might not be called
* for a given record.
*
* @param array $record Partial log record containing only a level key
*
* @return bool
*
* @phpstan-param array{level: Level} $record
*/
public function isHandling(array $record): bool;
/**
* Handles a record.
*
* All records may be passed to this method, and the handler should discard
* those that it does not want to handle.
*
* The return value of this function controls the bubbling process of the handler stack.
* Unless the bubbling is interrupted (by returning true), the Logger class will keep on
* calling further handlers in the stack with a given log record.
*
* @param array $record The record to handle
* @return bool true means that this handler handled the record, and that bubbling is not permitted.
* false means the record was either not processed or that this handler allows bubbling.
*
* @phpstan-param Record $record
*/
public function handle(array $record): bool;
/**
* Handles a set of records at once.
*
* @param array $records The records to handle (an array of record arrays)
*
* @phpstan-param Record[] $records
*/
public function handleBatch(array $records): void;
/**
* Closes the handler.
*
* Ends a log cycle and frees all resources used by the handler.
*
* Closing a Handler means flushing all buffers and freeing any open resources/handles.
*
* Implementations have to be idempotent (i.e. it should be possible to call close several times without breakage)
* and ideally handlers should be able to reopen themselves on handle() after they have been closed.
*
* This is useful at the end of a request and will be called automatically when the object
* is destroyed if you extend Monolog\Handler\Handler.
*
* If you are thinking of calling this method yourself, most likely you should be
* calling ResettableInterface::reset instead. Have a look.
*/
public function close(): void;
}
+136
View File
@@ -0,0 +1,136 @@
<?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\ResettableInterface;
use Monolog\Formatter\FormatterInterface;
/**
* This simple wrapper class can be used to extend handlers functionality.
*
* Example: A custom filtering that can be applied to any handler.
*
* Inherit from this class and override handle() like this:
*
* public function handle(array $record)
* {
* if ($record meets certain conditions) {
* return false;
* }
* return $this->handler->handle($record);
* }
*
* @author Alexey Karapetov <alexey@karapetov.com>
*/
class HandlerWrapper implements HandlerInterface, ProcessableHandlerInterface, FormattableHandlerInterface, ResettableInterface
{
/**
* @var HandlerInterface
*/
protected $handler;
public function __construct(HandlerInterface $handler)
{
$this->handler = $handler;
}
/**
* {@inheritDoc}
*/
public function isHandling(array $record): bool
{
return $this->handler->isHandling($record);
}
/**
* {@inheritDoc}
*/
public function handle(array $record): bool
{
return $this->handler->handle($record);
}
/**
* {@inheritDoc}
*/
public function handleBatch(array $records): void
{
$this->handler->handleBatch($records);
}
/**
* {@inheritDoc}
*/
public function close(): void
{
$this->handler->close();
}
/**
* {@inheritDoc}
*/
public function pushProcessor(callable $callback): HandlerInterface
{
if ($this->handler instanceof ProcessableHandlerInterface) {
$this->handler->pushProcessor($callback);
return $this;
}
throw new \LogicException('The wrapped handler does not implement ' . ProcessableHandlerInterface::class);
}
/**
* {@inheritDoc}
*/
public function popProcessor(): callable
{
if ($this->handler instanceof ProcessableHandlerInterface) {
return $this->handler->popProcessor();
}
throw new \LogicException('The wrapped handler does not implement ' . ProcessableHandlerInterface::class);
}
/**
* {@inheritDoc}
*/
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
if ($this->handler instanceof FormattableHandlerInterface) {
$this->handler->setFormatter($formatter);
return $this;
}
throw new \LogicException('The wrapped handler does not implement ' . FormattableHandlerInterface::class);
}
/**
* {@inheritDoc}
*/
public function getFormatter(): FormatterInterface
{
if ($this->handler instanceof FormattableHandlerInterface) {
return $this->handler->getFormatter();
}
throw new \LogicException('The wrapped handler does not implement ' . FormattableHandlerInterface::class);
}
public function reset()
{
if ($this->handler instanceof ResettableInterface) {
$this->handler->reset();
}
}
}
+74
View File
@@ -0,0 +1,74 @@
<?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;
use Monolog\Utils;
/**
* IFTTTHandler uses cURL to trigger IFTTT Maker actions
*
* Register a secret key and trigger/event name at https://ifttt.com/maker
*
* value1 will be the channel from monolog's Logger constructor,
* value2 will be the level name (ERROR, WARNING, ..)
* value3 will be the log record's message
*
* @author Nehal Patel <nehal@nehalpatel.me>
*/
class IFTTTHandler extends AbstractProcessingHandler
{
/** @var string */
private $eventName;
/** @var string */
private $secretKey;
/**
* @param string $eventName The name of the IFTTT Maker event that should be triggered
* @param string $secretKey A valid IFTTT secret key
*/
public function __construct(string $eventName, string $secretKey, $level = Logger::ERROR, bool $bubble = true)
{
if (!extension_loaded('curl')) {
throw new MissingExtensionException('The curl extension is needed to use the IFTTTHandler');
}
$this->eventName = $eventName;
$this->secretKey = $secretKey;
parent::__construct($level, $bubble);
}
/**
* {@inheritDoc}
*/
public function write(array $record): void
{
$postData = [
"value1" => $record["channel"],
"value2" => $record["level_name"],
"value3" => $record["message"],
];
$postString = Utils::jsonEncode($postData);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://maker.ifttt.com/trigger/" . $this->eventName . "/with/key/" . $this->secretKey);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postString);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
]);
Curl\Util::execute($ch);
}
}
+76
View File
@@ -0,0 +1,76 @@
<?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;
/**
* Inspired on LogEntriesHandler.
*
* @author Robert Kaufmann III <rok3@rok3.me>
* @author Gabriel Machado <gabriel.ms1@hotmail.com>
*/
class InsightOpsHandler extends SocketHandler
{
/**
* @var string
*/
protected $logToken;
/**
* @param string $token Log token supplied by InsightOps
* @param string $region Region where InsightOps account is hosted. Could be 'us' or 'eu'.
* @param bool $useSSL Whether or not SSL encryption should be used
*
* @throws MissingExtensionException If SSL encryption is set to true and OpenSSL is missing
*/
public function __construct(
string $token,
string $region = 'us',
bool $useSSL = true,
$level = Logger::DEBUG,
bool $bubble = true,
bool $persistent = false,
float $timeout = 0.0,
float $writingTimeout = 10.0,
?float $connectionTimeout = null,
?int $chunkSize = null
) {
if ($useSSL && !extension_loaded('openssl')) {
throw new MissingExtensionException('The OpenSSL PHP plugin is required to use SSL encrypted connection for InsightOpsHandler');
}
$endpoint = $useSSL
? 'ssl://' . $region . '.data.logs.insight.rapid7.com:443'
: $region . '.data.logs.insight.rapid7.com:80';
parent::__construct(
$endpoint,
$level,
$bubble,
$persistent,
$timeout,
$writingTimeout,
$connectionTimeout,
$chunkSize
);
$this->logToken = $token;
}
/**
* {@inheritDoc}
*/
protected function generateDataStream(array $record): string
{
return $this->logToken . ' ' . $record['formatted'];
}
}
+70
View File
@@ -0,0 +1,70 @@
<?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;
/**
* @author Robert Kaufmann III <rok3@rok3.me>
*/
class LogEntriesHandler extends SocketHandler
{
/**
* @var string
*/
protected $logToken;
/**
* @param string $token Log token supplied by LogEntries
* @param bool $useSSL Whether or not SSL encryption should be used.
* @param string $host Custom hostname to send the data to if needed
*
* @throws MissingExtensionException If SSL encryption is set to true and OpenSSL is missing
*/
public function __construct(
string $token,
bool $useSSL = true,
$level = Logger::DEBUG,
bool $bubble = true,
string $host = 'data.logentries.com',
bool $persistent = false,
float $timeout = 0.0,
float $writingTimeout = 10.0,
?float $connectionTimeout = null,
?int $chunkSize = null
) {
if ($useSSL && !extension_loaded('openssl')) {
throw new MissingExtensionException('The OpenSSL PHP plugin is required to use SSL encrypted connection for LogEntriesHandler');
}
$endpoint = $useSSL ? 'ssl://' . $host . ':443' : $host . ':80';
parent::__construct(
$endpoint,
$level,
$bubble,
$persistent,
$timeout,
$writingTimeout,
$connectionTimeout,
$chunkSize
);
$this->logToken = $token;
}
/**
* {@inheritDoc}
*/
protected function generateDataStream(array $record): string
{
return $this->logToken . ' ' . $record['formatted'];
}
}
+160
View File
@@ -0,0 +1,160 @@
<?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;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\LogglyFormatter;
use function array_key_exists;
use CurlHandle;
/**
* Sends errors to Loggly.
*
* @author Przemek Sobstel <przemek@sobstel.org>
* @author Adam Pancutt <adam@pancutt.com>
* @author Gregory Barchard <gregory@barchard.net>
*/
class LogglyHandler extends AbstractProcessingHandler
{
protected const HOST = 'logs-01.loggly.com';
protected const ENDPOINT_SINGLE = 'inputs';
protected const ENDPOINT_BATCH = 'bulk';
/**
* Caches the curl handlers for every given endpoint.
*
* @var resource[]|CurlHandle[]
*/
protected $curlHandlers = [];
/** @var string */
protected $token;
/** @var string[] */
protected $tag = [];
/**
* @param string $token API token supplied by Loggly
*
* @throws MissingExtensionException If the curl extension is missing
*/
public function __construct(string $token, $level = Logger::DEBUG, bool $bubble = true)
{
if (!extension_loaded('curl')) {
throw new MissingExtensionException('The curl extension is needed to use the LogglyHandler');
}
$this->token = $token;
parent::__construct($level, $bubble);
}
/**
* Loads and returns the shared curl handler for the given endpoint.
*
* @param string $endpoint
*
* @return resource|CurlHandle
*/
protected function getCurlHandler(string $endpoint)
{
if (!array_key_exists($endpoint, $this->curlHandlers)) {
$this->curlHandlers[$endpoint] = $this->loadCurlHandle($endpoint);
}
return $this->curlHandlers[$endpoint];
}
/**
* Starts a fresh curl session for the given endpoint and returns its handler.
*
* @param string $endpoint
*
* @return resource|CurlHandle
*/
private function loadCurlHandle(string $endpoint)
{
$url = sprintf("https://%s/%s/%s/", static::HOST, $endpoint, $this->token);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
return $ch;
}
/**
* @param string[]|string $tag
*/
public function setTag($tag): self
{
$tag = !empty($tag) ? $tag : [];
$this->tag = is_array($tag) ? $tag : [$tag];
return $this;
}
/**
* @param string[]|string $tag
*/
public function addTag($tag): self
{
if (!empty($tag)) {
$tag = is_array($tag) ? $tag : [$tag];
$this->tag = array_unique(array_merge($this->tag, $tag));
}
return $this;
}
protected function write(array $record): void
{
$this->send($record["formatted"], static::ENDPOINT_SINGLE);
}
public function handleBatch(array $records): void
{
$level = $this->level;
$records = array_filter($records, function ($record) use ($level) {
return ($record['level'] >= $level);
});
if ($records) {
$this->send($this->getFormatter()->formatBatch($records), static::ENDPOINT_BATCH);
}
}
protected function send(string $data, string $endpoint): void
{
$ch = $this->getCurlHandler($endpoint);
$headers = ['Content-Type: application/json'];
if (!empty($this->tag)) {
$headers[] = 'X-LOGGLY-TAG: '.implode(',', $this->tag);
}
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
Curl\Util::execute($ch, 5, false);
}
protected function getDefaultFormatter(): FormatterInterface
{
return new LogglyFormatter();
}
}
+106
View File
@@ -0,0 +1,106 @@
<?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;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\LogmaticFormatter;
/**
* @author Julien Breux <julien.breux@gmail.com>
*/
class LogmaticHandler extends SocketHandler
{
/**
* @var string
*/
private $logToken;
/**
* @var string
*/
private $hostname;
/**
* @var string
*/
private $appname;
/**
* @param string $token Log token supplied by Logmatic.
* @param string $hostname Host name supplied by Logmatic.
* @param string $appname Application name supplied by Logmatic.
* @param bool $useSSL Whether or not SSL encryption should be used.
*
* @throws MissingExtensionException If SSL encryption is set to true and OpenSSL is missing
*/
public function __construct(
string $token,
string $hostname = '',
string $appname = '',
bool $useSSL = true,
$level = Logger::DEBUG,
bool $bubble = true,
bool $persistent = false,
float $timeout = 0.0,
float $writingTimeout = 10.0,
?float $connectionTimeout = null,
?int $chunkSize = null
) {
if ($useSSL && !extension_loaded('openssl')) {
throw new MissingExtensionException('The OpenSSL PHP extension is required to use SSL encrypted connection for LogmaticHandler');
}
$endpoint = $useSSL ? 'ssl://api.logmatic.io:10515' : 'api.logmatic.io:10514';
$endpoint .= '/v1/';
parent::__construct(
$endpoint,
$level,
$bubble,
$persistent,
$timeout,
$writingTimeout,
$connectionTimeout,
$chunkSize
);
$this->logToken = $token;
$this->hostname = $hostname;
$this->appname = $appname;
}
/**
* {@inheritDoc}
*/
protected function generateDataStream(array $record): string
{
return $this->logToken . ' ' . $record['formatted'];
}
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter(): FormatterInterface
{
$formatter = new LogmaticFormatter();
if (!empty($this->hostname)) {
$formatter->setHostname($this->hostname);
}
if (!empty($this->appname)) {
$formatter->setAppname($this->appname);
}
return $formatter;
}
}
+95
View File
@@ -0,0 +1,95 @@
<?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\Formatter\FormatterInterface;
use Monolog\Formatter\HtmlFormatter;
/**
* Base class for all mail handlers
*
* @author Gyula Sallai
*
* @phpstan-import-type Record from \Monolog\Logger
*/
abstract class MailHandler extends AbstractProcessingHandler
{
/**
* {@inheritDoc}
*/
public function handleBatch(array $records): void
{
$messages = [];
foreach ($records as $record) {
if ($record['level'] < $this->level) {
continue;
}
/** @var Record $message */
$message = $this->processRecord($record);
$messages[] = $message;
}
if (!empty($messages)) {
$this->send((string) $this->getFormatter()->formatBatch($messages), $messages);
}
}
/**
* Send a mail with the given content
*
* @param string $content formatted email body to be sent
* @param array $records the array of log records that formed this content
*
* @phpstan-param Record[] $records
*/
abstract protected function send(string $content, array $records): void;
/**
* {@inheritDoc}
*/
protected function write(array $record): void
{
$this->send((string) $record['formatted'], [$record]);
}
/**
* @phpstan-param non-empty-array<Record> $records
* @phpstan-return Record
*/
protected function getHighestRecord(array $records): array
{
$highestRecord = null;
foreach ($records as $record) {
if ($highestRecord === null || $highestRecord['level'] < $record['level']) {
$highestRecord = $record;
}
}
return $highestRecord;
}
protected function isHtmlBody(string $body): bool
{
return ($body[0] ?? null) === '<';
}
/**
* Gets the default formatter.
*
* @return FormatterInterface
*/
protected function getDefaultFormatter(): FormatterInterface
{
return new HtmlFormatter();
}
}
+83
View File
@@ -0,0 +1,83 @@
<?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;
use Swift;
use Swift_Message;
/**
* MandrillHandler uses cURL to send the emails to the Mandrill API
*
* @author Adam Nicholson <adamnicholson10@gmail.com>
*/
class MandrillHandler extends MailHandler
{
/** @var Swift_Message */
protected $message;
/** @var string */
protected $apiKey;
/**
* @psalm-param Swift_Message|callable(): Swift_Message $message
*
* @param string $apiKey A valid Mandrill API key
* @param callable|Swift_Message $message An example message for real messages, only the body will be replaced
*/
public function __construct(string $apiKey, $message, $level = Logger::ERROR, bool $bubble = true)
{
parent::__construct($level, $bubble);
if (!$message instanceof Swift_Message && is_callable($message)) {
$message = $message();
}
if (!$message instanceof Swift_Message) {
throw new \InvalidArgumentException('You must provide either a Swift_Message instance or a callable returning it');
}
$this->message = $message;
$this->apiKey = $apiKey;
}
/**
* {@inheritDoc}
*/
protected function send(string $content, array $records): void
{
$mime = 'text/plain';
if ($this->isHtmlBody($content)) {
$mime = 'text/html';
}
$message = clone $this->message;
$message->setBody($content, $mime);
/** @phpstan-ignore-next-line */
if (version_compare(Swift::VERSION, '6.0.0', '>=')) {
$message->setDate(new \DateTimeImmutable());
} else {
/** @phpstan-ignore-next-line */
$message->setDate(time());
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://mandrillapp.com/api/1.0/messages/send-raw.json');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'key' => $this->apiKey,
'raw_message' => (string) $message,
'async' => false,
]));
Curl\Util::execute($ch);
}
}
@@ -0,0 +1,21 @@
<?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;
/**
* Exception can be thrown if an extension for a handler is missing
*
* @author Christian Bergau <cbergau86@gmail.com>
*/
class MissingExtensionException extends \Exception
{
}
+86
View File
@@ -0,0 +1,86 @@
<?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 MongoDB\Driver\BulkWrite;
use MongoDB\Driver\Manager;
use MongoDB\Client;
use Monolog\Logger;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\MongoDBFormatter;
/**
* Logs to a MongoDB database.
*
* Usage example:
*
* $log = new \Monolog\Logger('application');
* $client = new \MongoDB\Client('mongodb://localhost:27017');
* $mongodb = new \Monolog\Handler\MongoDBHandler($client, 'logs', 'prod');
* $log->pushHandler($mongodb);
*
* The above examples uses the MongoDB PHP library's client class; however, the
* MongoDB\Driver\Manager class from ext-mongodb is also supported.
*/
class MongoDBHandler extends AbstractProcessingHandler
{
/** @var \MongoDB\Collection */
private $collection;
/** @var Client|Manager */
private $manager;
/** @var string */
private $namespace;
/**
* Constructor.
*
* @param Client|Manager $mongodb MongoDB library or driver client
* @param string $database Database name
* @param string $collection Collection name
*/
public function __construct($mongodb, string $database, string $collection, $level = Logger::DEBUG, bool $bubble = true)
{
if (!($mongodb instanceof Client || $mongodb instanceof Manager)) {
throw new \InvalidArgumentException('MongoDB\Client or MongoDB\Driver\Manager instance required');
}
if ($mongodb instanceof Client) {
$this->collection = $mongodb->selectCollection($database, $collection);
} else {
$this->manager = $mongodb;
$this->namespace = $database . '.' . $collection;
}
parent::__construct($level, $bubble);
}
protected function write(array $record): void
{
if (isset($this->collection)) {
$this->collection->insertOne($record['formatted']);
}
if (isset($this->manager, $this->namespace)) {
$bulk = new BulkWrite;
$bulk->insert($record["formatted"]);
$this->manager->executeBulkWrite($this->namespace, $bulk);
}
}
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter(): FormatterInterface
{
return new MongoDBFormatter;
}
}
+174
View File
@@ -0,0 +1,174 @@
<?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;
use Monolog\Formatter\LineFormatter;
/**
* NativeMailerHandler uses the mail() function to send the emails
*
* @author Christophe Coevoet <stof@notk.org>
* @author Mark Garrett <mark@moderndeveloperllc.com>
*/
class NativeMailerHandler extends MailHandler
{
/**
* The email addresses to which the message will be sent
* @var string[]
*/
protected $to;
/**
* The subject of the email
* @var string
*/
protected $subject;
/**
* Optional headers for the message
* @var string[]
*/
protected $headers = [];
/**
* Optional parameters for the message
* @var string[]
*/
protected $parameters = [];
/**
* The wordwrap length for the message
* @var int
*/
protected $maxColumnWidth;
/**
* The Content-type for the message
* @var string|null
*/
protected $contentType;
/**
* The encoding for the message
* @var string
*/
protected $encoding = 'utf-8';
/**
* @param string|string[] $to The receiver of the mail
* @param string $subject The subject of the mail
* @param string $from The sender of the mail
* @param int $maxColumnWidth The maximum column width that the message lines will have
*/
public function __construct($to, string $subject, string $from, $level = Logger::ERROR, bool $bubble = true, int $maxColumnWidth = 70)
{
parent::__construct($level, $bubble);
$this->to = (array) $to;
$this->subject = $subject;
$this->addHeader(sprintf('From: %s', $from));
$this->maxColumnWidth = $maxColumnWidth;
}
/**
* Add headers to the message
*
* @param string|string[] $headers Custom added headers
*/
public function addHeader($headers): self
{
foreach ((array) $headers as $header) {
if (strpos($header, "\n") !== false || strpos($header, "\r") !== false) {
throw new \InvalidArgumentException('Headers can not contain newline characters for security reasons');
}
$this->headers[] = $header;
}
return $this;
}
/**
* Add parameters to the message
*
* @param string|string[] $parameters Custom added parameters
*/
public function addParameter($parameters): self
{
$this->parameters = array_merge($this->parameters, (array) $parameters);
return $this;
}
/**
* {@inheritDoc}
*/
protected function send(string $content, array $records): void
{
$contentType = $this->getContentType() ?: ($this->isHtmlBody($content) ? 'text/html' : 'text/plain');
if ($contentType !== 'text/html') {
$content = wordwrap($content, $this->maxColumnWidth);
}
$headers = ltrim(implode("\r\n", $this->headers) . "\r\n", "\r\n");
$headers .= 'Content-type: ' . $contentType . '; charset=' . $this->getEncoding() . "\r\n";
if ($contentType === 'text/html' && false === strpos($headers, 'MIME-Version:')) {
$headers .= 'MIME-Version: 1.0' . "\r\n";
}
$subject = $this->subject;
if ($records) {
$subjectFormatter = new LineFormatter($this->subject);
$subject = $subjectFormatter->format($this->getHighestRecord($records));
}
$parameters = implode(' ', $this->parameters);
foreach ($this->to as $to) {
mail($to, $subject, $content, $headers, $parameters);
}
}
public function getContentType(): ?string
{
return $this->contentType;
}
public function getEncoding(): string
{
return $this->encoding;
}
/**
* @param string $contentType The content type of the email - Defaults to text/plain. Use text/html for HTML messages.
*/
public function setContentType(string $contentType): self
{
if (strpos($contentType, "\n") !== false || strpos($contentType, "\r") !== false) {
throw new \InvalidArgumentException('The content type can not contain newline characters to prevent email header injection');
}
$this->contentType = $contentType;
return $this;
}
public function setEncoding(string $encoding): self
{
if (strpos($encoding, "\n") !== false || strpos($encoding, "\r") !== false) {
throw new \InvalidArgumentException('The encoding can not contain newline characters to prevent email header injection');
}
$this->encoding = $encoding;
return $this;
}
}
+199
View File
@@ -0,0 +1,199 @@
<?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;
use Monolog\Utils;
use Monolog\Formatter\NormalizerFormatter;
use Monolog\Formatter\FormatterInterface;
/**
* Class to record a log on a NewRelic application.
* Enabling New Relic High Security mode may prevent capture of useful information.
*
* This handler requires a NormalizerFormatter to function and expects an array in $record['formatted']
*
* @see https://docs.newrelic.com/docs/agents/php-agent
* @see https://docs.newrelic.com/docs/accounts-partnerships/accounts/security/high-security
*/
class NewRelicHandler extends AbstractProcessingHandler
{
/**
* Name of the New Relic application that will receive logs from this handler.
*
* @var ?string
*/
protected $appName;
/**
* Name of the current transaction
*
* @var ?string
*/
protected $transactionName;
/**
* Some context and extra data is passed into the handler as arrays of values. Do we send them as is
* (useful if we are using the API), or explode them for display on the NewRelic RPM website?
*
* @var bool
*/
protected $explodeArrays;
/**
* {@inheritDoc}
*
* @param string|null $appName
* @param bool $explodeArrays
* @param string|null $transactionName
*/
public function __construct(
$level = Logger::ERROR,
bool $bubble = true,
?string $appName = null,
bool $explodeArrays = false,
?string $transactionName = null
) {
parent::__construct($level, $bubble);
$this->appName = $appName;
$this->explodeArrays = $explodeArrays;
$this->transactionName = $transactionName;
}
/**
* {@inheritDoc}
*/
protected function write(array $record): void
{
if (!$this->isNewRelicEnabled()) {
throw new MissingExtensionException('The newrelic PHP extension is required to use the NewRelicHandler');
}
if ($appName = $this->getAppName($record['context'])) {
$this->setNewRelicAppName($appName);
}
if ($transactionName = $this->getTransactionName($record['context'])) {
$this->setNewRelicTransactionName($transactionName);
unset($record['formatted']['context']['transaction_name']);
}
if (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Throwable) {
newrelic_notice_error($record['message'], $record['context']['exception']);
unset($record['formatted']['context']['exception']);
} else {
newrelic_notice_error($record['message']);
}
if (isset($record['formatted']['context']) && is_array($record['formatted']['context'])) {
foreach ($record['formatted']['context'] as $key => $parameter) {
if (is_array($parameter) && $this->explodeArrays) {
foreach ($parameter as $paramKey => $paramValue) {
$this->setNewRelicParameter('context_' . $key . '_' . $paramKey, $paramValue);
}
} else {
$this->setNewRelicParameter('context_' . $key, $parameter);
}
}
}
if (isset($record['formatted']['extra']) && is_array($record['formatted']['extra'])) {
foreach ($record['formatted']['extra'] as $key => $parameter) {
if (is_array($parameter) && $this->explodeArrays) {
foreach ($parameter as $paramKey => $paramValue) {
$this->setNewRelicParameter('extra_' . $key . '_' . $paramKey, $paramValue);
}
} else {
$this->setNewRelicParameter('extra_' . $key, $parameter);
}
}
}
}
/**
* Checks whether the NewRelic extension is enabled in the system.
*
* @return bool
*/
protected function isNewRelicEnabled(): bool
{
return extension_loaded('newrelic');
}
/**
* Returns the appname where this log should be sent. Each log can override the default appname, set in this
* handler's constructor, by providing the appname in it's context.
*
* @param mixed[] $context
*/
protected function getAppName(array $context): ?string
{
if (isset($context['appname'])) {
return $context['appname'];
}
return $this->appName;
}
/**
* Returns the name of the current transaction. Each log can override the default transaction name, set in this
* handler's constructor, by providing the transaction_name in it's context
*
* @param mixed[] $context
*/
protected function getTransactionName(array $context): ?string
{
if (isset($context['transaction_name'])) {
return $context['transaction_name'];
}
return $this->transactionName;
}
/**
* Sets the NewRelic application that should receive this log.
*/
protected function setNewRelicAppName(string $appName): void
{
newrelic_set_appname($appName);
}
/**
* Overwrites the name of the current transaction
*/
protected function setNewRelicTransactionName(string $transactionName): void
{
newrelic_name_transaction($transactionName);
}
/**
* @param string $key
* @param mixed $value
*/
protected function setNewRelicParameter(string $key, $value): void
{
if (null === $value || is_scalar($value)) {
newrelic_add_custom_parameter($key, $value);
} else {
newrelic_add_custom_parameter($key, Utils::jsonEncode($value, null, true));
}
}
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter(): FormatterInterface
{
return new NormalizerFormatter();
}
}
+40
View File
@@ -0,0 +1,40 @@
<?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;
/**
* No-op
*
* This handler handles anything, but does nothing, and does not stop bubbling to the rest of the stack.
* This can be used for testing, or to disable a handler when overriding a configuration without
* influencing the rest of the stack.
*
* @author Roel Harbers <roelharbers@gmail.com>
*/
class NoopHandler extends Handler
{
/**
* {@inheritDoc}
*/
public function isHandling(array $record): bool
{
return true;
}
/**
* {@inheritDoc}
*/
public function handle(array $record): bool
{
return false;
}
}
+60
View File
@@ -0,0 +1,60 @@
<?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;
use Psr\Log\LogLevel;
/**
* Blackhole
*
* Any record it can handle will be thrown away. This can be used
* to put on top of an existing stack to override it temporarily.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @phpstan-import-type Level from \Monolog\Logger
* @phpstan-import-type LevelName from \Monolog\Logger
*/
class NullHandler extends Handler
{
/**
* @var int
*/
private $level;
/**
* @param string|int $level The minimum logging level at which this handler will be triggered
*
* @phpstan-param Level|LevelName|LogLevel::* $level
*/
public function __construct($level = Logger::DEBUG)
{
$this->level = Logger::toMonologLevel($level);
}
/**
* {@inheritDoc}
*/
public function isHandling(array $record): bool
{
return $record['level'] >= $this->level;
}
/**
* {@inheritDoc}
*/
public function handle(array $record): bool
{
return $record['level'] >= $this->level;
}
}
+149
View File
@@ -0,0 +1,149 @@
<?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;
use Monolog\Formatter\FormatterInterface;
/**
* Handler to only pass log messages when a certain threshold of number of messages is reached.
*
* This can be useful in cases of processing a batch of data, but you're for example only interested
* in case it fails catastrophically instead of a warning for 1 or 2 events. Worse things can happen, right?
*
* Usage example:
*
* ```
* $log = new Logger('application');
* $handler = new SomeHandler(...)
*
* // Pass all warnings to the handler when more than 10 & all error messages when more then 5
* $overflow = new OverflowHandler($handler, [Logger::WARNING => 10, Logger::ERROR => 5]);
*
* $log->pushHandler($overflow);
*```
*
* @author Kris Buist <krisbuist@gmail.com>
*/
class OverflowHandler extends AbstractHandler implements FormattableHandlerInterface
{
/** @var HandlerInterface */
private $handler;
/** @var int[] */
private $thresholdMap = [
Logger::DEBUG => 0,
Logger::INFO => 0,
Logger::NOTICE => 0,
Logger::WARNING => 0,
Logger::ERROR => 0,
Logger::CRITICAL => 0,
Logger::ALERT => 0,
Logger::EMERGENCY => 0,
];
/**
* Buffer of all messages passed to the handler before the threshold was reached
*
* @var mixed[][]
*/
private $buffer = [];
/**
* @param HandlerInterface $handler
* @param int[] $thresholdMap Dictionary of logger level => threshold
*/
public function __construct(
HandlerInterface $handler,
array $thresholdMap = [],
$level = Logger::DEBUG,
bool $bubble = true
) {
$this->handler = $handler;
foreach ($thresholdMap as $thresholdLevel => $threshold) {
$this->thresholdMap[$thresholdLevel] = $threshold;
}
parent::__construct($level, $bubble);
}
/**
* Handles a record.
*
* All records may be passed to this method, and the handler should discard
* those that it does not want to handle.
*
* The return value of this function controls the bubbling process of the handler stack.
* Unless the bubbling is interrupted (by returning true), the Logger class will keep on
* calling further handlers in the stack with a given log record.
*
* {@inheritDoc}
*/
public function handle(array $record): bool
{
if ($record['level'] < $this->level) {
return false;
}
$level = $record['level'];
if (!isset($this->thresholdMap[$level])) {
$this->thresholdMap[$level] = 0;
}
if ($this->thresholdMap[$level] > 0) {
// The overflow threshold is not yet reached, so we're buffering the record and lowering the threshold by 1
$this->thresholdMap[$level]--;
$this->buffer[$level][] = $record;
return false === $this->bubble;
}
if ($this->thresholdMap[$level] == 0) {
// This current message is breaking the threshold. Flush the buffer and continue handling the current record
foreach ($this->buffer[$level] ?? [] as $buffered) {
$this->handler->handle($buffered);
}
$this->thresholdMap[$level]--;
unset($this->buffer[$level]);
}
$this->handler->handle($record);
return false === $this->bubble;
}
/**
* {@inheritDoc}
*/
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
if ($this->handler instanceof FormattableHandlerInterface) {
$this->handler->setFormatter($formatter);
return $this;
}
throw new \UnexpectedValueException('The nested handler of type '.get_class($this->handler).' does not support formatters.');
}
/**
* {@inheritDoc}
*/
public function getFormatter(): FormatterInterface
{
if ($this->handler instanceof FormattableHandlerInterface) {
return $this->handler->getFormatter();
}
throw new \UnexpectedValueException('The nested handler of type '.get_class($this->handler).' does not support formatters.');
}
}
+263
View File
@@ -0,0 +1,263 @@
<?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\Formatter\LineFormatter;
use Monolog\Formatter\FormatterInterface;
use Monolog\Logger;
use Monolog\Utils;
use PhpConsole\Connector;
use PhpConsole\Handler as VendorPhpConsoleHandler;
use PhpConsole\Helper;
/**
* Monolog handler for Google Chrome extension "PHP Console"
*
* Display PHP error/debug log messages in Google Chrome console and notification popups, executes PHP code remotely
*
* Usage:
* 1. Install Google Chrome extension [now dead and removed from the chrome store]
* 2. See overview https://github.com/barbushin/php-console#overview
* 3. Install PHP Console library https://github.com/barbushin/php-console#installation
* 4. Example (result will looks like http://i.hizliresim.com/vg3Pz4.png)
*
* $logger = new \Monolog\Logger('all', array(new \Monolog\Handler\PHPConsoleHandler()));
* \Monolog\ErrorHandler::register($logger);
* echo $undefinedVar;
* $logger->debug('SELECT * FROM users', array('db', 'time' => 0.012));
* PC::debug($_SERVER); // PHP Console debugger for any type of vars
*
* @author Sergey Barbushin https://www.linkedin.com/in/barbushin
*
* @phpstan-import-type Record from \Monolog\Logger
* @deprecated Since 2.8.0 and 3.2.0, PHPConsole is abandoned and thus we will drop this handler in Monolog 4
*/
class PHPConsoleHandler extends AbstractProcessingHandler
{
/** @var array<string, mixed> */
private $options = [
'enabled' => true, // bool Is PHP Console server enabled
'classesPartialsTraceIgnore' => ['Monolog\\'], // array Hide calls of classes started with...
'debugTagsKeysInContext' => [0, 'tag'], // bool Is PHP Console server enabled
'useOwnErrorsHandler' => false, // bool Enable errors handling
'useOwnExceptionsHandler' => false, // bool Enable exceptions handling
'sourcesBasePath' => null, // string Base path of all project sources to strip in errors source paths
'registerHelper' => true, // bool Register PhpConsole\Helper that allows short debug calls like PC::debug($var, 'ta.g.s')
'serverEncoding' => null, // string|null Server internal encoding
'headersLimit' => null, // int|null Set headers size limit for your web-server
'password' => null, // string|null Protect PHP Console connection by password
'enableSslOnlyMode' => false, // bool Force connection by SSL for clients with PHP Console installed
'ipMasks' => [], // array Set IP masks of clients that will be allowed to connect to PHP Console: array('192.168.*.*', '127.0.0.1')
'enableEvalListener' => false, // bool Enable eval request to be handled by eval dispatcher(if enabled, 'password' option is also required)
'dumperDetectCallbacks' => false, // bool Convert callback items in dumper vars to (callback SomeClass::someMethod) strings
'dumperLevelLimit' => 5, // int Maximum dumped vars array or object nested dump level
'dumperItemsCountLimit' => 100, // int Maximum dumped var same level array items or object properties number
'dumperItemSizeLimit' => 5000, // int Maximum length of any string or dumped array item
'dumperDumpSizeLimit' => 500000, // int Maximum approximate size of dumped vars result formatted in JSON
'detectDumpTraceAndSource' => false, // bool Autodetect and append trace data to debug
'dataStorage' => null, // \PhpConsole\Storage|null Fixes problem with custom $_SESSION handler(see http://goo.gl/Ne8juJ)
];
/** @var Connector */
private $connector;
/**
* @param array<string, mixed> $options See \Monolog\Handler\PHPConsoleHandler::$options for more details
* @param Connector|null $connector Instance of \PhpConsole\Connector class (optional)
* @throws \RuntimeException
*/
public function __construct(array $options = [], ?Connector $connector = null, $level = Logger::DEBUG, bool $bubble = true)
{
if (!class_exists('PhpConsole\Connector')) {
throw new \RuntimeException('PHP Console library not found. See https://github.com/barbushin/php-console#installation');
}
parent::__construct($level, $bubble);
$this->options = $this->initOptions($options);
$this->connector = $this->initConnector($connector);
}
/**
* @param array<string, mixed> $options
*
* @return array<string, mixed>
*/
private function initOptions(array $options): array
{
$wrongOptions = array_diff(array_keys($options), array_keys($this->options));
if ($wrongOptions) {
throw new \RuntimeException('Unknown options: ' . implode(', ', $wrongOptions));
}
return array_replace($this->options, $options);
}
private function initConnector(?Connector $connector = null): Connector
{
if (!$connector) {
if ($this->options['dataStorage']) {
Connector::setPostponeStorage($this->options['dataStorage']);
}
$connector = Connector::getInstance();
}
if ($this->options['registerHelper'] && !Helper::isRegistered()) {
Helper::register();
}
if ($this->options['enabled'] && $connector->isActiveClient()) {
if ($this->options['useOwnErrorsHandler'] || $this->options['useOwnExceptionsHandler']) {
$handler = VendorPhpConsoleHandler::getInstance();
$handler->setHandleErrors($this->options['useOwnErrorsHandler']);
$handler->setHandleExceptions($this->options['useOwnExceptionsHandler']);
$handler->start();
}
if ($this->options['sourcesBasePath']) {
$connector->setSourcesBasePath($this->options['sourcesBasePath']);
}
if ($this->options['serverEncoding']) {
$connector->setServerEncoding($this->options['serverEncoding']);
}
if ($this->options['password']) {
$connector->setPassword($this->options['password']);
}
if ($this->options['enableSslOnlyMode']) {
$connector->enableSslOnlyMode();
}
if ($this->options['ipMasks']) {
$connector->setAllowedIpMasks($this->options['ipMasks']);
}
if ($this->options['headersLimit']) {
$connector->setHeadersLimit($this->options['headersLimit']);
}
if ($this->options['detectDumpTraceAndSource']) {
$connector->getDebugDispatcher()->detectTraceAndSource = true;
}
$dumper = $connector->getDumper();
$dumper->levelLimit = $this->options['dumperLevelLimit'];
$dumper->itemsCountLimit = $this->options['dumperItemsCountLimit'];
$dumper->itemSizeLimit = $this->options['dumperItemSizeLimit'];
$dumper->dumpSizeLimit = $this->options['dumperDumpSizeLimit'];
$dumper->detectCallbacks = $this->options['dumperDetectCallbacks'];
if ($this->options['enableEvalListener']) {
$connector->startEvalRequestsListener();
}
}
return $connector;
}
public function getConnector(): Connector
{
return $this->connector;
}
/**
* @return array<string, mixed>
*/
public function getOptions(): array
{
return $this->options;
}
public function handle(array $record): bool
{
if ($this->options['enabled'] && $this->connector->isActiveClient()) {
return parent::handle($record);
}
return !$this->bubble;
}
/**
* Writes the record down to the log of the implementing handler
*/
protected function write(array $record): void
{
if ($record['level'] < Logger::NOTICE) {
$this->handleDebugRecord($record);
} elseif (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Throwable) {
$this->handleExceptionRecord($record);
} else {
$this->handleErrorRecord($record);
}
}
/**
* @phpstan-param Record $record
*/
private function handleDebugRecord(array $record): void
{
$tags = $this->getRecordTags($record);
$message = $record['message'];
if ($record['context']) {
$message .= ' ' . Utils::jsonEncode($this->connector->getDumper()->dump(array_filter($record['context'])), null, true);
}
$this->connector->getDebugDispatcher()->dispatchDebug($message, $tags, $this->options['classesPartialsTraceIgnore']);
}
/**
* @phpstan-param Record $record
*/
private function handleExceptionRecord(array $record): void
{
$this->connector->getErrorsDispatcher()->dispatchException($record['context']['exception']);
}
/**
* @phpstan-param Record $record
*/
private function handleErrorRecord(array $record): void
{
$context = $record['context'];
$this->connector->getErrorsDispatcher()->dispatchError(
$context['code'] ?? null,
$context['message'] ?? $record['message'],
$context['file'] ?? null,
$context['line'] ?? null,
$this->options['classesPartialsTraceIgnore']
);
}
/**
* @phpstan-param Record $record
* @return string
*/
private function getRecordTags(array &$record)
{
$tags = null;
if (!empty($record['context'])) {
$context = & $record['context'];
foreach ($this->options['debugTagsKeysInContext'] as $key) {
if (!empty($context[$key])) {
$tags = $context[$key];
if ($key === 0) {
array_shift($context);
} else {
unset($context[$key]);
}
break;
}
}
}
return $tags ?: strtolower($record['level_name']);
}
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter(): FormatterInterface
{
return new LineFormatter('%message%');
}
}
+191
View File
@@ -0,0 +1,191 @@
<?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;
/**
* Stores to STDIN of any process, specified by a command.
*
* Usage example:
* <pre>
* $log = new Logger('myLogger');
* $log->pushHandler(new ProcessHandler('/usr/bin/php /var/www/monolog/someScript.php'));
* </pre>
*
* @author Kolja Zuelsdorf <koljaz@web.de>
*/
class ProcessHandler extends AbstractProcessingHandler
{
/**
* Holds the process to receive data on its STDIN.
*
* @var resource|bool|null
*/
private $process;
/**
* @var string
*/
private $command;
/**
* @var string|null
*/
private $cwd;
/**
* @var resource[]
*/
private $pipes = [];
/**
* @var array<int, string[]>
*/
protected const DESCRIPTOR_SPEC = [
0 => ['pipe', 'r'], // STDIN is a pipe that the child will read from
1 => ['pipe', 'w'], // STDOUT is a pipe that the child will write to
2 => ['pipe', 'w'], // STDERR is a pipe to catch the any errors
];
/**
* @param string $command Command for the process to start. Absolute paths are recommended,
* especially if you do not use the $cwd parameter.
* @param string|null $cwd "Current working directory" (CWD) for the process to be executed in.
* @throws \InvalidArgumentException
*/
public function __construct(string $command, $level = Logger::DEBUG, bool $bubble = true, ?string $cwd = null)
{
if ($command === '') {
throw new \InvalidArgumentException('The command argument must be a non-empty string.');
}
if ($cwd === '') {
throw new \InvalidArgumentException('The optional CWD argument must be a non-empty string or null.');
}
parent::__construct($level, $bubble);
$this->command = $command;
$this->cwd = $cwd;
}
/**
* Writes the record down to the log of the implementing handler
*
* @throws \UnexpectedValueException
*/
protected function write(array $record): void
{
$this->ensureProcessIsStarted();
$this->writeProcessInput($record['formatted']);
$errors = $this->readProcessErrors();
if (empty($errors) === false) {
throw new \UnexpectedValueException(sprintf('Errors while writing to process: %s', $errors));
}
}
/**
* Makes sure that the process is actually started, and if not, starts it,
* assigns the stream pipes, and handles startup errors, if any.
*/
private function ensureProcessIsStarted(): void
{
if (is_resource($this->process) === false) {
$this->startProcess();
$this->handleStartupErrors();
}
}
/**
* Starts the actual process and sets all streams to non-blocking.
*/
private function startProcess(): void
{
$this->process = proc_open($this->command, static::DESCRIPTOR_SPEC, $this->pipes, $this->cwd);
foreach ($this->pipes as $pipe) {
stream_set_blocking($pipe, false);
}
}
/**
* Selects the STDERR stream, handles upcoming startup errors, and throws an exception, if any.
*
* @throws \UnexpectedValueException
*/
private function handleStartupErrors(): void
{
$selected = $this->selectErrorStream();
if (false === $selected) {
throw new \UnexpectedValueException('Something went wrong while selecting a stream.');
}
$errors = $this->readProcessErrors();
if (is_resource($this->process) === false || empty($errors) === false) {
throw new \UnexpectedValueException(
sprintf('The process "%s" could not be opened: ' . $errors, $this->command)
);
}
}
/**
* Selects the STDERR stream.
*
* @return int|bool
*/
protected function selectErrorStream()
{
$empty = [];
$errorPipes = [$this->pipes[2]];
return stream_select($errorPipes, $empty, $empty, 1);
}
/**
* Reads the errors of the process, if there are any.
*
* @codeCoverageIgnore
* @return string Empty string if there are no errors.
*/
protected function readProcessErrors(): string
{
return (string) stream_get_contents($this->pipes[2]);
}
/**
* Writes to the input stream of the opened process.
*
* @codeCoverageIgnore
*/
protected function writeProcessInput(string $string): void
{
fwrite($this->pipes[0], $string);
}
/**
* {@inheritDoc}
*/
public function close(): void
{
if (is_resource($this->process)) {
foreach ($this->pipes as $pipe) {
fclose($pipe);
}
proc_close($this->process);
$this->process = null;
}
}
}
@@ -0,0 +1,44 @@
<?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\Processor\ProcessorInterface;
/**
* Interface to describe loggers that have processors
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @phpstan-import-type Record from \Monolog\Logger
*/
interface ProcessableHandlerInterface
{
/**
* Adds a processor in the stack.
*
* @psalm-param ProcessorInterface|callable(Record): Record $callback
*
* @param ProcessorInterface|callable $callback
* @return HandlerInterface self
*/
public function pushProcessor(callable $callback): HandlerInterface;
/**
* Removes the processor on top of the stack and returns it.
*
* @psalm-return ProcessorInterface|callable(Record): Record $callback
*
* @throws \LogicException In case the processor stack is empty
* @return callable|ProcessorInterface
*/
public function popProcessor(): callable;
}
@@ -0,0 +1,77 @@
<?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\ResettableInterface;
use Monolog\Processor\ProcessorInterface;
/**
* Helper trait for implementing ProcessableInterface
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @phpstan-import-type Record from \Monolog\Logger
*/
trait ProcessableHandlerTrait
{
/**
* @var callable[]
* @phpstan-var array<ProcessorInterface|callable(Record): Record>
*/
protected $processors = [];
/**
* {@inheritDoc}
*/
public function pushProcessor(callable $callback): HandlerInterface
{
array_unshift($this->processors, $callback);
return $this;
}
/**
* {@inheritDoc}
*/
public function popProcessor(): callable
{
if (!$this->processors) {
throw new \LogicException('You tried to pop from an empty processor stack.');
}
return array_shift($this->processors);
}
/**
* Processes a record.
*
* @phpstan-param Record $record
* @phpstan-return Record
*/
protected function processRecord(array $record): array
{
foreach ($this->processors as $processor) {
$record = $processor($record);
}
return $record;
}
protected function resetProcessors(): void
{
foreach ($this->processors as $processor) {
if ($processor instanceof ResettableInterface) {
$processor->reset();
}
}
}
}
+95
View File
@@ -0,0 +1,95 @@
<?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;
use Psr\Log\LoggerInterface;
use Monolog\Formatter\FormatterInterface;
/**
* Proxies log messages to an existing PSR-3 compliant logger.
*
* If a formatter is configured, the formatter's output MUST be a string and the
* formatted message will be fed to the wrapped PSR logger instead of the original
* log record's message.
*
* @author Michael Moussa <michael.moussa@gmail.com>
*/
class PsrHandler extends AbstractHandler implements FormattableHandlerInterface
{
/**
* PSR-3 compliant logger
*
* @var LoggerInterface
*/
protected $logger;
/**
* @var FormatterInterface|null
*/
protected $formatter;
/**
* @param LoggerInterface $logger The underlying PSR-3 compliant logger to which messages will be proxied
*/
public function __construct(LoggerInterface $logger, $level = Logger::DEBUG, bool $bubble = true)
{
parent::__construct($level, $bubble);
$this->logger = $logger;
}
/**
* {@inheritDoc}
*/
public function handle(array $record): bool
{
if (!$this->isHandling($record)) {
return false;
}
if ($this->formatter) {
$formatted = $this->formatter->format($record);
$this->logger->log(strtolower($record['level_name']), (string) $formatted, $record['context']);
} else {
$this->logger->log(strtolower($record['level_name']), $record['message'], $record['context']);
}
return false === $this->bubble;
}
/**
* Sets the formatter.
*
* @param FormatterInterface $formatter
*/
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
$this->formatter = $formatter;
return $this;
}
/**
* Gets the formatter.
*
* @return FormatterInterface
*/
public function getFormatter(): FormatterInterface
{
if (!$this->formatter) {
throw new \LogicException('No formatter has been set and this handler does not have a default formatter');
}
return $this->formatter;
}
}
+246
View File
@@ -0,0 +1,246 @@
<?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;
use Monolog\Utils;
use Psr\Log\LogLevel;
/**
* Sends notifications through the pushover api to mobile phones
*
* @author Sebastian Göttschkes <sebastian.goettschkes@googlemail.com>
* @see https://www.pushover.net/api
*
* @phpstan-import-type FormattedRecord from AbstractProcessingHandler
* @phpstan-import-type Level from \Monolog\Logger
* @phpstan-import-type LevelName from \Monolog\Logger
*/
class PushoverHandler extends SocketHandler
{
/** @var string */
private $token;
/** @var array<int|string> */
private $users;
/** @var string */
private $title;
/** @var string|int|null */
private $user = null;
/** @var int */
private $retry;
/** @var int */
private $expire;
/** @var int */
private $highPriorityLevel;
/** @var int */
private $emergencyLevel;
/** @var bool */
private $useFormattedMessage = false;
/**
* All parameters that can be sent to Pushover
* @see https://pushover.net/api
* @var array<string, bool>
*/
private $parameterNames = [
'token' => true,
'user' => true,
'message' => true,
'device' => true,
'title' => true,
'url' => true,
'url_title' => true,
'priority' => true,
'timestamp' => true,
'sound' => true,
'retry' => true,
'expire' => true,
'callback' => true,
];
/**
* Sounds the api supports by default
* @see https://pushover.net/api#sounds
* @var string[]
*/
private $sounds = [
'pushover', 'bike', 'bugle', 'cashregister', 'classical', 'cosmic', 'falling', 'gamelan', 'incoming',
'intermission', 'magic', 'mechanical', 'pianobar', 'siren', 'spacealarm', 'tugboat', 'alien', 'climb',
'persistent', 'echo', 'updown', 'none',
];
/**
* @param string $token Pushover api token
* @param string|array $users Pushover user id or array of ids the message will be sent to
* @param string|null $title Title sent to the Pushover API
* @param bool $useSSL Whether to connect via SSL. Required when pushing messages to users that are not
* the pushover.net app owner. OpenSSL is required for this option.
* @param string|int $highPriorityLevel The minimum logging level at which this handler will start
* sending "high priority" requests to the Pushover API
* @param string|int $emergencyLevel The minimum logging level at which this handler will start
* sending "emergency" requests to the Pushover API
* @param int $retry The retry parameter specifies how often (in seconds) the Pushover servers will
* send the same notification to the user.
* @param int $expire The expire parameter specifies how many seconds your notification will continue
* to be retried for (every retry seconds).
*
* @phpstan-param string|array<int|string> $users
* @phpstan-param Level|LevelName|LogLevel::* $highPriorityLevel
* @phpstan-param Level|LevelName|LogLevel::* $emergencyLevel
*/
public function __construct(
string $token,
$users,
?string $title = null,
$level = Logger::CRITICAL,
bool $bubble = true,
bool $useSSL = true,
$highPriorityLevel = Logger::CRITICAL,
$emergencyLevel = Logger::EMERGENCY,
int $retry = 30,
int $expire = 25200,
bool $persistent = false,
float $timeout = 0.0,
float $writingTimeout = 10.0,
?float $connectionTimeout = null,
?int $chunkSize = null
) {
$connectionString = $useSSL ? 'ssl://api.pushover.net:443' : 'api.pushover.net:80';
parent::__construct(
$connectionString,
$level,
$bubble,
$persistent,
$timeout,
$writingTimeout,
$connectionTimeout,
$chunkSize
);
$this->token = $token;
$this->users = (array) $users;
$this->title = $title ?: (string) gethostname();
$this->highPriorityLevel = Logger::toMonologLevel($highPriorityLevel);
$this->emergencyLevel = Logger::toMonologLevel($emergencyLevel);
$this->retry = $retry;
$this->expire = $expire;
}
protected function generateDataStream(array $record): string
{
$content = $this->buildContent($record);
return $this->buildHeader($content) . $content;
}
/**
* @phpstan-param FormattedRecord $record
*/
private function buildContent(array $record): string
{
// Pushover has a limit of 512 characters on title and message combined.
$maxMessageLength = 512 - strlen($this->title);
$message = ($this->useFormattedMessage) ? $record['formatted'] : $record['message'];
$message = Utils::substr($message, 0, $maxMessageLength);
$timestamp = $record['datetime']->getTimestamp();
$dataArray = [
'token' => $this->token,
'user' => $this->user,
'message' => $message,
'title' => $this->title,
'timestamp' => $timestamp,
];
if (isset($record['level']) && $record['level'] >= $this->emergencyLevel) {
$dataArray['priority'] = 2;
$dataArray['retry'] = $this->retry;
$dataArray['expire'] = $this->expire;
} elseif (isset($record['level']) && $record['level'] >= $this->highPriorityLevel) {
$dataArray['priority'] = 1;
}
// First determine the available parameters
$context = array_intersect_key($record['context'], $this->parameterNames);
$extra = array_intersect_key($record['extra'], $this->parameterNames);
// Least important info should be merged with subsequent info
$dataArray = array_merge($extra, $context, $dataArray);
// Only pass sounds that are supported by the API
if (isset($dataArray['sound']) && !in_array($dataArray['sound'], $this->sounds)) {
unset($dataArray['sound']);
}
return http_build_query($dataArray);
}
private function buildHeader(string $content): string
{
$header = "POST /1/messages.json HTTP/1.1\r\n";
$header .= "Host: api.pushover.net\r\n";
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
$header .= "Content-Length: " . strlen($content) . "\r\n";
$header .= "\r\n";
return $header;
}
protected function write(array $record): void
{
foreach ($this->users as $user) {
$this->user = $user;
parent::write($record);
$this->closeSocket();
}
$this->user = null;
}
/**
* @param int|string $value
*
* @phpstan-param Level|LevelName|LogLevel::* $value
*/
public function setHighPriorityLevel($value): self
{
$this->highPriorityLevel = Logger::toMonologLevel($value);
return $this;
}
/**
* @param int|string $value
*
* @phpstan-param Level|LevelName|LogLevel::* $value
*/
public function setEmergencyLevel($value): self
{
$this->emergencyLevel = Logger::toMonologLevel($value);
return $this;
}
/**
* Use the formatted message?
*/
public function useFormattedMessage(bool $value): self
{
$this->useFormattedMessage = $value;
return $this;
}
}
+101
View File
@@ -0,0 +1,101 @@
<?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\Formatter\LineFormatter;
use Monolog\Formatter\FormatterInterface;
use Monolog\Logger;
/**
* Logs to a Redis key using rpush
*
* usage example:
*
* $log = new Logger('application');
* $redis = new RedisHandler(new Predis\Client("tcp://localhost:6379"), "logs", "prod");
* $log->pushHandler($redis);
*
* @author Thomas Tourlourat <thomas@tourlourat.com>
*
* @phpstan-import-type FormattedRecord from AbstractProcessingHandler
*/
class RedisHandler extends AbstractProcessingHandler
{
/** @var \Predis\Client<\Predis\Client>|\Redis */
private $redisClient;
/** @var string */
private $redisKey;
/** @var int */
protected $capSize;
/**
* @param \Predis\Client<\Predis\Client>|\Redis $redis The redis instance
* @param string $key The key name to push records to
* @param int $capSize Number of entries to limit list size to, 0 = unlimited
*/
public function __construct($redis, string $key, $level = Logger::DEBUG, bool $bubble = true, int $capSize = 0)
{
if (!(($redis instanceof \Predis\Client) || ($redis instanceof \Redis))) {
throw new \InvalidArgumentException('Predis\Client or Redis instance required');
}
$this->redisClient = $redis;
$this->redisKey = $key;
$this->capSize = $capSize;
parent::__construct($level, $bubble);
}
/**
* {@inheritDoc}
*/
protected function write(array $record): void
{
if ($this->capSize) {
$this->writeCapped($record);
} else {
$this->redisClient->rpush($this->redisKey, $record["formatted"]);
}
}
/**
* Write and cap the collection
* Writes the record to the redis list and caps its
*
* @phpstan-param FormattedRecord $record
*/
protected function writeCapped(array $record): void
{
if ($this->redisClient instanceof \Redis) {
$mode = defined('\Redis::MULTI') ? \Redis::MULTI : 1;
$this->redisClient->multi($mode)
->rpush($this->redisKey, $record["formatted"])
->ltrim($this->redisKey, -$this->capSize, -1)
->exec();
} else {
$redisKey = $this->redisKey;
$capSize = $this->capSize;
$this->redisClient->transaction(function ($tx) use ($record, $redisKey, $capSize) {
$tx->rpush($redisKey, $record["formatted"]);
$tx->ltrim($redisKey, -$capSize, -1);
});
}
}
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter(): FormatterInterface
{
return new LineFormatter();
}
}
@@ -0,0 +1,67 @@
<?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\Formatter\LineFormatter;
use Monolog\Formatter\FormatterInterface;
use Monolog\Logger;
/**
* Sends the message to a Redis Pub/Sub channel using PUBLISH
*
* usage example:
*
* $log = new Logger('application');
* $redis = new RedisPubSubHandler(new Predis\Client("tcp://localhost:6379"), "logs", Logger::WARNING);
* $log->pushHandler($redis);
*
* @author Gaëtan Faugère <gaetan@fauge.re>
*/
class RedisPubSubHandler extends AbstractProcessingHandler
{
/** @var \Predis\Client<\Predis\Client>|\Redis */
private $redisClient;
/** @var string */
private $channelKey;
/**
* @param \Predis\Client<\Predis\Client>|\Redis $redis The redis instance
* @param string $key The channel key to publish records to
*/
public function __construct($redis, string $key, $level = Logger::DEBUG, bool $bubble = true)
{
if (!(($redis instanceof \Predis\Client) || ($redis instanceof \Redis))) {
throw new \InvalidArgumentException('Predis\Client or Redis instance required');
}
$this->redisClient = $redis;
$this->channelKey = $key;
parent::__construct($level, $bubble);
}
/**
* {@inheritDoc}
*/
protected function write(array $record): void
{
$this->redisClient->publish($this->channelKey, $record["formatted"]);
}
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter(): FormatterInterface
{
return new LineFormatter();
}
}
+131
View File
@@ -0,0 +1,131 @@
<?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 Rollbar\RollbarLogger;
use Throwable;
use Monolog\Logger;
/**
* Sends errors to Rollbar
*
* If the context data contains a `payload` key, that is used as an array
* of payload options to RollbarLogger's log method.
*
* Rollbar's context info will contain the context + extra keys from the log record
* merged, and then on top of that a few keys:
*
* - level (rollbar level name)
* - monolog_level (monolog level name, raw level, as rollbar only has 5 but monolog 8)
* - channel
* - datetime (unix timestamp)
*
* @author Paul Statezny <paulstatezny@gmail.com>
*/
class RollbarHandler extends AbstractProcessingHandler
{
/**
* @var RollbarLogger
*/
protected $rollbarLogger;
/** @var string[] */
protected $levelMap = [
Logger::DEBUG => 'debug',
Logger::INFO => 'info',
Logger::NOTICE => 'info',
Logger::WARNING => 'warning',
Logger::ERROR => 'error',
Logger::CRITICAL => 'critical',
Logger::ALERT => 'critical',
Logger::EMERGENCY => 'critical',
];
/**
* Records whether any log records have been added since the last flush of the rollbar notifier
*
* @var bool
*/
private $hasRecords = false;
/** @var bool */
protected $initialized = false;
/**
* @param RollbarLogger $rollbarLogger RollbarLogger object constructed with valid token
*/
public function __construct(RollbarLogger $rollbarLogger, $level = Logger::ERROR, bool $bubble = true)
{
$this->rollbarLogger = $rollbarLogger;
parent::__construct($level, $bubble);
}
/**
* {@inheritDoc}
*/
protected function write(array $record): void
{
if (!$this->initialized) {
// __destructor() doesn't get called on Fatal errors
register_shutdown_function(array($this, 'close'));
$this->initialized = true;
}
$context = $record['context'];
$context = array_merge($context, $record['extra'], [
'level' => $this->levelMap[$record['level']],
'monolog_level' => $record['level_name'],
'channel' => $record['channel'],
'datetime' => $record['datetime']->format('U'),
]);
if (isset($context['exception']) && $context['exception'] instanceof Throwable) {
$exception = $context['exception'];
unset($context['exception']);
$toLog = $exception;
} else {
$toLog = $record['message'];
}
// @phpstan-ignore-next-line
$this->rollbarLogger->log($context['level'], $toLog, $context);
$this->hasRecords = true;
}
public function flush(): void
{
if ($this->hasRecords) {
$this->rollbarLogger->flush();
$this->hasRecords = false;
}
}
/**
* {@inheritDoc}
*/
public function close(): void
{
$this->flush();
}
/**
* {@inheritDoc}
*/
public function reset()
{
$this->flush();
parent::reset();
}
}
+212
View File
@@ -0,0 +1,212 @@
<?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 InvalidArgumentException;
use Monolog\Logger;
use Monolog\Utils;
/**
* Stores logs to files that are rotated every day and a limited number of files are kept.
*
* This rotation is only intended to be used as a workaround. Using logrotate to
* handle the rotation is strongly encouraged when you can use it.
*
* @author Christophe Coevoet <stof@notk.org>
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class RotatingFileHandler extends StreamHandler
{
public const FILE_PER_DAY = 'Y-m-d';
public const FILE_PER_MONTH = 'Y-m';
public const FILE_PER_YEAR = 'Y';
/** @var string */
protected $filename;
/** @var int */
protected $maxFiles;
/** @var bool */
protected $mustRotate;
/** @var \DateTimeImmutable */
protected $nextRotation;
/** @var string */
protected $filenameFormat;
/** @var string */
protected $dateFormat;
/**
* @param string $filename
* @param int $maxFiles The maximal amount of files to keep (0 means unlimited)
* @param int|null $filePermission Optional file permissions (default (0644) are only for owner read/write)
* @param bool $useLocking Try to lock log file before doing any writes
*/
public function __construct(string $filename, int $maxFiles = 0, $level = Logger::DEBUG, bool $bubble = true, ?int $filePermission = null, bool $useLocking = false)
{
$this->filename = Utils::canonicalizePath($filename);
$this->maxFiles = $maxFiles;
$this->nextRotation = new \DateTimeImmutable('tomorrow');
$this->filenameFormat = '{filename}-{date}';
$this->dateFormat = static::FILE_PER_DAY;
parent::__construct($this->getTimedFilename(), $level, $bubble, $filePermission, $useLocking);
}
/**
* {@inheritDoc}
*/
public function close(): void
{
parent::close();
if (true === $this->mustRotate) {
$this->rotate();
}
}
/**
* {@inheritDoc}
*/
public function reset()
{
parent::reset();
if (true === $this->mustRotate) {
$this->rotate();
}
}
public function setFilenameFormat(string $filenameFormat, string $dateFormat): self
{
if (!preg_match('{^[Yy](([/_.-]?m)([/_.-]?d)?)?$}', $dateFormat)) {
throw new InvalidArgumentException(
'Invalid date format - format must be one of '.
'RotatingFileHandler::FILE_PER_DAY ("Y-m-d"), RotatingFileHandler::FILE_PER_MONTH ("Y-m") '.
'or RotatingFileHandler::FILE_PER_YEAR ("Y"), or you can set one of the '.
'date formats using slashes, underscores and/or dots instead of dashes.'
);
}
if (substr_count($filenameFormat, '{date}') === 0) {
throw new InvalidArgumentException(
'Invalid filename format - format must contain at least `{date}`, because otherwise rotating is impossible.'
);
}
$this->filenameFormat = $filenameFormat;
$this->dateFormat = $dateFormat;
$this->url = $this->getTimedFilename();
$this->close();
return $this;
}
/**
* {@inheritDoc}
*/
protected function write(array $record): void
{
// on the first record written, if the log is new, we rotate (once per day) after the log has been written so that the new file exists
if (null === $this->mustRotate) {
$this->mustRotate = null === $this->url || !file_exists($this->url);
}
// if the next rotation is expired, then we rotate immediately
if ($this->nextRotation <= $record['datetime']) {
$this->mustRotate = true;
$this->close(); // triggers rotation
}
parent::write($record);
if ($this->mustRotate) {
$this->close(); // triggers rotation
}
}
/**
* Rotates the files.
*/
protected function rotate(): void
{
// update filename
$this->url = $this->getTimedFilename();
$this->nextRotation = new \DateTimeImmutable('tomorrow');
$this->mustRotate = false;
// skip GC of old logs if files are unlimited
if (0 === $this->maxFiles) {
return;
}
$logFiles = glob($this->getGlobPattern());
if (false === $logFiles) {
// failed to glob
return;
}
if ($this->maxFiles >= count($logFiles)) {
// no files to remove
return;
}
// Sorting the files by name to remove the older ones
usort($logFiles, function ($a, $b) {
return strcmp($b, $a);
});
foreach (array_slice($logFiles, $this->maxFiles) as $file) {
if (is_writable($file)) {
// suppress errors here as unlink() might fail if two processes
// are cleaning up/rotating at the same time
set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline): bool {
return false;
});
unlink($file);
restore_error_handler();
}
}
}
protected function getTimedFilename(): string
{
$fileInfo = pathinfo($this->filename);
$timedFilename = str_replace(
['{filename}', '{date}'],
[$fileInfo['filename'], date($this->dateFormat)],
$fileInfo['dirname'] . '/' . $this->filenameFormat
);
if (isset($fileInfo['extension'])) {
$timedFilename .= '.'.$fileInfo['extension'];
}
return $timedFilename;
}
protected function getGlobPattern(): string
{
$fileInfo = pathinfo($this->filename);
$glob = str_replace(
['{filename}', '{date}'],
[$fileInfo['filename'], str_replace(
['Y', 'y', 'm', 'd'],
['[0-9][0-9][0-9][0-9]', '[0-9][0-9]', '[0-9][0-9]', '[0-9][0-9]'],
$this->dateFormat)
],
$fileInfo['dirname'] . '/' . $this->filenameFormat
);
if (isset($fileInfo['extension'])) {
$glob .= '.'.$fileInfo['extension'];
}
return $glob;
}
}
+132
View File
@@ -0,0 +1,132 @@
<?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\Formatter\FormatterInterface;
/**
* Sampling handler
*
* A sampled event stream can be useful for logging high frequency events in
* a production environment where you only need an idea of what is happening
* and are not concerned with capturing every occurrence. Since the decision to
* handle or not handle a particular event is determined randomly, the
* resulting sampled log is not guaranteed to contain 1/N of the events that
* occurred in the application, but based on the Law of large numbers, it will
* tend to be close to this ratio with a large number of attempts.
*
* @author Bryan Davis <bd808@wikimedia.org>
* @author Kunal Mehta <legoktm@gmail.com>
*
* @phpstan-import-type Record from \Monolog\Logger
* @phpstan-import-type Level from \Monolog\Logger
*/
class SamplingHandler extends AbstractHandler implements ProcessableHandlerInterface, FormattableHandlerInterface
{
use ProcessableHandlerTrait;
/**
* @var HandlerInterface|callable
* @phpstan-var HandlerInterface|callable(Record|array{level: Level}|null, HandlerInterface): HandlerInterface
*/
protected $handler;
/**
* @var int $factor
*/
protected $factor;
/**
* @psalm-param HandlerInterface|callable(Record|array{level: Level}|null, HandlerInterface): HandlerInterface $handler
*
* @param callable|HandlerInterface $handler Handler or factory callable($record|null, $samplingHandler).
* @param int $factor Sample factor (e.g. 10 means every ~10th record is sampled)
*/
public function __construct($handler, int $factor)
{
parent::__construct();
$this->handler = $handler;
$this->factor = $factor;
if (!$this->handler instanceof HandlerInterface && !is_callable($this->handler)) {
throw new \RuntimeException("The given handler (".json_encode($this->handler).") is not a callable nor a Monolog\Handler\HandlerInterface object");
}
}
public function isHandling(array $record): bool
{
return $this->getHandler($record)->isHandling($record);
}
public function handle(array $record): bool
{
if ($this->isHandling($record) && mt_rand(1, $this->factor) === 1) {
if ($this->processors) {
/** @var Record $record */
$record = $this->processRecord($record);
}
$this->getHandler($record)->handle($record);
}
return false === $this->bubble;
}
/**
* Return the nested handler
*
* If the handler was provided as a factory callable, this will trigger the handler's instantiation.
*
* @phpstan-param Record|array{level: Level}|null $record
*
* @return HandlerInterface
*/
public function getHandler(?array $record = null)
{
if (!$this->handler instanceof HandlerInterface) {
$this->handler = ($this->handler)($record, $this);
if (!$this->handler instanceof HandlerInterface) {
throw new \RuntimeException("The factory callable should return a HandlerInterface");
}
}
return $this->handler;
}
/**
* {@inheritDoc}
*/
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
$handler = $this->getHandler();
if ($handler instanceof FormattableHandlerInterface) {
$handler->setFormatter($formatter);
return $this;
}
throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.');
}
/**
* {@inheritDoc}
*/
public function getFormatter(): FormatterInterface
{
$handler = $this->getHandler();
if ($handler instanceof FormattableHandlerInterface) {
return $handler->getFormatter();
}
throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.');
}
}
+102
View File
@@ -0,0 +1,102 @@
<?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;
/**
* SendGridrHandler uses the SendGrid API v2 function to send Log emails, more information in https://sendgrid.com/docs/API_Reference/Web_API/mail.html
*
* @author Ricardo Fontanelli <ricardo.fontanelli@hotmail.com>
*/
class SendGridHandler extends MailHandler
{
/**
* The SendGrid API User
* @var string
*/
protected $apiUser;
/**
* The SendGrid API Key
* @var string
*/
protected $apiKey;
/**
* The email addresses to which the message will be sent
* @var string
*/
protected $from;
/**
* The email addresses to which the message will be sent
* @var string[]
*/
protected $to;
/**
* The subject of the email
* @var string
*/
protected $subject;
/**
* @param string $apiUser The SendGrid API User
* @param string $apiKey The SendGrid API Key
* @param string $from The sender of the email
* @param string|string[] $to The recipients of the email
* @param string $subject The subject of the mail
*/
public function __construct(string $apiUser, string $apiKey, string $from, $to, string $subject, $level = Logger::ERROR, bool $bubble = true)
{
if (!extension_loaded('curl')) {
throw new MissingExtensionException('The curl extension is needed to use the SendGridHandler');
}
parent::__construct($level, $bubble);
$this->apiUser = $apiUser;
$this->apiKey = $apiKey;
$this->from = $from;
$this->to = (array) $to;
$this->subject = $subject;
}
/**
* {@inheritDoc}
*/
protected function send(string $content, array $records): void
{
$message = [];
$message['api_user'] = $this->apiUser;
$message['api_key'] = $this->apiKey;
$message['from'] = $this->from;
foreach ($this->to as $recipient) {
$message['to[]'] = $recipient;
}
$message['subject'] = $this->subject;
$message['date'] = date('r');
if ($this->isHtmlBody($content)) {
$message['html'] = $content;
} else {
$message['text'] = $content;
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.sendgrid.com/api/mail.send.json');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($message));
Curl\Util::execute($ch, 2);
}
}
+387
View File
@@ -0,0 +1,387 @@
<?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\Slack;
use Monolog\Logger;
use Monolog\Utils;
use Monolog\Formatter\NormalizerFormatter;
use Monolog\Formatter\FormatterInterface;
/**
* Slack record utility helping to log to Slack webhooks or API.
*
* @author Greg Kedzierski <greg@gregkedzierski.com>
* @author Haralan Dobrev <hkdobrev@gmail.com>
* @see https://api.slack.com/incoming-webhooks
* @see https://api.slack.com/docs/message-attachments
*
* @phpstan-import-type FormattedRecord from \Monolog\Handler\AbstractProcessingHandler
* @phpstan-import-type Record from \Monolog\Logger
*/
class SlackRecord
{
public const COLOR_DANGER = 'danger';
public const COLOR_WARNING = 'warning';
public const COLOR_GOOD = 'good';
public const COLOR_DEFAULT = '#e3e4e6';
/**
* Slack channel (encoded ID or name)
* @var string|null
*/
private $channel;
/**
* Name of a bot
* @var string|null
*/
private $username;
/**
* User icon e.g. 'ghost', 'http://example.com/user.png'
* @var string|null
*/
private $userIcon;
/**
* 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;
/**
* Dot separated list of fields to exclude from slack message. E.g. ['context.field1', 'extra.field2']
* @var string[]
*/
private $excludeFields;
/**
* @var ?FormatterInterface
*/
private $formatter;
/**
* @var NormalizerFormatter
*/
private $normalizerFormatter;
/**
* @param string[] $excludeFields
*/
public function __construct(
?string $channel = null,
?string $username = null,
bool $useAttachment = true,
?string $userIcon = null,
bool $useShortAttachment = false,
bool $includeContextAndExtra = false,
array $excludeFields = array(),
?FormatterInterface $formatter = null
) {
$this
->setChannel($channel)
->setUsername($username)
->useAttachment($useAttachment)
->setUserIcon($userIcon)
->useShortAttachment($useShortAttachment)
->includeContextAndExtra($includeContextAndExtra)
->excludeFields($excludeFields)
->setFormatter($formatter);
if ($this->includeContextAndExtra) {
$this->normalizerFormatter = new NormalizerFormatter();
}
}
/**
* Returns required data in format that Slack
* is expecting.
*
* @phpstan-param FormattedRecord $record
* @phpstan-return mixed[]
*/
public function getSlackData(array $record): array
{
$dataArray = array();
$record = $this->removeExcludedFields($record);
if ($this->username) {
$dataArray['username'] = $this->username;
}
if ($this->channel) {
$dataArray['channel'] = $this->channel;
}
if ($this->formatter && !$this->useAttachment) {
/** @phpstan-ignore-next-line */
$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(),
'mrkdwn_in' => array('fields'),
'ts' => $record['datetime']->getTimestamp(),
'footer' => $this->username,
'footer_icon' => $this->userIcon,
);
if ($this->useShortAttachment) {
$attachment['title'] = $record['level_name'];
} else {
$attachment['title'] = 'Message';
$attachment['fields'][] = $this->generateAttachmentField('Level', $record['level_name']);
}
if ($this->includeContextAndExtra) {
foreach (array('extra', 'context') as $key) {
if (empty($record[$key])) {
continue;
}
if ($this->useShortAttachment) {
$attachment['fields'][] = $this->generateAttachmentField(
(string) $key,
$record[$key]
);
} 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->userIcon) {
if (filter_var($this->userIcon, FILTER_VALIDATE_URL)) {
$dataArray['icon_url'] = $this->userIcon;
} else {
$dataArray['icon_emoji'] = ":{$this->userIcon}:";
}
}
return $dataArray;
}
/**
* Returns a Slack message attachment color associated with
* provided level.
*/
public function getAttachmentColor(int $level): string
{
switch (true) {
case $level >= Logger::ERROR:
return static::COLOR_DANGER;
case $level >= Logger::WARNING:
return static::COLOR_WARNING;
case $level >= Logger::INFO:
return static::COLOR_GOOD;
default:
return static::COLOR_DEFAULT;
}
}
/**
* Stringifies an array of key/value pairs to be used in attachment fields
*
* @param mixed[] $fields
*/
public function stringify(array $fields): string
{
/** @var Record $fields */
$normalized = $this->normalizerFormatter->format($fields);
$hasSecondDimension = count(array_filter($normalized, 'is_array'));
$hasNonNumericKeys = !count(array_filter(array_keys($normalized), 'is_numeric'));
return $hasSecondDimension || $hasNonNumericKeys
? Utils::jsonEncode($normalized, JSON_PRETTY_PRINT|Utils::DEFAULT_JSON_FLAGS)
: Utils::jsonEncode($normalized, Utils::DEFAULT_JSON_FLAGS);
}
/**
* Channel used by the bot when posting
*
* @param ?string $channel
*
* @return static
*/
public function setChannel(?string $channel = null): self
{
$this->channel = $channel;
return $this;
}
/**
* Username used by the bot when posting
*
* @param ?string $username
*
* @return static
*/
public function setUsername(?string $username = null): self
{
$this->username = $username;
return $this;
}
public function useAttachment(bool $useAttachment = true): self
{
$this->useAttachment = $useAttachment;
return $this;
}
public function setUserIcon(?string $userIcon = null): self
{
$this->userIcon = $userIcon;
if (\is_string($userIcon)) {
$this->userIcon = trim($userIcon, ':');
}
return $this;
}
public function useShortAttachment(bool $useShortAttachment = false): self
{
$this->useShortAttachment = $useShortAttachment;
return $this;
}
public function includeContextAndExtra(bool $includeContextAndExtra = false): self
{
$this->includeContextAndExtra = $includeContextAndExtra;
if ($this->includeContextAndExtra) {
$this->normalizerFormatter = new NormalizerFormatter();
}
return $this;
}
/**
* @param string[] $excludeFields
*/
public function excludeFields(array $excludeFields = []): self
{
$this->excludeFields = $excludeFields;
return $this;
}
public function setFormatter(?FormatterInterface $formatter = null): self
{
$this->formatter = $formatter;
return $this;
}
/**
* Generates attachment field
*
* @param string|mixed[] $value
*
* @return array{title: string, value: string, short: false}
*/
private function generateAttachmentField(string $title, $value): array
{
$value = is_array($value)
? sprintf('```%s```', substr($this->stringify($value), 0, 1990))
: $value;
return array(
'title' => ucfirst($title),
'value' => $value,
'short' => false,
);
}
/**
* Generates a collection of attachment fields from array
*
* @param mixed[] $data
*
* @return array<array{title: string, value: string, short: false}>
*/
private function generateAttachmentFields(array $data): array
{
/** @var Record $data */
$normalized = $this->normalizerFormatter->format($data);
$fields = array();
foreach ($normalized as $key => $value) {
$fields[] = $this->generateAttachmentField((string) $key, $value);
}
return $fields;
}
/**
* Get a copy of record with fields excluded according to $this->excludeFields
*
* @phpstan-param FormattedRecord $record
*
* @return mixed[]
*/
private function removeExcludedFields(array $record): array
{
foreach ($this->excludeFields as $field) {
$keys = explode('.', $field);
$node = &$record;
$lastKey = end($keys);
foreach ($keys as $key) {
if (!isset($node[$key])) {
break;
}
if ($lastKey === $key) {
unset($node[$key]);
break;
}
$node = &$node[$key];
}
}
return $record;
}
}
+256
View File
@@ -0,0 +1,256 @@
<?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\Formatter\FormatterInterface;
use Monolog\Logger;
use Monolog\Utils;
use Monolog\Handler\Slack\SlackRecord;
/**
* Sends notifications through Slack API
*
* @author Greg Kedzierski <greg@gregkedzierski.com>
* @see https://api.slack.com/
*
* @phpstan-import-type FormattedRecord from AbstractProcessingHandler
*/
class SlackHandler extends SocketHandler
{
/**
* Slack API token
* @var string
*/
private $token;
/**
* Instance of the SlackRecord util class preparing data for Slack API.
* @var SlackRecord
*/
private $slackRecord;
/**
* @param string $token Slack API token
* @param string $channel Slack channel (encoded ID or name)
* @param string|null $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 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 string[] $excludeFields Dot separated list of fields to exclude from slack message. E.g. ['context.field1', 'extra.field2']
* @throws MissingExtensionException If no OpenSSL PHP extension configured
*/
public function __construct(
string $token,
string $channel,
?string $username = null,
bool $useAttachment = true,
?string $iconEmoji = null,
$level = Logger::CRITICAL,
bool $bubble = true,
bool $useShortAttachment = false,
bool $includeContextAndExtra = false,
array $excludeFields = array(),
bool $persistent = false,
float $timeout = 0.0,
float $writingTimeout = 10.0,
?float $connectionTimeout = null,
?int $chunkSize = null
) {
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,
$persistent,
$timeout,
$writingTimeout,
$connectionTimeout,
$chunkSize
);
$this->slackRecord = new SlackRecord(
$channel,
$username,
$useAttachment,
$iconEmoji,
$useShortAttachment,
$includeContextAndExtra,
$excludeFields
);
$this->token = $token;
}
public function getSlackRecord(): SlackRecord
{
return $this->slackRecord;
}
public function getToken(): string
{
return $this->token;
}
/**
* {@inheritDoc}
*/
protected function generateDataStream(array $record): string
{
$content = $this->buildContent($record);
return $this->buildHeader($content) . $content;
}
/**
* Builds the body of API call
*
* @phpstan-param FormattedRecord $record
*/
private function buildContent(array $record): string
{
$dataArray = $this->prepareContentData($record);
return http_build_query($dataArray);
}
/**
* @phpstan-param FormattedRecord $record
* @return string[]
*/
protected function prepareContentData(array $record): array
{
$dataArray = $this->slackRecord->getSlackData($record);
$dataArray['token'] = $this->token;
if (!empty($dataArray['attachments'])) {
$dataArray['attachments'] = Utils::jsonEncode($dataArray['attachments']);
}
return $dataArray;
}
/**
* Builds the header of the API Call
*/
private function buildHeader(string $content): string
{
$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}
*/
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, slack sometimes
* drops the request entirely.
*/
protected function finalizeWrite(): void
{
$res = $this->getResource();
if (is_resource($res)) {
@fread($res, 2048);
}
$this->closeSocket();
}
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
parent::setFormatter($formatter);
$this->slackRecord->setFormatter($formatter);
return $this;
}
public function getFormatter(): FormatterInterface
{
$formatter = parent::getFormatter();
$this->slackRecord->setFormatter($formatter);
return $formatter;
}
/**
* Channel used by the bot when posting
*/
public function setChannel(string $channel): self
{
$this->slackRecord->setChannel($channel);
return $this;
}
/**
* Username used by the bot when posting
*/
public function setUsername(string $username): self
{
$this->slackRecord->setUsername($username);
return $this;
}
public function useAttachment(bool $useAttachment): self
{
$this->slackRecord->useAttachment($useAttachment);
return $this;
}
public function setIconEmoji(string $iconEmoji): self
{
$this->slackRecord->setUserIcon($iconEmoji);
return $this;
}
public function useShortAttachment(bool $useShortAttachment): self
{
$this->slackRecord->useShortAttachment($useShortAttachment);
return $this;
}
public function includeContextAndExtra(bool $includeContextAndExtra): self
{
$this->slackRecord->includeContextAndExtra($includeContextAndExtra);
return $this;
}
/**
* @param string[] $excludeFields
*/
public function excludeFields(array $excludeFields): self
{
$this->slackRecord->excludeFields($excludeFields);
return $this;
}
}
+130
View File
@@ -0,0 +1,130 @@
<?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\Formatter\FormatterInterface;
use Monolog\Logger;
use Monolog\Utils;
use Monolog\Handler\Slack\SlackRecord;
/**
* Sends notifications through Slack Webhooks
*
* @author Haralan Dobrev <hkdobrev@gmail.com>
* @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|null $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 string[] $excludeFields Dot separated list of fields to exclude from slack message. E.g. ['context.field1', 'extra.field2']
*/
public function __construct(
string $webhookUrl,
?string $channel = null,
?string $username = null,
bool $useAttachment = true,
?string $iconEmoji = null,
bool $useShortAttachment = false,
bool $includeContextAndExtra = false,
$level = Logger::CRITICAL,
bool $bubble = true,
array $excludeFields = array()
) {
if (!extension_loaded('curl')) {
throw new MissingExtensionException('The curl extension is needed to use the SlackWebhookHandler');
}
parent::__construct($level, $bubble);
$this->webhookUrl = $webhookUrl;
$this->slackRecord = new SlackRecord(
$channel,
$username,
$useAttachment,
$iconEmoji,
$useShortAttachment,
$includeContextAndExtra,
$excludeFields
);
}
public function getSlackRecord(): SlackRecord
{
return $this->slackRecord;
}
public function getWebhookUrl(): string
{
return $this->webhookUrl;
}
/**
* {@inheritDoc}
*/
protected function write(array $record): void
{
$postData = $this->slackRecord->getSlackData($record);
$postString = Utils::jsonEncode($postData);
$ch = curl_init();
$options = array(
CURLOPT_URL => $this->webhookUrl,
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => array('Content-type: application/json'),
CURLOPT_POSTFIELDS => $postString,
);
if (defined('CURLOPT_SAFE_UPLOAD')) {
$options[CURLOPT_SAFE_UPLOAD] = true;
}
curl_setopt_array($ch, $options);
Curl\Util::execute($ch);
}
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
parent::setFormatter($formatter);
$this->slackRecord->setFormatter($formatter);
return $this;
}
public function getFormatter(): FormatterInterface
{
$formatter = parent::getFormatter();
$this->slackRecord->setFormatter($formatter);
return $formatter;
}
}
+448
View File
@@ -0,0 +1,448 @@
<?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;
/**
* Stores to any socket - uses fsockopen() or pfsockopen().
*
* @author Pablo de Leon Belloc <pablolb@gmail.com>
* @see http://php.net/manual/en/function.fsockopen.php
*
* @phpstan-import-type Record from \Monolog\Logger
* @phpstan-import-type FormattedRecord from AbstractProcessingHandler
*/
class SocketHandler extends AbstractProcessingHandler
{
/** @var string */
private $connectionString;
/** @var float */
private $connectionTimeout;
/** @var resource|null */
private $resource;
/** @var float */
private $timeout;
/** @var float */
private $writingTimeout;
/** @var ?int */
private $lastSentBytes = null;
/** @var ?int */
private $chunkSize;
/** @var bool */
private $persistent;
/** @var ?int */
private $errno = null;
/** @var ?string */
private $errstr = null;
/** @var ?float */
private $lastWritingAt = null;
/**
* @param string $connectionString Socket connection string
* @param bool $persistent Flag to enable/disable persistent connections
* @param float $timeout Socket timeout to wait until the request is being aborted
* @param float $writingTimeout Socket timeout to wait until the request should've been sent/written
* @param float|null $connectionTimeout Socket connect timeout to wait until the connection should've been
* established
* @param int|null $chunkSize Sets the chunk size. Only has effect during connection in the writing cycle
*
* @throws \InvalidArgumentException If an invalid timeout value (less than 0) is passed.
*/
public function __construct(
string $connectionString,
$level = Logger::DEBUG,
bool $bubble = true,
bool $persistent = false,
float $timeout = 0.0,
float $writingTimeout = 10.0,
?float $connectionTimeout = null,
?int $chunkSize = null
) {
parent::__construct($level, $bubble);
$this->connectionString = $connectionString;
if ($connectionTimeout !== null) {
$this->validateTimeout($connectionTimeout);
}
$this->connectionTimeout = $connectionTimeout ?? (float) ini_get('default_socket_timeout');
$this->persistent = $persistent;
$this->validateTimeout($timeout);
$this->timeout = $timeout;
$this->validateTimeout($writingTimeout);
$this->writingTimeout = $writingTimeout;
$this->chunkSize = $chunkSize;
}
/**
* Connect (if necessary) and write to the socket
*
* {@inheritDoc}
*
* @throws \UnexpectedValueException
* @throws \RuntimeException
*/
protected function write(array $record): void
{
$this->connectIfNotConnected();
$data = $this->generateDataStream($record);
$this->writeToSocket($data);
}
/**
* We will not close a PersistentSocket instance so it can be reused in other requests.
*/
public function close(): void
{
if (!$this->isPersistent()) {
$this->closeSocket();
}
}
/**
* Close socket, if open
*/
public function closeSocket(): void
{
if (is_resource($this->resource)) {
fclose($this->resource);
$this->resource = null;
}
}
/**
* Set socket connection to be persistent. It only has effect before the connection is initiated.
*/
public function setPersistent(bool $persistent): self
{
$this->persistent = $persistent;
return $this;
}
/**
* Set connection timeout. Only has effect before we connect.
*
* @see http://php.net/manual/en/function.fsockopen.php
*/
public function setConnectionTimeout(float $seconds): self
{
$this->validateTimeout($seconds);
$this->connectionTimeout = $seconds;
return $this;
}
/**
* Set write timeout. Only has effect before we connect.
*
* @see http://php.net/manual/en/function.stream-set-timeout.php
*/
public function setTimeout(float $seconds): self
{
$this->validateTimeout($seconds);
$this->timeout = $seconds;
return $this;
}
/**
* Set writing timeout. Only has effect during connection in the writing cycle.
*
* @param float $seconds 0 for no timeout
*/
public function setWritingTimeout(float $seconds): self
{
$this->validateTimeout($seconds);
$this->writingTimeout = $seconds;
return $this;
}
/**
* Set chunk size. Only has effect during connection in the writing cycle.
*/
public function setChunkSize(int $bytes): self
{
$this->chunkSize = $bytes;
return $this;
}
/**
* Get current connection string
*/
public function getConnectionString(): string
{
return $this->connectionString;
}
/**
* Get persistent setting
*/
public function isPersistent(): bool
{
return $this->persistent;
}
/**
* Get current connection timeout setting
*/
public function getConnectionTimeout(): float
{
return $this->connectionTimeout;
}
/**
* Get current in-transfer timeout
*/
public function getTimeout(): float
{
return $this->timeout;
}
/**
* Get current local writing timeout
*
* @return float
*/
public function getWritingTimeout(): float
{
return $this->writingTimeout;
}
/**
* Get current chunk size
*/
public function getChunkSize(): ?int
{
return $this->chunkSize;
}
/**
* Check to see if the socket is currently available.
*
* UDP might appear to be connected but might fail when writing. See http://php.net/fsockopen for details.
*/
public function isConnected(): bool
{
return is_resource($this->resource)
&& !feof($this->resource); // on TCP - other party can close connection.
}
/**
* Wrapper to allow mocking
*
* @return resource|false
*/
protected function pfsockopen()
{
return @pfsockopen($this->connectionString, -1, $this->errno, $this->errstr, $this->connectionTimeout);
}
/**
* Wrapper to allow mocking
*
* @return resource|false
*/
protected function fsockopen()
{
return @fsockopen($this->connectionString, -1, $this->errno, $this->errstr, $this->connectionTimeout);
}
/**
* Wrapper to allow mocking
*
* @see http://php.net/manual/en/function.stream-set-timeout.php
*
* @return bool
*/
protected function streamSetTimeout()
{
$seconds = floor($this->timeout);
$microseconds = round(($this->timeout - $seconds) * 1e6);
if (!is_resource($this->resource)) {
throw new \LogicException('streamSetTimeout called but $this->resource is not a resource');
}
return stream_set_timeout($this->resource, (int) $seconds, (int) $microseconds);
}
/**
* Wrapper to allow mocking
*
* @see http://php.net/manual/en/function.stream-set-chunk-size.php
*
* @return int|bool
*/
protected function streamSetChunkSize()
{
if (!is_resource($this->resource)) {
throw new \LogicException('streamSetChunkSize called but $this->resource is not a resource');
}
if (null === $this->chunkSize) {
throw new \LogicException('streamSetChunkSize called but $this->chunkSize is not set');
}
return stream_set_chunk_size($this->resource, $this->chunkSize);
}
/**
* Wrapper to allow mocking
*
* @return int|bool
*/
protected function fwrite(string $data)
{
if (!is_resource($this->resource)) {
throw new \LogicException('fwrite called but $this->resource is not a resource');
}
return @fwrite($this->resource, $data);
}
/**
* Wrapper to allow mocking
*
* @return mixed[]|bool
*/
protected function streamGetMetadata()
{
if (!is_resource($this->resource)) {
throw new \LogicException('streamGetMetadata called but $this->resource is not a resource');
}
return stream_get_meta_data($this->resource);
}
private function validateTimeout(float $value): void
{
if ($value < 0) {
throw new \InvalidArgumentException("Timeout must be 0 or a positive float (got $value)");
}
}
private function connectIfNotConnected(): void
{
if ($this->isConnected()) {
return;
}
$this->connect();
}
/**
* @phpstan-param FormattedRecord $record
*/
protected function generateDataStream(array $record): string
{
return (string) $record['formatted'];
}
/**
* @return resource|null
*/
protected function getResource()
{
return $this->resource;
}
private function connect(): void
{
$this->createSocketResource();
$this->setSocketTimeout();
$this->setStreamChunkSize();
}
private function createSocketResource(): void
{
if ($this->isPersistent()) {
$resource = $this->pfsockopen();
} else {
$resource = $this->fsockopen();
}
if (is_bool($resource)) {
throw new \UnexpectedValueException("Failed connecting to $this->connectionString ($this->errno: $this->errstr)");
}
$this->resource = $resource;
}
private function setSocketTimeout(): void
{
if (!$this->streamSetTimeout()) {
throw new \UnexpectedValueException("Failed setting timeout with stream_set_timeout()");
}
}
private function setStreamChunkSize(): void
{
if ($this->chunkSize && !$this->streamSetChunkSize()) {
throw new \UnexpectedValueException("Failed setting chunk size with stream_set_chunk_size()");
}
}
private function writeToSocket(string $data): void
{
$length = strlen($data);
$sent = 0;
$this->lastSentBytes = $sent;
while ($this->isConnected() && $sent < $length) {
if (0 == $sent) {
$chunk = $this->fwrite($data);
} else {
$chunk = $this->fwrite(substr($data, $sent));
}
if ($chunk === false) {
throw new \RuntimeException("Could not write to socket");
}
$sent += $chunk;
$socketInfo = $this->streamGetMetadata();
if (is_array($socketInfo) && $socketInfo['timed_out']) {
throw new \RuntimeException("Write timed-out");
}
if ($this->writingIsTimedOut($sent)) {
throw new \RuntimeException("Write timed-out, no data sent for `{$this->writingTimeout}` seconds, probably we got disconnected (sent $sent of $length)");
}
}
if (!$this->isConnected() && $sent < $length) {
throw new \RuntimeException("End-of-file reached, probably we got disconnected (sent $sent of $length)");
}
}
private function writingIsTimedOut(int $sent): bool
{
// convert to ms
if (0.0 == $this->writingTimeout) {
return false;
}
if ($sent !== $this->lastSentBytes) {
$this->lastWritingAt = microtime(true);
$this->lastSentBytes = $sent;
return false;
} else {
usleep(100);
}
if ((microtime(true) - $this->lastWritingAt) >= $this->writingTimeout) {
$this->closeSocket();
return true;
}
return false;
}
}
+62
View File
@@ -0,0 +1,62 @@
<?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 Aws\Sqs\SqsClient;
use Monolog\Logger;
use Monolog\Utils;
/**
* Writes to any sqs queue.
*
* @author Martijn van Calker <git@amvc.nl>
*/
class SqsHandler extends AbstractProcessingHandler
{
/** 256 KB in bytes - maximum message size in SQS */
protected const MAX_MESSAGE_SIZE = 262144;
/** 100 KB in bytes - head message size for new error log */
protected const HEAD_MESSAGE_SIZE = 102400;
/** @var SqsClient */
private $client;
/** @var string */
private $queueUrl;
public function __construct(SqsClient $sqsClient, string $queueUrl, $level = Logger::DEBUG, bool $bubble = true)
{
parent::__construct($level, $bubble);
$this->client = $sqsClient;
$this->queueUrl = $queueUrl;
}
/**
* {@inheritDoc}
*/
protected function write(array $record): void
{
if (!isset($record['formatted']) || 'string' !== gettype($record['formatted'])) {
throw new \InvalidArgumentException('SqsHandler accepts only formatted records as a string' . Utils::getRecordMessageForException($record));
}
$messageBody = $record['formatted'];
if (strlen($messageBody) >= static::MAX_MESSAGE_SIZE) {
$messageBody = Utils::substr($messageBody, 0, static::HEAD_MESSAGE_SIZE);
}
$this->client->sendMessage([
'QueueUrl' => $this->queueUrl,
'MessageBody' => $messageBody,
]);
}
}
+256
View File
@@ -0,0 +1,256 @@
<?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;
use Monolog\Utils;
/**
* Stores to any stream resource
*
* Can be used to store into php://stderr, remote and local files, etc.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @phpstan-import-type FormattedRecord from AbstractProcessingHandler
*/
class StreamHandler extends AbstractProcessingHandler
{
/** @const int */
protected const MAX_CHUNK_SIZE = 2147483647;
/** @const int 10MB */
protected const DEFAULT_CHUNK_SIZE = 10 * 1024 * 1024;
/** @var int */
protected $streamChunkSize;
/** @var resource|null */
protected $stream;
/** @var ?string */
protected $url = null;
/** @var ?string */
private $errorMessage = null;
/** @var ?int */
protected $filePermission;
/** @var bool */
protected $useLocking;
/** @var string */
protected $fileOpenMode;
/** @var true|null */
private $dirCreated = null;
/** @var bool */
private $retrying = false;
/**
* @param resource|string $stream If a missing path can't be created, an UnexpectedValueException will be thrown on first write
* @param int|null $filePermission Optional file permissions (default (0644) are only for owner read/write)
* @param bool $useLocking Try to lock log file before doing any writes
* @param string $fileOpenMode The fopen() mode used when opening a file, if $stream is a file path
*
* @throws \InvalidArgumentException If stream is not a resource or string
*/
public function __construct($stream, $level = Logger::DEBUG, bool $bubble = true, ?int $filePermission = null, bool $useLocking = false, $fileOpenMode = 'a')
{
parent::__construct($level, $bubble);
if (($phpMemoryLimit = Utils::expandIniShorthandBytes(ini_get('memory_limit'))) !== false) {
if ($phpMemoryLimit > 0) {
// use max 10% of allowed memory for the chunk size, and at least 100KB
$this->streamChunkSize = min(static::MAX_CHUNK_SIZE, max((int) ($phpMemoryLimit / 10), 100 * 1024));
} else {
// memory is unlimited, set to the default 10MB
$this->streamChunkSize = static::DEFAULT_CHUNK_SIZE;
}
} else {
// no memory limit information, set to the default 10MB
$this->streamChunkSize = static::DEFAULT_CHUNK_SIZE;
}
if (is_resource($stream)) {
$this->stream = $stream;
stream_set_chunk_size($this->stream, $this->streamChunkSize);
} elseif (is_string($stream)) {
$this->url = Utils::canonicalizePath($stream);
} else {
throw new \InvalidArgumentException('A stream must either be a resource or a string.');
}
$this->fileOpenMode = $fileOpenMode;
$this->filePermission = $filePermission;
$this->useLocking = $useLocking;
}
/**
* {@inheritDoc}
*/
public function close(): void
{
if ($this->url && is_resource($this->stream)) {
fclose($this->stream);
}
$this->stream = null;
$this->dirCreated = null;
}
/**
* Return the currently active stream if it is open
*
* @return resource|null
*/
public function getStream()
{
return $this->stream;
}
/**
* Return the stream URL if it was configured with a URL and not an active resource
*
* @return string|null
*/
public function getUrl(): ?string
{
return $this->url;
}
/**
* @return int
*/
public function getStreamChunkSize(): int
{
return $this->streamChunkSize;
}
/**
* {@inheritDoc}
*/
protected function write(array $record): void
{
if (!is_resource($this->stream)) {
$url = $this->url;
if (null === $url || '' === $url) {
throw new \LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().' . Utils::getRecordMessageForException($record));
}
$this->createDir($url);
$this->errorMessage = null;
set_error_handler(function (...$args) {
return $this->customErrorHandler(...$args);
});
try {
$stream = fopen($url, $this->fileOpenMode);
if ($this->filePermission !== null) {
@chmod($url, $this->filePermission);
}
} finally {
restore_error_handler();
}
if (!is_resource($stream)) {
$this->stream = null;
throw new \UnexpectedValueException(sprintf('The stream or file "%s" could not be opened in append mode: '.$this->errorMessage, $url) . Utils::getRecordMessageForException($record));
}
stream_set_chunk_size($stream, $this->streamChunkSize);
$this->stream = $stream;
}
$stream = $this->stream;
if (!is_resource($stream)) {
throw new \LogicException('No stream was opened yet' . Utils::getRecordMessageForException($record));
}
if ($this->useLocking) {
// ignoring errors here, there's not much we can do about them
flock($stream, LOCK_EX);
}
$this->errorMessage = null;
set_error_handler(function (...$args) {
return $this->customErrorHandler(...$args);
});
try {
$this->streamWrite($stream, $record);
} finally {
restore_error_handler();
}
if ($this->errorMessage !== null) {
$error = $this->errorMessage;
// close the resource if possible to reopen it, and retry the failed write
if (!$this->retrying && $this->url !== null && $this->url !== 'php://memory') {
$this->retrying = true;
$this->close();
$this->write($record);
return;
}
throw new \UnexpectedValueException('Writing to the log file failed: '.$error . Utils::getRecordMessageForException($record));
}
$this->retrying = false;
if ($this->useLocking) {
flock($stream, LOCK_UN);
}
}
/**
* Write to stream
* @param resource $stream
* @param array $record
*
* @phpstan-param FormattedRecord $record
*/
protected function streamWrite($stream, array $record): void
{
fwrite($stream, (string) $record['formatted']);
}
private function customErrorHandler(int $code, string $msg): bool
{
$this->errorMessage = preg_replace('{^(fopen|mkdir|fwrite)\(.*?\): }', '', $msg);
return true;
}
private function getDirFromStream(string $stream): ?string
{
$pos = strpos($stream, '://');
if ($pos === false) {
return dirname($stream);
}
if ('file://' === substr($stream, 0, 7)) {
return dirname(substr($stream, 7));
}
return null;
}
private function createDir(string $url): void
{
// Do not try to create dir if it has already been tried.
if ($this->dirCreated) {
return;
}
$dir = $this->getDirFromStream($url);
if (null !== $dir && !is_dir($dir)) {
$this->errorMessage = null;
set_error_handler(function (...$args) {
return $this->customErrorHandler(...$args);
});
$status = mkdir($dir, 0777, true);
restore_error_handler();
if (false === $status && !is_dir($dir) && strpos((string) $this->errorMessage, 'File exists') === false) {
throw new \UnexpectedValueException(sprintf('There is no existing directory at "%s" and it could not be created: '.$this->errorMessage, $dir));
}
}
$this->dirCreated = true;
}
}
+115
View File
@@ -0,0 +1,115 @@
<?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;
use Monolog\Utils;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\LineFormatter;
use Swift_Message;
use Swift;
/**
* SwiftMailerHandler uses Swift_Mailer to send the emails
*
* @author Gyula Sallai
*
* @phpstan-import-type Record from \Monolog\Logger
* @deprecated Since Monolog 2.6. Use SymfonyMailerHandler instead.
*/
class SwiftMailerHandler extends MailHandler
{
/** @var \Swift_Mailer */
protected $mailer;
/** @var Swift_Message|callable(string, Record[]): Swift_Message */
private $messageTemplate;
/**
* @psalm-param Swift_Message|callable(string, Record[]): Swift_Message $message
*
* @param \Swift_Mailer $mailer The mailer to use
* @param callable|Swift_Message $message An example message for real messages, only the body will be replaced
*/
public function __construct(\Swift_Mailer $mailer, $message, $level = Logger::ERROR, bool $bubble = true)
{
parent::__construct($level, $bubble);
@trigger_error('The SwiftMailerHandler is deprecated since Monolog 2.6. Use SymfonyMailerHandler instead.', E_USER_DEPRECATED);
$this->mailer = $mailer;
$this->messageTemplate = $message;
}
/**
* {@inheritDoc}
*/
protected function send(string $content, array $records): void
{
$this->mailer->send($this->buildMessage($content, $records));
}
/**
* Gets the formatter for the Swift_Message subject.
*
* @param string|null $format The format of the subject
*/
protected function getSubjectFormatter(?string $format): FormatterInterface
{
return new LineFormatter($format);
}
/**
* Creates instance of Swift_Message to be sent
*
* @param string $content formatted email body to be sent
* @param array $records Log records that formed the content
* @return Swift_Message
*
* @phpstan-param Record[] $records
*/
protected function buildMessage(string $content, array $records): Swift_Message
{
$message = null;
if ($this->messageTemplate instanceof Swift_Message) {
$message = clone $this->messageTemplate;
$message->generateId();
} elseif (is_callable($this->messageTemplate)) {
$message = ($this->messageTemplate)($content, $records);
}
if (!$message instanceof Swift_Message) {
$record = reset($records);
throw new \InvalidArgumentException('Could not resolve message as instance of Swift_Message or a callable returning it' . ($record ? Utils::getRecordMessageForException($record) : ''));
}
if ($records) {
$subjectFormatter = $this->getSubjectFormatter($message->getSubject());
$message->setSubject($subjectFormatter->format($this->getHighestRecord($records)));
}
$mime = 'text/plain';
if ($this->isHtmlBody($content)) {
$mime = 'text/html';
}
$message->setBody($content, $mime);
/** @phpstan-ignore-next-line */
if (version_compare(Swift::VERSION, '6.0.0', '>=')) {
$message->setDate(new \DateTimeImmutable());
} else {
/** @phpstan-ignore-next-line */
$message->setDate(time());
}
return $message;
}
}
@@ -0,0 +1,111 @@
<?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;
use Monolog\Utils;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\LineFormatter;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mailer\Transport\TransportInterface;
use Symfony\Component\Mime\Email;
/**
* SymfonyMailerHandler uses Symfony's Mailer component to send the emails
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @phpstan-import-type Record from \Monolog\Logger
*/
class SymfonyMailerHandler extends MailHandler
{
/** @var MailerInterface|TransportInterface */
protected $mailer;
/** @var Email|callable(string, Record[]): Email */
private $emailTemplate;
/**
* @psalm-param Email|callable(string, Record[]): Email $email
*
* @param MailerInterface|TransportInterface $mailer The mailer to use
* @param callable|Email $email An email template, the subject/body will be replaced
*/
public function __construct($mailer, $email, $level = Logger::ERROR, bool $bubble = true)
{
parent::__construct($level, $bubble);
$this->mailer = $mailer;
$this->emailTemplate = $email;
}
/**
* {@inheritDoc}
*/
protected function send(string $content, array $records): void
{
$this->mailer->send($this->buildMessage($content, $records));
}
/**
* Gets the formatter for the Swift_Message subject.
*
* @param string|null $format The format of the subject
*/
protected function getSubjectFormatter(?string $format): FormatterInterface
{
return new LineFormatter($format);
}
/**
* Creates instance of Email to be sent
*
* @param string $content formatted email body to be sent
* @param array $records Log records that formed the content
*
* @phpstan-param Record[] $records
*/
protected function buildMessage(string $content, array $records): Email
{
$message = null;
if ($this->emailTemplate instanceof Email) {
$message = clone $this->emailTemplate;
} elseif (is_callable($this->emailTemplate)) {
$message = ($this->emailTemplate)($content, $records);
}
if (!$message instanceof Email) {
$record = reset($records);
throw new \InvalidArgumentException('Could not resolve message as instance of Email or a callable returning it' . ($record ? Utils::getRecordMessageForException($record) : ''));
}
if ($records) {
$subjectFormatter = $this->getSubjectFormatter($message->getSubject());
$message->subject($subjectFormatter->format($this->getHighestRecord($records)));
}
if ($this->isHtmlBody($content)) {
if (null !== ($charset = $message->getHtmlCharset())) {
$message->html($content, $charset);
} else {
$message->html($content);
}
} else {
if (null !== ($charset = $message->getTextCharset())) {
$message->text($content, $charset);
} else {
$message->text($content);
}
}
return $message->date(new \DateTimeImmutable());
}
}
+68
View File
@@ -0,0 +1,68 @@
<?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;
use Monolog\Utils;
/**
* Logs to syslog service.
*
* usage example:
*
* $log = new Logger('application');
* $syslog = new SyslogHandler('myfacility', 'local6');
* $formatter = new LineFormatter("%channel%.%level_name%: %message% %extra%");
* $syslog->setFormatter($formatter);
* $log->pushHandler($syslog);
*
* @author Sven Paulus <sven@karlsruhe.org>
*/
class SyslogHandler extends AbstractSyslogHandler
{
/** @var string */
protected $ident;
/** @var int */
protected $logopts;
/**
* @param string $ident
* @param string|int $facility Either one of the names of the keys in $this->facilities, or a LOG_* facility constant
* @param int $logopts Option flags for the openlog() call, defaults to LOG_PID
*/
public function __construct(string $ident, $facility = LOG_USER, $level = Logger::DEBUG, bool $bubble = true, int $logopts = LOG_PID)
{
parent::__construct($facility, $level, $bubble);
$this->ident = $ident;
$this->logopts = $logopts;
}
/**
* {@inheritDoc}
*/
public function close(): void
{
closelog();
}
/**
* {@inheritDoc}
*/
protected function write(array $record): void
{
if (!openlog($this->ident, $this->logopts, $this->facility)) {
throw new \LogicException('Can\'t open syslog for ident "'.$this->ident.'" and facility "'.$this->facility.'"' . Utils::getRecordMessageForException($record));
}
syslog($this->logLevels[$record['level']], (string) $record['formatted']);
}
}
@@ -0,0 +1,88 @@
<?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\SyslogUdp;
use Monolog\Utils;
use Socket;
class UdpSocket
{
protected const DATAGRAM_MAX_LENGTH = 65023;
/** @var string */
protected $ip;
/** @var int */
protected $port;
/** @var resource|Socket|null */
protected $socket = null;
public function __construct(string $ip, int $port = 514)
{
$this->ip = $ip;
$this->port = $port;
}
/**
* @param string $line
* @param string $header
* @return void
*/
public function write($line, $header = "")
{
$this->send($this->assembleMessage($line, $header));
}
public function close(): void
{
if (is_resource($this->socket) || $this->socket instanceof Socket) {
socket_close($this->socket);
$this->socket = null;
}
}
/**
* @return resource|Socket
*/
protected function getSocket()
{
if (null !== $this->socket) {
return $this->socket;
}
$domain = AF_INET;
$protocol = SOL_UDP;
// Check if we are using unix sockets.
if ($this->port === 0) {
$domain = AF_UNIX;
$protocol = IPPROTO_IP;
}
$this->socket = socket_create($domain, SOCK_DGRAM, $protocol) ?: null;
if (null === $this->socket) {
throw new \RuntimeException('The UdpSocket to '.$this->ip.':'.$this->port.' could not be opened via socket_create');
}
return $this->socket;
}
protected function send(string $chunk): void
{
socket_sendto($this->getSocket(), $chunk, strlen($chunk), $flags = 0, $this->ip, $this->port);
}
protected function assembleMessage(string $line, string $header): string
{
$chunkSize = static::DATAGRAM_MAX_LENGTH - strlen($header);
return $header . Utils::substr($line, 0, $chunkSize);
}
}
+150
View File
@@ -0,0 +1,150 @@
<?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 DateTimeInterface;
use Monolog\Logger;
use Monolog\Handler\SyslogUdp\UdpSocket;
use Monolog\Utils;
/**
* A Handler for logging to a remote syslogd server.
*
* @author Jesper Skovgaard Nielsen <nulpunkt@gmail.com>
* @author Dominik Kukacka <dominik.kukacka@gmail.com>
*/
class SyslogUdpHandler extends AbstractSyslogHandler
{
const RFC3164 = 0;
const RFC5424 = 1;
const RFC5424e = 2;
/** @var array<self::RFC*, string> */
private $dateFormats = array(
self::RFC3164 => 'M d H:i:s',
self::RFC5424 => \DateTime::RFC3339,
self::RFC5424e => \DateTime::RFC3339_EXTENDED,
);
/** @var UdpSocket */
protected $socket;
/** @var string */
protected $ident;
/** @var self::RFC* */
protected $rfc;
/**
* @param string $host Either IP/hostname or a path to a unix socket (port must be 0 then)
* @param int $port Port number, or 0 if $host is a unix socket
* @param string|int $facility Either one of the names of the keys in $this->facilities, or a LOG_* facility constant
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not
* @param string $ident Program name or tag for each log message.
* @param int $rfc RFC to format the message for.
* @throws MissingExtensionException
*
* @phpstan-param self::RFC* $rfc
*/
public function __construct(string $host, int $port = 514, $facility = LOG_USER, $level = Logger::DEBUG, bool $bubble = true, string $ident = 'php', int $rfc = self::RFC5424)
{
if (!extension_loaded('sockets')) {
throw new MissingExtensionException('The sockets extension is required to use the SyslogUdpHandler');
}
parent::__construct($facility, $level, $bubble);
$this->ident = $ident;
$this->rfc = $rfc;
$this->socket = new UdpSocket($host, $port);
}
protected function write(array $record): void
{
$lines = $this->splitMessageIntoLines($record['formatted']);
$header = $this->makeCommonSyslogHeader($this->logLevels[$record['level']], $record['datetime']);
foreach ($lines as $line) {
$this->socket->write($line, $header);
}
}
public function close(): void
{
$this->socket->close();
}
/**
* @param string|string[] $message
* @return string[]
*/
private function splitMessageIntoLines($message): array
{
if (is_array($message)) {
$message = implode("\n", $message);
}
$lines = preg_split('/$\R?^/m', (string) $message, -1, PREG_SPLIT_NO_EMPTY);
if (false === $lines) {
$pcreErrorCode = preg_last_error();
throw new \RuntimeException('Could not preg_split: ' . $pcreErrorCode . ' / ' . Utils::pcreLastErrorMessage($pcreErrorCode));
}
return $lines;
}
/**
* Make common syslog header (see rfc5424 or rfc3164)
*/
protected function makeCommonSyslogHeader(int $severity, DateTimeInterface $datetime): string
{
$priority = $severity + $this->facility;
if (!$pid = getmypid()) {
$pid = '-';
}
if (!$hostname = gethostname()) {
$hostname = '-';
}
if ($this->rfc === self::RFC3164) {
// see https://github.com/phpstan/phpstan/issues/5348
// @phpstan-ignore-next-line
$dateNew = $datetime->setTimezone(new \DateTimeZone('UTC'));
$date = $dateNew->format($this->dateFormats[$this->rfc]);
return "<$priority>" .
$date . " " .
$hostname . " " .
$this->ident . "[" . $pid . "]: ";
}
$date = $datetime->format($this->dateFormats[$this->rfc]);
return "<$priority>1 " .
$date . " " .
$hostname . " " .
$this->ident . " " .
$pid . " - - ";
}
/**
* Inject your own socket, mainly used for testing
*/
public function setSocket(UdpSocket $socket): self
{
$this->socket = $socket;
return $this;
}
}
+274
View File
@@ -0,0 +1,274 @@
<?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 RuntimeException;
use Monolog\Logger;
use Monolog\Utils;
/**
* Handler send logs to Telegram using Telegram Bot API.
*
* How to use:
* 1) Create telegram bot with https://telegram.me/BotFather
* 2) Create a telegram channel where logs will be recorded.
* 3) Add created bot from step 1 to the created channel from step 2.
*
* Use telegram bot API key from step 1 and channel name with '@' prefix from step 2 to create instance of TelegramBotHandler
*
* @link https://core.telegram.org/bots/api
*
* @author Mazur Alexandr <alexandrmazur96@gmail.com>
*
* @phpstan-import-type Record from \Monolog\Logger
*/
class TelegramBotHandler extends AbstractProcessingHandler
{
private const BOT_API = 'https://api.telegram.org/bot';
/**
* The available values of parseMode according to the Telegram api documentation
*/
private const AVAILABLE_PARSE_MODES = [
'HTML',
'MarkdownV2',
'Markdown', // legacy mode without underline and strikethrough, use MarkdownV2 instead
];
/**
* The maximum number of characters allowed in a message according to the Telegram api documentation
*/
private const MAX_MESSAGE_LENGTH = 4096;
/**
* Telegram bot access token provided by BotFather.
* Create telegram bot with https://telegram.me/BotFather and use access token from it.
* @var string
*/
private $apiKey;
/**
* Telegram channel name.
* Since to start with '@' symbol as prefix.
* @var string
*/
private $channel;
/**
* The kind of formatting that is used for the message.
* See available options at https://core.telegram.org/bots/api#formatting-options
* or in AVAILABLE_PARSE_MODES
* @var ?string
*/
private $parseMode;
/**
* Disables link previews for links in the message.
* @var ?bool
*/
private $disableWebPagePreview;
/**
* Sends the message silently. Users will receive a notification with no sound.
* @var ?bool
*/
private $disableNotification;
/**
* True - split a message longer than MAX_MESSAGE_LENGTH into parts and send in multiple messages.
* False - truncates a message that is too long.
* @var bool
*/
private $splitLongMessages;
/**
* Adds 1-second delay between sending a split message (according to Telegram API to avoid 429 Too Many Requests).
* @var bool
*/
private $delayBetweenMessages;
/**
* @param string $apiKey Telegram bot access token provided by BotFather
* @param string $channel Telegram channel name
* @param bool $splitLongMessages Split a message longer than MAX_MESSAGE_LENGTH into parts and send in multiple messages
* @param bool $delayBetweenMessages Adds delay between sending a split message according to Telegram API
* @throws MissingExtensionException
*/
public function __construct(
string $apiKey,
string $channel,
$level = Logger::DEBUG,
bool $bubble = true,
?string $parseMode = null,
?bool $disableWebPagePreview = null,
?bool $disableNotification = null,
bool $splitLongMessages = false,
bool $delayBetweenMessages = false
)
{
if (!extension_loaded('curl')) {
throw new MissingExtensionException('The curl extension is needed to use the TelegramBotHandler');
}
parent::__construct($level, $bubble);
$this->apiKey = $apiKey;
$this->channel = $channel;
$this->setParseMode($parseMode);
$this->disableWebPagePreview($disableWebPagePreview);
$this->disableNotification($disableNotification);
$this->splitLongMessages($splitLongMessages);
$this->delayBetweenMessages($delayBetweenMessages);
}
public function setParseMode(?string $parseMode = null): self
{
if ($parseMode !== null && !in_array($parseMode, self::AVAILABLE_PARSE_MODES)) {
throw new \InvalidArgumentException('Unknown parseMode, use one of these: ' . implode(', ', self::AVAILABLE_PARSE_MODES) . '.');
}
$this->parseMode = $parseMode;
return $this;
}
public function disableWebPagePreview(?bool $disableWebPagePreview = null): self
{
$this->disableWebPagePreview = $disableWebPagePreview;
return $this;
}
public function disableNotification(?bool $disableNotification = null): self
{
$this->disableNotification = $disableNotification;
return $this;
}
/**
* True - split a message longer than MAX_MESSAGE_LENGTH into parts and send in multiple messages.
* False - truncates a message that is too long.
* @param bool $splitLongMessages
* @return $this
*/
public function splitLongMessages(bool $splitLongMessages = false): self
{
$this->splitLongMessages = $splitLongMessages;
return $this;
}
/**
* Adds 1-second delay between sending a split message (according to Telegram API to avoid 429 Too Many Requests).
* @param bool $delayBetweenMessages
* @return $this
*/
public function delayBetweenMessages(bool $delayBetweenMessages = false): self
{
$this->delayBetweenMessages = $delayBetweenMessages;
return $this;
}
/**
* {@inheritDoc}
*/
public function handleBatch(array $records): void
{
/** @var Record[] $messages */
$messages = [];
foreach ($records as $record) {
if (!$this->isHandling($record)) {
continue;
}
if ($this->processors) {
/** @var Record $record */
$record = $this->processRecord($record);
}
$messages[] = $record;
}
if (!empty($messages)) {
$this->send((string)$this->getFormatter()->formatBatch($messages));
}
}
/**
* @inheritDoc
*/
protected function write(array $record): void
{
$this->send($record['formatted']);
}
/**
* Send request to @link https://api.telegram.org/bot on SendMessage action.
* @param string $message
*/
protected function send(string $message): void
{
$messages = $this->handleMessageLength($message);
foreach ($messages as $key => $msg) {
if ($this->delayBetweenMessages && $key > 0) {
sleep(1);
}
$this->sendCurl($msg);
}
}
protected function sendCurl(string $message): void
{
$ch = curl_init();
$url = self::BOT_API . $this->apiKey . '/SendMessage';
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'text' => $message,
'chat_id' => $this->channel,
'parse_mode' => $this->parseMode,
'disable_web_page_preview' => $this->disableWebPagePreview,
'disable_notification' => $this->disableNotification,
]));
$result = Curl\Util::execute($ch);
if (!is_string($result)) {
throw new RuntimeException('Telegram API error. Description: No response');
}
$result = json_decode($result, true);
if ($result['ok'] === false) {
throw new RuntimeException('Telegram API error. Description: ' . $result['description']);
}
}
/**
* Handle a message that is too long: truncates or splits into several
* @param string $message
* @return string[]
*/
private function handleMessageLength(string $message): array
{
$truncatedMarker = ' (...truncated)';
if (!$this->splitLongMessages && strlen($message) > self::MAX_MESSAGE_LENGTH) {
return [Utils::substr($message, 0, self::MAX_MESSAGE_LENGTH - strlen($truncatedMarker)) . $truncatedMarker];
}
return str_split($message, self::MAX_MESSAGE_LENGTH);
}
}
+231
View File
@@ -0,0 +1,231 @@
<?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;
use Psr\Log\LogLevel;
/**
* Used for testing purposes.
*
* It records all records and gives you access to them for verification.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @method bool hasEmergency($record)
* @method bool hasAlert($record)
* @method bool hasCritical($record)
* @method bool hasError($record)
* @method bool hasWarning($record)
* @method bool hasNotice($record)
* @method bool hasInfo($record)
* @method bool hasDebug($record)
*
* @method bool hasEmergencyRecords()
* @method bool hasAlertRecords()
* @method bool hasCriticalRecords()
* @method bool hasErrorRecords()
* @method bool hasWarningRecords()
* @method bool hasNoticeRecords()
* @method bool hasInfoRecords()
* @method bool hasDebugRecords()
*
* @method bool hasEmergencyThatContains($message)
* @method bool hasAlertThatContains($message)
* @method bool hasCriticalThatContains($message)
* @method bool hasErrorThatContains($message)
* @method bool hasWarningThatContains($message)
* @method bool hasNoticeThatContains($message)
* @method bool hasInfoThatContains($message)
* @method bool hasDebugThatContains($message)
*
* @method bool hasEmergencyThatMatches($message)
* @method bool hasAlertThatMatches($message)
* @method bool hasCriticalThatMatches($message)
* @method bool hasErrorThatMatches($message)
* @method bool hasWarningThatMatches($message)
* @method bool hasNoticeThatMatches($message)
* @method bool hasInfoThatMatches($message)
* @method bool hasDebugThatMatches($message)
*
* @method bool hasEmergencyThatPasses($message)
* @method bool hasAlertThatPasses($message)
* @method bool hasCriticalThatPasses($message)
* @method bool hasErrorThatPasses($message)
* @method bool hasWarningThatPasses($message)
* @method bool hasNoticeThatPasses($message)
* @method bool hasInfoThatPasses($message)
* @method bool hasDebugThatPasses($message)
*
* @phpstan-import-type Record from \Monolog\Logger
* @phpstan-import-type Level from \Monolog\Logger
* @phpstan-import-type LevelName from \Monolog\Logger
*/
class TestHandler extends AbstractProcessingHandler
{
/** @var Record[] */
protected $records = [];
/** @var array<Level, Record[]> */
protected $recordsByLevel = [];
/** @var bool */
private $skipReset = false;
/**
* @return array
*
* @phpstan-return Record[]
*/
public function getRecords()
{
return $this->records;
}
/**
* @return void
*/
public function clear()
{
$this->records = [];
$this->recordsByLevel = [];
}
/**
* @return void
*/
public function reset()
{
if (!$this->skipReset) {
$this->clear();
}
}
/**
* @return void
*/
public function setSkipReset(bool $skipReset)
{
$this->skipReset = $skipReset;
}
/**
* @param string|int $level Logging level value or name
*
* @phpstan-param Level|LevelName|LogLevel::* $level
*/
public function hasRecords($level): bool
{
return isset($this->recordsByLevel[Logger::toMonologLevel($level)]);
}
/**
* @param string|array $record Either a message string or an array containing message and optionally context keys that will be checked against all records
* @param string|int $level Logging level value or name
*
* @phpstan-param array{message: string, context?: mixed[]}|string $record
* @phpstan-param Level|LevelName|LogLevel::* $level
*/
public function hasRecord($record, $level): bool
{
if (is_string($record)) {
$record = array('message' => $record);
}
return $this->hasRecordThatPasses(function ($rec) use ($record) {
if ($rec['message'] !== $record['message']) {
return false;
}
if (isset($record['context']) && $rec['context'] !== $record['context']) {
return false;
}
return true;
}, $level);
}
/**
* @param string|int $level Logging level value or name
*
* @phpstan-param Level|LevelName|LogLevel::* $level
*/
public function hasRecordThatContains(string $message, $level): bool
{
return $this->hasRecordThatPasses(function ($rec) use ($message) {
return strpos($rec['message'], $message) !== false;
}, $level);
}
/**
* @param string|int $level Logging level value or name
*
* @phpstan-param Level|LevelName|LogLevel::* $level
*/
public function hasRecordThatMatches(string $regex, $level): bool
{
return $this->hasRecordThatPasses(function (array $rec) use ($regex): bool {
return preg_match($regex, $rec['message']) > 0;
}, $level);
}
/**
* @param string|int $level Logging level value or name
* @return bool
*
* @psalm-param callable(Record, int): mixed $predicate
* @phpstan-param Level|LevelName|LogLevel::* $level
*/
public function hasRecordThatPasses(callable $predicate, $level)
{
$level = Logger::toMonologLevel($level);
if (!isset($this->recordsByLevel[$level])) {
return false;
}
foreach ($this->recordsByLevel[$level] as $i => $rec) {
if ($predicate($rec, $i)) {
return true;
}
}
return false;
}
/**
* {@inheritDoc}
*/
protected function write(array $record): void
{
$this->recordsByLevel[$record['level']][] = $record;
$this->records[] = $record;
}
/**
* @param string $method
* @param mixed[] $args
* @return bool
*/
public function __call($method, $args)
{
if (preg_match('/(.*)(Debug|Info|Notice|Warning|Error|Critical|Alert|Emergency)(.*)/', $method, $matches) > 0) {
$genericMethod = $matches[1] . ('Records' !== $matches[3] ? 'Record' : '') . $matches[3];
$level = constant('Monolog\Logger::' . strtoupper($matches[2]));
$callback = [$this, $genericMethod];
if (is_callable($callback)) {
$args[] = $level;
return call_user_func_array($callback, $args);
}
}
throw new \BadMethodCallException('Call to undefined method ' . get_class($this) . '::' . $method . '()');
}
}
@@ -0,0 +1,24 @@
<?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;
trait WebRequestRecognizerTrait
{
/**
* Checks if PHP's serving a web request
* @return bool
*/
protected function isWebRequest(): bool
{
return 'cli' !== \PHP_SAPI && 'phpdbg' !== \PHP_SAPI;
}
}
@@ -0,0 +1,81 @@
<?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;
/**
* Forwards records to multiple handlers suppressing failures of each handler
* and continuing through to give every handler a chance to succeed.
*
* @author Craig D'Amelio <craig@damelio.ca>
*
* @phpstan-import-type Record from \Monolog\Logger
*/
class WhatFailureGroupHandler extends GroupHandler
{
/**
* {@inheritDoc}
*/
public function handle(array $record): bool
{
if ($this->processors) {
/** @var Record $record */
$record = $this->processRecord($record);
}
foreach ($this->handlers as $handler) {
try {
$handler->handle($record);
} catch (\Throwable $e) {
// What failure?
}
}
return false === $this->bubble;
}
/**
* {@inheritDoc}
*/
public function handleBatch(array $records): void
{
if ($this->processors) {
$processed = array();
foreach ($records as $record) {
$processed[] = $this->processRecord($record);
}
/** @var Record[] $records */
$records = $processed;
}
foreach ($this->handlers as $handler) {
try {
$handler->handleBatch($records);
} catch (\Throwable $e) {
// What failure?
}
}
}
/**
* {@inheritDoc}
*/
public function close(): void
{
foreach ($this->handlers as $handler) {
try {
$handler->close();
} catch (\Throwable $e) {
// What failure?
}
}
}
}
+101
View File
@@ -0,0 +1,101 @@
<?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\Formatter\FormatterInterface;
use Monolog\Formatter\NormalizerFormatter;
use Monolog\Logger;
/**
* Handler sending logs to Zend Monitor
*
* @author Christian Bergau <cbergau86@gmail.com>
* @author Jason Davis <happydude@jasondavis.net>
*
* @phpstan-import-type FormattedRecord from AbstractProcessingHandler
*/
class ZendMonitorHandler extends AbstractProcessingHandler
{
/**
* Monolog level / ZendMonitor Custom Event priority map
*
* @var array<int, int>
*/
protected $levelMap = [];
/**
* @throws MissingExtensionException
*/
public function __construct($level = Logger::DEBUG, bool $bubble = true)
{
if (!function_exists('zend_monitor_custom_event')) {
throw new MissingExtensionException(
'You must have Zend Server installed with Zend Monitor enabled in order to use this handler'
);
}
//zend monitor constants are not defined if zend monitor is not enabled.
$this->levelMap = [
Logger::DEBUG => \ZEND_MONITOR_EVENT_SEVERITY_INFO,
Logger::INFO => \ZEND_MONITOR_EVENT_SEVERITY_INFO,
Logger::NOTICE => \ZEND_MONITOR_EVENT_SEVERITY_INFO,
Logger::WARNING => \ZEND_MONITOR_EVENT_SEVERITY_WARNING,
Logger::ERROR => \ZEND_MONITOR_EVENT_SEVERITY_ERROR,
Logger::CRITICAL => \ZEND_MONITOR_EVENT_SEVERITY_ERROR,
Logger::ALERT => \ZEND_MONITOR_EVENT_SEVERITY_ERROR,
Logger::EMERGENCY => \ZEND_MONITOR_EVENT_SEVERITY_ERROR,
];
parent::__construct($level, $bubble);
}
/**
* {@inheritDoc}
*/
protected function write(array $record): void
{
$this->writeZendMonitorCustomEvent(
Logger::getLevelName($record['level']),
$record['message'],
$record['formatted'],
$this->levelMap[$record['level']]
);
}
/**
* Write to Zend Monitor Events
* @param string $type Text displayed in "Class Name (custom)" field
* @param string $message Text displayed in "Error String"
* @param array $formatted Displayed in Custom Variables tab
* @param int $severity Set the event severity level (-1,0,1)
*
* @phpstan-param FormattedRecord $formatted
*/
protected function writeZendMonitorCustomEvent(string $type, string $message, array $formatted, int $severity): void
{
zend_monitor_custom_event($type, $message, $formatted, $severity);
}
/**
* {@inheritDoc}
*/
public function getDefaultFormatter(): FormatterInterface
{
return new NormalizerFormatter();
}
/**
* @return array<int, int>
*/
public function getLevelMap(): array
{
return $this->levelMap;
}
}